You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
427 lines
14 KiB
427 lines
14 KiB
/* |
|
* Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. |
|
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. |
|
*/ |
|
|
|
package org.jetbrains.compose.desktop.application.tasks |
|
|
|
import org.gradle.api.file.* |
|
import org.gradle.api.provider.ListProperty |
|
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.gradle.process.ExecResult |
|
import org.gradle.work.ChangeType |
|
import org.gradle.work.InputChanges |
|
import org.jetbrains.compose.desktop.application.dsl.MacOSSigningSettings |
|
import org.jetbrains.compose.desktop.application.dsl.TargetFormat |
|
import org.jetbrains.compose.desktop.application.internal.* |
|
import org.jetbrains.compose.desktop.application.internal.files.MacJarSignFileCopyingProcessor |
|
import org.jetbrains.compose.desktop.application.internal.files.SimpleFileCopyingProcessor |
|
import org.jetbrains.compose.desktop.application.internal.files.fileHash |
|
import org.jetbrains.compose.desktop.application.internal.validation.ValidatedMacOSSigningSettings |
|
import org.jetbrains.compose.desktop.application.internal.validation.validate |
|
import java.io.File |
|
import java.io.Serializable |
|
import java.util.* |
|
import javax.inject.Inject |
|
import kotlin.collections.HashMap |
|
import kotlin.collections.HashSet |
|
import java.io.ObjectInputStream |
|
import java.io.ObjectOutputStream |
|
|
|
abstract class AbstractJPackageTask @Inject constructor( |
|
@get:Input |
|
val targetFormat: TargetFormat, |
|
) : AbstractJvmToolOperationTask("jpackage") { |
|
@get:InputFiles |
|
val files: ConfigurableFileCollection = objects.fileCollection() |
|
|
|
@get:InputDirectory |
|
@get:Optional |
|
/** @see internal/wixToolset.kt */ |
|
val wixToolsetDir: DirectoryProperty = objects.directoryProperty() |
|
|
|
@get:Input |
|
@get:Optional |
|
val installationPath: Property<String?> = objects.nullableProperty() |
|
|
|
@get:InputFile |
|
@get:Optional |
|
@get:PathSensitive(PathSensitivity.ABSOLUTE) |
|
val licenseFile: RegularFileProperty = objects.fileProperty() |
|
|
|
@get:InputFile |
|
@get:Optional |
|
@get:PathSensitive(PathSensitivity.ABSOLUTE) |
|
val iconFile: RegularFileProperty = objects.fileProperty() |
|
|
|
@get:Input |
|
val launcherMainClass: Property<String> = objects.notNullProperty() |
|
|
|
@get:InputFile |
|
@get:PathSensitive(PathSensitivity.ABSOLUTE) |
|
val launcherMainJar: RegularFileProperty = objects.fileProperty() |
|
|
|
@get:Input |
|
@get:Optional |
|
val launcherArgs: ListProperty<String> = objects.listProperty(String::class.java) |
|
|
|
@get:Input |
|
@get:Optional |
|
val launcherJvmArgs: ListProperty<String> = objects.listProperty(String::class.java) |
|
|
|
@get:Input |
|
val packageName: Property<String> = objects.notNullProperty() |
|
|
|
@get:Input |
|
@get:Optional |
|
val packageDescription: Property<String?> = objects.nullableProperty() |
|
|
|
@get:Input |
|
@get:Optional |
|
val packageCopyright: Property<String?> = objects.nullableProperty() |
|
|
|
@get:Input |
|
@get:Optional |
|
val packageVendor: Property<String?> = objects.nullableProperty() |
|
|
|
@get:Input |
|
@get:Optional |
|
val packageVersion: Property<String?> = objects.nullableProperty() |
|
|
|
@get:Input |
|
@get:Optional |
|
val linuxShortcut: Property<Boolean?> = objects.nullableProperty() |
|
|
|
@get:Input |
|
@get:Optional |
|
val linuxPackageName: Property<String?> = objects.nullableProperty() |
|
|
|
@get:Input |
|
@get:Optional |
|
val linuxAppRelease: Property<String?> = objects.nullableProperty() |
|
|
|
@get:Input |
|
@get:Optional |
|
val linuxAppCategory: Property<String?> = objects.nullableProperty() |
|
|
|
@get:Input |
|
@get:Optional |
|
val linuxDebMaintainer: Property<String?> = objects.nullableProperty() |
|
|
|
@get:Input |
|
@get:Optional |
|
val linuxMenuGroup: Property<String?> = objects.nullableProperty() |
|
|
|
@get:Input |
|
@get:Optional |
|
val linuxRpmLicenseType: Property<String?> = objects.nullableProperty() |
|
|
|
@get:Input |
|
@get:Optional |
|
val macPackageName: Property<String?> = objects.nullableProperty() |
|
|
|
@get:Input |
|
@get:Optional |
|
val macDockName: Property<String?> = objects.nullableProperty() |
|
|
|
@get:Input |
|
@get:Optional |
|
val winConsole: Property<Boolean?> = objects.nullableProperty() |
|
|
|
@get:Input |
|
@get:Optional |
|
val winDirChooser: Property<Boolean?> = objects.nullableProperty() |
|
|
|
@get:Input |
|
@get:Optional |
|
val winPerUserInstall: Property<Boolean?> = objects.nullableProperty() |
|
|
|
@get:Input |
|
@get:Optional |
|
val winShortcut: Property<Boolean?> = objects.nullableProperty() |
|
|
|
@get:Input |
|
@get:Optional |
|
val winMenu: Property<Boolean?> = objects.nullableProperty() |
|
|
|
@get:Input |
|
@get:Optional |
|
val winMenuGroup: Property<String?> = objects.nullableProperty() |
|
|
|
@get:Input |
|
@get:Optional |
|
val winUpgradeUuid: Property<String?> = objects.nullableProperty() |
|
|
|
@get:InputDirectory |
|
@get:Optional |
|
val runtimeImage: DirectoryProperty = objects.directoryProperty() |
|
|
|
@get:InputDirectory |
|
@get:Optional |
|
val appImage: DirectoryProperty = objects.directoryProperty() |
|
|
|
@get:Input |
|
@get:Optional |
|
internal val nonValidatedMacBundleID: Property<String?> = objects.nullableProperty() |
|
|
|
@get:Optional |
|
@get:Nested |
|
internal var nonValidatedMacSigningSettings: MacOSSigningSettings? = null |
|
|
|
private inline fun <T> withValidatedMacOSSigning(fn: (ValidatedMacOSSigningSettings) -> T): T? = |
|
nonValidatedMacSigningSettings?.let { nonValidated -> |
|
if (currentOS == OS.MacOS && nonValidated.sign.get()) { |
|
fn(nonValidated.validate(nonValidatedMacBundleID)) |
|
} else null |
|
} |
|
|
|
@get:LocalState |
|
protected val signDir: Provider<Directory> = project.layout.buildDirectory.dir("compose/tmp/sign") |
|
|
|
@get:Internal |
|
private val libsDir: Provider<Directory> = workingDir.map { |
|
it.dir("libs") |
|
} |
|
|
|
@get:Internal |
|
private val libsMappingFile: Provider<RegularFile> = workingDir.map { |
|
it.file("libs-mapping.txt") |
|
} |
|
|
|
@get:Internal |
|
private val libsMapping = FilesMapping() |
|
|
|
override fun makeArgs(tmpDir: File): MutableList<String> = super.makeArgs(tmpDir).apply { |
|
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", tmpDir) |
|
cliArg("--runtime-image", runtimeImage) |
|
|
|
val mappedJar = libsMapping[launcherMainJar.ioFile] |
|
?: error("Main jar was not processed correctly: ${launcherMainJar.ioFile}") |
|
cliArg("--main-jar", mappedJar) |
|
cliArg("--main-class", launcherMainClass) |
|
|
|
when (currentOS) { |
|
OS.Windows -> { |
|
cliArg("--win-console", winConsole) |
|
} |
|
} |
|
cliArg("--icon", iconFile) |
|
launcherArgs.orNull?.forEach { |
|
cliArg("--arguments", "'$it'") |
|
} |
|
launcherJvmArgs.orNull?.forEach { |
|
cliArg("--java-options", "'$it'") |
|
} |
|
if (currentOS == OS.MacOS) { |
|
macDockName.orNull?.let { dockName -> |
|
cliArg("--java-options", "'-Xdock:name=$dockName'") |
|
} |
|
} |
|
} |
|
|
|
if (targetFormat != TargetFormat.AppImage) { |
|
// Args, that can only be used, when creating an installer |
|
cliArg("--app-image", appImage) |
|
cliArg("--install-dir", installationPath) |
|
cliArg("--license-file", licenseFile) |
|
|
|
when (currentOS) { |
|
OS.Linux -> { |
|
cliArg("--linux-shortcut", linuxShortcut) |
|
cliArg("--linux-package-name", linuxPackageName) |
|
cliArg("--linux-app-release", linuxAppRelease) |
|
cliArg("--linux-app-category", linuxAppCategory) |
|
cliArg("--linux-deb-maintainer", linuxDebMaintainer) |
|
cliArg("--linux-menu-group", linuxMenuGroup) |
|
cliArg("--linux-rpm-license-type", linuxRpmLicenseType) |
|
} |
|
OS.Windows -> { |
|
cliArg("--win-dir-chooser", winDirChooser) |
|
cliArg("--win-per-user-install", winPerUserInstall) |
|
cliArg("--win-shortcut", winShortcut) |
|
cliArg("--win-menu", winMenu) |
|
cliArg("--win-menu-group", winMenuGroup) |
|
cliArg("--win-upgrade-uuid", winUpgradeUuid) |
|
} |
|
} |
|
} |
|
|
|
cliArg("--type", targetFormat.id) |
|
|
|
cliArg("--dest", destinationDir) |
|
cliArg("--verbose", verbose) |
|
|
|
cliArg("--name", packageName) |
|
cliArg("--description", packageDescription) |
|
cliArg("--copyright", packageCopyright) |
|
cliArg("--app-version", packageVersion) |
|
cliArg("--vendor", packageVendor) |
|
|
|
when (currentOS) { |
|
OS.MacOS -> { |
|
cliArg("--mac-package-name", macPackageName) |
|
cliArg("--mac-package-identifier", nonValidatedMacBundleID) |
|
|
|
withValidatedMacOSSigning { signing -> |
|
cliArg("--mac-sign", true) |
|
cliArg("--mac-signing-key-user-name", signing.identity) |
|
cliArg("--mac-signing-keychain", signing.keychain) |
|
cliArg("--mac-package-signing-prefix", signing.prefix) |
|
} |
|
} |
|
} |
|
} |
|
|
|
private fun invalidateMappedLibs( |
|
inputChanges: InputChanges |
|
): Set<File> { |
|
val outdatedLibs = HashSet<File>() |
|
val libsDirFile = libsDir.ioFile |
|
|
|
fun invalidateAllLibs() { |
|
outdatedLibs.addAll(files.files) |
|
outdatedLibs.add(launcherMainJar.ioFile) |
|
|
|
logger.debug("Clearing all files in working dir: $libsDirFile") |
|
fileOperations.delete(libsDirFile) |
|
libsDirFile.mkdirs() |
|
} |
|
|
|
if (inputChanges.isIncremental) { |
|
val allChanges = inputChanges.getFileChanges(files).asSequence() + |
|
inputChanges.getFileChanges(launcherMainJar) |
|
|
|
try { |
|
for (change in allChanges) { |
|
libsMapping.remove(change.file)?.let { fileOperations.delete(it) } |
|
if (change.changeType != ChangeType.REMOVED) { |
|
outdatedLibs.add(change.file) |
|
} |
|
} |
|
} catch (e: Exception) { |
|
logger.debug("Could remove outdated libs incrementally: ${e.stacktraceToString()}") |
|
invalidateAllLibs() |
|
} |
|
} else { |
|
invalidateAllLibs() |
|
} |
|
|
|
return outdatedLibs |
|
} |
|
|
|
override fun prepareWorkingDir(inputChanges: InputChanges) { |
|
val libsDirFile = libsDir.ioFile |
|
val fileProcessor = |
|
withValidatedMacOSSigning { signing -> |
|
val tmpDirForSign = signDir.ioFile |
|
fileOperations.delete(tmpDirForSign) |
|
tmpDirForSign.mkdirs() |
|
|
|
MacJarSignFileCopyingProcessor( |
|
tempDir = tmpDirForSign, |
|
execOperations = execOperations, |
|
signing = signing |
|
) |
|
} ?: SimpleFileCopyingProcessor |
|
|
|
val outdatedLibs = invalidateMappedLibs(inputChanges) |
|
for (sourceFile in outdatedLibs) { |
|
assert(sourceFile.exists()) { "Lib file does not exist: $sourceFile" } |
|
|
|
val targetFileName = |
|
if (sourceFile.isJarFile) |
|
"${sourceFile.nameWithoutExtension}-${fileHash(sourceFile)}.jar" |
|
else sourceFile.name |
|
val targetFile = libsDirFile.resolve(targetFileName) |
|
fileProcessor.copy(sourceFile, targetFile) |
|
libsMapping[sourceFile] = targetFile |
|
} |
|
} |
|
|
|
override fun jvmToolEnvironment(): MutableMap<String, String> = |
|
super.jvmToolEnvironment().apply { |
|
if (currentOS == OS.Windows) { |
|
val wixDir = wixToolsetDir.ioFile |
|
val wixPath = wixDir.absolutePath |
|
val path = System.getenv("PATH") ?: "" |
|
put("PATH", "$wixPath;$path") |
|
} |
|
} |
|
|
|
|
|
override fun checkResult(result: ExecResult) { |
|
super.checkResult(result) |
|
val outputFile = findOutputFileOrDir(destinationDir.ioFile, targetFormat) |
|
logger.lifecycle("The distribution is written to ${outputFile.canonicalPath}") |
|
} |
|
|
|
override fun initState() { |
|
val mappingFile = libsMappingFile.ioFile |
|
if (mappingFile.exists()) { |
|
try { |
|
libsMapping.loadFrom(mappingFile) |
|
} catch (e: Exception) { |
|
fileOperations.delete(mappingFile) |
|
throw e |
|
} |
|
logger.debug("Loaded libs mapping from $mappingFile") |
|
} |
|
} |
|
|
|
override fun saveStateAfterFinish() { |
|
val mappingFile = libsMappingFile.ioFile |
|
libsMapping.saveTo(mappingFile) |
|
logger.debug("Saved libs mapping to $mappingFile") |
|
} |
|
} |
|
|
|
// Serializable is only needed to avoid breaking configuration cache: |
|
// https://docs.gradle.org/current/userguide/configuration_cache.html#config_cache:requirements |
|
private class FilesMapping : Serializable { |
|
private var mapping = HashMap<File, File>() |
|
|
|
operator fun get(key: File): File? = |
|
mapping[key] |
|
|
|
operator fun set(key: File, value: File) { |
|
mapping[key] = value |
|
} |
|
|
|
fun remove(key: File): File? = |
|
mapping.remove(key) |
|
|
|
fun loadFrom(mappingFile: File) { |
|
mappingFile.readLines().forEach { line -> |
|
if (line.isNotBlank()) { |
|
val (k, v) = line.split(File.pathSeparatorChar) |
|
mapping[File(k)] = File(v) |
|
} |
|
} |
|
} |
|
|
|
fun saveTo(mappingFile: File) { |
|
mappingFile.parentFile.mkdirs() |
|
mappingFile.bufferedWriter().use { writer -> |
|
mapping.entries |
|
.sortedBy { (k, _) -> k.absolutePath } |
|
.forEach { (k, v) -> |
|
writer.append(k.absolutePath) |
|
writer.append(File.pathSeparatorChar) |
|
writer.appendln(v.absolutePath) |
|
} |
|
} |
|
} |
|
|
|
private fun writeObject(stream: ObjectOutputStream) { |
|
stream.writeObject(mapping) |
|
} |
|
|
|
private fun readObject(stream: ObjectInputStream) { |
|
mapping = stream.readObject() as HashMap<File, File> |
|
} |
|
} |