diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/NativeDistributions.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/NativeDistributions.kt index d0f0166ad0..f0bb01dad5 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/NativeDistributions.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/NativeDistributions.kt @@ -25,6 +25,7 @@ open class NativeDistributions @Inject constructor( var copyright: String? = null var vendor: String? = null var packageVersion: String? = null + val appResourcesRootDir: DirectoryProperty = objects.directoryProperty() val outputBaseDir: DirectoryProperty = objects.directoryProperty().apply { set(layout.buildDirectory.dir("compose/binaries")) diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/ComposeSystemProperties.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/ComposeSystemProperties.kt new file mode 100644 index 0000000000..b26c0519d8 --- /dev/null +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/ComposeSystemProperties.kt @@ -0,0 +1,10 @@ +/* + * 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 + +internal const val APP_RESOURCES_DIR = "compose.application.resources.dir" +internal const val SKIKO_LIBRARY_PATH = "skiko.library.path" +internal const val CONFIGURE_SWING_GLOBALS = "compose.application.configure.swing.globals" 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 e8c3c385e1..0a67b6306a 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 @@ -30,6 +30,10 @@ internal fun MutableCollection.cliArg( cliArg(name, value.orNull, fn) } +internal fun MutableCollection.javaOption(value: String) { + cliArg("--java-options", "'$value'") +} + private fun defaultToString(): (T) -> String = { val asString = when (it) { diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureApplication.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureApplication.kt index 66ed99703e..90e3f6a6c2 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureApplication.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureApplication.kt @@ -21,7 +21,7 @@ import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType import java.io.File import java.util.* -private val defaultJvmArgs = listOf("-Dcompose.application.configure.swing.globals=true") +private val defaultJvmArgs = listOf("-D$CONFIGURE_SWING_GLOBALS=true") // todo: multiple launchers // todo: file associations @@ -80,6 +80,20 @@ internal fun Project.configurePackagingTasks(apps: Collection) { } } + val prepareAppResources = tasks.composeTask( + 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 createRuntimeImage = tasks.composeTask( taskName("createRuntimeImage", app) ) { @@ -95,7 +109,11 @@ internal fun Project.configurePackagingTasks(apps: Collection) { taskName("createDistributable", app), args = listOf(TargetFormat.AppImage) ) { - configurePackagingTask(app, createRuntimeImage = createRuntimeImage) + configurePackagingTask( + app, + createRuntimeImage = createRuntimeImage, + prepareAppResources = prepareAppResources + ) } val packageFormats = app.nativeDistributions.targetFormats.map { targetFormat -> @@ -110,7 +128,11 @@ internal fun Project.configurePackagingTasks(apps: Collection) { // in some cases there are failures with JDK 15. // See [AbstractJPackageTask.patchInfoPlistIfNeeded] if (currentOS != OS.MacOS) { - configurePackagingTask(app, createRuntimeImage = createRuntimeImage) + configurePackagingTask( + app, + createRuntimeImage = createRuntimeImage, + prepareAppResources = prepareAppResources + ) } else { configurePackagingTask(app, createAppImage = createDistributable) } @@ -153,7 +175,7 @@ internal fun Project.configurePackagingTasks(apps: Collection) { ) val run = project.tasks.composeTask(taskName("run", app)) { - configureRunTask(app) + configureRunTask(app, prepareAppResources = prepareAppResources) } } } @@ -161,7 +183,8 @@ internal fun Project.configurePackagingTasks(apps: Collection) { internal fun AbstractJPackageTask.configurePackagingTask( app: Application, createAppImage: TaskProvider? = null, - createRuntimeImage: TaskProvider? = null + createRuntimeImage: TaskProvider? = null, + prepareAppResources: TaskProvider? = null ) { enabled = targetFormat.isCompatibleWithCurrentOS @@ -175,6 +198,12 @@ internal fun AbstractJPackageTask.configurePackagingTask( runtimeImage.set(createRuntimeImage.flatMap { it.destinationDir }) } + prepareAppResources?.let { prepareResources -> + dependsOn(prepareResources) + val resourcesDir = project.layout.dir(prepareResources.map { it.destinationDir }) + appResourcesDir.set(resourcesDir) + } + configurePlatformSettings(app) app.nativeDistributions.let { executables -> @@ -276,10 +305,20 @@ internal fun AbstractJPackageTask.configurePlatformSettings(app: Application) { } } -private fun JavaExec.configureRunTask(app: Application) { +private fun JavaExec.configureRunTask( + app: Application, + prepareAppResources: TaskProvider +) { + dependsOn(prepareAppResources) + mainClass.set(provider { app.mainClass }) executable(javaExecutable(app.javaHomeOrDefault())) - jvmArgs = defaultJvmArgs + app.jvmArgs + jvmArgs = arrayListOf().apply { + addAll(defaultJvmArgs) + addAll(app.jvmArgs) + val appResourcesDir = prepareAppResources.get().destinationDir + add("-D$APP_RESOURCES_DIR=${appResourcesDir.absolutePath}") + } args = app.args val cp = project.objects.fileCollection() 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 bf9c2b002e..5be642a196 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 @@ -15,7 +15,6 @@ import org.gradle.api.tasks.Optional import org.gradle.process.ExecResult import org.gradle.work.ChangeType import org.gradle.work.InputChanges -import org.jetbrains.compose.desktop.application.dsl.InfoPlistSettings import org.jetbrains.compose.desktop.application.dsl.MacOSSigningSettings import org.jetbrains.compose.desktop.application.dsl.TargetFormat import org.jetbrains.compose.desktop.application.internal.* @@ -23,10 +22,8 @@ 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.validation.ValidatedMacOSSigningSettings import org.jetbrains.compose.desktop.application.internal.validation.validate import java.io.* -import java.nio.file.Files import java.util.* import java.util.zip.ZipEntry import javax.inject.Inject @@ -190,7 +187,7 @@ abstract class AbstractJPackageTask @Inject constructor( protected val signDir: Provider = project.layout.buildDirectory.dir("compose/tmp/sign") @get:LocalState - protected val resourcesDir: Provider = project.layout.buildDirectory.dir("compose/tmp/resources") + protected val jpackageResources: Provider = project.layout.buildDirectory.dir("compose/tmp/resources") @get:LocalState protected val skikoDir: Provider = project.layout.buildDirectory.dir("compose/tmp/skiko") @@ -200,6 +197,31 @@ abstract class AbstractJPackageTask @Inject constructor( it.dir("libs") } + @get:Internal + private val packagedResourcesDir: Provider = libsDir.map { + it.dir("resources") + } + + @get:Internal + val appResourcesDir: DirectoryProperty = objects.directoryProperty() + + /** + * Gradle runtime verification fails, + * if InputDirectory is not null, but a directory does not exist. + * The directory might not exist, because prepareAppResources task + * does not create output directory if there are no resources. + * + * To work around this, appResourcesDir is used as a real property, + * but it is annotated as @Internal, so it ignored during inputs checking. + * This property is used only for inputs checking. + * It returns appResourcesDir value if the underlying directory exists. + */ + @Suppress("unused") + @get:InputDirectory + @get:Optional + internal val appResourcesDirInputDirHackForVerification: Provider + get() = appResourcesDir.map { it.takeIf { it.asFile.exists() } } + @get:Internal private val libsMappingFile: Provider = workingDir.map { it.file("libs-mapping.txt") @@ -209,11 +231,16 @@ abstract class AbstractJPackageTask @Inject constructor( private val libsMapping = FilesMapping() override fun makeArgs(tmpDir: File): MutableList = super.makeArgs(tmpDir).apply { + fun appDir(vararg pathParts: String): String = + listOf("${'$'}APPDIR", *pathParts).joinToString(File.separator) { it } + if (targetFormat == TargetFormat.AppImage || appImage.orNull == null) { // Args, that can only be used, when creating an app image or an installer w/o --app-image parameter cliArg("--input", libsDir) cliArg("--runtime-image", runtimeImage) - cliArg("--resource-dir", resourcesDir) + cliArg("--resource-dir", jpackageResources) + + javaOption("-D$APP_RESOURCES_DIR=${appDir(packagedResourcesDir.ioFile.name)}") val mappedJar = libsMapping[launcherMainJar.ioFile]?.singleOrNull() ?: error("Main jar was not processed correctly: ${launcherMainJar.ioFile}") @@ -230,12 +257,12 @@ abstract class AbstractJPackageTask @Inject constructor( cliArg("--arguments", "'$it'") } launcherJvmArgs.orNull?.forEach { - cliArg("--java-options", "'$it'") + javaOption(it) } - cliArg("--java-options", "'-Dskiko.library.path=${'$'}APPDIR'") + javaOption("-D$SKIKO_LIBRARY_PATH=${appDir()}") if (currentOS == OS.MacOS) { macDockName.orNull?.let { dockName -> - cliArg("--java-options", "'-Xdock:name=$dockName'") + javaOption("-Xdock:name=$dockName") } } } @@ -363,12 +390,29 @@ abstract class AbstractJPackageTask @Inject constructor( } } - fileOperations.delete(resourcesDir) - fileOperations.mkdir(resourcesDir) + // todo: incremental copy + val destResourcesDir = packagedResourcesDir.ioFile + fileOperations.delete(destResourcesDir) + fileOperations.mkdir(destResourcesDir) + val appResourcesDir = appResourcesDir.ioFileOrNull + if (appResourcesDir != null) { + for (file in appResourcesDir.walk()) { + val relPath = file.relativeTo(appResourcesDir).path + val destFile = destResourcesDir.resolve(relPath) + if (file.isDirectory) { + fileOperations.mkdir(destFile) + } else { + file.copyTo(destFile) + } + } + } + + fileOperations.delete(jpackageResources) + fileOperations.mkdir(jpackageResources) if (currentOS == OS.MacOS) { InfoPlistBuilder(macExtraPlistKeysRawXml.orNull) .also { setInfoPlistValues(it) } - .writeToFile(resourcesDir.ioFile.resolve("Info.plist")) + .writeToFile(jpackageResources.ioFile.resolve("Info.plist")) } } 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 b1f5132b2a..b8c7fa7b7e 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 @@ -315,4 +315,15 @@ class DesktopApplicationTest : GradlePluginTestBase() { } } } + + @Test + fun resources() = with(testProject(TestProjects.resources)) { + gradle(":run").build().checks { check -> + check.taskOutcome(":run", TaskOutcome.SUCCESS) + } + + gradle(":runDistributable").build().checks { check -> + check.taskOutcome(":runDistributable", TaskOutcome.SUCCESS) + } + } } 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 1f7f1a67ad..7fe5184de1 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 @@ -17,6 +17,7 @@ object TestProjects { const val defaultArgs = "application/defaultArgs" const val defaultArgsOverride = "application/defaultArgsOverride" const val unpackSkiko = "application/unpackSkiko" + const val resources = "application/resources" const val jsMpp = "misc/jsMpp" const val jvmPreview = "misc/jvmPreview" } \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/application/resources/build.gradle b/gradle-plugins/compose/src/test/test-projects/application/resources/build.gradle new file mode 100644 index 0000000000..e4e8a01b88 --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/application/resources/build.gradle @@ -0,0 +1,31 @@ +import org.jetbrains.compose.desktop.application.dsl.TargetFormat + +plugins { + id "org.jetbrains.kotlin.jvm" + id "org.jetbrains.compose" +} + +repositories { + google() + mavenCentral() + maven { + url "https://maven.pkg.jetbrains.space/public/p/compose/dev" + } +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib" + implementation compose.desktop.currentOs +} + +compose.desktop { + application { + mainClass = "MainKt" + nativeDistributions { + targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) + packageVersion = "1.0.0" + + appResourcesRootDir.set(project.layout.projectDirectory.dir("resources")) + } + } +} diff --git a/gradle-plugins/compose/src/test/test-projects/application/resources/resources/common/common-resource.txt b/gradle-plugins/compose/src/test/test-projects/application/resources/resources/common/common-resource.txt new file mode 100644 index 0000000000..09a3c6a27c --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/application/resources/resources/common/common-resource.txt @@ -0,0 +1 @@ +common resource \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/application/resources/resources/linux-arm64/target-specific-resource.txt b/gradle-plugins/compose/src/test/test-projects/application/resources/resources/linux-arm64/target-specific-resource.txt new file mode 100644 index 0000000000..d5e56d78d6 --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/application/resources/resources/linux-arm64/target-specific-resource.txt @@ -0,0 +1 @@ +linux-arm64 only resource \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/application/resources/resources/linux-x64/target-specific-resource.txt b/gradle-plugins/compose/src/test/test-projects/application/resources/resources/linux-x64/target-specific-resource.txt new file mode 100644 index 0000000000..2a1dba9d8e --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/application/resources/resources/linux-x64/target-specific-resource.txt @@ -0,0 +1 @@ +linux-x64 only resource \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/application/resources/resources/linux/os-specific-resource.txt b/gradle-plugins/compose/src/test/test-projects/application/resources/resources/linux/os-specific-resource.txt new file mode 100644 index 0000000000..a31ea557e1 --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/application/resources/resources/linux/os-specific-resource.txt @@ -0,0 +1 @@ +linux only resource \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/application/resources/resources/macos-arm64/target-specific-resource.txt b/gradle-plugins/compose/src/test/test-projects/application/resources/resources/macos-arm64/target-specific-resource.txt new file mode 100644 index 0000000000..120b561cc1 --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/application/resources/resources/macos-arm64/target-specific-resource.txt @@ -0,0 +1 @@ +macos-arm64 only resource \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/application/resources/resources/macos-x64/target-specific-resource.txt b/gradle-plugins/compose/src/test/test-projects/application/resources/resources/macos-x64/target-specific-resource.txt new file mode 100644 index 0000000000..386a1efa07 --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/application/resources/resources/macos-x64/target-specific-resource.txt @@ -0,0 +1 @@ +macos-x64 only resource \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/application/resources/resources/macos/os-specific-resource.txt b/gradle-plugins/compose/src/test/test-projects/application/resources/resources/macos/os-specific-resource.txt new file mode 100644 index 0000000000..676fbe9de2 --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/application/resources/resources/macos/os-specific-resource.txt @@ -0,0 +1 @@ +macos only resource \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/application/resources/resources/windows-arm64/target-specific-resource.txt b/gradle-plugins/compose/src/test/test-projects/application/resources/resources/windows-arm64/target-specific-resource.txt new file mode 100644 index 0000000000..b76ca2430e --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/application/resources/resources/windows-arm64/target-specific-resource.txt @@ -0,0 +1 @@ +windows-arm64 only resource \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/application/resources/resources/windows-x64/target-specific-resource.txt b/gradle-plugins/compose/src/test/test-projects/application/resources/resources/windows-x64/target-specific-resource.txt new file mode 100644 index 0000000000..1de4b09cd9 --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/application/resources/resources/windows-x64/target-specific-resource.txt @@ -0,0 +1 @@ +windows-x64 only resource \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/application/resources/resources/windows/os-specific-resource.txt b/gradle-plugins/compose/src/test/test-projects/application/resources/resources/windows/os-specific-resource.txt new file mode 100644 index 0000000000..62794baeaf --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/application/resources/resources/windows/os-specific-resource.txt @@ -0,0 +1 @@ +windows only resource \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/application/resources/settings.gradle b/gradle-plugins/compose/src/test/test-projects/application/resources/settings.gradle new file mode 100644 index 0000000000..8d7ab43b40 --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/application/resources/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/resources/src/main/kotlin/main.kt b/gradle-plugins/compose/src/test/test-projects/application/resources/src/main/kotlin/main.kt new file mode 100644 index 0000000000..a69630f8ce --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/application/resources/src/main/kotlin/main.kt @@ -0,0 +1,50 @@ +import java.io.File + +fun main() { + checkContent("common-resource.txt", "common resource") + checkContent("os-specific-resource.txt", "$currentOS only resource") + checkContent("target-specific-resource.txt", "$currentTarget only resource") +} + +fun checkContent(actualFileName: String, expectedContent: String) { + val file = composeAppResource(actualFileName) + val actualContent = file.readText().trim() + check(actualContent == expectedContent) { + """ + Actual: '$actualContent' + Expected: '$expectedContent' + """.trimIndent() + } +} + +fun composeAppResource(path: String): File = + composeAppResourceDir.resolve(path) + +private val composeAppResourceDir: File by lazy { + val property = "compose.application.resources.dir" + val path = System.getProperty(property) ?: error("System property '$property' is not set!") + File(path) +} + +internal val currentTarget by lazy { + "$currentOS-$currentArch" +} + +internal val currentOS: String by lazy { + val os = System.getProperty("os.name") + when { + os.equals("Mac OS X", ignoreCase = true) -> "macos" + os.startsWith("Win", ignoreCase = true) -> "windows" + os.startsWith("Linux", ignoreCase = true) -> "linux" + else -> error("Unknown OS name: $os") + } +} + +internal val currentArch by lazy { + val osArch = System.getProperty("os.arch") + when (osArch) { + "x86_64", "amd64" -> "x64" + "aarch64" -> "arm64" + else -> error("Unsupported OS arch: $osArch") + } +} \ No newline at end of file