diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/MacSigner.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/MacSigner.kt index a38cab98..eb642f5a 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/MacSigner.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/MacSigner.kt @@ -6,19 +6,54 @@ package org.jetbrains.compose.desktop.application.internal import org.jetbrains.compose.desktop.application.internal.validation.ValidatedMacOSSigningSettings +import org.jetbrains.compose.internal.utils.Arch import org.jetbrains.compose.internal.utils.MacUtils +import org.jetbrains.compose.internal.utils.currentArch import java.io.File -import java.nio.file.Files import java.util.regex.Pattern +import kotlin.io.path.isExecutable -internal class MacSigner( - val settings: ValidatedMacOSSigningSettings, - private val runExternalTool: ExternalToolRunner -) { +internal abstract class MacSigner(protected val runTool: ExternalToolRunner) { + /** + * If [entitlements] file is provided, executables are signed with entitlements. + * Set [forceEntitlements] to `true` to sign all types of files with the provided [entitlements]. + */ + abstract fun sign( + file: File, + entitlements: File? = null, + forceEntitlements: Boolean = false + ) + + fun unsign(file: File) { + runTool.unsign(file) + } + + abstract val settings: ValidatedMacOSSigningSettings? +} + +internal class NoCertificateSigner(runTool: ExternalToolRunner) : MacSigner(runTool) { + override fun sign(file: File, entitlements: File?, forceEntitlements: Boolean) { + unsign(file) + if (currentArch == Arch.Arm64) { + // Apple Silicon requires binaries to be signed + // For local builds, ad hoc signatures are OK + // https://wiki.lazarus.freepascal.org/Code_Signing_for_macOS + runTool.codesign("--sign", "-", "-vvvv", file.absolutePath) + } + } + + override val settings: ValidatedMacOSSigningSettings? + get() = null +} + +internal class MacSignerImpl( + override val settings: ValidatedMacOSSigningSettings, + runTool: ExternalToolRunner +) : MacSigner(runTool) { private lateinit var signKey: String init { - runExternalTool( + runTool( MacUtils.security, args = listOfNotNull( "find-certificate", @@ -27,58 +62,26 @@ internal class MacSigner( settings.fullDeveloperID, settings.keychain?.absolutePath ), - processStdout = { stdout -> - signKey = findCertificate(stdout) - } + processStdout = { signKey = matchCertificates(it) } ) } - /** - * If [entitlements] file is provided, executables are signed with entitlements. - * Set [forceEntitlements] to `true` to sign all types of files with the provided [entitlements]. - */ - fun sign( + override fun sign( file: File, - entitlements: File? = null, - forceEntitlements: Boolean = false + entitlements: File?, + forceEntitlements: Boolean ) { - val args = arrayListOf( - "-vvvv", - "--timestamp", - "--options", "runtime", - "--force", - "--prefix", settings.prefix, - "--sign", signKey + runTool.unsign(file) + runTool.sign( + file = file, + signKey = signKey, + entitlements = entitlements?.takeIf { forceEntitlements || file.isExecutable }, + prefix = settings.prefix, + keychain = settings.keychain ) - - settings.keychain?.let { - args.add("--keychain") - args.add(it.absolutePath) - } - - if (forceEntitlements || Files.isExecutable(file.toPath())) { - entitlements?.let { - args.add("--entitlements") - args.add(it.absolutePath) - } - } - - args.add(file.absolutePath) - - runExternalTool(MacUtils.codesign, args) } - fun unsign(file: File) { - val args = listOf( - "-vvvv", - "--remove-signature", - file.absolutePath - ) - runExternalTool(MacUtils.codesign, args) - - } - - private fun findCertificate(certificates: String): String { + private fun matchCertificates(certificates: String): String { val regex = Pattern.compile("\"alis\"=\"([^\"]+)\"") val m = regex.matcher(certificates) if (!m.find()) { @@ -97,4 +100,34 @@ internal class MacSigner( ) return result } -} \ No newline at end of file +} + +private fun ExternalToolRunner.codesign(vararg args: String) = + this(MacUtils.codesign, args.toList()) + +private fun ExternalToolRunner.unsign(file: File) = + codesign("-vvvv", "--remove-signature", file.absolutePath) + +private fun ExternalToolRunner.sign( + file: File, + signKey: String, + entitlements: File?, + prefix: String?, + keychain: File? +) = codesign( + "-vvvv", + "--timestamp", + "--options", "runtime", + "--force", + *optionalArg("--prefix", prefix), + "--sign", signKey, + *optionalArg("--keychain", keychain?.absolutePath), + *optionalArg("--entitlements", entitlements?.absolutePath), + file.absolutePath +) + +private fun optionalArg(arg: String, value: String?): Array = + if (value != null) arrayOf(arg, value) else emptyArray() + +private val File.isExecutable: Boolean + get() = toPath().isExecutable() \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/MacSigningHelper.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/MacSigningHelper.kt index 0ece83a9..6ce42241 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/MacSigningHelper.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/MacSigningHelper.kt @@ -7,9 +7,9 @@ package org.jetbrains.compose.desktop.application.internal import org.jetbrains.compose.desktop.application.internal.files.isDylibPath import java.io.File +import java.nio.file.* import kotlin.io.path.isExecutable import kotlin.io.path.isRegularFile -import kotlin.io.path.isSymbolicLink internal class MacSigningHelper( private val macSigner: MacSigner, @@ -23,58 +23,18 @@ internal class MacSigningHelper( private val runtimeDir = appDir.resolve("Contents/runtime") fun modifyRuntimeIfNeeded() { - // Only resign modify the runtime if a provisioning profile or alternative entitlements file is provided. - // If no entitlements file is provided, the runtime cannot be resigned. - if (runtimeProvisioningProfile == null && - // When resigning the runtime, an app entitlements file is also needed. - (runtimeEntitlementsFile == null || entitlementsFile == null) - ) { - return - } - // Add the provisioning profile - runtimeProvisioningProfile?.let { - addRuntimeProvisioningProfile(runtimeDir, it) - } // Resign the runtime completely (and also the app dir only) - resignRuntimeAndAppDir(appDir, runtimeDir) - } - - private fun addRuntimeProvisioningProfile( - runtimeDir: File, - runtimeProvisioningProfile: File - ) { - runtimeProvisioningProfile.copyTo( - target = runtimeDir.resolve("Contents/embedded.provisionprofile"), - overwrite = true - ) - } - - private fun resignRuntimeAndAppDir( - appDir: File, - runtimeDir: File - ) { // Sign all libs and executables in runtime runtimeDir.walk().forEach { file -> val path = file.toPath() - if (path.isRegularFile() && (path.isExecutable() || path.toString().isDylibPath)) { - if (path.isSymbolicLink()) { - // Ignore symbolic links - } else { - // Resign file - macSigner.unsign(file) - macSigner.sign(file, runtimeEntitlementsFile) - } + if (path.isRegularFile(LinkOption.NOFOLLOW_LINKS) && (path.isExecutable() || file.name.isDylibPath)) { + macSigner.sign(file, runtimeEntitlementsFile) } } - // Resign runtime directory - macSigner.unsign(runtimeDir) macSigner.sign(runtimeDir, runtimeEntitlementsFile, forceEntitlements = true) - - // Resign app directory (contents other than runtime were already signed by jpackage) - macSigner.unsign(appDir) macSigner.sign(appDir, entitlementsFile, forceEntitlements = true) } } diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJPackageTask.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJPackageTask.kt index fca6a964..6653d9e1 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJPackageTask.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJPackageTask.kt @@ -23,11 +23,14 @@ import org.jetbrains.compose.desktop.application.internal.JvmRuntimeProperties import org.jetbrains.compose.desktop.application.internal.validation.validate import org.jetbrains.compose.internal.utils.* import java.io.* +import java.nio.file.LinkOption import java.util.* import javax.inject.Inject import kotlin.collections.HashMap import kotlin.collections.HashSet import kotlin.collections.ArrayList +import kotlin.io.path.isExecutable +import kotlin.io.path.isRegularFile abstract class AbstractJPackageTask @Inject constructor( @get:Input @@ -237,12 +240,17 @@ abstract class AbstractJPackageTask @Inject constructor( @get:Nested internal var nonValidatedMacSigningSettings: MacOSSigningSettings? = null + private val shouldSign: Boolean + get() = nonValidatedMacSigningSettings?.sign?.get() == true + private val macSigner: MacSigner? by lazy { val nonValidatedSettings = nonValidatedMacSigningSettings - if (currentOS == OS.MacOS && nonValidatedSettings?.sign?.get() == true) { - val validatedSettings = - nonValidatedSettings.validate(nonValidatedMacBundleID, project, macAppStore) - MacSigner(validatedSettings, runExternalTool) + if (currentOS == OS.MacOS) { + if (shouldSign) { + val validatedSettings = + nonValidatedSettings!!.validate(nonValidatedMacBundleID, project, macAppStore) + MacSignerImpl(validatedSettings, runExternalTool) + } else NoCertificateSigner(runExternalTool) } else null } @@ -389,11 +397,11 @@ abstract class AbstractJPackageTask @Inject constructor( cliArg("--mac-app-category", macAppCategory) cliArg("--mac-entitlements", macEntitlementsFile) - macSigner?.let { signer -> + macSigner?.settings?.let { signingSettings -> cliArg("--mac-sign", true) - cliArg("--mac-signing-key-user-name", signer.settings.identity) - cliArg("--mac-signing-keychain", signer.settings.keychain) - cliArg("--mac-package-signing-prefix", signer.settings.prefix) + cliArg("--mac-signing-key-user-name", signingSettings.identity) + cliArg("--mac-signing-keychain", signingSettings.keychain) + cliArg("--mac-package-signing-prefix", signingSettings.prefix) } } } @@ -434,19 +442,16 @@ abstract class AbstractJPackageTask @Inject constructor( return outdatedLibs } + private fun jarCopyingProcessor(): FileCopyingProcessor = + if (currentOS == OS.MacOS) { + val tmpDirForSign = signDir.ioFile + fileOperations.clearDirs(tmpDirForSign) + MacJarSignFileCopyingProcessor(macSigner!!, tmpDirForSign, jvmRuntimeInfo.majorVersion) + } else SimpleFileCopyingProcessor + override fun prepareWorkingDir(inputChanges: InputChanges) { val libsDir = libsDir.ioFile - val fileProcessor = - macSigner?.let { signer -> - val tmpDirForSign = signDir.ioFile - fileOperations.clearDirs(tmpDirForSign) - - MacJarSignFileCopyingProcessor( - signer, - tmpDirForSign, - jvmRuntimeVersion = jvmRuntimeInfo.majorVersion - ) - } ?: SimpleFileCopyingProcessor + val fileProcessor = jarCopyingProcessor() val mangleJarFilesNames = mangleJarFilesNames.get() fun copyFileToLibsDir(sourceFile: File): File { @@ -525,17 +530,30 @@ abstract class AbstractJPackageTask @Inject constructor( private fun modifyRuntimeOnMacOsIfNeeded() { if (currentOS != OS.MacOS || targetFormat != TargetFormat.AppImage) return - macSigner?.let { macSigner -> - val macSigningHelper = MacSigningHelper( - macSigner = macSigner, - runtimeProvisioningProfile = macRuntimeProvisioningProfile.ioFileOrNull, - entitlementsFile = macEntitlementsFile.ioFileOrNull, - runtimeEntitlementsFile = macRuntimeEntitlementsFile.ioFileOrNull, - destinationDir = destinationDir.ioFile, - packageName = packageName.get() - ) - macSigningHelper.modifyRuntimeIfNeeded() + + val appDir = destinationDir.ioFile.resolve("${packageName.get()}.app") + val runtimeDir = appDir.resolve("Contents/runtime") + + // Add the provisioning profile + macRuntimeProvisioningProfile.ioFileOrNull?.copyTo( + target = runtimeDir.resolve("Contents/embedded.provisionprofile"), + overwrite = true + ) + val appEntitlementsFile = macEntitlementsFile.ioFileOrNull + val runtimeEntitlementsFile = macRuntimeEntitlementsFile.ioFileOrNull + + val macSigner = macSigner!! + // Resign the runtime completely (and also the app dir only) + // Sign all libs and executables in runtime + runtimeDir.walk().forEach { file -> + val path = file.toPath() + if (path.isRegularFile(LinkOption.NOFOLLOW_LINKS) && (path.isExecutable() || file.name.isDylibPath)) { + macSigner.sign(file, runtimeEntitlementsFile) + } } + + macSigner.sign(runtimeDir, runtimeEntitlementsFile, forceEntitlements = true) + macSigner.sign(appDir, appEntitlementsFile, forceEntitlements = true) } override fun initState() {