Browse Source

Support packaging resources into distribution (#983)

Resolves #938
pull/988/head
Alexey Tsvetkov 3 years ago committed by GitHub
parent
commit
3070856954
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/NativeDistributions.kt
  2. 10
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/ComposeSystemProperties.kt
  3. 4
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/cliArgUtils.kt
  4. 53
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureApplication.kt
  5. 66
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJPackageTask.kt
  6. 11
      gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/gradle/DesktopApplicationTest.kt
  7. 1
      gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/TestProjects.kt
  8. 31
      gradle-plugins/compose/src/test/test-projects/application/resources/build.gradle
  9. 1
      gradle-plugins/compose/src/test/test-projects/application/resources/resources/common/common-resource.txt
  10. 1
      gradle-plugins/compose/src/test/test-projects/application/resources/resources/linux-arm64/target-specific-resource.txt
  11. 1
      gradle-plugins/compose/src/test/test-projects/application/resources/resources/linux-x64/target-specific-resource.txt
  12. 1
      gradle-plugins/compose/src/test/test-projects/application/resources/resources/linux/os-specific-resource.txt
  13. 1
      gradle-plugins/compose/src/test/test-projects/application/resources/resources/macos-arm64/target-specific-resource.txt
  14. 1
      gradle-plugins/compose/src/test/test-projects/application/resources/resources/macos-x64/target-specific-resource.txt
  15. 1
      gradle-plugins/compose/src/test/test-projects/application/resources/resources/macos/os-specific-resource.txt
  16. 1
      gradle-plugins/compose/src/test/test-projects/application/resources/resources/windows-arm64/target-specific-resource.txt
  17. 1
      gradle-plugins/compose/src/test/test-projects/application/resources/resources/windows-x64/target-specific-resource.txt
  18. 1
      gradle-plugins/compose/src/test/test-projects/application/resources/resources/windows/os-specific-resource.txt
  19. 11
      gradle-plugins/compose/src/test/test-projects/application/resources/settings.gradle
  20. 50
      gradle-plugins/compose/src/test/test-projects/application/resources/src/main/kotlin/main.kt

1
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 copyright: String? = null
var vendor: String? = null var vendor: String? = null
var packageVersion: String? = null var packageVersion: String? = null
val appResourcesRootDir: DirectoryProperty = objects.directoryProperty()
val outputBaseDir: DirectoryProperty = objects.directoryProperty().apply { val outputBaseDir: DirectoryProperty = objects.directoryProperty().apply {
set(layout.buildDirectory.dir("compose/binaries")) set(layout.buildDirectory.dir("compose/binaries"))

10
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"

4
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/cliArgUtils.kt

@ -30,6 +30,10 @@ internal fun <T : Any?> MutableCollection<String>.cliArg(
cliArg(name, value.orNull, fn) cliArg(name, value.orNull, fn)
} }
internal fun MutableCollection<String>.javaOption(value: String) {
cliArg("--java-options", "'$value'")
}
private fun <T : Any?> defaultToString(): (T) -> String = private fun <T : Any?> defaultToString(): (T) -> String =
{ {
val asString = when (it) { val asString = when (it) {

53
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.io.File
import java.util.* 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: multiple launchers
// todo: file associations // todo: file associations
@ -80,6 +80,20 @@ internal fun Project.configurePackagingTasks(apps: Collection<Application>) {
} }
} }
val prepareAppResources = tasks.composeTask<Sync>(
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<AbstractJLinkTask>( val createRuntimeImage = tasks.composeTask<AbstractJLinkTask>(
taskName("createRuntimeImage", app) taskName("createRuntimeImage", app)
) { ) {
@ -95,7 +109,11 @@ internal fun Project.configurePackagingTasks(apps: Collection<Application>) {
taskName("createDistributable", app), taskName("createDistributable", app),
args = listOf(TargetFormat.AppImage) args = listOf(TargetFormat.AppImage)
) { ) {
configurePackagingTask(app, createRuntimeImage = createRuntimeImage) configurePackagingTask(
app,
createRuntimeImage = createRuntimeImage,
prepareAppResources = prepareAppResources
)
} }
val packageFormats = app.nativeDistributions.targetFormats.map { targetFormat -> val packageFormats = app.nativeDistributions.targetFormats.map { targetFormat ->
@ -110,7 +128,11 @@ internal fun Project.configurePackagingTasks(apps: Collection<Application>) {
// in some cases there are failures with JDK 15. // in some cases there are failures with JDK 15.
// See [AbstractJPackageTask.patchInfoPlistIfNeeded] // See [AbstractJPackageTask.patchInfoPlistIfNeeded]
if (currentOS != OS.MacOS) { if (currentOS != OS.MacOS) {
configurePackagingTask(app, createRuntimeImage = createRuntimeImage) configurePackagingTask(
app,
createRuntimeImage = createRuntimeImage,
prepareAppResources = prepareAppResources
)
} else { } else {
configurePackagingTask(app, createAppImage = createDistributable) configurePackagingTask(app, createAppImage = createDistributable)
} }
@ -153,7 +175,7 @@ internal fun Project.configurePackagingTasks(apps: Collection<Application>) {
) )
val run = project.tasks.composeTask<JavaExec>(taskName("run", app)) { val run = project.tasks.composeTask<JavaExec>(taskName("run", app)) {
configureRunTask(app) configureRunTask(app, prepareAppResources = prepareAppResources)
} }
} }
} }
@ -161,7 +183,8 @@ internal fun Project.configurePackagingTasks(apps: Collection<Application>) {
internal fun AbstractJPackageTask.configurePackagingTask( internal fun AbstractJPackageTask.configurePackagingTask(
app: Application, app: Application,
createAppImage: TaskProvider<AbstractJPackageTask>? = null, createAppImage: TaskProvider<AbstractJPackageTask>? = null,
createRuntimeImage: TaskProvider<AbstractJLinkTask>? = null createRuntimeImage: TaskProvider<AbstractJLinkTask>? = null,
prepareAppResources: TaskProvider<Sync>? = null
) { ) {
enabled = targetFormat.isCompatibleWithCurrentOS enabled = targetFormat.isCompatibleWithCurrentOS
@ -175,6 +198,12 @@ internal fun AbstractJPackageTask.configurePackagingTask(
runtimeImage.set(createRuntimeImage.flatMap { it.destinationDir }) 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) configurePlatformSettings(app)
app.nativeDistributions.let { executables -> 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<Sync>
) {
dependsOn(prepareAppResources)
mainClass.set(provider { app.mainClass }) mainClass.set(provider { app.mainClass })
executable(javaExecutable(app.javaHomeOrDefault())) executable(javaExecutable(app.javaHomeOrDefault()))
jvmArgs = defaultJvmArgs + app.jvmArgs jvmArgs = arrayListOf<String>().apply {
addAll(defaultJvmArgs)
addAll(app.jvmArgs)
val appResourcesDir = prepareAppResources.get().destinationDir
add("-D$APP_RESOURCES_DIR=${appResourcesDir.absolutePath}")
}
args = app.args args = app.args
val cp = project.objects.fileCollection() val cp = project.objects.fileCollection()

66
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.process.ExecResult
import org.gradle.work.ChangeType import org.gradle.work.ChangeType
import org.gradle.work.InputChanges 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.MacOSSigningSettings
import org.jetbrains.compose.desktop.application.dsl.TargetFormat import org.jetbrains.compose.desktop.application.dsl.TargetFormat
import org.jetbrains.compose.desktop.application.internal.* 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.MacJarSignFileCopyingProcessor
import org.jetbrains.compose.desktop.application.internal.files.fileHash 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.files.transformJar
import org.jetbrains.compose.desktop.application.internal.validation.ValidatedMacOSSigningSettings
import org.jetbrains.compose.desktop.application.internal.validation.validate import org.jetbrains.compose.desktop.application.internal.validation.validate
import java.io.* import java.io.*
import java.nio.file.Files
import java.util.* import java.util.*
import java.util.zip.ZipEntry import java.util.zip.ZipEntry
import javax.inject.Inject import javax.inject.Inject
@ -190,7 +187,7 @@ abstract class AbstractJPackageTask @Inject constructor(
protected val signDir: Provider<Directory> = project.layout.buildDirectory.dir("compose/tmp/sign") protected val signDir: Provider<Directory> = project.layout.buildDirectory.dir("compose/tmp/sign")
@get:LocalState @get:LocalState
protected val resourcesDir: Provider<Directory> = project.layout.buildDirectory.dir("compose/tmp/resources") protected val jpackageResources: Provider<Directory> = project.layout.buildDirectory.dir("compose/tmp/resources")
@get:LocalState @get:LocalState
protected val skikoDir: Provider<Directory> = project.layout.buildDirectory.dir("compose/tmp/skiko") protected val skikoDir: Provider<Directory> = project.layout.buildDirectory.dir("compose/tmp/skiko")
@ -200,6 +197,31 @@ abstract class AbstractJPackageTask @Inject constructor(
it.dir("libs") it.dir("libs")
} }
@get:Internal
private val packagedResourcesDir: Provider<Directory> = 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<Directory>
get() = appResourcesDir.map { it.takeIf { it.asFile.exists() } }
@get:Internal @get:Internal
private val libsMappingFile: Provider<RegularFile> = workingDir.map { private val libsMappingFile: Provider<RegularFile> = workingDir.map {
it.file("libs-mapping.txt") it.file("libs-mapping.txt")
@ -209,11 +231,16 @@ abstract class AbstractJPackageTask @Inject constructor(
private val libsMapping = FilesMapping() private val libsMapping = FilesMapping()
override fun makeArgs(tmpDir: File): MutableList<String> = super.makeArgs(tmpDir).apply { override fun makeArgs(tmpDir: File): MutableList<String> = super.makeArgs(tmpDir).apply {
fun appDir(vararg pathParts: String): String =
listOf("${'$'}APPDIR", *pathParts).joinToString(File.separator) { it }
if (targetFormat == TargetFormat.AppImage || appImage.orNull == null) { 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 // Args, that can only be used, when creating an app image or an installer w/o --app-image parameter
cliArg("--input", libsDir) cliArg("--input", libsDir)
cliArg("--runtime-image", runtimeImage) 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() val mappedJar = libsMapping[launcherMainJar.ioFile]?.singleOrNull()
?: error("Main jar was not processed correctly: ${launcherMainJar.ioFile}") ?: error("Main jar was not processed correctly: ${launcherMainJar.ioFile}")
@ -230,12 +257,12 @@ abstract class AbstractJPackageTask @Inject constructor(
cliArg("--arguments", "'$it'") cliArg("--arguments", "'$it'")
} }
launcherJvmArgs.orNull?.forEach { 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) { if (currentOS == OS.MacOS) {
macDockName.orNull?.let { dockName -> 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) // todo: incremental copy
fileOperations.mkdir(resourcesDir) 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) { if (currentOS == OS.MacOS) {
InfoPlistBuilder(macExtraPlistKeysRawXml.orNull) InfoPlistBuilder(macExtraPlistKeysRawXml.orNull)
.also { setInfoPlistValues(it) } .also { setInfoPlistValues(it) }
.writeToFile(resourcesDir.ioFile.resolve("Info.plist")) .writeToFile(jpackageResources.ioFile.resolve("Info.plist"))
} }
} }

11
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)
}
}
} }

1
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 defaultArgs = "application/defaultArgs"
const val defaultArgsOverride = "application/defaultArgsOverride" const val defaultArgsOverride = "application/defaultArgsOverride"
const val unpackSkiko = "application/unpackSkiko" const val unpackSkiko = "application/unpackSkiko"
const val resources = "application/resources"
const val jsMpp = "misc/jsMpp" const val jsMpp = "misc/jsMpp"
const val jvmPreview = "misc/jvmPreview" const val jvmPreview = "misc/jvmPreview"
} }

31
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"))
}
}
}

1
gradle-plugins/compose/src/test/test-projects/application/resources/resources/common/common-resource.txt

@ -0,0 +1 @@
common resource

1
gradle-plugins/compose/src/test/test-projects/application/resources/resources/linux-arm64/target-specific-resource.txt

@ -0,0 +1 @@
linux-arm64 only resource

1
gradle-plugins/compose/src/test/test-projects/application/resources/resources/linux-x64/target-specific-resource.txt

@ -0,0 +1 @@
linux-x64 only resource

1
gradle-plugins/compose/src/test/test-projects/application/resources/resources/linux/os-specific-resource.txt

@ -0,0 +1 @@
linux only resource

1
gradle-plugins/compose/src/test/test-projects/application/resources/resources/macos-arm64/target-specific-resource.txt

@ -0,0 +1 @@
macos-arm64 only resource

1
gradle-plugins/compose/src/test/test-projects/application/resources/resources/macos-x64/target-specific-resource.txt

@ -0,0 +1 @@
macos-x64 only resource

1
gradle-plugins/compose/src/test/test-projects/application/resources/resources/macos/os-specific-resource.txt

@ -0,0 +1 @@
macos only resource

1
gradle-plugins/compose/src/test/test-projects/application/resources/resources/windows-arm64/target-specific-resource.txt

@ -0,0 +1 @@
windows-arm64 only resource

1
gradle-plugins/compose/src/test/test-projects/application/resources/resources/windows-x64/target-specific-resource.txt

@ -0,0 +1 @@
windows-x64 only resource

1
gradle-plugins/compose/src/test/test-projects/application/resources/resources/windows/os-specific-resource.txt

@ -0,0 +1 @@
windows only resource

11
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"

50
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")
}
}
Loading…
Cancel
Save