Browse Source

Fix dependencies clash when packaging

Fixes #319
pull/387/head
Alexey Tsvetkov 3 years ago committed by Alexey Tsvetkov
parent
commit
77387f3abb
  1. 10
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/diagnosticUtils.kt
  2. 2
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/files/FileCopyingProcessor.kt
  3. 4
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/files/MacJarSignFileCopyingProcessor.kt
  4. 2
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/files/SimpleFileCopyingProcessor.kt
  5. 20
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/files/fileUtils.kt
  6. 179
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJPackageTask.kt
  7. 7
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJvmToolOperationTask.kt
  8. 11
      gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/DesktopApplicationTest.kt
  9. 78
      gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/FileHashTest.kt
  10. 1
      gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/TestProject.kt
  11. 1
      gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/TestProjects.kt
  12. 36
      gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/assertUtils.kt
  13. 23
      gradle-plugins/compose/src/test/test-projects/application/moduleClashCli/app/build.gradle
  14. 7
      gradle-plugins/compose/src/test/test-projects/application/moduleClashCli/app/src/main/kotlin/main.kt
  15. 15
      gradle-plugins/compose/src/test/test-projects/application/moduleClashCli/build.gradle
  16. 7
      gradle-plugins/compose/src/test/test-projects/application/moduleClashCli/lib1/utils/build.gradle
  17. 5
      gradle-plugins/compose/src/test/test-projects/application/moduleClashCli/lib1/utils/src/main/kotlin/lib1/utils.kt
  18. 7
      gradle-plugins/compose/src/test/test-projects/application/moduleClashCli/lib2/utils/build.gradle
  19. 5
      gradle-plugins/compose/src/test/test-projects/application/moduleClashCli/lib2/utils/src/main/kotlin/lib2/utils.kt
  20. 11
      gradle-plugins/compose/src/test/test-projects/application/moduleClashCli/settings.gradle

10
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/diagnosticUtils.kt

@ -0,0 +1,10 @@
package org.jetbrains.compose.desktop.application.internal
import java.io.PrintWriter
import java.io.StringWriter
import java.lang.Exception
internal fun Exception.stacktraceToString(): String =
StringWriter().also { w ->
PrintWriter(w).use { pw -> printStackTrace(pw) }
}.toString()

2
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/FileCopyingProcessor.kt → gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/files/FileCopyingProcessor.kt

@ -1,4 +1,4 @@
package org.jetbrains.compose.desktop.application.internal
package org.jetbrains.compose.desktop.application.internal.files
import java.io.File

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

@ -1,6 +1,8 @@
package org.jetbrains.compose.desktop.application.internal
package org.jetbrains.compose.desktop.application.internal.files
import org.gradle.process.ExecOperations
import org.jetbrains.compose.desktop.application.internal.MacUtils
import org.jetbrains.compose.desktop.application.internal.isJarFile
import org.jetbrains.compose.desktop.application.internal.validation.ValidatedMacOSSigningSettings
import java.io.*
import java.util.regex.Pattern

2
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/SimpleFileCopyingProcessor.kt → gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/files/SimpleFileCopyingProcessor.kt

@ -1,4 +1,4 @@
package org.jetbrains.compose.desktop.application.internal
package org.jetbrains.compose.desktop.application.internal.files
import java.io.File

20
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/files/fileUtils.kt

@ -0,0 +1,20 @@
package org.jetbrains.compose.desktop.application.internal.files
import java.io.File
import java.security.DigestInputStream
import java.security.MessageDigest
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) {}
}
}
val digest = md5.digest()
return buildString(digest.size * 2) {
for (byte in digest) {
append(Integer.toHexString(0xFF and byte.toInt()))
}
}
}

179
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJPackageTask.kt

