Skip to content

Commit

Permalink
Unpack Skiko in native distributions
Browse files Browse the repository at this point in the history
Resolves #546
  • Loading branch information
AlexeyTsvetkov committed Apr 12, 2021
1 parent 76e3f45 commit 62d58b4
Show file tree
Hide file tree
Showing 9 changed files with 226 additions and 66 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ 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.io.ByteArrayOutputStream
import java.io.File
import java.io.PrintStream
import java.util.regex.Pattern
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
Expand Down Expand Up @@ -51,50 +53,46 @@ internal class MacJarSignFileCopyingProcessor(
}

override fun copy(source: File, target: File) {
if (!source.isJarFile) {
if (source.isJarFile) {
signNativeLibsInJar(source, target)
} else {
SimpleFileCopyingProcessor.copy(source, target)
return
if (source.name.isDylibPath) {
signDylib(target)
}
}
}

private fun signNativeLibsInJar(source: File, target: File) {
if (target.exists()) target.delete()

ZipInputStream(FileInputStream(source).buffered()).use { zin ->
ZipOutputStream(FileOutputStream(target).buffered()).use { zout ->
copyAndSignNativeLibs(zin, zout)
transformJar(source, target) { zin, zout, entry ->
if (entry.name.isDylibPath) {
signDylibEntry(zin, zout, entry)
} else {
zout.withNewEntry(ZipEntry(entry)) {
zin.copyTo(zout)
}
}
}
}

private fun copyAndSignNativeLibs(zin: ZipInputStream, zout: ZipOutputStream) {
for (sourceEntry in generateSequence { zin.nextEntry }) {
if (!sourceEntry.name.endsWith(".dylib")) {
zout.putNextEntry(ZipEntry(sourceEntry))
zin.copyTo(zout)
} else {

val unpackedDylibFile = tempDir.resolve(sourceEntry.name.substringAfterLast("/"))
try {
unpackedDylibFile.outputStream().buffered().use {
zin.copyTo(it)
}

signDylib(unpackedDylibFile)
val targetEntry = ZipEntry(sourceEntry.name).apply {
comment = sourceEntry.comment
extra = sourceEntry.extra
method = sourceEntry.method
size = unpackedDylibFile.length()
}
zout.putNextEntry(targetEntry)

unpackedDylibFile.inputStream().buffered().use {
it.copyTo(zout)
}
} finally {
unpackedDylibFile.delete()
}
private fun signDylibEntry(zin: ZipInputStream, zout: ZipOutputStream, sourceEntry: ZipEntry) {
val unpackedDylibFile = tempDir.resolve(sourceEntry.name.substringAfterLast("/"))
try {
zin.copyTo(unpackedDylibFile)
signDylib(unpackedDylibFile)
val targetEntry = ZipEntry(sourceEntry.name).apply {
comment = sourceEntry.comment
extra = sourceEntry.extra
method = sourceEntry.method
size = unpackedDylibFile.length()
}
zout.closeEntry()
zout.withNewEntry(ZipEntry(targetEntry)) {
unpackedDylibFile.copyTo(zout)
}
} finally {
unpackedDylibFile.delete()
}
}

Expand All @@ -120,4 +118,7 @@ internal class MacJarSignFileCopyingProcessor(
exec.args(*args.toTypedArray())
}.assertNormalExitValue()
}
}
}

private val String.isDylibPath
get() = endsWith(".dylib")
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@

package org.jetbrains.compose.desktop.application.internal.files

import java.io.File
import java.io.*
import java.security.DigestInputStream
import java.security.MessageDigest
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream

internal fun fileHash(file: File): String {
val md5 = MessageDigest.getInstance("MD5")
Expand All @@ -22,4 +25,36 @@ internal fun fileHash(file: File): String {
append(Integer.toHexString(0xFF and byte.toInt()))
}
}
}

internal inline fun transformJar(
sourceJar: File,
targetJar: File,
fn: (zin: ZipInputStream, zout: ZipOutputStream, entry: ZipEntry) -> Unit
) {
ZipInputStream(FileInputStream(sourceJar).buffered()).use { zin ->
ZipOutputStream(FileOutputStream(targetJar).buffered()).use { zout ->
for (sourceEntry in generateSequence { zin.nextEntry }) {
fn(zin, zout, sourceEntry)
}
}
}
}

internal inline fun ZipOutputStream.withNewEntry(zipEntry: ZipEntry, fn: () -> Unit) {
putNextEntry(zipEntry)
fn()
closeEntry()
}

internal fun InputStream.copyTo(file: File) {
file.outputStream().buffered().use { os ->
copyTo(os)
}
}

internal fun File.copyTo(os: OutputStream) {
inputStream().buffered().use { bis ->
bis.copyTo(os)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
package org.jetbrains.compose.desktop.application.tasks

import org.gradle.api.file.*
import org.gradle.api.internal.file.FileOperations
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
Expand All @@ -17,19 +18,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.*
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.files.transformJar
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.io.*
import java.util.*
import java.util.zip.ZipEntry
import javax.inject.Inject
import kotlin.collections.HashMap
import kotlin.collections.HashSet
import java.io.ObjectInputStream
import java.io.ObjectOutputStream
import kotlin.collections.ArrayList

abstract class AbstractJPackageTask @Inject constructor(
@get:Input
Expand Down Expand Up @@ -181,6 +182,9 @@ abstract class AbstractJPackageTask @Inject constructor(
@get:LocalState
protected val signDir: Provider<Directory> = project.layout.buildDirectory.dir("compose/tmp/sign")

@get:LocalState
protected val skikoDir: Provider<Directory> = project.layout.buildDirectory.dir("compose/tmp/skiko")

@get:Internal
private val libsDir: Provider<Directory> = workingDir.map {
it.dir("libs")
Expand All @@ -197,10 +201,10 @@ abstract class AbstractJPackageTask @Inject constructor(
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("--input", libsDir)
cliArg("--runtime-image", runtimeImage)

val mappedJar = libsMapping[launcherMainJar.ioFile]
val mappedJar = libsMapping[launcherMainJar.ioFile]?.singleOrNull()
?: error("Main jar was not processed correctly: ${launcherMainJar.ioFile}")
cliArg("--main-jar", mappedJar)
cliArg("--main-class", launcherMainClass)
Expand All @@ -217,6 +221,7 @@ abstract class AbstractJPackageTask @Inject constructor(
launcherJvmArgs.orNull?.forEach {
cliArg("--java-options", "'$it'")
}
cliArg("--java-options", "'-Dskiko.library.path=${'$'}APPDIR'")
if (currentOS == OS.MacOS) {
macDockName.orNull?.let { dockName ->
cliArg("--java-options", "'-Xdock:name=$dockName'")
Expand Down Expand Up @@ -298,7 +303,9 @@ abstract class AbstractJPackageTask @Inject constructor(

try {
for (change in allChanges) {
libsMapping.remove(change.file)?.let { fileOperations.delete(it) }
libsMapping.remove(change.file)?.let { files ->
files.forEach { fileOperations.delete(it) }
}
if (change.changeType != ChangeType.REMOVED) {
outdatedLibs.add(change.file)
}
Expand All @@ -315,7 +322,7 @@ abstract class AbstractJPackageTask @Inject constructor(
}

override fun prepareWorkingDir(inputChanges: InputChanges) {
val libsDirFile = libsDir.ioFile
val libsDir = libsDir.ioFile
val fileProcessor =
withValidatedMacOSSigning { signing ->
val tmpDirForSign = signDir.ioFile
Expand All @@ -328,18 +335,25 @@ abstract class AbstractJPackageTask @Inject constructor(
signing = signing
)
} ?: SimpleFileCopyingProcessor
fun copyFileToLibsDir(sourceFile: File): File {
val targetFileName =
if (sourceFile.isJarFile) "${sourceFile.nameWithoutExtension}-${fileHash(sourceFile)}.jar"
else sourceFile.name
val targetFile = libsDir.resolve(targetFileName)
fileProcessor.copy(sourceFile, targetFile)
return 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
libsMapping[sourceFile] = if (isSkikoForCurrentOS(sourceFile)) {
val unpackedFiles = unpackSkikoForCurrentOS(sourceFile, skikoDir.ioFile, fileOperations)
unpackedFiles.map { copyFileToLibsDir(it) }
} else {
listOf(copyFileToLibsDir(sourceFile))
}
}
}

Expand Down Expand Up @@ -422,23 +436,25 @@ abstract class AbstractJPackageTask @Inject constructor(
// 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>()
private var mapping = HashMap<File, List<File>>()

operator fun get(key: File): File? =
operator fun get(key: File): List<File>? =
mapping[key]

operator fun set(key: File, value: File) {
operator fun set(key: File, value: List<File>) {
mapping[key] = value
}

fun remove(key: File): File? =
fun remove(key: File): List<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)
val paths = line.splitToSequence(File.pathSeparatorChar)
val lib = File(paths.first())
val mappedFiles = paths.drop(1).mapTo(ArrayList()) { File(it) }
mapping[lib] = mappedFiles
}
}
}
Expand All @@ -448,10 +464,9 @@ private class FilesMapping : Serializable {
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)
.forEach { (k, values) ->
(sequenceOf(k) + values.asSequence())
.joinTo(writer, separator = File.pathSeparator, transform = { it.absolutePath })
}
}
}
Expand All @@ -461,6 +476,39 @@ private class FilesMapping : Serializable {
}

private fun readObject(stream: ObjectInputStream) {
mapping = stream.readObject() as HashMap<File, File>
mapping = stream.readObject() as HashMap<File, List<File>>
}
}

private fun isSkikoForCurrentOS(lib: File): Boolean =
lib.name.startsWith("skiko-jvm-runtime-${currentOS.id}-${currentArch.id}")
&& lib.name.endsWith(".jar")

private fun unpackSkikoForCurrentOS(sourceJar: File, skikoDir: File, fileOperations: FileOperations): List<File> {
val entriesToUnpack = when (currentOS) {
OS.MacOS -> setOf("libskiko-macos-${currentArch.id}.dylib")
OS.Linux -> setOf("skiko-windows-${currentArch.id}.dll", "icudtl.dat")
OS.Windows -> setOf("libskiko-linux-${currentArch.id}.so")
}
}

// output files: unpacked libs, corresponding .sha256 files, and target jar
val outputFiles = ArrayList<File>(entriesToUnpack.size * 2 + 1)
val targetJar = skikoDir.resolve(sourceJar.name)
outputFiles.add(targetJar)

fileOperations.delete(skikoDir)
fileOperations.mkdir(skikoDir)
transformJar(sourceJar, targetJar) { zin, zout, entry ->
// check both entry or entry.sha256
if (entry.name.removeSuffix(".sha256") in entriesToUnpack) {
val unpackedFile = skikoDir.resolve(entry.name.substringAfterLast("/"))
zin.copyTo(unpackedFile)
outputFiles.add(unpackedFile)
} else {
zout.withNewEntry(ZipEntry(entry)) {
zin.copyTo(zout)
}
}
}
return outputFiles
}
Loading

0 comments on commit 62d58b4

Please sign in to comment.