Browse Source

Validate versions for native distributables (#405)

Resolves #91, resolves #205, resolves #341, resolves #393
pull/406/head
Alexey Tsvetkov 3 years ago committed by GitHub
parent
commit
0dd5d7fd46
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 11
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/NativeDistributions.kt
  2. 7
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/PlatformSettings.kt
  3. 6
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/TargetFormat.kt
  4. 12
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureApplication.kt
  5. 39
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/packageVersions.kt
  6. 164
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/validation/validatePackageVersions.kt
  7. 6
      gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/DesktopApplicationTest.kt
  8. 2
      gradle-plugins/compose/src/test/test-projects/application/jvm/build.gradle
  9. 2
      gradle-plugins/compose/src/test/test-projects/application/jvmKotlinDsl/build.gradle.kts
  10. 3
      gradle-plugins/compose/src/test/test-projects/application/moduleClashCli/app/build.gradle
  11. 2
      gradle-plugins/compose/src/test/test-projects/application/mpp/build.gradle
  12. 69
      tutorials/Native_distributions_and_local_execution/README.md

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

@ -15,7 +15,16 @@ open class NativeDistributions @Inject constructor(
var description: String? = null
var copyright: String? = null
var vendor: String? = null
var version: String? = null
@Deprecated(
"version is deprecated, use packageVersion instead",
replaceWith = ReplaceWith("packageVersion")
)
var version: String?
get() = packageVersion
set(value) {
packageVersion = value
}
var packageVersion: String? = null
val outputBaseDir: DirectoryProperty = objects.directoryProperty().apply {
set(layout.buildDirectory.dir("compose/binaries"))

7
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/PlatformSettings.kt

@ -7,10 +7,13 @@ import javax.inject.Inject
abstract class PlatformSettings (objects: ObjectFactory) {
val iconFile: RegularFileProperty = objects.fileProperty()
var packageVersion: String? = null
}
open class MacOSPlatformSettings @Inject constructor(objects: ObjectFactory): PlatformSettings(objects) {
var packageName: String? = null
var dmgPackageVersion: String? = null
var pkgPackageVersion: String? = null
/**
* An application's unique identifier across Apple's ecosystem.
@ -40,6 +43,8 @@ open class LinuxPlatformSettings @Inject constructor(objects: ObjectFactory): Pl
var debMaintainer: String? = null
var menuGroup: String? = null
var rpmLicenseType: String? = null
var debPackageVersion: String? = null
var rpmPackageVersion: String? = null
}
open class WindowsPlatformSettings @Inject constructor(objects: ObjectFactory): PlatformSettings(objects) {
@ -51,4 +56,6 @@ open class WindowsPlatformSettings @Inject constructor(objects: ObjectFactory):
get() = field || menuGroup != null
var menuGroup: String? = null
var upgradeUuid: String? = null
var msiPackageVersion: String? = null
var exePackageVersion: String? = null
}

6
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/TargetFormat.kt

@ -5,9 +5,9 @@ import org.jetbrains.compose.desktop.application.internal.currentOS
enum class TargetFormat(
internal val id: String,
private vararg val compatibleOSs: OS
internal val targetOS: OS
) {
AppImage("app-image", *OS.values()),
AppImage("app-image", currentOS),
Deb("deb", OS.Linux),
Rpm("rpm", OS.Linux),
Dmg("dmg", OS.MacOS),
@ -17,7 +17,7 @@ enum class TargetFormat(
val isCompatibleWithCurrentOS: Boolean by lazy { isCompatibleWith(currentOS) }
internal fun isCompatibleWith(targetOS: OS): Boolean = targetOS in compatibleOSs
internal fun isCompatibleWith(os: OS): Boolean = os == targetOS
val outputDirName: String
get() = if (this == AppImage) "app" else id

12
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureApplication.kt

@ -10,6 +10,7 @@ import org.gradle.api.tasks.*
import org.gradle.jvm.tasks.Jar
import org.jetbrains.compose.desktop.application.dsl.Application
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.kotlin.gradle.dsl.KotlinMultiplatformExtension
import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
@ -28,6 +29,7 @@ fun configureApplicationImpl(project: Project, app: Application) {
app.from(mainSourceSet)
}
}
project.validatePackageVersions(app)
project.configurePackagingTasks(listOf(app))
project.configureWix()
}
@ -141,7 +143,7 @@ internal fun AbstractJPackageTask.configurePackagingTask(
packageDescription.set(provider { executables.description })
packageCopyright.set(provider { executables.copyright })
packageVendor.set(provider { executables.vendor })
packageVersion.set(app._packageVersionInternal(project))
packageVersion.set(packageVersionFor(project, app, targetFormat))
}
destinationDir.set(app.nativeDistributions.outputBaseDir.map { it.dir("${app.name}/${targetFormat.outputDirName}") })
@ -265,7 +267,7 @@ private fun Jar.configurePackageUberJarForCurrentOS(app: Application) {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
archiveAppendix.set(currentTarget.id)
archiveBaseName.set(app._packageNameProvider(project))
archiveVersion.set(app._packageVersionInternal(project))
archiveVersion.set(packageVersionFor(project, app, TargetFormat.AppImage))
destinationDirectory.set(project.layout.buildDirectory.dir("compose/jars"))
doLast {
@ -294,12 +296,6 @@ private inline fun <reified T : Task> TaskContainer.composeTask(
internal fun Application._packageNameProvider(project: Project): Provider<String> =
project.provider { nativeDistributions.packageName ?: project.name }
internal fun Application._packageVersionInternal(project: Project): Provider<String?> =
project.provider {
nativeDistributions.version
?: project.version.toString().takeIf { it != "unspecified" }
}
@OptIn(ExperimentalStdlibApi::class)
private fun taskName(action: String, app: Application, suffix: String? = null): String =
listOf(

39
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/packageVersions.kt

@ -0,0 +1,39 @@
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.Application
import org.jetbrains.compose.desktop.application.dsl.NativeDistributions
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
internal fun packageVersionFor(
project: Project,
app: Application,
targetFormat: TargetFormat
): Provider<String?> =
project.provider {
app.nativeDistributions.packageVersionFor(targetFormat)
?: project.version.toString().takeIf { it != "unspecified" }
}
private fun NativeDistributions.packageVersionFor(
targetFormat: TargetFormat
): String? {
val formatSpecificVersion: String? = when (targetFormat) {
TargetFormat.AppImage -> null
TargetFormat.Deb -> linux.debPackageVersion
TargetFormat.Rpm -> linux.rpmPackageVersion
TargetFormat.Dmg -> macOS.dmgPackageVersion
TargetFormat.Pkg -> macOS.pkgPackageVersion
TargetFormat.Exe -> windows.exePackageVersion
TargetFormat.Msi -> windows.msiPackageVersion
}
val osSpecificVersion: String? = when (targetFormat.targetOS) {
OS.Linux -> linux.packageVersion
OS.MacOS -> macOS.packageVersion
OS.Windows -> windows.packageVersion
}
return formatSpecificVersion
?: osSpecificVersion
?: packageVersion
}

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

@ -0,0 +1,164 @@
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.Application
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
import org.jetbrains.compose.desktop.application.internal.OS
import org.jetbrains.compose.desktop.application.internal.packageVersionFor
internal fun Project.validatePackageVersions(app: Application) {
val errors = ErrorsCollector()
for (targetFormat in app.nativeDistributions.targetFormats) {
val packageVersion = packageVersionFor(project, app, targetFormat).orNull
if (packageVersion == null) {
errors.addError(targetFormat, "no version was specified")
continue
}
val versionChecker: VersionChecker? = when (targetFormat) {
TargetFormat.AppImage -> null
TargetFormat.Deb -> DebVersionChecker
TargetFormat.Rpm -> RpmVersionChecker
TargetFormat.Msi, TargetFormat.Exe -> WindowsVersionChecker
TargetFormat.Dmg, TargetFormat.Pkg -> MacVersionChecker
}
versionChecker?.apply {
if (!isValid(packageVersion)) {
errors.addError(
targetFormat,
"'$packageVersion' is not a valid version",
correctFormat = correctFormat
)
}
}
}
if (errors.errors.isNotEmpty()) {
throw GradleException(errors.errors.joinToString("\n"))
}
}
private class ErrorsCollector {
private val myErrors = arrayListOf<String>()
val errors: List<String>
get() = myErrors
fun addError(
targetFormat: TargetFormat,
error: String,
correctFormat: String? = null
) {
val msg = buildString {
appendln("* Illegal version for '$targetFormat': $error.")
if (correctFormat != null) {
appendln(" * Correct format: $correctFormat")
}
appendln(" * You can specify the correct version using DSL properties: " +
dslPropertiesFor(targetFormat).joinToString(", ")
)
}
myErrors.add(msg)
}
}
private fun dslPropertiesFor(
targetFormat: TargetFormat
): List<String> {
val nativeDistributions = "nativeDistributions"
val linux = "$nativeDistributions.linux"
val macOS = "$nativeDistributions.macOS"
val windows = "$nativeDistributions.windows"
val packageVersion = "packageVersion"
val formatSpecificProperty: String? = when (targetFormat) {
TargetFormat.AppImage -> null
TargetFormat.Deb -> "$linux.debPackageVersion"
TargetFormat.Rpm -> "$linux.rpmPackageVersion"
TargetFormat.Dmg -> "$macOS.dmgPackageVersion"
TargetFormat.Pkg -> "$macOS.pkgPackageVersion"
TargetFormat.Exe -> "$windows.exePackageVersion"
TargetFormat.Msi -> "$windows.msiPackageVersion"
}
val osSettingsProperty: String = when (targetFormat.targetOS) {
OS.Linux -> "$linux.$packageVersion"
OS.MacOS -> "$macOS.$packageVersion"
OS.Windows -> "$windows.$packageVersion"
}
val appSpecificProperty = "$nativeDistributions.$packageVersion"
return listOfNotNull(
formatSpecificProperty,
osSettingsProperty,
appSpecificProperty,
)
}
private interface VersionChecker {
val correctFormat: String
fun isValid(version: String): Boolean
}
private object DebVersionChecker : VersionChecker {
override val correctFormat = """|'[EPOCH:]UPSTREAM_VERSION[-DEBIAN_REVISION]', where:
| * EPOCH is an optional non-negative integer;
| * UPSTREAM_VERSION may contain only alphanumerics and the characters '.', '+', '-', '~' and must start with a digit;
| * DEBIAN_REVISION is optional and may contain only alphanumerics and the characters '.', '+', '~';
| * see https://www.debian.org/doc/debian-policy/ch-controlfields.html#version for details;
""".trimMargin()
override fun isValid(version: String): Boolean =
version.matches(debRegex)
private val debRegex = (
/* EPOCH */"([0-9]+:)?" +
/* UPSTREAM_VERSION */ "[0-9][0-9a-zA-Z.+\\-~]*" +
/* DEBIAN_REVISION */ "(-[0-9a-zA-Z.+~]+)?").toRegex()
}
private object RpmVersionChecker : VersionChecker {
override val correctFormat = "rpm package version must not contain a dash '-'"
override fun isValid(version: String): Boolean =
!version.contains("-")
}
private object WindowsVersionChecker : VersionChecker {
override val correctFormat = """|'MAJOR.MINOR.BUILD', where:
| * MAJOR is a non-negative integer with a maximum value of 255;
| * MINOR is a non-negative integer with a maximum value of 255;
| * BUILD is a non-negative integer with a maximum value of 65535;
""".trimMargin()
override fun isValid(version: String): Boolean {
val parts = version.split(".").map { it.toIntOrNull() }
if (parts.size != 3) return false
return parts[0].isIntInRange(0, 255)
&& parts[1].isIntInRange(0, 255)
&& parts[2].isIntInRange(0, 65535)
}
private fun Int?.isIntInRange(min: Int, max: Int) =
this != null && this >= min && this <= max
}
private object MacVersionChecker : VersionChecker {
override val correctFormat = """|'MAJOR[.MINOR][.PATCH]', where:
| * MAJOR is an integer > 0;
| * MINOR is an optional non-negative integer;
| * PATCH is an optional non-negative integer;
""".trimMargin()
override fun isValid(version: String): Boolean {
val parts = version.split(".").map { it.toIntOrNull() }
return parts.isNotEmpty()
&& parts.size <= 3
&& parts.all { it != null && it >= 0 }
&& (parts.first() ?: 0) > 0
}
}

6
gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/DesktopApplicationTest.kt

@ -65,12 +65,12 @@ class DesktopApplicationTest : GradlePluginTestBase() {
val packageFile = packageDirFiles.single()
if (currentOS == OS.Linux) {
val expectedName = "test-package_1.0-1_amd64.$ext"
val expectedName = "test-package_1.0.0-1_amd64.$ext"
check(packageFile.name.equals(expectedName, ignoreCase = true)) {
"Expected '$expectedName' package in $packageDir, got '${packageFile.name}'"
}
} else {
Assert.assertEquals(packageFile.name, "TestPackage-1.0.$ext", "Unexpected package name")
Assert.assertEquals(packageFile.name, "TestPackage-1.0.0.$ext", "Unexpected package name")
}
assertEquals(TaskOutcome.SUCCESS, result.task(":package${ext.capitalize()}")?.outcome)
assertEquals(TaskOutcome.SUCCESS, result.task(":package")?.outcome)
@ -90,7 +90,7 @@ class DesktopApplicationTest : GradlePluginTestBase() {
gradle(":packageUberJarForCurrentOS").build().let { result ->
assertEquals(TaskOutcome.SUCCESS, result.task(":packageUberJarForCurrentOS")?.outcome)
val resultJarFile = file("build/compose/jars/TestPackage-${currentTarget.id}-1.0.jar")
val resultJarFile = file("build/compose/jars/TestPackage-${currentTarget.id}-1.0.0.jar")
resultJarFile.checkExists()
JarFile(resultJarFile).use { jar ->

2
gradle-plugins/compose/src/test/test-projects/application/jvm/build.gradle

@ -25,7 +25,7 @@ compose.desktop {
nativeDistributions {
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
version = "1.0"
packageVersion = "1.0.0"
packageName = "TestPackage"
description = "Test description"
copyright = "Test Copyright Holder"

2
gradle-plugins/compose/src/test/test-projects/application/jvmKotlinDsl/build.gradle.kts

@ -23,6 +23,8 @@ compose.desktop {
mainClass = "MainKt"
nativeDistributions {
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
packageVersion = "1.0.0"
}
}
}

3
gradle-plugins/compose/src/test/test-projects/application/moduleClashCli/app/build.gradle

@ -16,8 +16,5 @@ dependencies {
compose.desktop {
application {
mainClass = "MainKt"
nativeDistributions {
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
}
}
}

2
gradle-plugins/compose/src/test/test-projects/application/mpp/build.gradle

@ -35,7 +35,7 @@ compose.desktop {
nativeDistributions {
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
version = "1.0"
packageVersion = "1.0.0"
packageName = "TestPackage"
description = "Test description"
copyright = "Test Copyright Holder"

69
tutorials/Native_distributions_and_local_execution/README.md

@ -81,6 +81,75 @@ to run such applications will be faced with an error like this:
See [our tutorial](/tutorials/Signing_and_notarization_on_macOS/README.md)
on how to sign and notarize your application.
## Specifying package version
You must specify a package version for native distribution packages.
You can use the following DSL properties (in order of descending priority):
* `nativeDistributions.<os>.<packageFormat>PackageVersion` specifies a version for a single package format;
* `nativeDistributions.<os>.packageVersion` specifies a version for a single target OS;
* `nativeDistributions.packageVersion` specifies a version for all packages;
``` kotlin
compose.desktop {
application {
nativeDistributions {
// a version for all distributables
packageVersion = "..."
linux {
// a version for all Linux distributables
packageVersion = "..."
// a version only for the deb package
debVersion = "..."
// a version only for the rpm package
rpmVersion = "..."
}
macOS {
// a version for all macOS distributables
packageVersion = "..."
// a version only for the dmg package
dmgVersion = "..."
// a version only for the pkg package
pkgVersion = "..."
}
windows {
// a version for all Windows distributables
packageVersion = "..."
// a version only for the msi package
msiVersion = "..."
// a version only for the exe package
exeVersion = "..."
}
}
}
}
```
Versions must follow the rules:
* For `dmg` and `pkg`:
* The format is `MAJOR[.MINOR][.PATCH]`, where:
* `MAJOR` is an integer > 0;
* `MINOR` is an optional non-negative integer;
* `PATCH` is an optional non-negative integer;
* For `msi` and `exe`:
* The format is `MAJOR.MINOR.BUILD`, where:
* `MAJOR` is a non-negative integer with a maximum value of 255;
* `MINOR` is a non-negative integer with a maximum value of 255;
* `BUILD` is a non-negative integer with a maximum value of 65535;
* For `deb`:
* The format is `[EPOCH:]UPSTREAM_VERSION[-DEBIAN_REVISION]`, where:
* `EPOCH` is an optional non-negative integer;
* `UPSTREAM_VERSION`
* may contain only alphanumerics and the characters `.`, `+`, `-`, `~`;
* must start with a digit;
* `DEBIAN_REVISION`
* is optional;
* may contain only alphanumerics and the characters `.`, `+`, `~`;
* See [Debian documentation](https://www.debian.org/doc/debian-policy/ch-controlfields.html#version) for more details;
* For `rpm`:
* A version must not contain the `-` (dash) character.
## Customizing JDK version
The plugin uses `jpackage`, which is available since [JDK 14](https://openjdk.java.net/projects/jdk/14/).

Loading…
Cancel
Save