Browse Source

Merge branch 'master' into release/1.5.10

release/1.5.10 v1.5.10-beta02
Nikolay Rykunov 1 year ago
parent
commit
54fc56b6cb
  1. 79
      CHANGELOG.md
  2. 5
      benchmarks/kn-performance/.gitignore
  3. 5
      benchmarks/kn-performance/README.md
  4. 83
      benchmarks/kn-performance/build.gradle.kts
  5. 10
      benchmarks/kn-performance/gradle.properties
  6. BIN
      benchmarks/kn-performance/gradle/wrapper/gradle-wrapper.jar
  7. 5
      benchmarks/kn-performance/gradle/wrapper/gradle-wrapper.properties
  8. 185
      benchmarks/kn-performance/gradlew
  9. 91
      benchmarks/kn-performance/gradlew.bat
  10. 18
      benchmarks/kn-performance/settings.gradle.kts
  11. 151
      benchmarks/kn-performance/src/commonMain/kotlin/Benchmarks.kt
  12. 76
      benchmarks/kn-performance/src/commonMain/kotlin/MeasureComposable.kt
  13. 38
      benchmarks/kn-performance/src/commonMain/kotlin/benchmarks/animation/AnimatedVisibility.kt
  14. 106
      benchmarks/kn-performance/src/commonMain/kotlin/benchmarks/lazygrid/LazyGrid.kt
  15. 321
      benchmarks/kn-performance/src/commonMain/kotlin/benchmarks/visualeffects/HappyNY.kt
  16. 36
      benchmarks/kn-performance/src/commonMain/resources/compose-multiplatform.xml
  17. 9
      benchmarks/kn-performance/src/desktopMain/kotlin/main.desktop.kt
  18. 66
      benchmarks/kn-performance/src/macosMain/kotlin/main.macos.kt
  19. 19
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposeCompilerKotlinSupportPlugin.kt
  20. 14
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposeExtension.kt
  21. 11
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/MacOSNotarizationSettings.kt
  22. 6
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/ComposeProjectProperties.kt
  23. 8
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/ExternalToolRunner.kt
  24. 13
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt
  25. 20
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/validation/ValidatedMacOSNotarizationSettings.kt
  26. 60
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCheckNotarizationStatusTask.kt
  27. 63
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractNotarizationTask.kt
  28. 82
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractUploadAppForNotarizationTask.kt
  29. 5
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/internal/configureExperimental.kt
  30. 39
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/web/internal/configureExperimentalWebApplication.kt
  31. 16
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/web/WebExtension.kt
  32. 8
      html/svg/src/jsMain/kotlin/org/jetbrains/compose/web/svg/svgAttrs.kt
  33. 79
      tutorials/Signing_and_notarization_on_macOS/README.md
  34. BIN
      tutorials/Signing_and_notarization_on_macOS/notarization-team-id.png

79
CHANGELOG.md