@ -1,13 +1,11 @@
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.RegularFileProperty
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.process.ExecSpec
import org.gradle.work.ChangeType
@ -15,10 +13,19 @@ 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
@ -166,12 +173,28 @@ abstract class AbstractJPackageTask @Inject constructor(
@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)
cliArg("--main-jar", launcherMainJar.ioFile.name)
val mappedJar = libsMapping[launcherMainJar.ioFile]
?: error("Main jar was not processed correctly: ${launcherMainJar.ioFile}")
cliArg("--main-jar", mappedJar)
cliArg("--main-class", launcherMainClass)
when (currentOS) {
@ -241,10 +264,45 @@ abstract class AbstractJPackageTask @Inject constructor(
}
}
override fun prepareWorkingDir(inputChanges: InputChanges) {
val workingDir = workingDir.ioFile
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)
// todo: parallel processing
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
@ -258,35 +316,17 @@ abstract class AbstractJPackageTask @Inject constructor(
)
} ?: SimpleFileCopyingProcessor
if (inputChanges.isIncremental) {
logger.debug("Updating working dir incrementally: $workingDir")
val allChanges = inputChanges.getFileChanges(files).asSequence() +
inputChanges.getFileChanges(launcherMainJar)
allChanges.forEach { fileChange ->
val sourceFile = fileChange.file
val targetFile = workingDir.resolve(sourceFile.name)
if (fileChange.changeType == ChangeType.REMOVED) {
fileOperations.delete(targetFile)
logger.debug("Deleted: $targetFile")
} else {
fileProcessor.copy(sourceFile, targetFile)
logger.debug("Updated: $targetFile")
}
}
} else {
logger.debug("Updating working dir non-incrementally: $workingDir")
fileOperations.delete(workingDir)
fileOperations.mkdir(workingDir)
files.forEach { sourceFile ->
val targetFile = workingDir.resolve(sourceFile.name)
if (targetFile.exists()) {
// todo: handle possible clashes
logger.warn("w: File already exists: $targetFile")
}
fileProcessor.copy(sourceFile, targetFile)
}
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
}
}
@ -309,4 +349,69 @@ abstract class AbstractJPackageTask @Inject constructor(
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>
}
}

7
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJvmToolOperationTask.kt

@ -16,8 +16,6 @@ import org.gradle.process.ExecSpec
import org.gradle.work.InputChanges
import org.jetbrains.compose.desktop.application.internal.*
import org.jetbrains.compose.desktop.application.internal.ComposeProperties
import org.jetbrains.compose.desktop.application.internal.OS
import org.jetbrains.compose.desktop.application.internal.currentOS
import org.jetbrains.compose.desktop.application.internal.notNullProperty
import java.io.File
import javax.inject.Inject
@ -68,6 +66,7 @@ abstract class AbstractJvmToolOperationTask(private val toolName: String) : Defa
@TaskAction
fun run(inputChanges: InputChanges) {
initState()
val javaHomePath = javaHome.get()
val jtool = File(javaHomePath).resolve("bin/${executableName(toolName)}")
@ -96,5 +95,9 @@ abstract class AbstractJvmToolOperationTask(private val toolName: String) : Defa
fileOperations.delete(workingDir)
}
}
saveStateAfterFinish()
}
protected open fun initState() {}
protected open fun saveStateAfterFinish() {}
}

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

@ -5,7 +5,6 @@ import org.jetbrains.compose.desktop.application.internal.OS
import org.jetbrains.compose.desktop.application.internal.currentOS
import org.jetbrains.compose.desktop.application.internal.currentTarget
import org.jetbrains.compose.test.*
import org.junit.jupiter.api.Assertions.assertArrayEquals
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import java.util.jar.JarFile
@ -93,4 +92,14 @@ class DesktopApplicationTest : GradlePluginTestBase() {
}
}
}
@Test
fun testModuleClash() = with(testProject(TestProjects.moduleClashCli)) {
gradle(":app:runDistributable").build().checks { check ->
check.taskOutcome(":app:createDistributable", TaskOutcome.SUCCESS)
check.taskOutcome(":app:runDistributable", TaskOutcome.SUCCESS)
check.logContains("Called lib1#util()")
check.logContains("Called lib2#util()")
}
}
}

78
gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/FileHashTest.kt

@ -0,0 +1,78 @@
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.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir
import java.io.File
import java.util.concurrent.TimeUnit
class FileHashTest {
@TempDir
lateinit var workingDir: File
private val inputDir: File
get() = workingDir.resolve("inputs").apply { mkdirs() }
private fun inputFile(name: String, content: String): File {
return inputDir.resolve(name).apply { writeText(content) }
}
private fun outputFile(name: String) =
workingDir.resolve("outputs/$name").apply {
parentFile.mkdirs()
}
@Test
fun testFileHashIsAffectedByContent() {
val input1 = inputFile("input1.txt", "1")
val initJar = createJar("init", input1)
input1.writeText("2")
val modifiedJar = createJar("modified", input1)
val initHash = fileHash(initJar)
val modifiedHash = fileHash(modifiedJar)
Assert.assertNotEquals(modifiedHash, initHash)
}
private fun createJar(outputFileName: String, vararg files: File): File {
val outputFile = outputFile(outputFileName)
val cmd = arrayListOf(jarUtilFile.absolutePath, "cvf", outputFile.absolutePath)
for (file in files) {
cmd.add(file.relativeTo(inputDir).path)
}
val outFile = workingDir.resolve("jar-stdout.txt").apply { delete() }
val errFile = workingDir.resolve("jar-error.txt").apply { delete() }
val process = ProcessBuilder(cmd).run {
redirectOutput(outFile)
redirectError(errFile)
directory(inputDir)
start()
}
if (!process.waitFor(30, TimeUnit.SECONDS)) {
error("Process hang up: [${cmd.joinToString(" ")}]")
}
val exitCode = process.exitValue()
check(exitCode == 0) {
"""
Stdout log: $outFile
Error log: $errFile
Process exited with error: $exitCode
"""
}
outFile.delete()
errFile.delete()
return outputFile
}
}
private val jarUtilFile = run {
val javaHome = File(System.getProperty("java.home"))
val executableName = if (currentOS == OS.Windows) "jar.exe" else "jar"
javaHome.resolve("bin/$executableName")
}

