From 544f773f5afe840139db7a637773f9115a9a2874 Mon Sep 17 00:00:00 2001 From: Alexey Tsvetkov Date: Tue, 13 Sep 2022 16:02:55 +0200 Subject: [PATCH] Introduce ProGuard integration for Compose/Desktop packaging Resolves #1174 --- examples/imageviewer/desktop/rules.pro | 12 + .../org/jetbrains/compose/ComposePlugin.kt | 5 +- .../compose/desktop/DesktopExtension.kt | 3 +- .../desktop/application/dsl/JvmApplication.kt | 78 +-- .../dsl/JvmApplicationBuildTypes.kt | 46 ++ .../application/dsl/NativeApplication.kt | 2 - .../application/dsl/ProguardSettings.kt | 24 + .../internal/ConfigurationSource.kt | 44 -- .../internal/ExternalToolRunner.kt | 17 +- .../internal/JvmApplicationContext.kt | 76 +++ .../internal/JvmApplicationData.kt | 40 ++ .../internal/JvmApplicationInternal.kt | 72 +++ .../internal/JvmApplicationRuntimeFiles.kt | 74 +++ ...eProperties.kt => JvmRuntimeProperties.kt} | 10 +- .../desktop/application/internal/JvmTasks.kt | 52 ++ .../application/internal/cliArgUtils.kt | 8 +- .../application/internal/configureDesktop.kt | 16 +- .../internal/configureJvmApplication.kt | 608 +++++++++--------- .../application/internal/diagnosticUtils.kt | 2 +- .../application/internal/dirLayoutUtils.kt | 28 + .../desktop/application/internal/dslUtils.kt | 24 - .../files/MacJarSignFileCopyingProcessor.kt | 21 +- .../application/internal/files/fileUtils.kt | 76 ++- .../application/internal/gradleUtils.kt | 37 +- .../desktop/application/internal/osUtils.kt | 27 +- .../application/internal/packageVersions.kt | 10 +- .../validation/validatePackageVersions.kt | 9 +- .../application/internal/wixToolset.kt | 10 +- .../AbstractCheckNativeDistributionRuntime.kt | 9 +- .../AbstractCheckNotarizationStatusTask.kt | 7 +- .../application/tasks/AbstractJLinkTask.kt | 6 +- .../application/tasks/AbstractJPackageTask.kt | 50 +- .../tasks/AbstractJvmToolOperationTask.kt | 2 +- ...ctNativeMacApplicationPackageAppDirTask.kt | 3 +- ...AbstractNativeMacApplicationPackageTask.kt | 1 - .../application/tasks/AbstractProguardTask.kt | 150 +++++ .../tasks/AbstractSuggestModulesTask.kt | 9 +- .../AbstractUploadAppForNotarizationTask.kt | 2 + .../preview/internal/configurePreview.kt | 26 +- ...kDefaultComposeApplicationResourcesTask.kt | 4 + .../uikit/internal/registerSimulatorTasks.kt | 2 - .../jetbrains/compose/internal/stringUtils.kt | 5 + .../default-compose-desktop-rules.pro | 33 + .../org/jetbrains/compose/FileHashTest.kt | 6 +- .../compose/gradle/DesktopApplicationTest.kt | 15 + .../jetbrains/compose/test/TestProjects.kt | 1 + .../org/jetbrains/compose/test/assertUtils.kt | 16 + .../application/proguard/build.gradle | 30 + .../proguard/main-image.expected.png | Bin 0 -> 140 bytes .../proguard/main-methods.expected.txt | 3 + .../application/proguard/rules.pro | 7 + .../application/proguard/settings.gradle | 11 + .../proguard/src/main/kotlin/Main.kt | 76 +++ gradle-plugins/gradle.properties | 4 +- 54 files changed, 1332 insertions(+), 577 deletions(-) create mode 100644 examples/imageviewer/desktop/rules.pro create mode 100644 gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/JvmApplicationBuildTypes.kt create mode 100644 gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/ProguardSettings.kt delete mode 100644 gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/ConfigurationSource.kt create mode 100644 gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/JvmApplicationContext.kt create mode 100644 gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/JvmApplicationData.kt create mode 100644 gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/JvmApplicationInternal.kt create mode 100644 gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/JvmApplicationRuntimeFiles.kt rename gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/{JavaRuntimeProperties.kt => JvmRuntimeProperties.kt} (73%) create mode 100644 gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/JvmTasks.kt create mode 100644 gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/dirLayoutUtils.kt delete mode 100644 gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/dslUtils.kt create mode 100644 gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractProguardTask.kt create mode 100644 gradle-plugins/compose/src/main/resources/default-compose-desktop-rules.pro create mode 100644 gradle-plugins/compose/src/test/test-projects/application/proguard/build.gradle create mode 100644 gradle-plugins/compose/src/test/test-projects/application/proguard/main-image.expected.png create mode 100644 gradle-plugins/compose/src/test/test-projects/application/proguard/main-methods.expected.txt create mode 100644 gradle-plugins/compose/src/test/test-projects/application/proguard/rules.pro create mode 100644 gradle-plugins/compose/src/test/test-projects/application/proguard/settings.gradle create mode 100644 gradle-plugins/compose/src/test/test-projects/application/proguard/src/main/kotlin/Main.kt diff --git a/examples/imageviewer/desktop/rules.pro b/examples/imageviewer/desktop/rules.pro new file mode 100644 index 00000000000..a01578e9999 --- /dev/null +++ b/examples/imageviewer/desktop/rules.pro @@ -0,0 +1,12 @@ +# Ktor +-keep class io.ktor.** { *; } +-keepclassmembers class io.ktor.** { volatile ; } +-keep class io.ktor.client.engine.cio.** { *; } +-keep class kotlinx.coroutines.** { *; } +-dontwarn kotlinx.atomicfu.** +-dontwarn io.netty.** +-dontwarn com.typesafe.** +-dontwarn org.slf4j.** + +# Obfuscation breaks coroutines/ktor for some reason +-dontobfuscate diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposePlugin.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposePlugin.kt index 82dc3171520..7ef5012223c 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposePlugin.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposePlugin.kt @@ -26,9 +26,6 @@ import org.jetbrains.compose.desktop.preview.internal.initializePreview import org.jetbrains.compose.experimental.dsl.ExperimentalExtension import org.jetbrains.compose.experimental.internal.checkExperimentalTargetsWithSkikoIsEnabled import org.jetbrains.compose.experimental.internal.configureExperimental -import org.jetbrains.compose.internal.COMPOSE_PLUGIN_ID -import org.jetbrains.compose.internal.KOTLIN_JS_PLUGIN_ID -import org.jetbrains.compose.internal.KOTLIN_MPP_PLUGIN_ID import org.jetbrains.compose.web.WebExtension import org.jetbrains.kotlin.gradle.plugin.KotlinDependencyHandler import org.jetbrains.kotlin.gradle.tasks.KotlinCompile @@ -46,7 +43,7 @@ class ComposePlugin : Plugin { setUpGroovyDslExtensions(project) } - project.initializePreview() + project.initializePreview(desktopExtension) composeExtension.extensions.create("web", WebExtension::class.java) project.plugins.apply(ComposeCompilerKotlinSupportPlugin::class.java) diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/DesktopExtension.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/DesktopExtension.kt index 4e4d6169d7f..a7a3234e27e 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/DesktopExtension.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/DesktopExtension.kt @@ -9,6 +9,7 @@ import org.gradle.api.Action import org.gradle.api.model.ObjectFactory import org.gradle.api.plugins.ExtensionAware import org.jetbrains.compose.desktop.application.dsl.JvmApplication +import org.jetbrains.compose.desktop.application.internal.JvmApplicationInternal import org.jetbrains.compose.desktop.application.dsl.NativeApplication import javax.inject.Inject @@ -17,7 +18,7 @@ abstract class DesktopExtension @Inject constructor(private val objectFactory: O private set val application: JvmApplication by lazy { _isJvmApplicationInitialized = true - objectFactory.newInstance(JvmApplication::class.java, "main") + objectFactory.newInstance(JvmApplicationInternal::class.java, "main") } fun application(fn: Action) { fn.execute(application) diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/JvmApplication.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/JvmApplication.kt index fae1d7114a7..7769f3567e6 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/JvmApplication.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/JvmApplication.kt @@ -8,65 +8,27 @@ package org.jetbrains.compose.desktop.application.dsl import org.gradle.api.Action import org.gradle.api.Task import org.gradle.api.file.RegularFileProperty -import org.gradle.api.model.ObjectFactory import org.gradle.api.tasks.SourceSet -import org.jetbrains.compose.desktop.application.internal.ConfigurationSource import org.jetbrains.kotlin.gradle.plugin.KotlinTarget -import org.jetbrains.kotlin.gradle.targets.jvm.KotlinJvmTarget -import java.util.* -import javax.inject.Inject -open class JvmApplication @Inject constructor( - @Suppress("unused") - val name: String, - objects: ObjectFactory -) { - internal var _configurationSource: ConfigurationSource? = null - private set - internal var _isDefaultConfigurationEnabled = true - private set - internal val _fromFiles = objects.fileCollection() - internal val _dependenciesTaskNames = ArrayList() - - fun from(from: SourceSet) { - _configurationSource = ConfigurationSource.GradleSourceSet(from) - } - fun from(from: KotlinTarget) { - check(from is KotlinJvmTarget) { "Non JVM Kotlin MPP targets are not supported: ${from.javaClass.canonicalName} " + - "is not subtype of ${KotlinJvmTarget::class.java.canonicalName}" } - _configurationSource = ConfigurationSource.KotlinMppTarget(from) - } - fun disableDefaultConfiguration() { - _isDefaultConfigurationEnabled = false - } - - fun fromFiles(vararg files: Any) { - _fromFiles.from(*files) - } - - fun dependsOn(vararg tasks: String) { - _dependenciesTaskNames.addAll(tasks) - } - fun dependsOn(vararg tasks: Task) { - tasks.mapTo(_dependenciesTaskNames) { it.path } - } - - var mainClass: String? = null - val mainJar: RegularFileProperty = objects.fileProperty() - var javaHome: String? = null - - val args: MutableList = ArrayList() - fun args(vararg args: String) { - this.args.addAll(args) - } - - val jvmArgs: MutableList = ArrayList() - fun jvmArgs(vararg jvmArgs: String) { - this.jvmArgs.addAll(jvmArgs) - } - - val nativeDistributions: JvmApplicationDistributions = objects.newInstance(JvmApplicationDistributions::class.java) - fun nativeDistributions(fn: Action) { - fn.execute(nativeDistributions) - } +abstract class JvmApplication { + abstract fun from(from: SourceSet) + abstract fun from(from: KotlinTarget) + abstract fun disableDefaultConfiguration() + abstract fun dependsOn(vararg tasks: Task) + abstract fun dependsOn(vararg tasks: String) + abstract fun fromFiles(vararg files: Any) + + abstract var mainClass: String? + abstract val mainJar: RegularFileProperty + abstract var javaHome: String + abstract val args: MutableList + abstract fun args(vararg args: String) + abstract val jvmArgs: MutableList + abstract fun jvmArgs(vararg jvmArgs: String) + abstract val nativeDistributions: JvmApplicationDistributions + abstract fun nativeDistributions(fn: Action) + abstract val buildTypes: JvmApplicationBuildTypes + abstract fun buildTypes(fn: Action) } + diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/JvmApplicationBuildTypes.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/JvmApplicationBuildTypes.kt new file mode 100644 index 00000000000..92aafbabf1e --- /dev/null +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/JvmApplicationBuildTypes.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2020-2022 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.dsl + +import org.gradle.api.Action +import org.gradle.api.model.ObjectFactory +import org.jetbrains.compose.desktop.application.internal.new +import javax.inject.Inject + +abstract class JvmApplicationBuildTypes @Inject constructor( + objects: ObjectFactory +) { + /** + * The default build type does not have a classifier + * to preserve compatibility with tasks, existing before + * the introduction of the release build type, + * e.g. we don't want to break existing packageDmg, + * createDistributable tasks after the introduction + * of packageReleaseDmg and createReleaseDistributable tasks. + */ + internal val default: JvmApplicationBuildType = objects.new("") + + val release: JvmApplicationBuildType = objects.new("release").apply { + proguard.isEnabled.set(true) + } + fun release(fn: Action) { + fn.execute(release) + } +} + +abstract class JvmApplicationBuildType @Inject constructor( + /** + * A classifier distinguishes tasks and directories of one build type from another. + * E.g. `release` build type produces packageReleaseDmg task. + */ + internal val classifier: String, + objects: ObjectFactory, +) { + val proguard: ProguardSettings = objects.new() + fun proguard(fn: Action) { + fn.execute(proguard) + } +} diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/NativeApplication.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/NativeApplication.kt index ec66a9ac468..0fecc79bdc2 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/NativeApplication.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/NativeApplication.kt @@ -7,10 +7,8 @@ package org.jetbrains.compose.desktop.application.dsl import org.gradle.api.Action import org.gradle.api.model.ObjectFactory -import org.jetbrains.compose.desktop.application.internal.ConfigurationSource import org.jetbrains.kotlin.gradle.plugin.KotlinTarget import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget -import org.jetbrains.kotlin.gradle.targets.jvm.KotlinJvmTarget import org.jetbrains.kotlin.konan.target.Family import javax.inject.Inject diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/ProguardSettings.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/ProguardSettings.kt new file mode 100644 index 00000000000..685f23b6673 --- /dev/null +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/ProguardSettings.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2020-2022 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.dsl + +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.Property +import org.jetbrains.compose.desktop.application.internal.notNullProperty +import org.jetbrains.compose.desktop.application.internal.nullableProperty +import javax.inject.Inject + +private const val DEFAULT_PROGUARD_VERSION = "7.2.2" + +abstract class ProguardSettings @Inject constructor( + objects: ObjectFactory, +) { + val version: Property = objects.notNullProperty(DEFAULT_PROGUARD_VERSION) + val maxHeapSize: Property = objects.nullableProperty() + val configurationFiles: ConfigurableFileCollection = objects.fileCollection() + val isEnabled: Property = objects.notNullProperty(false) +} diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/ConfigurationSource.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/ConfigurationSource.kt deleted file mode 100644 index 7bb9765de96..00000000000 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/ConfigurationSource.kt +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2020-2022 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.internal - -import org.gradle.api.Project -import org.gradle.api.file.FileCollection -import org.gradle.api.tasks.SourceSet -import org.gradle.api.tasks.TaskProvider -import org.gradle.jvm.tasks.Jar -import org.jetbrains.kotlin.gradle.targets.jvm.KotlinJvmTarget - -internal sealed class ConfigurationSource { - abstract val jarTaskName: String - - abstract fun runtimeClasspath(project: Project): FileCollection - - fun jarTask(project: Project): TaskProvider = - project.tasks.named(jarTaskName, Jar::class.java) - - class GradleSourceSet(val sourceSet: SourceSet) : ConfigurationSource() { - override val jarTaskName: String - get() = sourceSet.jarTaskName - - override fun runtimeClasspath(project: Project): FileCollection = - project.objects.fileCollection().apply { - from(jarTask(project).flatMap { it.archiveFile }) - from(sourceSet.runtimeClasspath.filter { it.path.endsWith(".jar") }) - } - } - - class KotlinMppTarget(val target: KotlinJvmTarget) : ConfigurationSource() { - override val jarTaskName: String - get() = target.artifactsTaskName - - override fun runtimeClasspath(project: Project): FileCollection = - project.objects.fileCollection().apply { - from(jarTask(project).flatMap { it.archiveFile }) - from(target.compilations.getByName("main").runtimeDependencyFiles.filter { it.path.endsWith(".jar") }) - } - } -} \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/ExternalToolRunner.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/ExternalToolRunner.kt index 7f3e6a998fa..4138f487fc1 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/ExternalToolRunner.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/ExternalToolRunner.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. + * Copyright 2020-2022 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. */ @@ -19,6 +19,12 @@ internal class ExternalToolRunner( private val logsDir: Provider, private val execOperations: ExecOperations ) { + internal enum class LogToConsole { + Always, + Never, + OnlyWhenVerbose + } + operator fun invoke( tool: File, args: Collection, @@ -26,13 +32,12 @@ internal class ExternalToolRunner( workingDir: File? = null, checkExitCodeIsNormal: Boolean = true, processStdout: Function1? = null, - forceLogToFile: Boolean = false + logToConsole: LogToConsole = LogToConsole.OnlyWhenVerbose ): ExecResult { val logsDir = logsDir.ioFile logsDir.mkdirs() val toolName = tool.nameWithoutExtension - val logToConsole = verbose.get() && !forceLogToFile val outFile = logsDir.resolve("${toolName}-${currentTimeStamp()}-out.txt") val errFile = logsDir.resolve("${toolName}-${currentTimeStamp()}-err.txt") @@ -46,6 +51,12 @@ internal class ExternalToolRunner( // check exit value later spec.isIgnoreExitValue = true + @Suppress("NAME_SHADOWING") + val logToConsole = when (logToConsole) { + LogToConsole.Always -> true + LogToConsole.Never -> false + LogToConsole.OnlyWhenVerbose -> verbose.get() + } if (logToConsole) { spec.standardOutput = spec.standardOutput.alsoOutputTo(outFileStream) spec.errorOutput = spec.errorOutput.alsoOutputTo(errFileStream) diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/JvmApplicationContext.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/JvmApplicationContext.kt new file mode 100644 index 00000000000..2f51ceb46b0 --- /dev/null +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/JvmApplicationContext.kt @@ -0,0 +1,76 @@ +/* + * Copyright 2020-2022 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.internal + +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.api.file.Directory +import org.gradle.api.provider.Provider +import org.jetbrains.compose.desktop.application.dsl.JvmApplicationBuildType +import org.jetbrains.compose.internal.KOTLIN_JVM_PLUGIN_ID +import org.jetbrains.compose.internal.KOTLIN_MPP_PLUGIN_ID +import org.jetbrains.compose.internal.javaSourceSets +import org.jetbrains.compose.internal.joinDashLowercaseNonEmpty +import org.jetbrains.compose.internal.mppExt +import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType + +internal data class JvmApplicationContext( + val project: Project, + private val appInternal: JvmApplicationInternal, + val buildType: JvmApplicationBuildType, + private val taskGroup: String = composeDesktopTaskGroup +) { + val app: JvmApplicationData + get() = appInternal.data + + val appDirName: String + get() = joinDashLowercaseNonEmpty(appInternal.name, buildType.classifier) + + val appTmpDir: Provider + get() = project.layout.buildDirectory.dir( + "compose/tmp/$appDirName" + ) + + fun T.useAppRuntimeFiles(fn: T.(JvmApplicationRuntimeFiles) -> Unit) { + val runtimeFiles = app.jvmApplicationRuntimeFilesProvider?.jvmApplicationRuntimeFiles(project) + ?: JvmApplicationRuntimeFiles( + allRuntimeJars = app.fromFiles, + mainJar = app.mainJar, + taskDependencies = app.dependenciesTaskNames.toTypedArray() + ) + runtimeFiles.configureUsageBy(this, fn) + } + + val tasks = JvmTasks(project, buildType, taskGroup) + + val packageNameProvider: Provider + get() = project.provider { appInternal.nativeDistributions.packageName ?: project.name } + + inline fun provider(noinline fn: () -> T): Provider = + project.provider(fn) + + fun configureDefaultApp() { + if (project.plugins.hasPlugin(KOTLIN_MPP_PLUGIN_ID)) { + var isJvmTargetConfigured = false + project.mppExt.targets.all { target -> + if (target.platformType == KotlinPlatformType.jvm) { + if (!isJvmTargetConfigured) { + appInternal.from(target) + isJvmTargetConfigured = true + } else { + project.logger.error("w: Default configuration for Compose Desktop Application is disabled: " + + "multiple Kotlin JVM targets definitions are detected. " + + "Specify, which target to use by using `compose.desktop.application.from(kotlinMppTarget)`") + appInternal.disableDefaultConfiguration() + } + } + } + } else if (project.plugins.hasPlugin(KOTLIN_JVM_PLUGIN_ID)) { + val mainSourceSet = project.javaSourceSets.getByName("main") + appInternal.from(mainSourceSet) + } + } +} \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/JvmApplicationData.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/JvmApplicationData.kt new file mode 100644 index 00000000000..b444fcf21ab --- /dev/null +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/JvmApplicationData.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2020-2022 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.internal + +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.Provider +import org.gradle.api.provider.ProviderFactory +import org.jetbrains.compose.desktop.application.dsl.JvmApplicationDistributions +import org.jetbrains.compose.desktop.application.dsl.JvmApplicationBuildTypes +import javax.inject.Inject + +internal open class JvmApplicationData @Inject constructor( + objects: ObjectFactory, + private val providers: ProviderFactory +) { + var jvmApplicationRuntimeFilesProvider: JvmApplicationRuntimeFilesProvider? = null + var isDefaultConfigurationEnabled: Boolean = true + val fromFiles: ConfigurableFileCollection = objects.fileCollection() + val dependenciesTaskNames: MutableList = ArrayList() + var mainClass: String? = null + val mainJar: RegularFileProperty = objects.fileProperty() + + private var customJavaHome: String? = null + var javaHome: String + get() = customJavaHome ?: System.getProperty("java.home") ?: error("'java.home' system property is not set") + set(value) { + customJavaHome = value + } + val javaHomeProvider: Provider + get() = providers.provider { javaHome } + val args: MutableList = ArrayList() + val jvmArgs: MutableList = ArrayList() + val nativeDistributions: JvmApplicationDistributions = objects.new() + val buildTypes: JvmApplicationBuildTypes = objects.new() +} \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/JvmApplicationInternal.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/JvmApplicationInternal.kt new file mode 100644 index 00000000000..46cc2dbecce --- /dev/null +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/JvmApplicationInternal.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2020-2022 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.internal + +import org.gradle.api.Action +import org.gradle.api.Task +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.model.ObjectFactory +import org.gradle.api.tasks.SourceSet +import org.jetbrains.compose.desktop.application.dsl.JvmApplication +import org.jetbrains.compose.desktop.application.dsl.JvmApplicationDistributions +import org.jetbrains.compose.desktop.application.dsl.JvmApplicationBuildTypes +import org.jetbrains.kotlin.gradle.plugin.KotlinTarget +import org.jetbrains.kotlin.gradle.targets.jvm.KotlinJvmTarget +import javax.inject.Inject + +internal open class JvmApplicationInternal @Inject constructor( + val name: String, + objects: ObjectFactory +) : JvmApplication() { + internal val data: JvmApplicationData = objects.new() + + final override fun from(from: SourceSet) { + data.jvmApplicationRuntimeFilesProvider = JvmApplicationRuntimeFilesProvider.FromGradleSourceSet(from) + } + final override fun from(from: KotlinTarget) { + check(from is KotlinJvmTarget) { "Non JVM Kotlin MPP targets are not supported: ${from.javaClass.canonicalName} " + + "is not subtype of ${KotlinJvmTarget::class.java.canonicalName}" } + data.jvmApplicationRuntimeFilesProvider = JvmApplicationRuntimeFilesProvider.FromKotlinMppTarget(from) + } + final override fun disableDefaultConfiguration() { + data.isDefaultConfigurationEnabled = false + } + + final override fun fromFiles(vararg files: Any) { + data.fromFiles.from(*files) + } + + final override fun dependsOn(vararg tasks: String) { + data.dependenciesTaskNames.addAll(tasks) + } + final override fun dependsOn(vararg tasks: Task) { + tasks.mapTo(data.dependenciesTaskNames) { it.path } + } + + final override var mainClass: String? by data::mainClass + final override val mainJar: RegularFileProperty by data::mainJar + final override var javaHome: String by data::javaHome + + final override val args: MutableList by data::args + final override fun args(vararg args: String) { + data.args.addAll(args) + } + + final override val jvmArgs: MutableList by data::jvmArgs + final override fun jvmArgs(vararg jvmArgs: String) { + data.jvmArgs.addAll(jvmArgs) + } + + final override val nativeDistributions: JvmApplicationDistributions by data::nativeDistributions + final override fun nativeDistributions(fn: Action) { + fn.execute(data.nativeDistributions) + } + + final override val buildTypes: JvmApplicationBuildTypes by data::buildTypes + final override fun buildTypes(fn: Action) { + fn.execute(data.buildTypes) + } +} \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/JvmApplicationRuntimeFiles.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/JvmApplicationRuntimeFiles.kt new file mode 100644 index 00000000000..e0f2cc7f6ed --- /dev/null +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/JvmApplicationRuntimeFiles.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2020-2022 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.internal + +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.api.file.FileCollection +import org.gradle.api.file.RegularFile +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.SourceSet +import org.gradle.jvm.tasks.Jar +import org.jetbrains.kotlin.gradle.targets.jvm.KotlinJvmTarget + +internal class JvmApplicationRuntimeFiles( + val allRuntimeJars: FileCollection, + val mainJar: Provider, + private val taskDependencies: Array +) { + operator fun component1() = allRuntimeJars + operator fun component2() = mainJar + + fun configureUsageBy(task: T, fn: T.(JvmApplicationRuntimeFiles) -> Unit) { + task.dependsOn(taskDependencies) + task.fn(this) + } +} + +internal sealed class JvmApplicationRuntimeFilesProvider { + abstract fun jvmApplicationRuntimeFiles(project: Project): JvmApplicationRuntimeFiles + + abstract class GradleJvmApplicationRuntimeFilesProvider : JvmApplicationRuntimeFilesProvider() { + protected abstract val jarTaskName: String + protected abstract val runtimeFiles: FileCollection + + override fun jvmApplicationRuntimeFiles(project: Project): JvmApplicationRuntimeFiles { + val jarTask = project.tasks.named(jarTaskName, Jar::class.java) + val mainJar = jarTask.flatMap { it.archiveFile } + val runtimeJarFiles = project.objects.fileCollection().apply { + from(mainJar) + from(runtimeFiles.filter { it.path.endsWith(".jar") }) + } + return JvmApplicationRuntimeFiles(runtimeJarFiles, mainJar, arrayOf(jarTask)) + + } + } + + class FromGradleSourceSet(private val sourceSet: SourceSet) : GradleJvmApplicationRuntimeFilesProvider() { + override val jarTaskName: String + get() = sourceSet.jarTaskName + + override val runtimeFiles: FileCollection + get() = sourceSet.runtimeClasspath + } + + class FromKotlinMppTarget(private val target: KotlinJvmTarget) : GradleJvmApplicationRuntimeFilesProvider() { + override val jarTaskName: String + get() = target.artifactsTaskName + + override val runtimeFiles: FileCollection + get() = target.compilations.getByName("main").runtimeDependencyFiles + } + + class Custom( + private val runtimeJarFiles: FileCollection, + private val mainJar: Provider, + private val taskDependencies: Array + ) : JvmApplicationRuntimeFilesProvider() { + override fun jvmApplicationRuntimeFiles(project: Project): JvmApplicationRuntimeFiles = + JvmApplicationRuntimeFiles(runtimeJarFiles, mainJar, taskDependencies) + } +} \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/JavaRuntimeProperties.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/JvmRuntimeProperties.kt similarity index 73% rename from gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/JavaRuntimeProperties.kt rename to gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/JvmRuntimeProperties.kt index ddb02b6b7ce..3d57b3f001a 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/JavaRuntimeProperties.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/JvmRuntimeProperties.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. + * Copyright 2020-2022 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. */ @@ -10,7 +10,7 @@ import java.io.ObjectInputStream import java.io.ObjectOutputStream import java.io.Serializable -internal data class JavaRuntimeProperties( +internal data class JvmRuntimeProperties( val majorVersion: Int, val availableModules: List, ) : Serializable { @@ -18,7 +18,7 @@ internal data class JavaRuntimeProperties( @Suppress("unused") private val serialVersionUid: Long = 0 - fun writeToFile(properties: JavaRuntimeProperties, file: File) { + fun writeToFile(properties: JvmRuntimeProperties, file: File) { file.parentFile.mkdirs() file.delete() file.createNewFile() @@ -27,9 +27,9 @@ internal data class JavaRuntimeProperties( } } - fun readFromFile(file: File): JavaRuntimeProperties = + fun readFromFile(file: File): JvmRuntimeProperties = ObjectInputStream(file.inputStream().buffered()).use { ois -> - ois.readObject() as JavaRuntimeProperties + ois.readObject() as JvmRuntimeProperties } } } \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/JvmTasks.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/JvmTasks.kt new file mode 100644 index 00000000000..cc24c16cb9a --- /dev/null +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/JvmTasks.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2020-2022 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.internal + +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.api.tasks.TaskProvider +import org.jetbrains.compose.desktop.application.dsl.JvmApplicationBuildType +import org.jetbrains.compose.internal.uppercaseFirstChar + +internal class JvmTasks( + private val project: Project, + private val buildType: JvmApplicationBuildType, + private val taskGroup: String? = composeDesktopTaskGroup +) { + /** + * Registers new Compose/Desktop tasks. + * Naming pattern for tasks is: [taskNameAction][taskNameClassifier][taskNameObject] + * Where: + * [taskNameAction] -- name for a task's action (e.g. 'run' or 'package') + * taskNameDisambiguationClassifier -- optional name for an disambiguation classifier (e.g. 'release') + * [taskNameObject] -- name for an object of action (e.g. 'distributable' or 'dmg') + * Examples: 'runDistributable', 'runReleaseDistributable', 'packageDmg', 'packageReleaseDmg' + */ + inline fun register( + taskNameAction: String, + taskNameObject: String = "", + args: List = emptyList(), + noinline configureFn: T.() -> Unit = {} + ): TaskProvider { + val buildTypeClassifier = buildType.classifier.uppercaseFirstChar() + val objectClassifier = taskNameObject.uppercaseFirstChar() + val taskName = "$taskNameAction$buildTypeClassifier$objectClassifier" + return register(taskName, klass = T::class.java, args = args, configureFn = configureFn) + } + + fun register( + name: String, + klass: Class, + args: List, + configureFn: T.() -> Unit + ): TaskProvider = + project.tasks.register(name, klass, *args.toTypedArray()).apply { + configure { task -> + task.group = taskGroup + task.configureFn() + } + } +} \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/cliArgUtils.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/cliArgUtils.kt index 0a67b6306a2..3a98bfc6f12 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/cliArgUtils.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/cliArgUtils.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. + * Copyright 2020-2022 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. */ @@ -7,6 +7,7 @@ package org.jetbrains.compose.desktop.application.internal import org.gradle.api.file.FileSystemLocation import org.gradle.api.provider.Provider +import org.jetbrains.compose.desktop.application.internal.files.normalizedPath import java.io.File internal fun MutableCollection.cliArg( @@ -42,7 +43,4 @@ private fun defaultToString(): (T) -> String = else -> it.toString() } "\"$asString\"" - } - -internal fun File.normalizedPath() = - if (currentOS == OS.Windows) absolutePath.replace("\\", "\\\\") else absolutePath \ No newline at end of file + } \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureDesktop.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureDesktop.kt index 0581a51d065..869b0532287 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureDesktop.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureDesktop.kt @@ -11,17 +11,17 @@ import org.jetbrains.compose.desktop.tasks.AbstractUnpackDefaultComposeApplicati import org.jetbrains.compose.internal.registerTask internal fun configureDesktop(project: Project, desktopExtension: DesktopExtension) { - val unpackDefaultResources = lazy { - project.registerTask( - "unpackDefaultComposeDesktopApplicationResources" - ) {} - } - if (desktopExtension._isJvmApplicationInitialized) { - configureJvmApplication(project, desktopExtension.application, unpackDefaultResources.value) + val appInternal = desktopExtension.application as JvmApplicationInternal + val defaultBuildType = appInternal.data.buildTypes.default + val appData = JvmApplicationContext(project, appInternal, defaultBuildType) + appData.configureJvmApplication() } if (desktopExtension._isNativeApplicationInitialized) { - configureNativeApplication(project, desktopExtension.nativeApplication, unpackDefaultResources.value) + val unpackDefaultResources = project.registerTask( + "unpackDefaultComposeDesktopNativeApplicationResources" + ) {} + configureNativeApplication(project, desktopExtension.nativeApplication, unpackDefaultResources) } } \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt index cb5329c8b21..32f3d335096 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt @@ -1,355 +1,388 @@ /* - * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. + * Copyright 2020-2022 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.internal -import org.gradle.api.* -import org.gradle.api.file.Directory +import org.gradle.api.DefaultTask import org.gradle.api.file.DuplicatesStrategy import org.gradle.api.file.FileCollection import org.gradle.api.provider.Provider -import org.gradle.api.tasks.* +import org.gradle.api.tasks.JavaExec +import org.gradle.api.tasks.Sync +import org.gradle.api.tasks.TaskProvider import org.gradle.jvm.tasks.Jar -import org.jetbrains.compose.desktop.application.dsl.JvmApplication import org.jetbrains.compose.desktop.application.dsl.TargetFormat import org.jetbrains.compose.desktop.application.internal.validation.validatePackageVersions import org.jetbrains.compose.desktop.application.tasks.* import org.jetbrains.compose.desktop.tasks.AbstractUnpackDefaultComposeApplicationResourcesTask -import org.jetbrains.compose.internal.* -import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType +import org.jetbrains.compose.internal.joinDashLowercaseNonEmpty import java.io.File -import java.util.* private val defaultJvmArgs = listOf("-D$CONFIGURE_SWING_GLOBALS=true") +internal const val composeDesktopTaskGroup = "compose desktop" // todo: multiple launchers // todo: file associations // todo: use workers -internal fun configureJvmApplication( - project: Project, - app: JvmApplication, - unpackDefaultResources: TaskProvider -) { - if (app._isDefaultConfigurationEnabled) { - if (project.plugins.hasPlugin(KOTLIN_MPP_PLUGIN_ID)) { - project.configureFromMppPlugin(app) - } else if (project.plugins.hasPlugin(KOTLIN_JVM_PLUGIN_ID)) { - val mainSourceSet = project.javaSourceSets.getByName("main") - app.from(mainSourceSet) - } +internal fun JvmApplicationContext.configureJvmApplication() { + if (app.isDefaultConfigurationEnabled) { + configureDefaultApp() } - project.validatePackageVersions(app) - project.configurePackagingTasks(listOf(app), unpackDefaultResources) - project.configureWix() -} -internal fun Project.configureFromMppPlugin(mainApplication: JvmApplication) { - var isJvmTargetConfigured = false - mppExt.targets.all { target -> - if (target.platformType == KotlinPlatformType.jvm) { - if (!isJvmTargetConfigured) { - mainApplication.from(target) - isJvmTargetConfigured = true - } else { - logger.error("w: Default configuration for Compose Desktop Application is disabled: " + - "multiple Kotlin JVM targets definitions are detected. " + - "Specify, which target to use by using `compose.desktop.application.from(kotlinMppTarget)`") - mainApplication.disableDefaultConfiguration() - } - } + validatePackageVersions() + val commonTasks = configureCommonJvmDesktopTasks() + configurePackagingTasks(commonTasks) + copy(buildType = app.buildTypes.release).configurePackagingTasks(commonTasks) + if (currentOS == OS.Windows) { + configureWix() } } -internal fun Project.configurePackagingTasks( - apps: Collection, - unpackDefaultResources: TaskProvider -) { - for (app in apps) { - val checkRuntime = tasks.composeDesktopJvmTask( - taskName("checkRuntime", app) - ) { - javaHome.set(provider { app.javaHomeOrDefault() }) - javaRuntimePropertiesFile.set( - project.layout.buildDirectory.file("compose/tmp/${app.name}/runtime-properties/properties.bin") - ) - } +internal class CommonJvmDesktopTasks( + val unpackDefaultResources: TaskProvider, + val checkRuntime: TaskProvider, + val suggestRuntimeModules: TaskProvider, + val prepareAppResources: TaskProvider, + val createRuntimeImage: TaskProvider +) + +private fun JvmApplicationContext.configureCommonJvmDesktopTasks(): CommonJvmDesktopTasks { + val unpackDefaultResources = tasks.register( + taskNameAction = "unpack", + taskNameObject = "DefaultComposeDesktopJvmApplicationResources" + ) {} + + val checkRuntime = tasks.register( + taskNameAction = "check", + taskNameObject = "runtime" + ) { + javaHome.set(app.javaHomeProvider) + javaRuntimePropertiesFile.set(jvmTmpDirForTask().file("properties.bin")) + } - tasks.composeDesktopJvmTask(taskName("suggestRuntimeModules", app)) { - dependsOn(checkRuntime) - javaHome.set(provider { app.javaHomeOrDefault() }) - modules.set(provider { app.nativeDistributions.modules }) + val suggestRuntimeModules = tasks.register( + taskNameAction = "suggest", + taskNameObject = "runtimeModules" + ) { + dependsOn(checkRuntime) + javaHome.set(app.javaHomeProvider) + modules.set(provider { app.nativeDistributions.modules }) - app._configurationSource?.let { configSource -> - dependsOn(configSource.jarTaskName) - files.from(configSource.runtimeClasspath(project)) - launcherMainJar.set(app.mainJar.orElse(configSource.jarTask(project).flatMap { it.archiveFile })) - } + useAppRuntimeFiles { (jarFiles, mainJar) -> + files.from(jarFiles) + launcherMainJar.set(mainJar) } + } - val prepareAppResources = tasks.composeDesktopJvmTask( - taskName("prepareAppResources", app) - ) { - val appResourcesRootDir = app.nativeDistributions.appResourcesRootDir - if (appResourcesRootDir.isPresent) { - from(appResourcesRootDir.dir("common")) - from(appResourcesRootDir.dir(currentOS.id)) - from(appResourcesRootDir.dir(currentTarget.id)) - } - - val destDir = project.layout.buildDirectory.dir("compose/tmp/${app.name}/resources") - into(destDir) + val prepareAppResources = tasks.register( + taskNameAction = "prepare", + taskNameObject = "appResources" + ) { + val appResourcesRootDir = app.nativeDistributions.appResourcesRootDir + if (appResourcesRootDir.isPresent) { + from(appResourcesRootDir.dir("common")) + from(appResourcesRootDir.dir(currentOS.id)) + from(appResourcesRootDir.dir(currentTarget.id)) } + into(jvmTmpDirForTask()) + } - val createRuntimeImage = tasks.composeDesktopJvmTask( - taskName("createRuntimeImage", app) + val createRuntimeImage = tasks.register( + taskNameAction = "create", + taskNameObject = "runtimeImage" + ) { + dependsOn(checkRuntime) + javaHome.set(app.javaHomeProvider) + modules.set(provider { app.nativeDistributions.modules }) + includeAllModules.set(provider { app.nativeDistributions.includeAllModules }) + javaRuntimePropertiesFile.set(checkRuntime.flatMap { it.javaRuntimePropertiesFile }) + destinationDir.set(appTmpDir.dir("runtime")) + } + + return CommonJvmDesktopTasks( + unpackDefaultResources, + checkRuntime, + suggestRuntimeModules, + prepareAppResources, + createRuntimeImage + ) +} + +private fun JvmApplicationContext.configurePackagingTasks( + commonTasks: CommonJvmDesktopTasks +) { + val runProguard = if (buildType.proguard.isEnabled.orNull == true) { + tasks.register( + taskNameAction = "proguard", + taskNameObject = "Jars" ) { - dependsOn(checkRuntime) - javaHome.set(provider { app.javaHomeOrDefault() }) - modules.set(provider { app.nativeDistributions.modules }) - includeAllModules.set(provider { app.nativeDistributions.includeAllModules }) - javaRuntimePropertiesFile.set(checkRuntime.flatMap { it.javaRuntimePropertiesFile }) - destinationDir.set(project.layout.buildDirectory.dir("compose/tmp/${app.name}/runtime")) + configureProguardTask(this, commonTasks.unpackDefaultResources) } + } else null + + val createDistributable = tasks.register( + taskNameAction = "create", + taskNameObject = "distributable", + args = listOf(TargetFormat.AppImage) + ) { + configurePackageTask( + this, + createRuntimeImage = commonTasks.createRuntimeImage, + prepareAppResources = commonTasks.prepareAppResources, + checkRuntime = commonTasks.checkRuntime, + unpackDefaultResources = commonTasks.unpackDefaultResources, + runProguard = runProguard + ) + } - val createDistributable = tasks.composeDesktopJvmTask( - taskName("createDistributable", app), - args = listOf(TargetFormat.AppImage) + val packageFormats = app.nativeDistributions.targetFormats.map { targetFormat -> + val packageFormat = tasks.register( + taskNameAction = "package", + taskNameObject = targetFormat.name, + args = listOf(targetFormat) ) { - configurePackagingTask( - app, - createRuntimeImage = createRuntimeImage, - prepareAppResources = prepareAppResources, - checkRuntime = checkRuntime, - unpackDefaultResources = unpackDefaultResources - ) + // On Mac we want to patch bundled Info.plist file, + // so we create an app image, change its Info.plist, + // then create an installer based on the app image. + // We could create an installer the same way on other platforms, but + // in some cases there are failures with JDK 15. + // See [AbstractJPackageTask.patchInfoPlistIfNeeded] + if (currentOS != OS.MacOS) { + configurePackageTask( + this, + createRuntimeImage = commonTasks.createRuntimeImage, + prepareAppResources = commonTasks.prepareAppResources, + checkRuntime = commonTasks.checkRuntime, + unpackDefaultResources = commonTasks.unpackDefaultResources, + runProguard = runProguard + ) + } else { + configurePackageTask( + this, + createAppImage = createDistributable, + checkRuntime = commonTasks.checkRuntime, + unpackDefaultResources = commonTasks.unpackDefaultResources + ) + } } - val packageFormats = app.nativeDistributions.targetFormats.map { targetFormat -> - val packageFormat = tasks.composeDesktopJvmTask( - taskName("package", app, targetFormat.name), + if (targetFormat.isCompatibleWith(OS.MacOS)) { + check(targetFormat == TargetFormat.Dmg || targetFormat == TargetFormat.Pkg) { + "Unexpected target format for MacOS: $targetFormat" + } + + val notarizationRequestsDir = project.layout.buildDirectory.dir("compose/notarization/$app") + tasks.register( + taskNameAction = "notarize", + taskNameObject = targetFormat.name, args = listOf(targetFormat) ) { - // On Mac we want to patch bundled Info.plist file, - // so we create an app image, change its Info.plist, - // then create an installer based on the app image. - // We could create an installer the same way on other platforms, but - // in some cases there are failures with JDK 15. - // See [AbstractJPackageTask.patchInfoPlistIfNeeded] - if (currentOS != OS.MacOS) { - configurePackagingTask( - app, - createRuntimeImage = createRuntimeImage, - prepareAppResources = prepareAppResources, - checkRuntime = checkRuntime, - unpackDefaultResources = unpackDefaultResources - ) - } else { - configurePackagingTask( - app, - createAppImage = createDistributable, - checkRuntime = checkRuntime, - unpackDefaultResources = unpackDefaultResources - ) - } + dependsOn(packageFormat) + inputDir.set(packageFormat.flatMap { it.destinationDir }) + requestsDir.set(notarizationRequestsDir) + configureCommonNotarizationSettings(this) } - if (targetFormat.isCompatibleWith(OS.MacOS)) { - check(targetFormat == TargetFormat.Dmg || targetFormat == TargetFormat.Pkg) { - "Unexpected target format for MacOS: $targetFormat" - } - - val notarizationRequestsDir = project.layout.buildDirectory.dir("compose/notarization/${app.name}") - val upload = tasks.composeDesktopJvmTask( - taskName("notarize", app, targetFormat.name), - args = listOf(targetFormat) - ) { - configureUploadForNotarizationTask(app, packageFormat, notarizationRequestsDir) - } - - tasks.composeDesktopJvmTask( - taskName("checkNotarizationStatus", app) - ) { - configureCheckNotarizationStatusTask(app, notarizationRequestsDir) - } + tasks.register( + taskNameAction = "check", + taskNameObject = "notarizationStatus" + ) { + requestDir.set(notarizationRequestsDir) + configureCommonNotarizationSettings(this) } - - packageFormat } - val packageAll = tasks.composeDesktopJvmTask(taskName("package", app)) { - dependsOn(packageFormats) - } + packageFormat + } - val packageUberJarForCurrentOS = project.tasks.composeDesktopJvmTask(taskName("package", app, "uberJarForCurrentOS")) { - configurePackageUberJarForCurrentOS(app) - } + val packageAll = tasks.register( + taskNameAction = "package" + ) { + dependsOn(packageFormats) + } - val runDistributable = project.tasks.composeDesktopJvmTask( - taskName("runDistributable", app), - args = listOf(createDistributable) - ) + val packageUberJarForCurrentOS = tasks.register( + taskNameAction = "package", + taskNameObject = "uberJarForCurrentOS" + ) { + configurePackageUberJarForCurrentOS(this) + } - val run = project.tasks.composeDesktopJvmTask(taskName("run", app)) { - configureRunTask(app, prepareAppResources = prepareAppResources) - } + val runDistributable = tasks.register( + taskNameAction = "run", + taskNameObject = "distributable", + args = listOf(createDistributable) + ) + + val run = tasks.register(taskNameAction = "run") { + configureRunTask(this, commonTasks.prepareAppResources) } } -internal fun AbstractJPackageTask.configurePackagingTask( - app: JvmApplication, +private fun JvmApplicationContext.configureProguardTask( + proguard: AbstractProguardTask, + unpackDefaultResources: TaskProvider +): AbstractProguardTask = proguard.apply { + val settings = buildType.proguard + mainClass.set(app.mainClass) + proguardVersion.set(settings.version) + configurationFiles.from(settings.configurationFiles) + + dependsOn(unpackDefaultResources) + defaultComposeRulesFile.set(unpackDefaultResources.flatMap { it.resources.defaultComposeProguardRules }) + + maxHeapSize.set(settings.maxHeapSize) + destinationDir.set(appTmpDir.dir("proguard")) + javaHome.set(app.javaHomeProvider) + + useAppRuntimeFiles { files -> + inputFiles.from(files.allRuntimeJars) + mainJar.set(files.mainJar) + } +} + +private fun JvmApplicationContext.configurePackageTask( + packageTask: AbstractJPackageTask, createAppImage: TaskProvider? = null, createRuntimeImage: TaskProvider? = null, prepareAppResources: TaskProvider? = null, checkRuntime: TaskProvider? = null, - unpackDefaultResources: TaskProvider + unpackDefaultResources: TaskProvider, + runProguard: Provider? = null ) { - enabled = targetFormat.isCompatibleWithCurrentOS + packageTask.enabled = packageTask.targetFormat.isCompatibleWithCurrentOS createAppImage?.let { createAppImage -> - dependsOn(createAppImage) - appImage.set(createAppImage.flatMap { it.destinationDir }) + packageTask.dependsOn(createAppImage) + packageTask.appImage.set(createAppImage.flatMap { it.destinationDir }) } createRuntimeImage?.let { createRuntimeImage -> - dependsOn(createRuntimeImage) - runtimeImage.set(createRuntimeImage.flatMap { it.destinationDir }) + packageTask.dependsOn(createRuntimeImage) + packageTask.runtimeImage.set(createRuntimeImage.flatMap { it.destinationDir }) } prepareAppResources?.let { prepareResources -> - dependsOn(prepareResources) - val resourcesDir = project.layout.dir(prepareResources.map { it.destinationDir }) - appResourcesDir.set(resourcesDir) + packageTask.dependsOn(prepareResources) + val resourcesDir = packageTask.project.layout.dir(prepareResources.map { it.destinationDir }) + packageTask.appResourcesDir.set(resourcesDir) } checkRuntime?.let { checkRuntime -> - dependsOn(checkRuntime) - javaRuntimePropertiesFile.set(checkRuntime.flatMap { it.javaRuntimePropertiesFile }) + packageTask.dependsOn(checkRuntime) + packageTask.javaRuntimePropertiesFile.set(checkRuntime.flatMap { it.javaRuntimePropertiesFile }) } - configurePlatformSettings(app, unpackDefaultResources) + this.configurePlatformSettings(packageTask, unpackDefaultResources) app.nativeDistributions.let { executables -> - packageName.set(app._packageNameProvider(project)) - packageDescription.set(provider { executables.description }) - packageCopyright.set(provider { executables.copyright }) - packageVendor.set(provider { executables.vendor }) - packageVersion.set(packageVersionFor(project, app, targetFormat)) - licenseFile.set(executables.licenseFile) + packageTask.packageName.set(packageNameProvider) + packageTask.packageDescription.set(packageTask.provider { executables.description }) + packageTask.packageCopyright.set(packageTask.provider { executables.copyright }) + packageTask.packageVendor.set(packageTask.provider { executables.vendor }) + packageTask.packageVersion.set(packageVersionFor(packageTask.targetFormat)) + packageTask.licenseFile.set(executables.licenseFile) } - destinationDir.set(app.nativeDistributions.outputBaseDir.map { it.dir("${app.name}/${targetFormat.outputDirName}") }) - javaHome.set(provider { app.javaHomeOrDefault() }) - - launcherMainJar.set(app.mainJar.orNull) - files.from(app._fromFiles) - dependsOn(*app._dependenciesTaskNames.toTypedArray()) - - app._configurationSource?.let { configSource -> - dependsOn(configSource.jarTaskName) - files.from(configSource.runtimeClasspath(project)) - launcherMainJar.set(app.mainJar.orElse(configSource.jarTask(project).flatMap { it.archiveFile })) + packageTask.destinationDir.set(app.nativeDistributions.outputBaseDir.map { + it.dir("$appDirName/${packageTask.targetFormat.outputDirName}") + }) + packageTask.javaHome.set(app.javaHomeProvider) + + if (runProguard != null) { + packageTask.dependsOn(runProguard) + packageTask.files.from(project.fileTree(runProguard.flatMap { it.destinationDir })) + packageTask.launcherMainJar.set(runProguard.flatMap { it.mainJarInDestinationDir }) + packageTask.mangleJarFilesNames.set(false) + } else { + packageTask.useAppRuntimeFiles { (runtimeJars, mainJar) -> + files.from(runtimeJars) + launcherMainJar.set(mainJar) + } } - launcherMainClass.set(provider { app.mainClass }) - launcherJvmArgs.set(provider { defaultJvmArgs + app.jvmArgs }) - launcherArgs.set(provider { app.args }) + packageTask.launcherMainClass.set(provider { app.mainClass }) + packageTask.launcherJvmArgs.set(provider { defaultJvmArgs + app.jvmArgs }) + packageTask.launcherArgs.set(provider { app.args }) } -internal fun AbstractUploadAppForNotarizationTask.configureUploadForNotarizationTask( - app: JvmApplication, - packageFormat: TaskProvider, - requestsDir: Provider +internal fun JvmApplicationContext.configureCommonNotarizationSettings( + notarizationTask: AbstractNotarizationTask ) { - dependsOn(packageFormat) - inputDir.set(packageFormat.flatMap { it.destinationDir }) - this.requestsDir.set(requestsDir) - configureCommonNotarizationSettings(app) + notarizationTask.nonValidatedBundleID.set(app.nativeDistributions.macOS.bundleID) + notarizationTask.nonValidatedNotarizationSettings = app.nativeDistributions.macOS.notarization } -internal fun AbstractCheckNotarizationStatusTask.configureCheckNotarizationStatusTask( - app: JvmApplication, - requestsDir: Provider -) { - requestDir.set(requestsDir) - configureCommonNotarizationSettings(app) -} - -internal fun AbstractNotarizationTask.configureCommonNotarizationSettings( - app: JvmApplication -) { - nonValidatedBundleID.set(app.nativeDistributions.macOS.bundleID) - nonValidatedNotarizationSettings = app.nativeDistributions.macOS.notarization -} - -internal fun AbstractJPackageTask.configurePlatformSettings( - app: JvmApplication, +internal fun JvmApplicationContext.configurePlatformSettings( + packageTask: AbstractJPackageTask, unpackDefaultResources: TaskProvider ) { - dependsOn(unpackDefaultResources) + packageTask.dependsOn(unpackDefaultResources) when (currentOS) { OS.Linux -> { app.nativeDistributions.linux.also { linux -> - linuxShortcut.set(provider { linux.shortcut }) - linuxAppCategory.set(provider { linux.appCategory }) - linuxAppRelease.set(provider { linux.appRelease }) - linuxDebMaintainer.set(provider { linux.debMaintainer }) - linuxMenuGroup.set(provider { linux.menuGroup }) - linuxPackageName.set(provider { linux.packageName }) - linuxRpmLicenseType.set(provider { linux.rpmLicenseType }) - iconFile.set(linux.iconFile.orElse(unpackDefaultResources.flatMap { it.resources.linuxIcon })) - installationPath.set(linux.installationPath) + packageTask.linuxShortcut.set(provider { linux.shortcut }) + packageTask.linuxAppCategory.set(provider { linux.appCategory }) + packageTask.linuxAppRelease.set(provider { linux.appRelease }) + packageTask.linuxDebMaintainer.set(provider { linux.debMaintainer }) + packageTask.linuxMenuGroup.set(provider { linux.menuGroup }) + packageTask.linuxPackageName.set(provider { linux.packageName }) + packageTask.linuxRpmLicenseType.set(provider { linux.rpmLicenseType }) + packageTask.iconFile.set(linux.iconFile.orElse(unpackDefaultResources.flatMap { it.resources.linuxIcon })) + packageTask.installationPath.set(linux.installationPath) } } OS.Windows -> { app.nativeDistributions.windows.also { win -> - winConsole.set(provider { win.console }) - winDirChooser.set(provider { win.dirChooser }) - winPerUserInstall.set(provider { win.perUserInstall }) - winShortcut.set(provider { win.shortcut }) - winMenu.set(provider { win.menu }) - winMenuGroup.set(provider { win.menuGroup }) - winUpgradeUuid.set(provider { win.upgradeUuid }) - iconFile.set(win.iconFile.orElse(unpackDefaultResources.flatMap { it.resources.windowsIcon })) - installationPath.set(win.installationPath) + packageTask.winConsole.set(provider { win.console }) + packageTask.winDirChooser.set(provider { win.dirChooser }) + packageTask.winPerUserInstall.set(provider { win.perUserInstall }) + packageTask.winShortcut.set(provider { win.shortcut }) + packageTask.winMenu.set(provider { win.menu }) + packageTask.winMenuGroup.set(provider { win.menuGroup }) + packageTask.winUpgradeUuid.set(provider { win.upgradeUuid }) + packageTask.iconFile.set(win.iconFile.orElse(unpackDefaultResources.flatMap { it.resources.windowsIcon })) + packageTask.installationPath.set(win.installationPath) } } OS.MacOS -> { app.nativeDistributions.macOS.also { mac -> - macPackageName.set(provider { mac.packageName }) - macDockName.set( + packageTask.macPackageName.set(provider { mac.packageName }) + packageTask.macDockName.set( if (mac.setDockNameSameAsPackageName) - provider { mac.dockName }.orElse(macPackageName).orElse(packageName) + provider { mac.dockName } + .orElse(packageTask.macPackageName).orElse(packageTask.packageName) else provider { mac.dockName } ) - macAppStore.set(mac.appStore) - macAppCategory.set(mac.appCategory) - macEntitlementsFile.set(mac.entitlementsFile) - macRuntimeEntitlementsFile.set(mac.runtimeEntitlementsFile) - packageBuildVersion.set(packageBuildVersionFor(project, app, targetFormat)) - nonValidatedMacBundleID.set(provider { mac.bundleID }) - macProvisioningProfile.set(mac.provisioningProfile) - macRuntimeProvisioningProfile.set(mac.runtimeProvisioningProfile) - macExtraPlistKeysRawXml.set(provider { mac.infoPlistSettings.extraKeysRawXml }) - nonValidatedMacSigningSettings = app.nativeDistributions.macOS.signing - iconFile.set(mac.iconFile.orElse(unpackDefaultResources.flatMap { it.resources.macIcon })) - installationPath.set(mac.installationPath) + packageTask.macAppStore.set(mac.appStore) + packageTask.macAppCategory.set(mac.appCategory) + packageTask.macEntitlementsFile.set(mac.entitlementsFile) + packageTask.macRuntimeEntitlementsFile.set(mac.runtimeEntitlementsFile) + packageTask.packageBuildVersion.set(packageBuildVersionFor(packageTask.targetFormat)) + packageTask.nonValidatedMacBundleID.set(provider { mac.bundleID }) + packageTask.macProvisioningProfile.set(mac.provisioningProfile) + packageTask.macRuntimeProvisioningProfile.set(mac.runtimeProvisioningProfile) + packageTask.macExtraPlistKeysRawXml.set(provider { mac.infoPlistSettings.extraKeysRawXml }) + packageTask.nonValidatedMacSigningSettings = app.nativeDistributions.macOS.signing + packageTask.iconFile.set(mac.iconFile.orElse(unpackDefaultResources.flatMap { it.resources.macIcon })) + packageTask.installationPath.set(mac.installationPath) } } } } -private fun JavaExec.configureRunTask( - app: JvmApplication, +private fun JvmApplicationContext.configureRunTask( + exec: JavaExec, prepareAppResources: TaskProvider ) { - dependsOn(prepareAppResources) + exec.dependsOn(prepareAppResources) - mainClass.set(provider { app.mainClass }) - executable(javaExecutable(app.javaHomeOrDefault())) - jvmArgs = arrayListOf().apply { + exec.mainClass.set(exec.provider { app.mainClass }) + exec.executable(javaExecutable(app.javaHome)) + exec.jvmArgs = arrayListOf().apply { addAll(defaultJvmArgs) if (currentOS == OS.MacOS) { @@ -361,74 +394,35 @@ private fun JavaExec.configureRunTask( val appResourcesDir = prepareAppResources.get().destinationDir add("-D$APP_RESOURCES_DIR=${appResourcesDir.absolutePath}") } - args = app.args - - val cp = project.objects.fileCollection() - // adding a null value will cause future invocations of `from` to throw an NPE - app.mainJar.orNull?.let { cp.from(it) } - cp.from(app._fromFiles) - dependsOn(*app._dependenciesTaskNames.toTypedArray()) - - app._configurationSource?.let { configSource -> - dependsOn(configSource.jarTaskName) - cp.from(configSource.runtimeClasspath(project)) + exec.args = app.args + exec.useAppRuntimeFiles { (runtimeJars, _) -> + classpath = runtimeJars } - - classpath = cp } -private fun Jar.configurePackageUberJarForCurrentOS(app: JvmApplication) { - fun flattenJars(files: FileCollection): FileCollection = - project.files({ - files.map { if (it.isZipOrJar()) project.zipTree(it) else it } - }) +private fun JvmApplicationContext.configurePackageUberJarForCurrentOS(jar: Jar) { + fun flattenJars(files: FileCollection): FileCollection = + jar.project.files({ + files.map { if (it.isZipOrJar()) jar.project.zipTree(it) else it } + }) - // adding a null value will cause future invocations of `from` to throw an NPE - app.mainJar.orNull?.let { from(it) } - from(flattenJars(app._fromFiles)) - dependsOn(*app._dependenciesTaskNames.toTypedArray()) - app._configurationSource?.let { configSource -> - dependsOn(configSource.jarTaskName) - from(flattenJars(configSource.runtimeClasspath(project))) - } + jar.useAppRuntimeFiles { (runtimeJars, _) -> + from(flattenJars(runtimeJars)) + } - app.mainClass?.let { manifest.attributes["Main-Class"] = it } - duplicatesStrategy = DuplicatesStrategy.EXCLUDE - archiveAppendix.set(currentTarget.id) - archiveBaseName.set(app._packageNameProvider(project)) - archiveVersion.set(packageVersionFor(project, app, TargetFormat.AppImage)) - destinationDirectory.set(project.layout.buildDirectory.dir("compose/jars")) + app.mainClass?.let { jar.manifest.attributes["Main-Class"] = it } + jar.duplicatesStrategy = DuplicatesStrategy.EXCLUDE + jar.archiveAppendix.set(currentTarget.id) + jar.archiveBaseName.set(packageNameProvider) + jar.archiveVersion.set(packageVersionFor(TargetFormat.AppImage)) + jar.destinationDirectory.set(jar.project.layout.buildDirectory.dir("compose/jars")) - doLast { - logger.lifecycle("The jar is written to ${archiveFile.ioFile.canonicalPath}") - } + jar.doLast { + jar.logger.lifecycle("The jar is written to ${jar.archiveFile.ioFile.canonicalPath}") } +} private fun File.isZipOrJar() = name.endsWith(".jar", ignoreCase = true) - || name.endsWith(".zip", ignoreCase = true) - -internal fun JvmApplication.javaHomeOrDefault(): String = - javaHome ?: System.getProperty("java.home") - -private inline fun TaskContainer.composeDesktopJvmTask( - name: String, - args: List = emptyList(), - noinline configureFn: T.() -> Unit = {} -) = register(name, T::class.java, *args.toTypedArray()).apply { - configure { - it.group = "compose desktop" - it.configureFn() - } -} - -internal fun JvmApplication._packageNameProvider(project: Project): Provider = - project.provider { nativeDistributions.packageName ?: project.name } - -private fun taskName(action: String, app: JvmApplication, suffix: String? = null): String = - listOfNotNull( - action, - app.name.takeIf { it != "main" }?.uppercaseFirstChar(), - suffix?.uppercaseFirstChar() - ).joinToString("") + || name.endsWith(".zip", ignoreCase = true) \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/diagnosticUtils.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/diagnosticUtils.kt index b7041679b3d..ecb09d127f6 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/diagnosticUtils.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/diagnosticUtils.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. + * Copyright 2020-2022 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. */ diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/dirLayoutUtils.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/dirLayoutUtils.kt new file mode 100644 index 00000000000..9cbb8414120 --- /dev/null +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/dirLayoutUtils.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2020-2022 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.internal + +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.api.file.Directory +import org.gradle.api.file.ProjectLayout +import org.gradle.api.provider.Provider + +internal val Project.jvmDirs: JvmDirectoriesProvider + get() = JvmDirectoriesProvider(project.layout) + +internal fun Task.jvmTmpDirForTask(): Provider = + project.jvmDirs.tmpDir(name) + +internal class JvmDirectoriesProvider( + private val layout: ProjectLayout +) { + val composeDir: Provider + get() = layout.buildDirectory.dir("compose") + + fun tmpDir(name: String): Provider = + composeDir.dir("tmp/$name") +} \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/dslUtils.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/dslUtils.kt deleted file mode 100644 index 118d050b9b9..00000000000 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/dslUtils.kt +++ /dev/null @@ -1,24 +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.internal - -import org.gradle.api.Task -import org.gradle.api.model.ObjectFactory -import org.gradle.api.provider.Property -import org.gradle.api.provider.Provider - -@SuppressWarnings("UNCHECKED_CAST") -internal inline fun ObjectFactory.nullableProperty(): Property = - property(T::class.java) as Property - -internal inline fun ObjectFactory.notNullProperty(): Property = - property(T::class.java) - -internal inline fun ObjectFactory.notNullProperty(defaultValue: T): Property = - property(T::class.java).value(defaultValue) - -internal inline fun Task.provider(noinline fn: () -> T): Provider = - project.provider(fn) \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/files/MacJarSignFileCopyingProcessor.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/files/MacJarSignFileCopyingProcessor.kt index 3804feff206..ff1bcc3e164 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/files/MacJarSignFileCopyingProcessor.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/files/MacJarSignFileCopyingProcessor.kt @@ -6,7 +6,6 @@ package org.jetbrains.compose.desktop.application.internal.files import org.jetbrains.compose.desktop.application.internal.MacSigner -import org.jetbrains.compose.desktop.application.internal.isJarFile import java.io.File import java.util.zip.ZipEntry import java.util.zip.ZipInputStream @@ -55,30 +54,22 @@ internal class MacJarSignFileCopyingProcessor( private fun signNativeLibsInJar(source: File, target: File) { if (target.exists()) target.delete() - transformJar(source, target) { zin, zout, entry -> + transformJar(source, target) { entry, zin, zout -> if (entry.name.isDylibPath) { - signDylibEntry(zin, zout, entry) + signDylibEntry(entry, zin, zout) } else { - zout.withNewEntry(ZipEntry(entry)) { - zin.copyTo(zout) - } + copyZipEntry(entry, zin, zout) } } } - private fun signDylibEntry(zin: ZipInputStream, zout: ZipOutputStream, sourceEntry: ZipEntry) { + private fun signDylibEntry(sourceEntry: ZipEntry, zin: ZipInputStream, zout: ZipOutputStream) { val unpackedDylibFile = tempDir.resolve(sourceEntry.name.substringAfterLast("/")) try { zin.copyTo(unpackedDylibFile) signer.sign(unpackedDylibFile) - val targetEntry = ZipEntry(sourceEntry.name).apply { - comment = sourceEntry.comment - extra = sourceEntry.extra - method = sourceEntry.method - size = unpackedDylibFile.length() - } - zout.withNewEntry(ZipEntry(targetEntry)) { - unpackedDylibFile.copyTo(zout) + unpackedDylibFile.inputStream().buffered().use { + copyZipEntry(sourceEntry, from = it, to = zout) } } finally { unpackedDylibFile.delete() diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/files/fileUtils.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/files/fileUtils.kt index 87dcba5841d..ae9d5fa51ff 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/files/fileUtils.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/files/fileUtils.kt @@ -1,10 +1,14 @@ /* - * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. + * Copyright 2020-2022 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.internal.files +import org.gradle.api.tasks.Internal +import org.jetbrains.compose.desktop.application.dsl.TargetFormat +import org.jetbrains.compose.desktop.application.internal.OS +import org.jetbrains.compose.desktop.application.internal.currentOS import java.io.* import java.security.DigestInputStream import java.security.MessageDigest @@ -12,13 +16,27 @@ import java.util.zip.ZipEntry import java.util.zip.ZipInputStream import java.util.zip.ZipOutputStream -internal fun fileHash(file: File): String { - val md5 = MessageDigest.getInstance("MD5") - file.inputStream().buffered().use { fis -> - DigestInputStream(fis, md5).use { ds -> - while (ds.read() != -1) {} +internal fun File.mangledName(): String = + buildString { + append(nameWithoutExtension) + append("-") + append(contentHash()) + val ext = extension + if (ext.isNotBlank()) { + append(".$ext") } } + +internal fun File.contentHash(): String { + val md5 = MessageDigest.getInstance("MD5") + if (isDirectory) { + walk() + .filter { it.isFile } + .sortedBy { it.relativeTo(this).path } + .forEach { md5.digestContent(it) } + } else { + md5.digestContent(this) + } val digest = md5.digest() return buildString(digest.size * 2) { for (byte in digest) { @@ -27,20 +45,42 @@ internal fun fileHash(file: File): String { } } +private fun MessageDigest.digestContent(file: File) { + file.inputStream().buffered().use { fis -> + DigestInputStream(fis, this).use { ds -> + while (ds.read() != -1) {} + } + } +} + internal inline fun transformJar( sourceJar: File, targetJar: File, - fn: (zin: ZipInputStream, zout: ZipOutputStream, entry: ZipEntry) -> Unit + fn: (entry: ZipEntry, zin: ZipInputStream, zout: ZipOutputStream) -> Unit ) { ZipInputStream(FileInputStream(sourceJar).buffered()).use { zin -> ZipOutputStream(FileOutputStream(targetJar).buffered()).use { zout -> for (sourceEntry in generateSequence { zin.nextEntry }) { - fn(zin, zout, sourceEntry) + fn(sourceEntry, zin, zout) } } } } +internal fun copyZipEntry( + entry: ZipEntry, + from: InputStream, + to: ZipOutputStream, +) { + val newEntry = ZipEntry(entry.name).apply { + comment = entry.comment + extra = entry.extra + } + to.withNewEntry(newEntry) { + from.copyTo(to) + } +} + internal inline fun ZipOutputStream.withNewEntry(zipEntry: ZipEntry, fn: () -> Unit) { putNextEntry(zipEntry) fn() @@ -53,8 +93,20 @@ internal fun InputStream.copyTo(file: File) { } } -internal fun File.copyTo(os: OutputStream) { - inputStream().buffered().use { bis -> - bis.copyTo(os) +@Internal +internal fun findOutputFileOrDir(dir: File, targetFormat: TargetFormat): File = + when (targetFormat) { + TargetFormat.AppImage -> dir + else -> dir.walk().first { it.isFile && it.name.endsWith(targetFormat.fileExt) } + } + +internal fun File.checkExistingFile(): File = + apply { + check(isFile) { "'$absolutePath' does not exist" } } -} \ No newline at end of file + +internal val File.isJarFile: Boolean + get() = name.endsWith(".jar", ignoreCase = true) && isFile + +internal fun File.normalizedPath() = + if (currentOS == OS.Windows) absolutePath.replace("\\", "\\\\") else absolutePath diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/gradleUtils.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/gradleUtils.kt index 3203cf4dfff..163ef12fb6b 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/gradleUtils.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/gradleUtils.kt @@ -1,14 +1,49 @@ /* - * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. + * Copyright 2020-2022 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.internal +import org.gradle.api.Task +import org.gradle.api.file.Directory import org.gradle.api.file.FileSystemLocation +import org.gradle.api.file.RegularFile +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.Property import org.gradle.api.provider.Provider +import org.gradle.api.tasks.TaskProvider import java.io.File +@SuppressWarnings("UNCHECKED_CAST") +internal inline fun ObjectFactory.nullableProperty(): Property = + property(T::class.java) as Property + +internal inline fun ObjectFactory.notNullProperty(): Property = + property(T::class.java) + +internal inline fun ObjectFactory.notNullProperty(defaultValue: T): Property = + property(T::class.java).value(defaultValue) + +internal inline fun Provider.toProperty(objects: ObjectFactory): Property = + objects.property(T::class.java).value(this) + +internal inline fun Task.provider(noinline fn: () -> T): Provider = + project.provider(fn) + +internal fun Provider.file(relativePath: String): Provider = + map { it.file(relativePath) } + +internal fun Provider.dir(relativePath: String): Provider = + map { it.dir(relativePath) } + +internal inline fun ObjectFactory.new(vararg params: Any): T = + newInstance(T::class.java, *params) + +internal fun TaskProvider.dependsOn(vararg dependencies: Any) { + configure { it.dependsOn(*dependencies) } +} + internal val Provider.ioFile: File get() = get().asFile diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/osUtils.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/osUtils.kt index c0d52a2f47b..cd6e77b3fc9 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/osUtils.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/osUtils.kt @@ -1,13 +1,12 @@ /* - * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. + * Copyright 2020-2022 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.internal import org.gradle.api.provider.Provider -import org.gradle.api.tasks.Internal -import org.jetbrains.compose.desktop.application.dsl.TargetFormat +import org.jetbrains.compose.desktop.application.internal.files.checkExistingFile import org.jetbrains.compose.desktop.application.tasks.MIN_JAVA_RUNTIME_VERSION import java.io.File @@ -89,26 +88,14 @@ internal object UnixUtils { } } -internal fun jvmToolFile(toolName: String, javaHome: Provider): File { - val jtool = File(javaHome.get()).resolve("bin/${executableName(toolName)}") +internal fun jvmToolFile(toolName: String, javaHome: Provider): File = + jvmToolFile(toolName, File(javaHome.get())) + +internal fun jvmToolFile(toolName: String, javaHome: File): File { + val jtool = javaHome.resolve("bin/${executableName(toolName)}") check(jtool.isFile) { "Invalid JDK: $jtool is not a file! \n" + "Ensure JAVA_HOME or buildSettings.javaHome is set to JDK $MIN_JAVA_RUNTIME_VERSION or newer" } return jtool } - -@Internal -internal fun findOutputFileOrDir(dir: File, targetFormat: TargetFormat): File = - when (targetFormat) { - TargetFormat.AppImage -> dir - else -> dir.walk().first { it.isFile && it.name.endsWith(targetFormat.fileExt) } - } - -internal fun File.checkExistingFile(): File = - apply { - check(isFile) { "'$absolutePath' does not exist" } - } - -internal val File.isJarFile: Boolean - get() = name.endsWith(".jar", ignoreCase = true) && isFile diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/packageVersions.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/packageVersions.kt index 8bf209b2f26..f8a599f4665 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/packageVersions.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/packageVersions.kt @@ -5,15 +5,11 @@ package org.jetbrains.compose.desktop.application.internal -import org.gradle.api.Project import org.gradle.api.provider.Provider -import org.jetbrains.compose.desktop.application.dsl.JvmApplication import org.jetbrains.compose.desktop.application.dsl.JvmApplicationDistributions import org.jetbrains.compose.desktop.application.dsl.TargetFormat -internal fun packageVersionFor( - project: Project, - app: JvmApplication, +internal fun JvmApplicationContext.packageVersionFor( targetFormat: TargetFormat ): Provider = project.provider { @@ -44,9 +40,7 @@ private fun JvmApplicationDistributions.packageVersionFor( ?: packageVersion } -internal fun packageBuildVersionFor( - project: Project, - app: JvmApplication, +internal fun JvmApplicationContext.packageBuildVersionFor( targetFormat: TargetFormat ): Provider = project.provider { diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/validation/validatePackageVersions.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/validation/validatePackageVersions.kt index 5934d4b4e36..020e3337673 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/validation/validatePackageVersions.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/validation/validatePackageVersions.kt @@ -6,14 +6,13 @@ package org.jetbrains.compose.desktop.application.internal.validation import org.gradle.api.GradleException -import org.gradle.api.Project -import org.jetbrains.compose.desktop.application.dsl.JvmApplication import org.jetbrains.compose.desktop.application.dsl.TargetFormat +import org.jetbrains.compose.desktop.application.internal.JvmApplicationContext import org.jetbrains.compose.desktop.application.internal.OS import org.jetbrains.compose.desktop.application.internal.packageBuildVersionFor import org.jetbrains.compose.desktop.application.internal.packageVersionFor -internal fun Project.validatePackageVersions(app: JvmApplication) { +internal fun JvmApplicationContext.validatePackageVersions() { val errors = ErrorsCollector() for (targetFormat in app.nativeDistributions.targetFormats) { @@ -25,7 +24,7 @@ internal fun Project.validatePackageVersions(app: JvmApplication) { TargetFormat.Dmg, TargetFormat.Pkg -> MacVersionChecker } - val packageVersion = packageVersionFor(project, app, targetFormat).orNull + val packageVersion = packageVersionFor(targetFormat).orNull if (packageVersion == null) { errors.addError(targetFormat, "no version was specified") } else { @@ -41,7 +40,7 @@ internal fun Project.validatePackageVersions(app: JvmApplication) { } if (targetFormat.targetOS == OS.MacOS) { - val packageBuildVersion = packageBuildVersionFor(project, app, targetFormat).orNull + val packageBuildVersion = packageBuildVersionFor(targetFormat).orNull if (packageBuildVersion == null) { errors.addError(targetFormat, "no build version was specified") } else { diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/wixToolset.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/wixToolset.kt index af95a3cd93d..bde366f2ec0 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/wixToolset.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/wixToolset.kt @@ -16,14 +16,14 @@ internal const val UNZIP_WIX_TOOLSET_TASK_NAME = "unzipWix" internal const val WIX_PATH_ENV_VAR = "WIX_PATH" internal const val DOWNLOAD_WIX_PROPERTY = "compose.desktop.application.downloadWix" -internal fun Project.configureWix() { - if (currentOS != OS.Windows) return +internal fun JvmApplicationContext.configureWix() { + check(currentOS == OS.Windows) { "Should not be called for non-Windows OS: $currentOS" } val wixPath = System.getenv()[WIX_PATH_ENV_VAR] if (wixPath != null) { val wixDir = File(wixPath) check(wixDir.isDirectory) { "$WIX_PATH_ENV_VAR value is not a valid directory: $wixDir" } - eachWindowsPackageTask { + project.eachWindowsPackageTask { wixToolsetDir.set(wixDir) } return @@ -42,10 +42,10 @@ internal fun Project.configureWix() { } val unzip = root.tasks.maybeCreate(UNZIP_WIX_TOOLSET_TASK_NAME, Copy::class.java).apply { dependsOn(download) - from(zipTree(zipFile)) + from(project.zipTree(zipFile)) destinationDir = unzipDir } - eachWindowsPackageTask { + project.eachWindowsPackageTask { dependsOn(unzip) wixToolsetDir.set(unzipDir) } diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCheckNativeDistributionRuntime.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCheckNativeDistributionRuntime.kt index bf315710946..953c00c704f 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCheckNativeDistributionRuntime.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCheckNativeDistributionRuntime.kt @@ -10,10 +10,11 @@ import org.gradle.api.file.RegularFileProperty import org.gradle.api.provider.Property import org.gradle.api.provider.Provider import org.gradle.api.tasks.* -import org.jetbrains.compose.desktop.application.internal.JavaRuntimeProperties +import org.jetbrains.compose.desktop.application.internal.JvmRuntimeProperties import org.jetbrains.compose.desktop.application.internal.executableName import org.jetbrains.compose.desktop.application.internal.ioFile import org.jetbrains.compose.desktop.application.internal.notNullProperty +import org.jetbrains.compose.desktop.application.internal.ExternalToolRunner import org.jetbrains.compose.desktop.tasks.AbstractComposeDesktopTask import java.io.File @@ -65,7 +66,7 @@ abstract class AbstractCheckNativeDistributionRuntime : AbstractComposeDesktopTa runExternalTool( tool = javaExec, args = listOf("--list-modules"), - forceLogToFile = true, + logToConsole = ExternalToolRunner.LogToConsole.Never, processStdout = { stdout -> stdout.lineSequence().forEach { line -> val moduleName = line.trim().substringBefore("@") @@ -76,8 +77,8 @@ abstract class AbstractCheckNativeDistributionRuntime : AbstractComposeDesktopTa } ) - val properties = JavaRuntimeProperties(javaRuntimeVersion, modules) - JavaRuntimeProperties.writeToFile(properties, javaRuntimePropertiesFile.ioFile) + val properties = JvmRuntimeProperties(javaRuntimeVersion, modules) + JvmRuntimeProperties.writeToFile(properties, javaRuntimePropertiesFile.ioFile) } private fun getJavaRuntimeVersionUnsafe(): String? { diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCheckNotarizationStatusTask.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCheckNotarizationStatusTask.kt index cad177c8031..307e38a6612 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCheckNotarizationStatusTask.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCheckNotarizationStatusTask.kt @@ -7,6 +7,7 @@ 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.desktop.application.internal.MacUtils import org.jetbrains.compose.desktop.application.internal.NOTARIZATION_REQUEST_INFO_FILE_NAME import org.jetbrains.compose.desktop.application.internal.NotarizationRequestInfo @@ -49,11 +50,7 @@ abstract class AbstractCheckNotarizationStatusTask : AbstractNotarizationTask() "--username", notarization.appleID, "--password", notarization.password ), - processStdout = { output -> - if (!verbose.get()) { - logger.quiet(output) - } - } + logToConsole = ExternalToolRunner.LogToConsole.Always ) } catch (e: Exception) { logger.error("Could not check notarization request '${request.uuid}'", e) diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJLinkTask.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJLinkTask.kt index be42b959438..05a7488d131 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJLinkTask.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJLinkTask.kt @@ -12,9 +12,9 @@ import org.gradle.api.tasks.Input import org.gradle.api.tasks.InputFile import org.gradle.api.tasks.Optional import org.jetbrains.compose.desktop.application.internal.RuntimeCompressionLevel -import org.jetbrains.compose.desktop.application.internal.* -import org.jetbrains.compose.desktop.application.internal.JavaRuntimeProperties +import org.jetbrains.compose.desktop.application.internal.JvmRuntimeProperties import org.jetbrains.compose.desktop.application.internal.cliArg +import org.jetbrains.compose.desktop.application.internal.ioFile import org.jetbrains.compose.desktop.application.internal.notNullProperty import org.jetbrains.compose.desktop.application.internal.nullableProperty import java.io.File @@ -50,7 +50,7 @@ abstract class AbstractJLinkTask : AbstractJvmToolOperationTask("jlink") { override fun makeArgs(tmpDir: File): MutableList = super.makeArgs(tmpDir).apply { val modulesToInclude = if (includeAllModules.get()) { - JavaRuntimeProperties.readFromFile(javaRuntimePropertiesFile.ioFile).availableModules + JvmRuntimeProperties.readFromFile(javaRuntimePropertiesFile.ioFile).availableModules } else modules.get() modulesToInclude.forEach { m -> cliArg("--add-modules", m) diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJPackageTask.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJPackageTask.kt index e820ee84a94..b26cf12d05a 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJPackageTask.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJPackageTask.kt @@ -20,12 +20,10 @@ import org.jetbrains.compose.desktop.application.dsl.TargetFormat import org.jetbrains.compose.desktop.application.internal.* import org.jetbrains.compose.desktop.application.internal.files.* import org.jetbrains.compose.desktop.application.internal.files.MacJarSignFileCopyingProcessor -import org.jetbrains.compose.desktop.application.internal.files.fileHash -import org.jetbrains.compose.desktop.application.internal.files.transformJar +import org.jetbrains.compose.desktop.application.internal.JvmRuntimeProperties import org.jetbrains.compose.desktop.application.internal.validation.validate import java.io.* import java.util.* -import java.util.zip.ZipEntry import javax.inject.Inject import kotlin.collections.HashMap import kotlin.collections.HashSet @@ -38,6 +36,32 @@ abstract class AbstractJPackageTask @Inject constructor( @get:InputFiles val files: ConfigurableFileCollection = objects.fileCollection() + /** + * A hack to avoid conflicts between jar files in a flat dir. + * We receive input jar files as a list (FileCollection) of files. + * At that point we don't have access to jar files' coordinates. + * + * Some files can have the same simple names. + * For example, a project containing two modules: + * 1. :data:utils + * 2. :ui:utils + * produces: + * 1. /data/utils/build/../utils.jar + * 2. /ui/utils/build/../utils.jar + * + * jpackage expects all files to be in one input directory (not sure), + * so the natural workaround to avoid overwrites/conflicts is to add a content hash + * to a file name. A better solution would be to preserve coordinates or relative paths, + * but it is not straightforward at this point. + * + * The flag is needed for two things: + * 1. Give users the ability to turn off the mangling, if they need to preserve names; + * 2. Proguard transformation already flattens jar files & mangles names, so we don't + * need to mangle twice. + */ + @get:Input + val mangleJarFilesNames: Property = objects.notNullProperty(true) + @get:InputDirectory @get:Optional /** @see internal/wixToolset.kt */ @@ -383,7 +407,6 @@ abstract class AbstractJPackageTask @Inject constructor( fun invalidateAllLibs() { outdatedLibs.addAll(files.files) - outdatedLibs.add(launcherMainJar.ioFile) logger.debug("Clearing all files in working dir: $libsDirFile") fileOperations.delete(libsDirFile) @@ -391,8 +414,7 @@ abstract class AbstractJPackageTask @Inject constructor( } if (inputChanges.isIncremental) { - val allChanges = inputChanges.getFileChanges(files).asSequence() + - inputChanges.getFileChanges(launcherMainJar) + val allChanges = inputChanges.getFileChanges(files).asSequence() try { for (change in allChanges) { @@ -422,18 +444,20 @@ abstract class AbstractJPackageTask @Inject constructor( fileOperations.delete(tmpDirForSign) tmpDirForSign.mkdirs() - val jvmRuntimeInfo = JavaRuntimeProperties.readFromFile(javaRuntimePropertiesFile.ioFile) + val jvmRuntimeInfo = JvmRuntimeProperties.readFromFile(javaRuntimePropertiesFile.ioFile) MacJarSignFileCopyingProcessor( signer, tmpDirForSign, jvmRuntimeVersion = jvmRuntimeInfo.majorVersion ) } ?: SimpleFileCopyingProcessor + + val mangleJarFilesNames = mangleJarFilesNames.get() fun copyFileToLibsDir(sourceFile: File): File { - val targetFileName = - if (sourceFile.isJarFile) "${sourceFile.nameWithoutExtension}-${fileHash(sourceFile)}.jar" + val targetName = + if (mangleJarFilesNames && sourceFile.isJarFile) sourceFile.mangledName() else sourceFile.name - val targetFile = libsDir.resolve(targetFileName) + val targetFile = libsDir.resolve(targetName) fileProcessor.copy(sourceFile, targetFile) return targetFile } @@ -632,16 +656,14 @@ private fun unpackSkikoForCurrentOS(sourceJar: File, skikoDir: File, fileOperati fileOperations.delete(skikoDir) fileOperations.mkdir(skikoDir) - transformJar(sourceJar, targetJar) { zin, zout, entry -> + transformJar(sourceJar, targetJar) { entry, zin, zout -> // check both entry or entry.sha256 if (entry.name.removeSuffix(".sha256") in entriesToUnpack) { val unpackedFile = skikoDir.resolve(entry.name.substringAfterLast("/")) zin.copyTo(unpackedFile) outputFiles.add(unpackedFile) } else { - zout.withNewEntry(ZipEntry(entry)) { - zin.copyTo(zout) - } + copyZipEntry(entry, zin, zout) } } return outputFiles diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJvmToolOperationTask.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJvmToolOperationTask.kt index 5b5d76025b1..675347b86cb 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJvmToolOperationTask.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJvmToolOperationTask.kt @@ -14,8 +14,8 @@ import org.gradle.api.tasks.* import org.gradle.process.ExecResult import org.gradle.work.InputChanges import org.jetbrains.compose.desktop.application.internal.ComposeProperties -import org.jetbrains.compose.desktop.application.internal.jvmToolFile import org.jetbrains.compose.desktop.application.internal.ioFile +import org.jetbrains.compose.desktop.application.internal.jvmToolFile import org.jetbrains.compose.desktop.application.internal.notNullProperty import org.jetbrains.compose.desktop.tasks.AbstractComposeDesktopTask import java.io.File diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractNativeMacApplicationPackageAppDirTask.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractNativeMacApplicationPackageAppDirTask.kt index 708558b8ed2..c3419aec0cd 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractNativeMacApplicationPackageAppDirTask.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractNativeMacApplicationPackageAppDirTask.kt @@ -9,13 +9,12 @@ import org.gradle.api.file.RegularFileProperty import org.gradle.api.provider.Property import org.gradle.api.tasks.* import org.gradle.api.tasks.Optional -import org.jetbrains.compose.desktop.application.internal.* import org.jetbrains.compose.desktop.application.internal.InfoPlistBuilder import org.jetbrains.compose.desktop.application.internal.PlistKeys import org.jetbrains.compose.desktop.application.internal.ioFile import org.jetbrains.compose.desktop.application.internal.notNullProperty +import org.jetbrains.compose.desktop.application.internal.nullableProperty import java.io.File -import java.util.* private const val KOTLIN_NATIVE_MIN_SUPPORTED_MAC_OS = "10.13" diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractNativeMacApplicationPackageTask.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractNativeMacApplicationPackageTask.kt index 1178f928a4b..77e541e991c 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractNativeMacApplicationPackageTask.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractNativeMacApplicationPackageTask.kt @@ -7,7 +7,6 @@ package org.jetbrains.compose.desktop.application.tasks import org.gradle.api.file.Directory import org.gradle.api.file.DirectoryProperty -import org.gradle.api.file.FileSystemLocation import org.gradle.api.provider.Property import org.gradle.api.provider.Provider import org.gradle.api.tasks.* diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractProguardTask.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractProguardTask.kt new file mode 100644 index 00000000000..859a8a978ca --- /dev/null +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractProguardTask.kt @@ -0,0 +1,150 @@ +/* + * Copyright 2020-2022 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.ConfigurableFileCollection +import org.gradle.api.file.Directory +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFile +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.* +import org.gradle.api.tasks.Optional +import org.jetbrains.compose.desktop.application.internal.* +import org.jetbrains.compose.desktop.application.internal.ioFile +import org.jetbrains.compose.desktop.application.internal.files.mangledName +import org.jetbrains.compose.desktop.application.internal.files.normalizedPath +import org.jetbrains.compose.desktop.application.internal.notNullProperty +import org.jetbrains.compose.desktop.application.internal.nullableProperty +import org.jetbrains.compose.desktop.tasks.AbstractComposeDesktopTask +import java.io.File +import java.io.Writer +import kotlin.collections.LinkedHashMap + +abstract class AbstractProguardTask : AbstractComposeDesktopTask() { + @get:InputFiles + val inputFiles: ConfigurableFileCollection = objects.fileCollection() + + @get:InputFile + val mainJar: RegularFileProperty = objects.fileProperty() + + @get:Internal + internal val mainJarInDestinationDir: Provider = mainJar.flatMap { + destinationDir.file(it.asFile.name) + } + + @get:InputFiles + val configurationFiles: ConfigurableFileCollection = objects.fileCollection() + + // todo: DSL for excluding default rules + // also consider pulling coroutines rules from coroutines artifact + // https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-core/jvm/resources/META-INF/proguard/coroutines.pro + @get:Optional + @get:InputFile + val defaultComposeRulesFile: RegularFileProperty = objects.fileProperty() + + @get:Input + val proguardVersion: Property = objects.notNullProperty() + + @get:Input + val javaHome: Property = objects.notNullProperty(System.getProperty("java.home")) + + @get:Input + val mainClass: Property = objects.notNullProperty() + + @get:Internal + val maxHeapSize: Property = objects.nullableProperty() + + @get:OutputDirectory + val destinationDir: DirectoryProperty = objects.directoryProperty() + + @get:LocalState + protected val workingDir: Provider = project.layout.buildDirectory.dir("compose/tmp/$name") + + private val rootConfigurationFile = workingDir.map { it.file("root-config.pro") } + + private val jarsConfigurationFile = workingDir.map { it.file("jars-config.pro") } + + @TaskAction + fun execute() { + val javaHome = File(javaHome.get()) + val proguardFiles = project.configurations.detachedConfiguration( + project.dependencies.create("com.guardsquare:proguard-gradle:${proguardVersion.get()}") + ).files + + cleanDirs(destinationDir, workingDir) + val destinationDir = destinationDir.ioFile.absoluteFile + + // todo: can be cached for a jdk + val jmods = javaHome.resolve("jmods").walk().filter { + it.isFile && it.path.endsWith("jmod", ignoreCase = true) + }.toList() + + val inputToOutputJars = LinkedHashMap() + // avoid mangling mainJar + inputToOutputJars[mainJar.ioFile] = mainJarInDestinationDir.ioFile + for (inputFile in inputFiles) { + if (inputFile.name.endsWith(".jar", ignoreCase = true)) { + inputToOutputJars.putIfAbsent(inputFile, destinationDir.resolve(inputFile.mangledName())) + } else { + inputFile.copyTo(destinationDir.resolve(inputFile.name)) + } + } + + jarsConfigurationFile.ioFile.bufferedWriter().use { writer -> + for ((input, output) in inputToOutputJars.entries) { + writer.writeLn("-injars '${input.normalizedPath()}'") + writer.writeLn("-outjars '${output.normalizedPath()}'") + } + + for (jmod in jmods) { + writer.writeLn("-libraryjars '${jmod.normalizedPath()}'(!**.jar;!module-info.class)") + } + } + + rootConfigurationFile.ioFile.bufferedWriter().use { writer -> + writer.writeLn(""" + -keep public class ${mainClass.get()} { + public static void main(java.lang.String[]); + } + """.trimIndent()) + + val includeFiles = sequenceOf( + jarsConfigurationFile.ioFile, + defaultComposeRulesFile.ioFile + ) + configurationFiles.files.asSequence() + for (configFile in includeFiles.filterNotNull()) { + writer.writeLn("-include '${configFile.normalizedPath()}'") + } + } + + val javaBinary = jvmToolFile(toolName = "java", javaHome = javaHome) + val args = arrayListOf().apply { + val maxHeapSize = maxHeapSize.orNull + if (maxHeapSize != null) { + add("-Xmx:$maxHeapSize") + } + cliArg("-cp", proguardFiles.map { it.normalizedPath() }.joinToString(File.pathSeparator)) + add("proguard.ProGuard") + // todo: consider separate flag + cliArg("-verbose", verbose) + cliArg("-include", rootConfigurationFile) + } + + runExternalTool( + tool = javaBinary, + args = args, + environment = emptyMap(), + logToConsole = ExternalToolRunner.LogToConsole.Always + ).assertNormalExitValue() + } + + private fun Writer.writeLn(s: String) { + write(s) + write("\n") + } +} \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractSuggestModulesTask.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractSuggestModulesTask.kt index a8380da42be..2a5e55d82f6 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractSuggestModulesTask.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractSuggestModulesTask.kt @@ -13,9 +13,12 @@ import org.gradle.api.provider.Property import org.gradle.api.provider.Provider import org.gradle.api.tasks.* import org.jetbrains.compose.desktop.application.dsl.DEFAULT_RUNTIME_MODULES -import org.jetbrains.compose.desktop.application.internal.* import org.jetbrains.compose.desktop.application.internal.ComposeProperties -import org.jetbrains.compose.desktop.application.internal.normalizedPath +import org.jetbrains.compose.desktop.application.internal.ioFile +import org.jetbrains.compose.desktop.application.internal.ExternalToolRunner +import org.jetbrains.compose.desktop.application.internal.jvmToolFile +import org.jetbrains.compose.desktop.application.internal.files.normalizedPath +import org.jetbrains.compose.desktop.application.internal.notNullProperty import org.jetbrains.compose.desktop.tasks.AbstractComposeDesktopTask abstract class AbstractSuggestModulesTask : AbstractComposeDesktopTask() { @@ -59,7 +62,7 @@ abstract class AbstractSuggestModulesTask : AbstractComposeDesktopTask() { runExternalTool( tool = jtool, args = args, - forceLogToFile = true, + logToConsole = ExternalToolRunner.LogToConsole.Never, processStdout = { output -> val defaultModules = hashSetOf(*DEFAULT_RUNTIME_MODULES) val suggestedModules = output.splitToSequence(",") diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractUploadAppForNotarizationTask.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractUploadAppForNotarizationTask.kt index f1d51858874..1385c7bc9df 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractUploadAppForNotarizationTask.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractUploadAppForNotarizationTask.kt @@ -9,6 +9,8 @@ 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 java.io.File import java.time.LocalDateTime import java.time.format.DateTimeFormatter diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/preview/internal/configurePreview.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/preview/internal/configurePreview.kt index 9126804c839..4b7de910b8c 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/preview/internal/configurePreview.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/preview/internal/configurePreview.kt @@ -1,34 +1,42 @@ package org.jetbrains.compose.desktop.preview.internal import org.gradle.api.Project -import org.jetbrains.compose.desktop.application.internal.ConfigurationSource +import org.jetbrains.compose.desktop.DesktopExtension +import org.jetbrains.compose.desktop.application.internal.JvmApplicationRuntimeFilesProvider import org.jetbrains.compose.desktop.preview.tasks.AbstractConfigureDesktopPreviewTask import org.jetbrains.compose.internal.* import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType import org.jetbrains.kotlin.gradle.targets.jvm.KotlinJvmTarget -fun Project.initializePreview() { +fun Project.initializePreview(desktopExtension: DesktopExtension) { plugins.withId(KOTLIN_MPP_PLUGIN_ID) { mppExt.targets.all { target -> if (target.platformType == KotlinPlatformType.jvm) { - val config = ConfigurationSource.KotlinMppTarget(target as KotlinJvmTarget) - registerConfigurePreviewTask(project, config, targetName = target.name) + val runtimeFilesProvider = JvmApplicationRuntimeFilesProvider.FromKotlinMppTarget(target as KotlinJvmTarget) + registerConfigurePreviewTask(project, runtimeFilesProvider, targetName = target.name) } } } plugins.withId(KOTLIN_JVM_PLUGIN_ID) { - val config = ConfigurationSource.GradleSourceSet(project.javaSourceSets.getByName("main")) - registerConfigurePreviewTask(project, config) + val sourceSet = project.javaSourceSets.getByName("main") + val runtimeFilesProvider = JvmApplicationRuntimeFilesProvider.FromGradleSourceSet(sourceSet) + registerConfigurePreviewTask(project, runtimeFilesProvider) } } -private fun registerConfigurePreviewTask(project: Project, config: ConfigurationSource, targetName: String = "") { +private fun registerConfigurePreviewTask( + project: Project, + runtimeFilesProvider: JvmApplicationRuntimeFilesProvider, + targetName: String = "" +) { + val runtimeFiles = runtimeFilesProvider.jvmApplicationRuntimeFiles(project) project.tasks.register( previewTaskName(targetName), AbstractConfigureDesktopPreviewTask::class.java ) { previewTask -> - previewTask.dependsOn(config.jarTask(project)) - previewTask.previewClasspath = config.runtimeClasspath(project) + runtimeFiles.configureUsageBy(previewTask) { (runtimeJars, _) -> + previewClasspath = runtimeJars + } } } diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/tasks/AbstractUnpackDefaultComposeApplicationResourcesTask.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/tasks/AbstractUnpackDefaultComposeApplicationResourcesTask.kt index 9b0c75c44bd..a62312bcc23 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/tasks/AbstractUnpackDefaultComposeApplicationResourcesTask.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/tasks/AbstractUnpackDefaultComposeApplicationResourcesTask.kt @@ -15,11 +15,14 @@ import org.gradle.api.tasks.TaskAction import org.jetbrains.compose.ComposeBuildConfig import org.jetbrains.compose.desktop.application.internal.ioFile +private const val DEFAULT_COMPOSE_PROGUARD_RULES_FILE_NAME = "default-compose-desktop-rules.pro" + abstract class AbstractUnpackDefaultComposeApplicationResourcesTask : AbstractComposeDesktopTask() { internal class DefaultResourcesProvider(resourcesRootDir: Provider) { val macIcon: Provider = resourcesRootDir.map { it.file("default-icon-mac.icns") } val windowsIcon: Provider = resourcesRootDir.map { it.file("default-icon-windows.ico") } val linuxIcon: Provider = resourcesRootDir.map { it.file("default-icon-linux.png") } + val defaultComposeProguardRules: Provider = resourcesRootDir.map { it.file(DEFAULT_COMPOSE_PROGUARD_RULES_FILE_NAME) } } @OutputDirectory @@ -37,6 +40,7 @@ abstract class AbstractUnpackDefaultComposeApplicationResourcesTask : AbstractCo unpack(iconSourcePath("mac", "icns"), resources.macIcon) unpack(iconSourcePath("windows", "ico"), resources.windowsIcon) unpack(iconSourcePath("linux", "png"), resources.linuxIcon) + unpack(DEFAULT_COMPOSE_PROGUARD_RULES_FILE_NAME, resources.defaultComposeProguardRules) } private fun iconSourcePath(platformName: String, iconExt: String): String = diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/registerSimulatorTasks.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/registerSimulatorTasks.kt index fda96082e1a..aedab089042 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/registerSimulatorTasks.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/registerSimulatorTasks.kt @@ -13,8 +13,6 @@ import org.jetbrains.compose.desktop.application.internal.currentArch import org.jetbrains.compose.experimental.dsl.DeployTarget import org.jetbrains.compose.experimental.dsl.UiKitConfiguration import org.jetbrains.compose.experimental.uikit.tasks.AbstractComposeIosTask -import java.io.File - fun Project.registerSimulatorTasks( id: String, diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/stringUtils.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/stringUtils.kt index 1ea23171b15..202b5494c70 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/stringUtils.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/stringUtils.kt @@ -33,6 +33,11 @@ private inline fun String.transformFirstCharIfNeeded( return this } +internal fun joinDashLowercaseNonEmpty(vararg parts: String): String = + parts + .filter { it.isNotEmpty() } + .joinToString(separator = "-") { it.lowercase() } + internal fun joinLowerCamelCase(vararg parts: String): String = parts.withIndex().joinToString(separator = "") { (i, part) -> if (i == 0) part.lowercaseFirstChar() else part.uppercaseFirstChar() diff --git a/gradle-plugins/compose/src/main/resources/default-compose-desktop-rules.pro b/gradle-plugins/compose/src/main/resources/default-compose-desktop-rules.pro new file mode 100644 index 00000000000..2ef55d7a5ec --- /dev/null +++ b/gradle-plugins/compose/src/main/resources/default-compose-desktop-rules.pro @@ -0,0 +1,33 @@ +-keep class kotlin.** { *; } +-keep class org.jetbrains.skia.** { *; } +-keep class org.jetbrains.skiko.** { *; } + +-assumenosideeffects public class androidx.compose.runtime.ComposerKt { + void sourceInformation(androidx.compose.runtime.Composer,java.lang.String); + void sourceInformationMarkerStart(androidx.compose.runtime.Composer,int,java.lang.String); + void sourceInformationMarkerEnd(androidx.compose.runtime.Composer); + boolean isTraceInProgress(); + void traceEventStart(int, java.lang.String); + void traceEventEnd(); +} + +# Kotlinx Coroutines Rules +# https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-core/jvm/resources/META-INF/proguard/coroutines.pro + +-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {} +-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {} +-keepclassmembers class kotlinx.coroutines.** { + volatile ; +} +-keepclassmembers class kotlin.coroutines.SafeContinuation { + volatile ; +} +-dontwarn java.lang.instrument.ClassFileTransformer +-dontwarn sun.misc.SignalHandler +-dontwarn java.lang.instrument.Instrumentation +-dontwarn sun.misc.Signal +-dontwarn java.lang.ClassValue +-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement + +# https://github.com/Kotlin/kotlinx.coroutines/issues/2046 +-dontwarn android.annotation.SuppressLint \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/FileHashTest.kt b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/FileHashTest.kt index b437ff2fa62..a92e26a8e96 100644 --- a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/FileHashTest.kt +++ b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/FileHashTest.kt @@ -8,7 +8,7 @@ package org.jetbrains.compose import org.gradle.internal.impldep.org.testng.Assert import org.jetbrains.compose.desktop.application.internal.OS import org.jetbrains.compose.desktop.application.internal.currentOS -import org.jetbrains.compose.desktop.application.internal.files.fileHash +import org.jetbrains.compose.desktop.application.internal.files.contentHash import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir import java.io.File @@ -38,8 +38,8 @@ class FileHashTest { input1.writeText("2") val modifiedJar = createJar("modified", input1) - val initHash = fileHash(initJar) - val modifiedHash = fileHash(modifiedJar) + val initHash = initJar.contentHash() + val modifiedHash = modifiedJar.contentHash() Assert.assertNotEquals(modifiedHash, initHash) } diff --git a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/gradle/DesktopApplicationTest.kt b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/gradle/DesktopApplicationTest.kt index b7144d8a111..f6c2bd53396 100644 --- a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/gradle/DesktopApplicationTest.kt +++ b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/gradle/DesktopApplicationTest.kt @@ -61,6 +61,21 @@ class DesktopApplicationTest : GradlePluginTestBase() { gradle(":package", "--dry-run").build() } + @Test + fun proguard(): Unit = with(testProject(TestProjects.proguard)) { + gradle(":runReleaseDistributable").build().checks { check -> + check.taskOutcome(":proguardReleaseJars", TaskOutcome.SUCCESS) + + assertEqualTextFiles(file("main-methods.actual.txt"), file("main-methods.expected.txt")) + + val actualMainImage = file("main-image.actual.png") + val expectedMainImage = file("main-image.expected.png") + assert(actualMainImage.readBytes().contentEquals(expectedMainImage.readBytes())) { + "The actual image '$actualMainImage' does not match the expected image '$expectedMainImage'" + } + } + } + @Test fun packageJvm() = with(testProject(TestProjects.jvm)) { testPackageNativeExecutables() diff --git a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/TestProjects.kt b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/TestProjects.kt index 7fe5184de14..bc9093ca29c 100644 --- a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/TestProjects.kt +++ b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/TestProjects.kt @@ -8,6 +8,7 @@ package org.jetbrains.compose.test object TestProjects { const val jvm = "application/jvm" const val mpp = "application/mpp" + const val proguard = "application/proguard" const val jvmKotlinDsl = "application/jvmKotlinDsl" const val moduleClashCli = "application/moduleClashCli" const val javaLogger = "application/javaLogger" diff --git a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/assertUtils.kt b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/assertUtils.kt index 40942d82704..cc500f0376d 100644 --- a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/assertUtils.kt +++ b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/assertUtils.kt @@ -5,8 +5,12 @@ package org.jetbrains.compose.test +import org.gradle.internal.impldep.junit.framework.Assert import org.gradle.testkit.runner.BuildResult import org.gradle.testkit.runner.TaskOutcome +import org.junit.jupiter.api.Assertions +import java.io.File +import kotlin.math.exp internal fun Collection.checkContains(vararg elements: T) { val expectedElements = elements.toMutableSet() @@ -46,4 +50,16 @@ internal fun String.checkContains(substring: String) { if (!contains(substring)) { throw AssertionError("String '$substring' is not found in text:\n$this") } +} + +internal fun assertEqualTextFiles(actual: File, expected: File) { + fun File.normalizedText() = readLines().joinToString("\n") { it.trim() } + + val actualText = actual.normalizedText() + val expectedText = expected.normalizedText() + Assertions.assertEquals( + expectedText, + actualText, + "Expected file '$expected' differs from actual file '$actual'" + ) } \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/application/proguard/build.gradle b/gradle-plugins/compose/src/test/test-projects/application/proguard/build.gradle new file mode 100644 index 00000000000..dfa5d41cc5e --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/application/proguard/build.gradle @@ -0,0 +1,30 @@ +import org.jetbrains.compose.desktop.application.dsl.TargetFormat + +plugins { + id "org.jetbrains.kotlin.jvm" + id "org.jetbrains.compose" +} + +repositories { + jetbrainsCompose() +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib" + implementation compose.desktop.currentOs +} + +compose.desktop { + application { + mainClass = "Main" + args(project.projectDir.absolutePath) + nativeDistributions { + targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) + } + args(project.projectDir.absolutePath) + + buildTypes.release.proguard { + configurationFiles.from("rules.pro") + } + } +} \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/application/proguard/main-image.expected.png b/gradle-plugins/compose/src/test/test-projects/application/proguard/main-image.expected.png new file mode 100644 index 0000000000000000000000000000000000000000..525220fb6bf3e4ebbd146242c0fb222cc0a89ad5 GIT binary patch literal 140 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2V8<6ZZI=>f4u@pObhHwBu4M$1`knim2;us<^ zwe`$FUIqmY=Zk&EUgUh+yTEP2g(cIMRR(b#5}5W~s71NPO0p?qecQJFdeh9kv$pKN ll@Xe|C0usPYQ9^6;;*_1PE?xit literal 0 HcmV?d00001 diff --git a/gradle-plugins/compose/src/test/test-projects/application/proguard/main-methods.expected.txt b/gradle-plugins/compose/src/test/test-projects/application/proguard/main-methods.expected.txt new file mode 100644 index 00000000000..3fc7a4f8c76 --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/application/proguard/main-methods.expected.txt @@ -0,0 +1,3 @@ +keptByKeepRule +main +mainShape \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/application/proguard/rules.pro b/gradle-plugins/compose/src/test/test-projects/application/proguard/rules.pro new file mode 100644 index 00000000000..ae99cff552d --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/application/proguard/rules.pro @@ -0,0 +1,7 @@ +-keep public class Main { + public void keptByKeepRule(...); +} + +-keepclassmembernames public class Main { + *; +} \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/application/proguard/settings.gradle b/gradle-plugins/compose/src/test/test-projects/application/proguard/settings.gradle new file mode 100644 index 00000000000..8d7ab43b40d --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/application/proguard/settings.gradle @@ -0,0 +1,11 @@ +pluginManagement { + plugins { + id 'org.jetbrains.kotlin.jvm' version 'KOTLIN_VERSION_PLACEHOLDER' + id 'org.jetbrains.compose' version 'COMPOSE_VERSION_PLACEHOLDER' + } + repositories { + mavenLocal() + gradlePluginPortal() + } +} +rootProject.name = "simple" \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/application/proguard/src/main/kotlin/Main.kt b/gradle-plugins/compose/src/test/test-projects/application/proguard/src/main/kotlin/Main.kt new file mode 100644 index 00000000000..e7d73f0092f --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/application/proguard/src/main/kotlin/Main.kt @@ -0,0 +1,76 @@ +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.GenericShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.renderComposeScene +import org.jetbrains.skia.EncodedImageFormat +import java.io.File +import java.util.* + +object Main { + @JvmStatic + @OptIn(ExperimentalComposeUiApi::class) + fun main(args: Array) { + val workingDir = args.getOrNull(0)?.let { File(it) } + workingDir?.mkdirs() + if (workingDir == null || !workingDir.isDirectory) { + error("Working directory must be passes as the first argument. '$workingDir' is not a directory") + } + + val image = renderComposeScene(height = 10, width = 10) { + mainShape() + } + val encodedImage = image.encodeToData(EncodedImageFormat.PNG) ?: error("Could not encode image as png") + workingDir.resolve("main-image.actual.png").writeBytes(encodedImage.bytes) + + val mainMethods = this.javaClass.declaredMethods + .mapTo(TreeSet()) { it.name } + .joinToString("\n") + workingDir.resolve("main-methods.actual.txt").writeText(mainMethods) + } + + @Composable + fun mainShape() { + triangle(Color.Magenta) + } + + @Composable + fun unused() { + transitivelyUnused() + } + + @Composable + fun transitivelyUnused() { + triangle(Color.Gray) + } + + @Composable + fun keptByKeepRule() { + fillShape(Color.Blue, CircleShape) + } +} + +@Composable +fun triangle(color: Color) { + fillShape(color, GenericShape { size, _ -> + moveTo(size.width / 2f, 0f) + lineTo(size.width, size.height) + lineTo(0f, size.height) + }) +} + +@Composable +fun fillShape(color: Color, shape: Shape){ + Column(modifier = Modifier.fillMaxWidth().wrapContentSize(Alignment.Center)) { + Box( + modifier = Modifier.clip(shape).fillMaxSize().background(color) + ) + } +} \ No newline at end of file diff --git a/gradle-plugins/gradle.properties b/gradle-plugins/gradle.properties index a56ca4a0142..4ecce170188 100644 --- a/gradle-plugins/gradle.properties +++ b/gradle-plugins/gradle.properties @@ -6,7 +6,7 @@ kotlin.code.style=official # unless overridden by COMPOSE_GRADLE_PLUGIN_COMPOSE_VERSION env var. # # __LATEST_COMPOSE_RELEASE_VERSION__ -compose.version=0.0.0-master-dev673 +compose.version=1.2.0-alpha01-dev774 compose.compiler.version=1.3.0-alpha01 @@ -19,7 +19,7 @@ compose.compiler.compatible.kotlin.version=1.7.10 # This Kotlin version should be used in test or for checking plugin's compatibility, # not for building the Gradle plugin itself! # __KOTLIN_COMPOSE_VERSION__ -kotlin.version=1.6.21 +kotlin.version=1.7.10 # A version of Gradle plugin, that will be published, # unless overridden by COMPOSE_GRADLE_PLUGIN_VERSION env var.