@ -1,3 +1,82 @@
# 1.5.2 (September 2023)
## Desktop
### Fixes
- [Application crash on touch (using touchscreen) when onClick modifier is used](https://github.com/JetBrains/compose-multiplatform/issues/3655)
## Dependencies
This version of Compose Multiplatform is based on the next Jetpack Compose libraries:
- [Compiler 1.5.3](https://developer.android.com/jetpack/androidx/releases/compose-compiler#1.5.3)
- [Runtime 1.5.0](https://developer.android.com/jetpack/androidx/releases/compose-runtime#1.5.0)
- [UI 1.5.0](https://developer.android.com/jetpack/androidx/releases/compose-ui#1.5.0)
- [Foundation 1.5.0](https://developer.android.com/jetpack/androidx/releases/compose-foundation#1.5.0)
- [Material 1.5.0](https://developer.android.com/jetpack/androidx/releases/compose-material#1.5.0)
- [Material3 1.1.1](https://developer.android.com/jetpack/androidx/releases/compose-material3#1.1.1)
# 1.5.10-beta01 (September 2023)
## Common
### Features
* [Support Kotlin 1.9.20-Beta](https://github.com/JetBrains/compose-multiplatform/pull/3656)
* Introduce Material 3 components in common
* [`ModalBottomSheet`](https://github.com/JetBrains/compose-multiplatform-core/pull/794)
* [`SearchBar` and `DockedSearchBar`](https://github.com/JetBrains/compose-multiplatform-core/pull/801)
* [`ExposedDropDownMenu`](https://github.com/JetBrains/compose-multiplatform-core/pull/787)
* [Introduce Material component `ExposedDropDownMenu` in common](https://github.com/JetBrains/compose-multiplatform-core/pull/793)
* [Introduce `WindowInfo.containerSize` experimental api](https://github.com/JetBrains/compose-multiplatform-core/pull/785)
## iOS
### Breaking changes
* [Having `kotlin.native.cacheKind = none` will result in a build error.](https://github.com/JetBrains/compose-multiplatform/pull/3667)
### Features
* [Compilation speed up due to enabling compiler caches for Kotlin 1.9.20+](https://github.com/JetBrains/compose-multiplatform/pull/3648)
* [Added crossfade animation during orientation change when used within UIKit hierarchy](https://github.com/JetBrains/compose-multiplatform-core/pull/778)
* [Compose Multiplatform should warn when `CADisableMinimumFrameDurationOnPhone` is not configured properly](https://github.com/JetBrains/compose-multiplatform/issues/3634)
* [Fast delete mode on software keyboard. When you hold a backspace, “turbo mode” is enabled after deleting the first 21 symbols. In turbo mode each tick deletes two words.](https://github.com/JetBrains/compose-multiplatform/issues/2991)
* [On a long scrollable TextFields, If it’s scrolled up to caret position while typing. Then it stopped on the line above the line with a caret.](https://github.com/JetBrains/compose-multiplatform-core/pull/804)
* [Add `UIViewController` lifetime hooks](https://github.com/JetBrains/compose-multiplatform-core/pull/779)
* [Implement iOS native feel scrolls for large text fields](https://github.com/JetBrains/compose-multiplatform-core/pull/771)
### Fixes
* [Rendering synchronization of multiple `UIKitView`s within a screen](https://github.com/JetBrains/compose-multiplatform/issues/3534)
* [Today's date is not highlighted with a circle in the material3 datePicker on iOS](https://github.com/JetBrains/compose-multiplatform/issues/3591)
* [Fix text-to-speech crash in iOS 16.0.*](https://github.com/JetBrains/compose-multiplatform/issues/2984)
* [Compose window is shown before the first frame is rendered](https://github.com/JetBrains/compose-multiplatform/issues/3492)
* [iOS TextField, Compound emojis are being treated as many symbols](https://github.com/JetBrains/compose-multiplatform/issues/3104)
* [Use `CADisplayLink.targetTimestamp` value as the time for animation frames](https://github.com/JetBrains/compose-multiplatform-core/pull/796)
* [iOS. Improved performance on 120 hz devices](https://github.com/JetBrains/compose-multiplatform-core/pull/797)
## Desktop
### Fixes
* [`LocalLayoutDirection` isn't propagated into `DialogWindow`](https://github.com/JetBrains/compose-multiplatform/issues/3382)
* [CompositionLocals given in application scope are not take into account in window scope (such as `LocalLayoutDirection`)](https://github.com/JetBrains/compose-multiplatform/issues/3571)
* [Fix accessibility issue with actions in popups](https://github.com/JetBrains/compose-multiplatform-core/pull/792)
* [Apply custom Dialog's scrim blend mode only when window is transparent](https://github.com/JetBrains/compose-multiplatform-core/pull/812)
## Gradle Plugin
### Fixes
* [Increase Kotlinx Serialization version used by the Compose Gradle Plugin](https://github.com/JetBrains/compose-multiplatform/issues/3479)
## Dependencies
This version of Compose Multiplatform is based on the next Jetpack Compose libraries:
* [Compiler 1.5.3](https://developer.android.com/jetpack/androidx/releases/compose-compiler#1.5.3)
* [Runtime 1.5.1](https://developer.android.com/jetpack/androidx/releases/compose-runtime#1.5.1)
* [UI 1.5.1](https://developer.android.com/jetpack/androidx/releases/compose-ui#1.5.1)
* [Foundation 1.5.1](https://developer.android.com/jetpack/androidx/releases/compose-foundation#1.5.1)
* [Material 1.5.1](https://developer.android.com/jetpack/androidx/releases/compose-material#1.5.1)
* [Material3 1.1.1](https://developer.android.com/jetpack/androidx/releases/compose-material3#1.1.1)
# 1.5.1 (September 2023) # 1.5.1 (September 2023)
## Common ## Common

5
benchmarks/kn-performance/.gitignore vendored

@ -0,0 +1,5 @@
local.properties
build
.gradle
.idea
.DS_Store

5
benchmarks/kn-performance/README.md

@ -0,0 +1,5 @@
# Compose benchmarks for K/N
## Run native on MacOS
- `./gradlew runReleaseExecutableMacosArm64` (Works on Arm64 processors)
- `./gradlew runReleaseExecutableMacosX64` (Works on Intel processors)

83
benchmarks/kn-performance/build.gradle.kts

@ -0,0 +1,83 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
kotlin("multiplatform")
id("org.jetbrains.compose")
}
version = "1.0-SNAPSHOT"
repositories {
mavenLocal()
google()
mavenCentral()
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
}
kotlin {
jvm("desktop")
macosX64 {
binaries {
executable {
entryPoint = "main"
freeCompilerArgs += listOf(
"-linker-option", "-framework", "-linker-option", "Metal"
)
}
}
}
macosArm64 {
binaries {
executable {
entryPoint = "main"
freeCompilerArgs += listOf(
"-linker-option", "-framework", "-linker-option", "Metal"
)
}
}
}
sourceSets {
val commonMain by getting {
dependencies {
implementation(compose.ui)
implementation(compose.foundation)
implementation(compose.material)
implementation(compose.runtime)
@OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class)
implementation(compose.components.resources)
}
}
val desktopMain by getting {
dependencies {
implementation(compose.desktop.currentOs)
runtimeOnly("org.jetbrains.kotlinx:kotlinx-coroutines-swing:1.7.1")
}
}
val nativeMain by creating {
dependsOn(commonMain)
}
val macosMain by creating {
dependsOn(nativeMain)
}
val macosX64Main by getting {
dependsOn(macosMain)
}
val macosArm64Main by getting {
dependsOn(macosMain)
}
}
}
compose.desktop {
application {
mainClass = "Main_desktopKt"
}
}
tasks.withType<KotlinCompile> {
kotlinOptions.jvmTarget = "11"
}

10
benchmarks/kn-performance/gradle.properties

@ -0,0 +1,10 @@
compose.version=1.5.1
kotlin.version=1.9.10
org.gradle.jvmargs=-Xmx3g
kotlin.native.useEmbeddableCompilerJar=true
compose.desktop.verbose=true
android.useAndroidX=true
kotlin.js.webpack.major.version=4
org.jetbrains.compose.experimental.jscanvas.enabled=true
org.jetbrains.compose.experimental.macos.enabled=true
org.jetbrains.compose.experimental.uikit.enabled=true

BIN
benchmarks/kn-performance/gradle/wrapper/gradle-wrapper.jar vendored

Binary file not shown.

5
benchmarks/kn-performance/gradle/wrapper/gradle-wrapper.properties vendored

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

185
benchmarks/kn-performance/gradlew vendored

@ -0,0 +1,185 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"

91
benchmarks/kn-performance/gradlew.bat vendored

@ -0,0 +1,91 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

18
benchmarks/kn-performance/settings.gradle.kts

@ -0,0 +1,18 @@
pluginManagement {
repositories {
mavenLocal()
mavenCentral()
gradlePluginPortal()
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
google()
}
plugins {
val kotlinVersion = extra["kotlin.version"] as String
kotlin("multiplatform").version(kotlinVersion)
val composeVersion = extra["compose.version"] as String
id("org.jetbrains.compose").version(composeVersion)
}
}
rootProject.name = "compose-benchmarks"

151
benchmarks/kn-performance/src/commonMain/kotlin/Benchmarks.kt

@ -0,0 +1,151 @@
import androidx.compose.runtime.Composable
import benchmarks.animation.AnimatedVisibility
import benchmarks.lazygrid.LazyGrid
import benchmarks.visualeffects.NYContent
import kotlin.math.roundToInt
import kotlin.time.Duration
enum class BenchmarkFrameTimeKind {
CPU, GPU
}
fun BenchmarkFrameTimeKind.toPrettyPrintString(): String =
when (this) {
BenchmarkFrameTimeKind.CPU -> "CPU"
BenchmarkFrameTimeKind.GPU -> "GPU"
}
data class BenchmarkFrame(
val cpuDuration: Duration,
val gpuDuration: Duration
) {
fun duration(kind: BenchmarkFrameTimeKind): Duration =
when (kind) {
BenchmarkFrameTimeKind.CPU -> cpuDuration
BenchmarkFrameTimeKind.GPU -> gpuDuration
}
}
data class BenchmarkPercentileAverage(
val percentile: Double,
val average: Duration
)
data class MissedFrames(
val count: Int,
val ratio: Double
) {
fun prettyPrint(description: String) {
println(
"""
Missed frames ($description):
- count: $count
- ratio: $ratio
""".trimIndent()
)
}
}
data class BenchmarkStats(
val frameBudget: Duration,
val percentileCPUAverage: List<BenchmarkPercentileAverage>,
val percentileGPUAverage: List<BenchmarkPercentileAverage>,
val noBufferingMissedFrames: MissedFrames,
val doubleBufferingMissedFrames: MissedFrames
) {
fun prettyPrint() {
percentileCPUAverage.prettyPrint(BenchmarkFrameTimeKind.CPU)
println()
percentileGPUAverage.prettyPrint(BenchmarkFrameTimeKind.GPU)
println()
noBufferingMissedFrames.prettyPrint("no buffering")
doubleBufferingMissedFrames.prettyPrint("double buffering")
}
private fun List<BenchmarkPercentileAverage>.prettyPrint(kind: BenchmarkFrameTimeKind) {
forEach {
println("Worst p${(it.percentile * 100).roundToInt()} ${kind.toPrettyPrintString()} average: ${it.average}")
}
}
}
class BenchmarkResult(
private val frameBudget: Duration,
private val frames: List<BenchmarkFrame>,
) {
private fun percentileAverageFrameTime(percentile: Double, kind: BenchmarkFrameTimeKind): Duration {
require(percentile in 0.0..1.0)
val startIndex = ((frames.size - 1) * percentile).roundToInt()
val percentileFrames = frames.sortedBy { it.duration(kind) }.subList(frames.size - startIndex - 1, frames.size)
return averageDuration(percentileFrames) {
it.duration(kind)
}
}
fun generateStats(): BenchmarkStats {
val noBufferingMissedFramesCount = frames.count {
it.cpuDuration + it.gpuDuration > frameBudget
}
val doubleBufferingMissedFrames = frames.count {
maxOf(it.cpuDuration, it.gpuDuration) > frameBudget
}
return BenchmarkStats(
frameBudget,
listOf(0.01, 0.02, 0.05, 0.1, 0.25, 0.5).map { percentile ->
val average = percentileAverageFrameTime(percentile, BenchmarkFrameTimeKind.CPU)
BenchmarkPercentileAverage(percentile, average)
},
listOf(0.01, 0.1, 0.5).map { percentile ->
val average = percentileAverageFrameTime(percentile, BenchmarkFrameTimeKind.GPU)
BenchmarkPercentileAverage(percentile, average)
},
MissedFrames(noBufferingMissedFramesCount, noBufferingMissedFramesCount / frames.size.toDouble()),
MissedFrames(doubleBufferingMissedFrames, doubleBufferingMissedFrames / frames.size.toDouble())
)
}
private fun averageDuration(frames: List<BenchmarkFrame>, selector: (BenchmarkFrame) -> Duration): Duration =
frames.fold(Duration.ZERO) { acc, frame ->
acc + selector(frame)
} / frames.size
}
fun runBenchmark(
name: String,
width: Int,
height: Int,
targetFps: Int,
frameCount: Int,
graphicsContext: GraphicsContext?,
content: @Composable () -> Unit
): BenchmarkStats {
val stats = measureComposable(frameCount, width, height, targetFps, graphicsContext, content).generateStats()
println(name)
stats.prettyPrint()
println()
return stats
}
fun runBenchmarks(
width: Int = 1920,
height: Int = 1080,
targetFps: Int = 120,
graphicsContext: GraphicsContext? = null
) {
println("Running emulating $targetFps FPS")
runBenchmark("AnimatedVisibility", width, height, targetFps, 1000, graphicsContext) { AnimatedVisibility() }
runBenchmark("LazyGrid", width, height, targetFps, 1000, graphicsContext) { LazyGrid() }
runBenchmark("VisualEffects", width, height, targetFps, 1000, graphicsContext) { NYContent(width, height) }
}

76
benchmarks/kn-performance/src/commonMain/kotlin/MeasureComposable.kt

@ -0,0 +1,76 @@
import androidx.compose.runtime.Composable
import androidx.compose.ui.ComposeScene
import androidx.compose.ui.unit.Constraints
import org.jetbrains.skia.DirectContext
import org.jetbrains.skia.Surface
import kotlin.time.Duration
import kotlin.time.Duration.Companion.nanoseconds
import kotlin.time.ExperimentalTime
import kotlin.time.measureTime
import kotlinx.coroutines.*
const val nanosPerSecond = 1E9.toLong()
const val millisPerSecond = 1e3.toLong()
const val nanosPerMillisecond = nanosPerSecond / millisPerSecond
interface GraphicsContext {
fun surface(width: Int, height: Int): Surface
suspend fun awaitGPUCompletion()
}
@OptIn(ExperimentalTime::class)
fun measureComposable(
frameCount: Int = 500,
width: Int,
height: Int,
targetFps: Int,
graphicsContext: GraphicsContext?,
content: @Composable () -> Unit
): BenchmarkResult = runBlocking {
val scene = ComposeScene()
try {
val nanosPerFrame = (1.0 / targetFps.toDouble() * nanosPerSecond).toLong()
scene.setContent(content)
scene.constraints = Constraints.fixed(width, height)
val surface = graphicsContext?.surface(width, height) ?: Surface.makeNull(width, height)
val frames = MutableList(frameCount) {
BenchmarkFrame(Duration.INFINITE, Duration.INFINITE)
}
var nanoTime = 0L
repeat(frameCount) {
val frameTime = measureTime {
val cpuTime = measureTime {
scene.render(surface.canvas, nanoTime)
surface.flushAndSubmit(false)
}
val gpuTime = measureTime {
graphicsContext?.awaitGPUCompletion()
}
frames[it] = BenchmarkFrame(cpuTime, gpuTime)
}
val actualNanosPerFrame = frameTime.inWholeNanoseconds
val nanosUntilDeadline = nanosPerFrame - actualNanosPerFrame
// Emulate waiting for next vsync
if (nanosUntilDeadline > 0) {
delay(nanosUntilDeadline / nanosPerMillisecond)
}
nanoTime += maxOf(actualNanosPerFrame, nanosPerFrame)
}
BenchmarkResult(
nanosPerFrame.nanoseconds,
frames
)
} finally {
scene.close()
}
}

38
benchmarks/kn-performance/src/commonMain/kotlin/benchmarks/animation/AnimatedVisibility.kt

@ -0,0 +1,38 @@
package benchmarks.animation
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import kotlinx.coroutines.delay
import org.jetbrains.compose.resources.ExperimentalResourceApi
import org.jetbrains.compose.resources.painterResource
@OptIn(ExperimentalResourceApi::class)
@Composable
fun AnimatedVisibility() {
MaterialTheme {
var showImage by remember { mutableStateOf(false) }
LaunchedEffect(showImage) {
delay(200)
showImage = !showImage
}
Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
AnimatedVisibility(showImage) {
Image(
painterResource("compose-multiplatform.xml"),
null
)
}
}
}
}

106
benchmarks/kn-performance/src/commonMain/kotlin/benchmarks/lazygrid/LazyGrid.kt

@ -0,0 +1,106 @@
package benchmarks.lazygrid
import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.material.Card
import androidx.compose.material.Checkbox
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.withFrameMillis
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
@Composable
fun LazyGrid() {
val itemCount = 12000
val entries = remember {List(itemCount) { Entry("$it") }}
val state = rememberLazyGridState()
var smoothScroll by remember { mutableStateOf(false)}
MaterialTheme {
Column {
Row {
Checkbox(
checked = smoothScroll,
onCheckedChange = { value -> smoothScroll = value}
)
Text (text = "Smooth scroll", modifier = Modifier.align(Alignment.CenterVertically))
}
LazyVerticalGrid(
columns = GridCells.Fixed(4),
modifier = Modifier.fillMaxWidth().semantics { contentDescription = "IamLazy" },
state = state
) {
items(entries) {
ListCell(it)
}
}
}
}
var curItem by remember { mutableStateOf(0) }
var direct by remember { mutableStateOf(true) }
if (smoothScroll) {
LaunchedEffect(Unit) {
while (smoothScroll) {
withFrameMillis { }
curItem = state.firstVisibleItemIndex
if (curItem == 0) direct = true
if (curItem > itemCount - 100) direct = false
state.scrollBy(if (direct) 5f else -5f)
}
}
} else {
LaunchedEffect(curItem) {
withFrameMillis { }
curItem += if (direct) 50 else -50
if (curItem >= itemCount) {
direct = false
curItem = itemCount - 1
} else if (curItem <= 0) {
direct = true
curItem = 0
}
state.scrollToItem(curItem)
}
}
}
data class Entry(val contents: String)
@Composable
private fun ListCell(entry: Entry) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
) {
Text(
text = entry.contents,
textAlign = TextAlign.Center,
style = MaterialTheme.typography.h5,
modifier = Modifier.padding(16.dp)
)
}
}

321
benchmarks/kn-performance/src/commonMain/kotlin/benchmarks/visualeffects/HappyNY.kt

@ -0,0 +1,321 @@
package benchmarks.visualeffects
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Surface
import androidx.compose.runtime.*
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.*
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import kotlin.math.*
import kotlin.random.Random
const val snowCount = 80
const val starCount = 60
const val rocketPartsCount = 30
data class SnowFlake(
var x: Dp,
var y: Dp,
val scale: Float,
var v: Double,
var alpha: Float,
var angle: Float,
var rotate: Int,
var phase: Double
)
data class Star(val x: Dp, val y: Dp, val color: Color, val size: Dp)
val random = Random(123)
fun random(): Float = random.nextFloat()
class DoubleRocket(val particle: Particle) {
private val STATE_ROCKET = 0
private val STATE_SMALL_ROCKETS = 1
var state = STATE_ROCKET
var rockets: Array<Rocket> = emptyArray()
private fun checkState(time: Long) {
if (particle.vy > -3.0 && state == STATE_ROCKET) {
explode(time)
}
if (state == STATE_SMALL_ROCKETS) {
var done = true
rockets.forEach {
if (!it.exploded) {
it.checkExplode(time)
}
if (!it.checkDone()) {
done = false
}
}
if (done) {
reset()
}
}
}
private fun reset() {
state = STATE_ROCKET
particle.x = 0.0
particle.y = 1000.0
particle.vx = 2.1
particle.vy = -12.5
}
private fun explode(time: Long) {
val colors = arrayOf(Color(0xff, 0, 0), Color(192, 255, 192), Color(192, 212, 255))
rockets = Array(7) {
val v = 1.2f + 1.0 * random()
val angle = 2 * PI * random()
Rocket(
Particle(
particle.x,
particle.y,
v * sin(angle) + particle.vx,
v * cos(angle) + particle.vy - 0.5f,
colors[it % colors.size]
), colors[it % colors.size], time
)
}
state = STATE_SMALL_ROCKETS
}
fun move(time: Long, prevTime: Long) {
if (rocket.state == rocket.STATE_ROCKET) {
rocket.particle.move(time, prevTime)
rocket.particle.gravity(time, prevTime)
} else {
rocket.rockets.forEach {
it.move(time, prevTime)
}
}
rocket.checkState(time)
}
@Composable
fun draw() {
if (state == rocket.STATE_ROCKET) {
particle.draw()
} else {
rockets.forEach {
it.draw()
}
}
}
}
class Rocket(val particle: Particle, val color: Color, val startTime: Long = 0) {
var exploded = false
var parts: Array<Particle> = emptyArray()
fun checkExplode(time: Long) {
if (time - startTime > 1200000000) {
explode()
}
}
private fun explode() {
parts = Array(rocketPartsCount) {
val v = 0.5f + 1.5 * random()
val angle = 2 * PI * random()
Particle(particle.x, particle.y, v * sin(angle) + particle.vx, v * cos(angle) + particle.vy, color, 1)
}
exploded = true
}
fun checkDone(): Boolean {
if (!exploded) return false
parts.forEach {
if (it.y < 800) return false
}
return true
}
fun move(time: Long, prevTime: Long) {
if (!exploded) {
particle.move(time, prevTime)
particle.gravity(time, prevTime)
checkExplode(time)
} else {
parts.forEach {
it.move(time, prevTime)
it.gravity(time, prevTime)
}
}
}
@Composable
fun draw() {
if (!exploded) {
particle.draw()
} else {
parts.forEach {
it.draw()
}
}
}
}
class Particle(var x: Double, var y: Double, var vx: Double, var vy: Double, val color: Color, val type: Int = 0) {
fun move(time: Long, prevTime: Long) {
x = (x + vx * (time - prevTime) / 30000000)
y = (y + vy * (time - prevTime) / 30000000)
}
fun gravity(time: Long, prevTime: Long) {
vy = vy + 1.0f * (time - prevTime) / 300000000
}
@Composable
fun draw() {
val alphaFactor = if (type == 0) 1.0f else 1 / (1 + abs(vy / 5)).toFloat()
Box(Modifier.size(5.dp).offset(x.dp, y.dp).alpha(alphaFactor).clip(CircleShape).background(color))
for (i in 1..5) {
Box(
Modifier.size(4.dp).offset((x - vx / 2 * i).dp, (y - vy / 2 * i).dp)
.alpha(alphaFactor * (1 - 0.18f * i)).clip(CircleShape).background(color)
)
}
}
}
val rocket = DoubleRocket(Particle(0.0, 1000.0, 2.1, -12.5, Color.White))
fun prepareStarsAndSnowFlakes(stars: SnapshotStateList<Star>, snowFlakes: SnapshotStateList<SnowFlake>, width: Int, height: Int) {
for (i in 0..snowCount) {
snowFlakes.add(
SnowFlake(
(50 + (width - 50) * random()).dp,
(height * random()).dp,
0.1f + 0.2f * random(),
1.5 + 3 * random(),
(0.4f + 0.4 * random()).toFloat(),
60 * random(),
Random.nextInt(1, 5) - 3,
random() * 2 * PI
)
)
}
val colors = arrayOf(Color.Red, Color.Yellow, Color.Green, Color.Yellow, Color.Cyan, Color.Magenta, Color.White)
for (i in 0..starCount) {
stars.add(
Star(
(width * random()).dp,
(height * random()).dp,
colors[Random.nextInt(colors.size)],
(3 + 5 * random()).dp
)
)
}
}
@Composable
fun NYContent(width: Int, height: Int) {
var time by remember { mutableStateOf(0L) }
val startTime = remember { 0L }
var prevTime by remember { mutableStateOf(0L) }
val snowFlakes = remember { mutableStateListOf<SnowFlake>() }
val stars = remember { mutableStateListOf<Star>() }
var flickering2 by remember { mutableStateOf(true) }
remember { prepareStarsAndSnowFlakes(stars, snowFlakes, width, height) }
Surface(
modifier = Modifier.fillMaxSize().padding(5.dp).shadow(3.dp, RoundedCornerShape(20.dp)),
color = Color.Black,
shape = RoundedCornerShape(20.dp)
) {
LaunchedEffect(Unit) {
while (true) {
withFrameNanos {
prevTime = time
time = it
}
}
}
if (flickering2) {
if (time - startTime > 15500000000) { //note, that startTime has been updated above
flickering2 = false
}
}
rocket.move(time, prevTime)
Box(Modifier.fillMaxSize()) {
snow(time, prevTime, snowFlakes, startTime, height)
starrySky(stars)
rocket.draw()
}
}
}
@Composable
fun starrySky(stars: SnapshotStateList<Star>) {
stars.forEach {
star(it.x, it.y, it.color, size = it.size)
}
}
@Composable
fun star(x: Dp, y: Dp, color: Color = Color.White, size: Dp) {
Box(Modifier.offset(x, y).scale(1.0f, 0.2f).rotate(45f).size(size).background(color))
Box(Modifier.offset(x, y).scale(0.2f, 1.0f).rotate(45f).size(size).background(color))
}
@Composable
fun snow(time: Long, prevTime: Long, snowFlakes: SnapshotStateList<SnowFlake>, startTime: Long, height: Int) {
val deltaAngle = (time - startTime) / 100000000
with(LocalDensity.current) {
snowFlakes.forEach {
var y = it.y + ((it.v * (time - prevTime)) / 300000000).dp
if (y > (height + 20).dp) {
y = -20.dp
}
it.y = y
val x = it.x + (15 * sin(time.toDouble() / 3000000000 + it.phase)).dp
snowFlake(Modifier.offset(x, y).scale(it.scale).rotate(it.angle + deltaAngle * it.rotate), it.alpha)
}
}
}
@Composable
fun snowFlake(modifier: Modifier, alpha: Float = 0.8f) {
Box(modifier) {
snowFlakeInt(0, 0f, 30.dp, 0.dp, alpha)
snowFlakeInt(0, 60f, 15.dp, 25.dp, alpha)
snowFlakeInt(0, 120f, -15.dp, 25.dp, alpha)
snowFlakeInt(0, 180f, -30.dp, 0.dp, alpha)
snowFlakeInt(0, 240f, -15.dp, -25.dp, alpha)
snowFlakeInt(0, 300f, 15.dp, -25.dp, alpha)
}
}
@Composable
fun snowFlakeInt(level: Int, angle: Float, shiftX: Dp, shiftY: Dp, alpha: Float) {
if (level > 3) return
Box(
Modifier.offset(shiftX, shiftY).rotate(angle).width(100.dp).height(10.dp).scale(0.6f).alpha(1f)
.background(Color.White.copy(alpha = alpha))
) {
snowFlakeInt(level + 1, 30f, 12.dp, 20.dp, alpha * 0.8f)
snowFlakeInt(level + 1, -30f, 12.dp, -20.dp, alpha * 0.8f)
}
}

36
benchmarks/kn-performance/src/commonMain/resources/compose-multiplatform.xml

@ -0,0 +1,36 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="600dp"
android:height="600dp"
android:viewportWidth="600"
android:viewportHeight="600">
<path
android:pathData="M301.21,418.53C300.97,418.54 300.73,418.56 300.49,418.56C297.09,418.59 293.74,417.72 290.79,416.05L222.6,377.54C220.63,376.43 219,374.82 217.85,372.88C216.7,370.94 216.09,368.73 216.07,366.47L216.07,288.16C216.06,287.32 216.09,286.49 216.17,285.67C216.38,283.54 216.91,281.5 217.71,279.6L199.29,268.27L177.74,256.19C175.72,260.43 174.73,265.23 174.78,270.22L174.79,387.05C174.85,393.89 178.57,400.2 184.53,403.56L286.26,461.02C290.67,463.51 295.66,464.8 300.73,464.76C300.91,464.76 301.09,464.74 301.27,464.74C301.24,449.84 301.22,439.23 301.22,439.23L301.21,418.53Z"
android:fillColor="#041619"
android:fillType="nonZero"/>
<path
android:pathData="M409.45,242.91L312.64,188.23C303.64,183.15 292.58,183.26 283.68,188.51L187.92,245C183.31,247.73 179.93,251.62 177.75,256.17L177.74,256.19L199.29,268.27L217.71,279.6C217.83,279.32 217.92,279.02 218.05,278.74C218.24,278.36 218.43,277.98 218.64,277.62C219.06,276.88 219.52,276.18 220.04,275.51C221.37,273.8 223.01,272.35 224.87,271.25L289.06,233.39C290.42,232.59 291.87,231.96 293.39,231.51C295.53,230.87 297.77,230.6 300,230.72C302.98,230.88 305.88,231.73 308.47,233.2L373.37,269.85C375.54,271.08 377.49,272.68 379.13,274.57C379.68,275.19 380.18,275.85 380.65,276.53C380.86,276.84 381.05,277.15 381.24,277.47L397.79,266.39L420.34,252.93L420.31,252.88C417.55,248.8 413.77,245.35 409.45,242.91Z"
android:fillColor="#37BF6E"
android:fillType="nonZero"/>
<path
android:pathData="M381.24,277.47C381.51,277.92 381.77,278.38 382.01,278.84C382.21,279.24 382.39,279.65 382.57,280.06C382.91,280.88 383.19,281.73 383.41,282.59C383.74,283.88 383.92,285.21 383.93,286.57L383.93,361.1C383.96,363.95 383.35,366.77 382.16,369.36C381.93,369.86 381.69,370.35 381.42,370.83C379.75,373.79 377.32,376.27 374.39,378L310.2,415.87C307.47,417.48 304.38,418.39 301.21,418.53L301.22,439.23C301.22,439.23 301.24,449.84 301.27,464.74C306.1,464.61 310.91,463.3 315.21,460.75L410.98,404.25C419.88,399 425.31,389.37 425.22,379.03L425.22,267.85C425.17,262.48 423.34,257.34 420.34,252.93L397.79,266.39L381.24,277.47Z"
android:fillColor="#3870B2"
android:fillType="nonZero"/>
<path
android:pathData="M177.75,256.17C179.93,251.62 183.31,247.73 187.92,245L283.68,188.51C292.58,183.26 303.64,183.15 312.64,188.23L409.45,242.91C413.77,245.35 417.55,248.8 420.31,252.88L420.34,252.93L498.59,206.19C494.03,199.46 487.79,193.78 480.67,189.75L320.86,99.49C306.01,91.1 287.75,91.27 273.07,99.95L114.99,193.2C107.39,197.69 101.81,204.11 98.21,211.63L177.74,256.19L177.75,256.17ZM301.27,464.74C301.09,464.74 300.91,464.76 300.73,464.76C295.66,464.8 290.67,463.51 286.26,461.02L184.53,403.56C178.57,400.2 174.85,393.89 174.79,387.05L174.78,270.22C174.73,265.23 175.72,260.43 177.74,256.19L98.21,211.63C94.86,218.63 93.23,226.58 93.31,234.82L93.31,427.67C93.42,438.97 99.54,449.37 109.4,454.92L277.31,549.77C284.6,553.88 292.84,556.01 301.2,555.94L301.2,555.8C301.39,543.78 301.33,495.26 301.27,464.74Z"
android:strokeWidth="10"
android:fillColor="#00000000"
android:strokeColor="#083042"
android:fillType="nonZero"/>
<path
android:pathData="M498.59,206.19L420.34,252.93C423.34,257.34 425.17,262.48 425.22,267.85L425.22,379.03C425.31,389.37 419.88,399 410.98,404.25L315.21,460.75C310.91,463.3 306.1,464.61 301.27,464.74C301.33,495.26 301.39,543.78 301.2,555.8L301.2,555.94C309.48,555.87 317.74,553.68 325.11,549.32L483.18,456.06C497.87,447.39 506.85,431.49 506.69,414.43L506.69,230.91C506.6,222.02 503.57,213.5 498.59,206.19Z"
android:strokeWidth="10"
android:fillColor="#00000000"
android:strokeColor="#083042"
android:fillType="nonZero"/>
<path
android:pathData="M301.2,555.94C292.84,556.01 284.6,553.88 277.31,549.76L109.4,454.92C99.54,449.37 93.42,438.97 93.31,427.67L93.31,234.82C93.23,226.58 94.86,218.63 98.21,211.63C101.81,204.11 107.39,197.69 114.99,193.2L273.07,99.95C287.75,91.27 306.01,91.1 320.86,99.49L480.67,189.75C487.79,193.78 494.03,199.46 498.59,206.19C503.57,213.5 506.6,222.02 506.69,230.91L506.69,414.43C506.85,431.49 497.87,447.39 483.18,456.06L325.11,549.32C317.74,553.68 309.48,555.87 301.2,555.94Z"
android:strokeWidth="10"
android:fillColor="#00000000"
android:strokeColor="#083042"
android:fillType="nonZero"/>
</vector>

9
benchmarks/kn-performance/src/desktopMain/kotlin/main.desktop.kt

@ -0,0 +1,9 @@
/*
* Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
fun main() = runBlocking(Dispatchers.Main) { runBenchmarks() }

66
benchmarks/kn-performance/src/macosMain/kotlin/main.macos.kt

@ -0,0 +1,66 @@
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.objcPtr
import org.jetbrains.skia.*
import platform.Metal.*
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
/*
* Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
@OptIn(ExperimentalForeignApi::class)
fun main() {
val graphicsContext = object : GraphicsContext {
private val device = MTLCreateSystemDefaultDevice() ?: throw IllegalStateException("Can't create MTLDevice")
private val commandQueue = device.newCommandQueue() ?: throw IllegalStateException("Can't create MTLCommandQueue")
private val directContext = DirectContext.makeMetal(device.objcPtr(), commandQueue.objcPtr())
private var cachedSurface: Surface? = null
override fun surface(width: Int, height: Int): Surface {
val oldSurface = cachedSurface
if (oldSurface != null && oldSurface.width == width && oldSurface.height == height) {
return oldSurface
}
val descriptor = MTLTextureDescriptor()
descriptor.width = width.toULong()
descriptor.height = height.toULong()
descriptor.usage = MTLTextureUsageShaderRead or MTLTextureUsageShaderWrite or MTLTextureUsageRenderTarget
descriptor.textureType = MTLTextureType2D
descriptor.pixelFormat = MTLPixelFormatBGRA8Unorm
descriptor.mipmapLevelCount = 1UL
val texture = device.newTextureWithDescriptor(descriptor) ?: throw IllegalStateException("Can't create MTLTexture")
val renderTarget = BackendRenderTarget.makeMetal(width, height, texture.objcPtr())
return Surface.makeFromBackendRenderTarget(
directContext,
renderTarget,
SurfaceOrigin.TOP_LEFT,
SurfaceColorFormat.BGRA_8888,
ColorSpace.sRGB,
SurfaceProps(pixelGeometry = PixelGeometry.UNKNOWN)
).also {
cachedSurface = it
} ?: throw IllegalStateException("Can't create Surface")
}
override suspend fun awaitGPUCompletion() {
val commandBuffer = commandQueue.commandBuffer() ?: return
suspendCoroutine { continuation ->
commandBuffer.addCompletedHandler {
continuation.resume(Unit)
}
commandBuffer.commit()
}
}
}
runBenchmarks(graphicsContext = graphicsContext)
}

19
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposeCompilerKotlinSupportPlugin.kt

@ -16,6 +16,8 @@ import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinJsIrTarget
class ComposeCompilerKotlinSupportPlugin : KotlinCompilerPluginSupportPlugin { class ComposeCompilerKotlinSupportPlugin : KotlinCompilerPluginSupportPlugin {
private lateinit var composeCompilerArtifactProvider: ComposeCompilerArtifactProvider private lateinit var composeCompilerArtifactProvider: ComposeCompilerArtifactProvider
private lateinit var applicableForPlatformTypes: Provider<Set<KotlinPlatformType>>
override fun apply(target: Project) { override fun apply(target: Project) {
super.apply(target) super.apply(target)
@ -27,6 +29,8 @@ class ComposeCompilerKotlinSupportPlugin : KotlinCompilerPluginSupportPlugin {
ComposeCompilerCompatibility.compilerVersionFor(target.getKotlinPluginVersion()) ComposeCompilerCompatibility.compilerVersionFor(target.getKotlinPluginVersion())
} }
applicableForPlatformTypes = composeExt.platformTypes
collectUnsupportedCompilerPluginUsages(target) collectUnsupportedCompilerPluginUsages(target)
} }
} }
@ -62,15 +66,14 @@ class ComposeCompilerKotlinSupportPlugin : KotlinCompilerPluginSupportPlugin {
override fun getPluginArtifactForNative(): SubpluginArtifact = override fun getPluginArtifactForNative(): SubpluginArtifact =
composeCompilerArtifactProvider.compilerHostedArtifact composeCompilerArtifactProvider.compilerHostedArtifact
override fun isApplicable(kotlinCompilation: KotlinCompilation<*>): Boolean = override fun isApplicable(kotlinCompilation: KotlinCompilation<*>): Boolean {
when (kotlinCompilation.target.platformType) { val applicableTo = applicableForPlatformTypes.get()
KotlinPlatformType.common -> true
KotlinPlatformType.jvm -> true return when (val type = kotlinCompilation.target.platformType) {
KotlinPlatformType.js -> isApplicableJsTarget(kotlinCompilation.target) KotlinPlatformType.js -> isApplicableJsTarget(kotlinCompilation.target) && applicableTo.contains(type)
KotlinPlatformType.androidJvm -> true else -> applicableTo.contains(type)
KotlinPlatformType.native -> true
KotlinPlatformType.wasm -> false
} }
}
private fun isApplicableJsTarget(kotlinTarget: KotlinTarget): Boolean { private fun isApplicableJsTarget(kotlinTarget: KotlinTarget): Boolean {
if (kotlinTarget !is KotlinJsIrTarget) return false if (kotlinTarget !is KotlinJsIrTarget) return false

14
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposeExtension.kt

@ -10,7 +10,9 @@ import org.gradle.api.model.ObjectFactory
import org.gradle.api.plugins.ExtensionAware import org.gradle.api.plugins.ExtensionAware
import org.gradle.api.provider.ListProperty import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property import org.gradle.api.provider.Property
import org.gradle.api.provider.SetProperty
import org.jetbrains.compose.internal.utils.nullableProperty import org.jetbrains.compose.internal.utils.nullableProperty
import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
import javax.inject.Inject import javax.inject.Inject
abstract class ComposeExtension @Inject constructor( abstract class ComposeExtension @Inject constructor(
@ -41,5 +43,17 @@ abstract class ComposeExtension @Inject constructor(
*/ */
val kotlinCompilerPluginArgs: ListProperty<String> = objects.listProperty(String::class.java) val kotlinCompilerPluginArgs: ListProperty<String> = objects.listProperty(String::class.java)
/**
* A set of kotlin platform types to which the Compose plugin will be applied.
* By default, it contains all KotlinPlatformType values.
* It can be used to disable the Compose plugin for one or more targets:
* ```
* platformTypes.set(platformTypes.get() - KotlinPlatformType.native)
* ```
*/
val platformTypes: SetProperty<KotlinPlatformType> = objects.setProperty(KotlinPlatformType::class.java).apply {
set(KotlinPlatformType.values().toMutableSet())
}
val dependencies = ComposePlugin.Dependencies(project) val dependencies = ComposePlugin.Dependencies(project)
} }

11
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/MacOSNotarizationSettings.kt

@ -9,6 +9,7 @@ import org.gradle.api.model.ObjectFactory
import org.gradle.api.provider.Property import org.gradle.api.provider.Property
import org.gradle.api.provider.ProviderFactory import org.gradle.api.provider.ProviderFactory
import org.gradle.api.tasks.Input import org.gradle.api.tasks.Input
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.Optional import org.gradle.api.tasks.Optional
import org.jetbrains.compose.desktop.application.internal.ComposeProperties import org.jetbrains.compose.desktop.application.internal.ComposeProperties
import org.jetbrains.compose.internal.utils.nullableProperty import org.jetbrains.compose.internal.utils.nullableProperty
@ -35,7 +36,15 @@ abstract class MacOSNotarizationSettings {
@get:Input @get:Input
@get:Optional @get:Optional
val teamID: Property<String?> = objects.nullableProperty<String>().apply {
set(ComposeProperties.macNotarizationTeamID(providers))
}
@Deprecated("This option is no longer supported and got replaced by teamID", level = DeprecationLevel.ERROR)
@get:Internal
val ascProvider: Property<String?> = objects.nullableProperty<String>().apply { val ascProvider: Property<String?> = objects.nullableProperty<String>().apply {
set(ComposeProperties.macNotarizationAscProvider(providers)) set(providers.provider {
throw UnsupportedOperationException("This option is not supported by notary tool and was replaced by teamID")
})
} }
} }

6
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/ComposeProjectProperties.kt

@ -19,7 +19,7 @@ internal object ComposeProperties {
internal const val MAC_SIGN_PREFIX = "compose.desktop.mac.signing.prefix" internal const val MAC_SIGN_PREFIX = "compose.desktop.mac.signing.prefix"
internal const val MAC_NOTARIZATION_APPLE_ID = "compose.desktop.mac.notarization.appleID" internal const val MAC_NOTARIZATION_APPLE_ID = "compose.desktop.mac.notarization.appleID"
internal const val MAC_NOTARIZATION_PASSWORD = "compose.desktop.mac.notarization.password" internal const val MAC_NOTARIZATION_PASSWORD = "compose.desktop.mac.notarization.password"
internal const val MAC_NOTARIZATION_ASC_PROVIDER = "compose.desktop.mac.notarization.ascProvider" internal const val MAC_NOTARIZATION_TEAM_ID_PROVIDER = "compose.desktop.mac.notarization.teamID"
internal const val CHECK_JDK_VENDOR = "compose.desktop.packaging.checkJdkVendor" internal const val CHECK_JDK_VENDOR = "compose.desktop.packaging.checkJdkVendor"
fun isVerbose(providers: ProviderFactory): Provider<Boolean> = fun isVerbose(providers: ProviderFactory): Provider<Boolean> =
@ -46,8 +46,8 @@ internal object ComposeProperties {
fun macNotarizationPassword(providers: ProviderFactory): Provider<String?> = fun macNotarizationPassword(providers: ProviderFactory): Provider<String?> =
providers.valueOrNull(MAC_NOTARIZATION_PASSWORD) providers.valueOrNull(MAC_NOTARIZATION_PASSWORD)
fun macNotarizationAscProvider(providers: ProviderFactory): Provider<String?> = fun macNotarizationTeamID(providers: ProviderFactory): Provider<String?> =
providers.valueOrNull(MAC_NOTARIZATION_ASC_PROVIDER) providers.valueOrNull(MAC_NOTARIZATION_TEAM_ID_PROVIDER)
fun checkJdkVendor(providers: ProviderFactory): Provider<Boolean> = fun checkJdkVendor(providers: ProviderFactory): Provider<Boolean> =
providers.valueOrNull(CHECK_JDK_VENDOR).toBooleanProvider(true) providers.valueOrNull(CHECK_JDK_VENDOR).toBooleanProvider(true)

8
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/ExternalToolRunner.kt

@ -11,6 +11,7 @@ import org.gradle.api.provider.Provider
import org.gradle.process.ExecOperations import org.gradle.process.ExecOperations
import org.gradle.process.ExecResult import org.gradle.process.ExecResult
import org.jetbrains.compose.internal.utils.ioFile import org.jetbrains.compose.internal.utils.ioFile
import java.io.ByteArrayInputStream
import java.io.File import java.io.File
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
@ -33,7 +34,8 @@ internal class ExternalToolRunner(
workingDir: File? = null, workingDir: File? = null,
checkExitCodeIsNormal: Boolean = true, checkExitCodeIsNormal: Boolean = true,
processStdout: Function1<String, Unit>? = null, processStdout: Function1<String, Unit>? = null,
logToConsole: LogToConsole = LogToConsole.OnlyWhenVerbose logToConsole: LogToConsole = LogToConsole.OnlyWhenVerbose,
stdinStr: String? = null
): ExecResult { ): ExecResult {
val logsDir = logsDir.ioFile val logsDir = logsDir.ioFile
logsDir.mkdirs() logsDir.mkdirs()
@ -52,6 +54,10 @@ internal class ExternalToolRunner(
// check exit value later // check exit value later
spec.isIgnoreExitValue = true spec.isIgnoreExitValue = true
if (stdinStr != null) {
spec.standardInput = ByteArrayInputStream(stdinStr.toByteArray())
}
@Suppress("NAME_SHADOWING") @Suppress("NAME_SHADOWING")
val logToConsole = when (logToConsole) { val logToConsole = when (logToConsole) {
LogToConsole.Always -> true LogToConsole.Always -> true

13
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt

@ -186,23 +186,13 @@ private fun JvmApplicationContext.configurePackagingTasks(
"Unexpected target format for MacOS: $targetFormat" "Unexpected target format for MacOS: $targetFormat"
} }
val notarizationRequestsDir = project.layout.buildDirectory.dir("compose/notarization/$app") tasks.register<AbstractNotarizationTask>(
tasks.register<AbstractUploadAppForNotarizationTask>(
taskNameAction = "notarize", taskNameAction = "notarize",
taskNameObject = targetFormat.name, taskNameObject = targetFormat.name,
args = listOf(targetFormat) args = listOf(targetFormat)
) { ) {
dependsOn(packageFormat) dependsOn(packageFormat)
inputDir.set(packageFormat.flatMap { it.destinationDir }) inputDir.set(packageFormat.flatMap { it.destinationDir })
requestsDir.set(notarizationRequestsDir)
configureCommonNotarizationSettings(this)
}
tasks.register<AbstractCheckNotarizationStatusTask>(
taskNameAction = "check",
taskNameObject = "notarizationStatus"
) {
requestDir.set(notarizationRequestsDir)
configureCommonNotarizationSettings(this) configureCommonNotarizationSettings(this)
} }
} }
@ -351,7 +341,6 @@ private fun JvmApplicationContext.configurePackageTask(
internal fun JvmApplicationContext.configureCommonNotarizationSettings( internal fun JvmApplicationContext.configureCommonNotarizationSettings(
notarizationTask: AbstractNotarizationTask notarizationTask: AbstractNotarizationTask
) { ) {
notarizationTask.nonValidatedBundleID.set(app.nativeDistributions.macOS.bundleID)
notarizationTask.nonValidatedNotarizationSettings = app.nativeDistributions.macOS.notarization notarizationTask.nonValidatedNotarizationSettings = app.nativeDistributions.macOS.notarization
} }

20
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/validation/ValidatedMacOSNotarizationSettings.kt

@ -5,36 +5,33 @@
package org.jetbrains.compose.desktop.application.internal.validation package org.jetbrains.compose.desktop.application.internal.validation
import org.gradle.api.provider.Provider
import org.jetbrains.compose.desktop.application.dsl.MacOSNotarizationSettings import org.jetbrains.compose.desktop.application.dsl.MacOSNotarizationSettings
import org.jetbrains.compose.desktop.application.internal.ComposeProperties import org.jetbrains.compose.desktop.application.internal.ComposeProperties
internal data class ValidatedMacOSNotarizationSettings( internal data class ValidatedMacOSNotarizationSettings(
val bundleID: String,
val appleID: String, val appleID: String,
val password: String, val password: String,
val ascProvider: String? val teamID: String
) )
internal fun MacOSNotarizationSettings?.validate( internal fun MacOSNotarizationSettings?.validate(): ValidatedMacOSNotarizationSettings {
bundleIDProvider: Provider<String?>
): ValidatedMacOSNotarizationSettings {
checkNotNull(this) { checkNotNull(this) {
ERR_NOTARIZATION_SETTINGS_ARE_NOT_PROVIDED ERR_NOTARIZATION_SETTINGS_ARE_NOT_PROVIDED
} }
val bundleID = validateBundleID(bundleIDProvider)
check(!appleID.orNull.isNullOrEmpty()) { check(!appleID.orNull.isNullOrEmpty()) {
ERR_APPLE_ID_IS_EMPTY ERR_APPLE_ID_IS_EMPTY
} }
check(!password.orNull.isNullOrEmpty()) { check(!password.orNull.isNullOrEmpty()) {
ERR_PASSWORD_IS_EMPTY ERR_PASSWORD_IS_EMPTY
} }
check(!teamID.orNull.isNullOrEmpty()) {
TEAM_ID_IS_EMPTY
}
return ValidatedMacOSNotarizationSettings( return ValidatedMacOSNotarizationSettings(
bundleID = bundleID,
appleID = appleID.orNull!!, appleID = appleID.orNull!!,
password = password.orNull!!, password = password.orNull!!,
ascProvider = ascProvider.orNull teamID = teamID.orNull!!
) )
} }
@ -51,3 +48,8 @@ private val ERR_PASSWORD_IS_EMPTY =
| * Use '${ComposeProperties.MAC_NOTARIZATION_PASSWORD}' Gradle property; | * Use '${ComposeProperties.MAC_NOTARIZATION_PASSWORD}' Gradle property;
| * Or use 'nativeDistributions.macOS.notarization.password' DSL property; | * Or use 'nativeDistributions.macOS.notarization.password' DSL property;
""".trimMargin() """.trimMargin()
private val TEAM_ID_IS_EMPTY =
"""|$ERR_PREFIX teamID is null or empty. To specify:
| * Use '${ComposeProperties.MAC_NOTARIZATION_TEAM_ID_PROVIDER}' Gradle property;
| * Or use 'nativeDistributions.macOS.notarization.teamID' DSL property;
""".trimMargin()

60
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCheckNotarizationStatusTask.kt

@ -1,60 +0,0 @@
/*
* Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package org.jetbrains.compose.desktop.application.tasks
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.tasks.*
import org.jetbrains.compose.desktop.application.internal.ExternalToolRunner
import org.jetbrains.compose.internal.utils.MacUtils
import org.jetbrains.compose.desktop.application.internal.NOTARIZATION_REQUEST_INFO_FILE_NAME
import org.jetbrains.compose.desktop.application.internal.NotarizationRequestInfo
import org.jetbrains.compose.internal.utils.ioFile
abstract class AbstractCheckNotarizationStatusTask : AbstractNotarizationTask() {
@get:Internal
val requestDir: DirectoryProperty = objects.directoryProperty()
@TaskAction
fun run() {
val notarization = validateNotarization()
val requests = HashSet<NotarizationRequestInfo>()
for (file in requestDir.ioFile.walk()) {
if (file.isFile && file.name == NOTARIZATION_REQUEST_INFO_FILE_NAME) {
try {
val status = NotarizationRequestInfo()
status.loadFrom(file)
requests.add(status)
} catch (e: Exception) {
logger.error("Invalid notarization request status file: $file", e)
}
}
}
if (requests.isEmpty()) {
logger.quiet("No existing notarization requests")
return
}
for (request in requests.sortedBy { it.uploadTime }) {
try {
logger.quiet("Checking status of notarization request '${request.uuid}'")
runExternalTool(
tool = MacUtils.xcrun,
args = listOf(
"altool",
"--notarization-info", request.uuid,
"--username", notarization.appleID,
"--password", notarization.password
),
logToConsole = ExternalToolRunner.LogToConsole.Always
)
} catch (e: Exception) {
logger.error("Could not check notarization request '${request.uuid}'", e)
}
}
}
}

63
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractNotarizationTask.kt

@ -5,24 +5,67 @@
package org.jetbrains.compose.desktop.application.tasks package org.jetbrains.compose.desktop.application.tasks
import org.gradle.api.provider.Property import org.gradle.api.file.DirectoryProperty
import org.gradle.api.tasks.Input import org.gradle.api.tasks.*
import org.gradle.api.tasks.Nested
import org.gradle.api.tasks.Optional
import org.jetbrains.compose.desktop.application.dsl.MacOSNotarizationSettings import org.jetbrains.compose.desktop.application.dsl.MacOSNotarizationSettings
import org.jetbrains.compose.internal.utils.nullableProperty import org.jetbrains.compose.desktop.application.dsl.TargetFormat
import org.jetbrains.compose.desktop.application.internal.files.checkExistingFile
import org.jetbrains.compose.desktop.application.internal.files.findOutputFileOrDir
import org.jetbrains.compose.desktop.application.internal.validation.ValidatedMacOSNotarizationSettings
import org.jetbrains.compose.desktop.application.internal.validation.validate import org.jetbrains.compose.desktop.application.internal.validation.validate
import org.jetbrains.compose.desktop.tasks.AbstractComposeDesktopTask import org.jetbrains.compose.desktop.tasks.AbstractComposeDesktopTask
import org.jetbrains.compose.internal.utils.MacUtils
import org.jetbrains.compose.internal.utils.ioFile
import java.io.File
import javax.inject.Inject
abstract class AbstractNotarizationTask : AbstractComposeDesktopTask() { abstract class AbstractNotarizationTask @Inject constructor(
@get:Input @get:Input
@get:Optional val targetFormat: TargetFormat
internal val nonValidatedBundleID: Property<String?> = objects.nullableProperty() ) : AbstractComposeDesktopTask() {
@get:Nested @get:Nested
@get:Optional @get:Optional
internal var nonValidatedNotarizationSettings: MacOSNotarizationSettings? = null internal var nonValidatedNotarizationSettings: MacOSNotarizationSettings? = null
internal fun validateNotarization() = @get:InputDirectory
nonValidatedNotarizationSettings.validate(nonValidatedBundleID) val inputDir: DirectoryProperty = objects.directoryProperty()
init {
check(targetFormat != TargetFormat.AppImage) { "${TargetFormat.AppImage} cannot be notarized!" }
}
@TaskAction
fun run() {
val notarization = nonValidatedNotarizationSettings.validate()
val packageFile = findOutputFileOrDir(inputDir.ioFile, targetFormat).checkExistingFile()
notarize(notarization, packageFile)
staple(packageFile)
}
private fun notarize(
notarization: ValidatedMacOSNotarizationSettings,
packageFile: File
) {
logger.info("Uploading '${packageFile.name}' for notarization")
val args = listOfNotNull(
"notarytool",
"submit",
"--wait",
"--apple-id",
notarization.appleID,
"--team-id",
notarization.teamID,
packageFile.absolutePath
)
runExternalTool(tool = MacUtils.xcrun, args = args, stdinStr = notarization.password)
}
private fun staple(packageFile: File) {
runExternalTool(
tool = MacUtils.xcrun,
args = listOf("stapler", "staple", packageFile.absolutePath)
)
}
} }

82
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractUploadAppForNotarizationTask.kt

@ -1,82 +0,0 @@
/*
* Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package org.jetbrains.compose.desktop.application.tasks
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.tasks.*
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
import org.jetbrains.compose.desktop.application.internal.*
import org.jetbrains.compose.desktop.application.internal.files.checkExistingFile
import org.jetbrains.compose.desktop.application.internal.files.findOutputFileOrDir
import org.jetbrains.compose.internal.utils.MacUtils
import org.jetbrains.compose.internal.utils.ioFile
import java.io.File
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import javax.inject.Inject
abstract class AbstractUploadAppForNotarizationTask @Inject constructor(
@get:Input
val targetFormat: TargetFormat
) : AbstractNotarizationTask() {
@get:InputDirectory
val inputDir: DirectoryProperty = objects.directoryProperty()
@get:Internal
val requestsDir: DirectoryProperty = objects.directoryProperty()
init {
check(targetFormat != TargetFormat.AppImage) { "${TargetFormat.AppImage} cannot be notarized!" }
}
@TaskAction
fun run() {
val notarization = validateNotarization()
val packageFile = findOutputFileOrDir(inputDir.ioFile, targetFormat).checkExistingFile()
logger.quiet("Uploading '${packageFile.name}' for notarization (package id: '${notarization.bundleID}')")
val args = arrayListOf(
"altool",
"--notarize-app",
"--primary-bundle-id", notarization.bundleID,
"--username", notarization.appleID,
"--password", notarization.password,
"--file", packageFile.absolutePath
)
if (notarization.ascProvider != null) {
args.add("--asc-provider")
args.add(notarization.ascProvider)
}
runExternalTool(
tool = MacUtils.xcrun,
args = args,
processStdout = { output ->
processUploadToolOutput(packageFile, output)
}
)
}
private fun processUploadToolOutput(packageFile: File, output: String) {
val m = "RequestUUID = ([A-Za-z0-9\\-]+)".toRegex().find(output)
?: error("Could not determine RequestUUID from output: $output")
val requestId = m.groupValues[1]
val uploadTime = LocalDateTime.now()
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm-ss"))
val requestDir = requestsDir.ioFile.resolve("$uploadTime-${targetFormat.id}")
val packageCopy = requestDir.resolve(packageFile.name)
packageFile.copyTo(packageCopy)
val requestInfo = NotarizationRequestInfo(uuid = requestId, uploadTime = uploadTime)
val requestInfoFile = requestDir.resolve(NOTARIZATION_REQUEST_INFO_FILE_NAME)
requestInfo.saveTo(requestInfoFile)
logger.quiet("Request UUID: $requestId")
logger.quiet("Request UUID is saved to ${requestInfoFile.absolutePath}")
logger.quiet("Uploaded file is saved to ${packageCopy.absolutePath}")
}
}

5
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/internal/configureExperimental.kt

@ -25,8 +25,7 @@ internal fun Project.configureExperimental(
if (experimentalExt.web._isApplicationInitialized) { if (experimentalExt.web._isApplicationInitialized) {
val webExt = composeExt.extensions.getByType(WebExtension::class.java) val webExt = composeExt.extensions.getByType(WebExtension::class.java)
for (target in webExt.targetsToConfigure(project)) { webExt.targetsToConfigure(project)
target.configureExperimentalWebApplication(experimentalExt.web.application) .configureExperimentalWebApplication(project, experimentalExt.web.application)
}
} }
} }

39
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/web/internal/configureExperimentalWebApplication.kt

@ -18,25 +18,30 @@ import org.jetbrains.compose.internal.utils.registerTask
import org.jetbrains.compose.internal.utils.uppercaseFirstChar import org.jetbrains.compose.internal.utils.uppercaseFirstChar
import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinJsIrTarget import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinJsIrTarget
internal fun KotlinJsIrTarget.configureExperimentalWebApplication(app: ExperimentalWebApplication) { internal fun Collection<KotlinJsIrTarget>.configureExperimentalWebApplication(
val mainCompilation = compilations.getByName("main") project: Project,
val unpackedRuntimeDir = project.layout.buildDirectory.dir("compose/skiko-wasm/$targetName") app: ExperimentalWebApplication
val taskName = "unpackSkikoWasmRuntime${targetName.uppercaseFirstChar()}" ) {
mainCompilation.defaultSourceSet.resources.srcDir(unpackedRuntimeDir) val skikoJsWasmRuntimeConfiguration = project.configurations.create("COMPOSE_SKIKO_JS_WASM_RUNTIME")
val skikoJsWasmRuntimeDependency = skikoVersionProvider(project).map { skikoVersion ->
val skikoJsWasmRuntimeDependency = skikoVersionProvider(project) project.dependencies.create("org.jetbrains.skiko:skiko-js-wasm-runtime:$skikoVersion")
.map { skikoVersion ->
project.dependencies.create("org.jetbrains.skiko:skiko-js-wasm-runtime:$skikoVersion")
}
val skikoJsWasmRuntimeConfiguration = project.configurations.create("COMPOSE_SKIKO_JS_WASM_RUNTIME").defaultDependencies {
it.addLater(skikoJsWasmRuntimeDependency)
} }
val unpackRuntime = project.registerTask<ExperimentalUnpackSkikoWasmRuntimeTask>(taskName) { skikoJsWasmRuntimeConfiguration.defaultDependencies {
skikoRuntimeFiles = skikoJsWasmRuntimeConfiguration it.addLater(skikoJsWasmRuntimeDependency)
outputDir.set(unpackedRuntimeDir)
} }
project.tasks.named(mainCompilation.processResourcesTaskName).configure { processResourcesTask -> forEach {
processResourcesTask.dependsOn(unpackRuntime) val mainCompilation = it.compilations.getByName("main")
val unpackedRuntimeDir = project.layout.buildDirectory.dir("compose/skiko-wasm/${it.targetName}")
val taskName = "unpackSkikoWasmRuntime${it.targetName.uppercaseFirstChar()}"
mainCompilation.defaultSourceSet.resources.srcDir(unpackedRuntimeDir)
val unpackRuntime = project.registerTask<ExperimentalUnpackSkikoWasmRuntimeTask>(taskName) {
skikoRuntimeFiles = skikoJsWasmRuntimeConfiguration
outputDir.set(unpackedRuntimeDir)
}
project.tasks.named(mainCompilation.processResourcesTaskName).configure { processResourcesTask ->
processResourcesTask.dependsOn(unpackRuntime)
}
} }
} }

16
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/web/WebExtension.kt

@ -20,6 +20,10 @@ abstract class WebExtension : ExtensionAware {
// public api // public api
@Suppress("unused") @Suppress("unused")
@Deprecated(
"""By default, Compose is applied to all declared K/JS-IR targets.
If you need to not apply Compose for K/JS, please exclude `KotlinPlatformType.js` from `compose.platformTypes`"""
)
fun targets(vararg targets: KotlinTarget) { fun targets(vararg targets: KotlinTarget) {
check(requestedTargets == null) { check(requestedTargets == null) {
"compose.web.targets() was already set!" "compose.web.targets() was already set!"
@ -53,20 +57,12 @@ abstract class WebExtension : ExtensionAware {
if (mppExt != null) { if (mppExt != null) {
val mppTargets = mppExt.targets.asMap.values val mppTargets = mppExt.targets.asMap.values
val jsIRTargets = mppTargets.filterIsInstanceTo(LinkedHashSet<KotlinJsIrTarget>()) val jsIRTargets = mppTargets.filterIsInstanceTo(LinkedHashSet<KotlinJsIrTarget>())
return jsIRTargets
return if (jsIRTargets.size > 1) {
project.logger.error(
"w: Default configuration for Compose for Web is disabled: " +
"multiple Kotlin JS IR targets are defined. " +
"Specify Compose for Web Kotlin targets by using `compose.web.targets()`"
)
emptySet()
} else jsIRTargets
} }
val jsExt = project.kotlinJsExtOrNull val jsExt = project.kotlinJsExtOrNull
if (jsExt != null) { if (jsExt != null) {
val target = jsExt.target val target = jsExt.js()
return if (target is KotlinJsIrTarget) { return if (target is KotlinJsIrTarget) {
setOf(target) setOf(target)
} else { } else {

8
html/svg/src/jsMain/kotlin/org/jetbrains/compose/web/svg/svgAttrs.kt

@ -41,6 +41,14 @@ fun AttrsScope<SVGElement>.fillRule(fill: String) {
attr("fill-rule", fill) attr("fill-rule", fill)
} }
fun AttrsScope<SVGElement>.fillOpacity(fill: Number) {
attr("fill-opacity", fill.toString())
}
fun AttrsScope<SVGElement>.fillOpacity(fill: CSSPercentageValue) {
attr("fill-opacity", fill.toString())
}
fun AttrsScope<SVGElement>.href(href: String) { fun AttrsScope<SVGElement>.href(href: String) {
attr("href", href) attr("href", href)
} }

79
tutorials/Signing_and_notarization_on_macOS/README.md

@ -221,37 +221,57 @@ The following Gradle properties can be used instead of DSL properties:
Those properties could be stored in `$HOME/.gradle/gradle.properties` to use across multiple applications. Those properties could be stored in `$HOME/.gradle/gradle.properties` to use across multiple applications.
### Configuring notarization settings ### Notarization
Notarization is only required for apps outside the App Store. Distributing your macOS application outside the App Store
requires notarization.
Notarization involves submitting your application to Apple for verification.
If your software passes the verification,
it's signed by Apple, stating that it has been notarized.
To notarize your app, you can use `notarize<PACKAGING_FORMAT>` task:
```
./gradlew notarizeDmg \
-Pcompose.desktop.mac.notarization.appleID=<APPLE_ID> \
-Pcompose.desktop.mac.notarization.password=<PASSWORD> \
-Pcompose.desktop.mac.notarization.teamID=<TEAM_ID>
```
where:
* `<APPLE_ID>` — your Apple ID;
* `<PASSWORD>` — the app-specific password created previously;
* `<TEAM_ID>` — your Team. To get a table of team IDs associated with a given username and password, run:
```
xcrun altool --list-providers -u <Apple ID> -p <Notarization password>"
```
<img alt="Team ID" src="notarization-team-id.png" />
``` kotlin
macOS {
notarization {
appleID.set("john.doe@example.com")
password.set("@keychain:NOTARIZATION_PASSWORD")
// optional The following tasks can be used for notarization:
ascProvider.set("<TEAM_ID>") * `notarizeDmg` — build, sign and notarize `.dmg` installer;
* `notarizeReleaseDmg` — same as `notarizeDmg`, but with [ProGuard](tutorials/Native_distributions_and_local_execution/README.md).
* `notarizePkg` — build, sign and notarize `.pkg` installer;
* `notarizeReleasePkg` — same as `notarizePkg`, but with [ProGuard](tutorials/Native_distributions_and_local_execution/README.md).
The notarization settings can also be set using the DSL.
For example, it is possible to pass credentials using environment variables:
```
compose.desktop.application {
nativeDistributions {
macOS {
notarization {
val providers = project.providers
appleID.set(providers.environmentVariable("NOTARIZATION_APPLE_ID"))
password.set(providers.environmentVariable("NOTARIZATION_PASSWORD"))
teamId.set(providers.environmentVariable("NOTARIZATION_TEAM_ID"))
}
}
} }
} }
``` ```
* Set `appleID` to your Apple ID. According to Apple, for 98 percent of software notarization completes within 15 minutes.
* Alternatively, the `compose.desktop.mac.notarization.appleID` Gradle property can be used. To learn more on how to avoid long response times, check [the official documentation](https://developer.apple.com/documentation/security/notarizing_macos_software_before_distribution/customizing_the_notarization_workflow#3561440).
* Set `password` to the app-specific password created previously.
* Alternatively, the `compose.desktop.mac.notarization.password` Gradle property can be used.
* Don't write raw password directly into a build script.
* If the password was added to the keychain, as described previously, it can be referenced as
```
@keychain:NOTARIZATION_PASSWORD
```
* Set `ascProvider` to your Team ID, if your account is associated with multiple teams.
* Alternatively, the `compose.desktop.mac.notarization.ascProvider` Gradle property can be used.
* To get a table of team IDs associated with a given username and password, run:
```
xcrun altool --list-providers -u <Apple ID> -p <Notarization password>"
```
### Configuring provisioning profile ### Configuring provisioning profile
@ -375,14 +395,3 @@ The following tasks are available:
(no separate step is required). (no separate step is required).
* Use `notarize<PACKAGING_FORMAT>` (e.g. `notarizeDmg`) to upload an application for notarization. * Use `notarize<PACKAGING_FORMAT>` (e.g. `notarizeDmg`) to upload an application for notarization.
Notarization is only required for apps outside the App Store. Notarization is only required for apps outside the App Store.
Once the upload finishes, a `RequestUUID` will be printed.
The notarization process takes some time.
Once the notarization process finishes, an email will be sent to you.
Uploaded file is saved to `<BUILD_DIR>/compose/notarization/main/<UPLOAD_DATE>-<PACKAGING_FORMAT>`
* Use `checkNotarizationStatus` to check a status of
last notarization requests. You can also use a command-line command to check any notarization request:
```
xcrun altool --notarization-info <RequestUUID>
--username <Apple_ID>
--password "@keychain:NOTARIZATION_PASSWORD"
```

BIN
tutorials/Signing_and_notarization_on_macOS/notarization-team-id.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Loading…
Cancel
Save