1
gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/TestProject.kt

@ -36,6 +36,7 @@ data class TestProject(
withGradleVersion(TestProperties.gradleVersionForTests)
withProjectDir(workingDir)
withArguments(args.toList() + additionalArgs)
forwardOutput()
}
@Suppress("DeprecatedCallableAddReplaceWith")

1
gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/TestProjects.kt

@ -4,4 +4,5 @@ object TestProjects {
const val jvm = "application/jvm"
const val mpp = "application/mpp"
const val jvmKotlinDsl = "application/jvmKotlinDsl"
const val moduleClashCli = "application/moduleClashCli"
}

36
gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/assertUtils.kt

@ -1,9 +1,45 @@
package org.jetbrains.compose.test
import org.gradle.testkit.runner.BuildResult
import org.gradle.testkit.runner.TaskOutcome
internal fun <T> Collection<T>.checkContains(vararg elements: T) {
val expectedElements = elements.toMutableSet()
forEach { expectedElements.remove(it) }
if (expectedElements.isNotEmpty()) {
error("Expected elements are missing from the collection: [${expectedElements.joinToString(", ")}]")
}
}
internal fun BuildResult.checks(fn: (BuildResultChecks) -> Unit) {
fn(BuildResultChecks(this))
}
internal class BuildResultChecks(private val result: BuildResult) {
fun logContains(substring: String) {
if (!result.output.contains(substring)) {
throw AssertionError("Test output does not contain the expected string: '$substring'")
}
}
fun taskOutcome(task: String, expectedOutcome: TaskOutcome) {
val actualOutcome = result.task(task)?.outcome
if (actualOutcome != expectedOutcome) {
throw AssertionError(
"""|Unexpected outcome for task '$task'
|Expected: $expectedOutcome
|Actual: $actualOutcome
""".trimMargin())
}
}
}
internal fun BuildResult.checkOutputLogContains(substring: String) {
if (output.contains(substring)) return
println("Test output:")
output.lineSequence().forEach {
println(" > $it")
}
throw AssertionError("Test output does not contain the expected string: '$substring'")
}

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

@ -0,0 +1,23 @@
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
plugins {
id "org.jetbrains.kotlin.jvm"
id "org.jetbrains.compose"
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib"
implementation compose.desktop.currentOs
implementation project(":lib1:utils")
implementation project(":lib2:utils")
}
compose.desktop {
application {
mainClass = "MainKt"
nativeDistributions {
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
}
}
}

7
gradle-plugins/compose/src/test/test-projects/application/moduleClashCli/app/src/main/kotlin/main.kt

@ -0,0 +1,7 @@
import lib1.*
import lib2.*
fun main() {
lib1.util()
lib2.util()
}

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

@ -0,0 +1,15 @@
plugins {
id "org.jetbrains.kotlin.jvm" apply false
id "org.jetbrains.compose" apply false
}
subprojects {
repositories {
google()
mavenCentral()
jcenter()
maven {
url "https://maven.pkg.jetbrains.space/public/p/compose/dev"
}
}
}

7
gradle-plugins/compose/src/test/test-projects/application/moduleClashCli/lib1/utils/build.gradle

@ -0,0 +1,7 @@
plugins {
id "org.jetbrains.kotlin.jvm"
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib"
}

5
gradle-plugins/compose/src/test/test-projects/application/moduleClashCli/lib1/utils/src/main/kotlin/lib1/utils.kt

@ -0,0 +1,5 @@
package lib1
fun util() {
println("Called lib1#util()")
}

7
gradle-plugins/compose/src/test/test-projects/application/moduleClashCli/lib2/utils/build.gradle

@ -0,0 +1,7 @@
plugins {
id "org.jetbrains.kotlin.jvm"
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib"
}

5
gradle-plugins/compose/src/test/test-projects/application/moduleClashCli/lib2/utils/src/main/kotlin/lib2/utils.kt

@ -0,0 +1,5 @@
package lib2
fun util() {
println("Called lib2#util()")
}

11
gradle-plugins/compose/src/test/test-projects/application/moduleClashCli/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()
}
}
include ':lib1:utils', ':lib2:utils', ':app'
Loading…
Cancel
Save