diff --git a/docs/macosx-specific-properties.md b/docs/macosx-specific-properties.md index 57a4c291..d104c2e4 100644 --- a/docs/macosx-specific-properties.md +++ b/docs/macosx-specific-properties.md @@ -16,6 +16,8 @@ path/to/entitlements.plist true|false true|false + true|false + xcrun_notarytool_profile_name path/to/png @@ -62,11 +64,13 @@ ## Signing properties | Property | Mandatory | Default value | Description | -| ------------------ | --------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | +|--------------------| --------- |---------------|-----------------------------------------------------------------------------------------------------------------------------------------------------| | `developerId` | :x: | | Signing identity. | | `entitlements` | :x: | | Path to [entitlements](https://developer.apple.com/documentation/bundleresources/entitlements) file. | | `codesignApp` | :x: | `true` | If it is set to `false`, generated app will not be codesigned. | | `hardenedCodesign` | :x: | `true` | If it is set to `true`, enable [hardened runtime](https://developer.apple.com/documentation/security/hardened_runtime) if MacOS version>= 10.13.6. | +| `notarizeApp` | :x: | `false` | If it is set to `true`, generated app will be submitted to apple for notarization and the ticket will be stapled. | +| `keyChainProfile` | :x: | | Profile name originally provided to `xcrun notarytool store-credentials`. Must be set if `notarizeApp` is `true`. | `macStartup` | :x: | `SCRIPT` | App startup type, using a `SCRIPT` or a binary (compiled version of the script: `UNIVERSAL`, `X86_64` or `ARM64`). | ## DMG generation properties diff --git a/src/main/java/io/github/fvarrui/javapackager/gradle/LinuxTaskConfig.java b/src/main/java/io/github/fvarrui/javapackager/gradle/LinuxTaskConfig.java new file mode 100644 index 00000000..3c6c9679 --- /dev/null +++ b/src/main/java/io/github/fvarrui/javapackager/gradle/LinuxTaskConfig.java @@ -0,0 +1,44 @@ +package io.github.fvarrui.javapackager.gradle; + +import io.github.fvarrui.javapackager.model.LinuxConfig; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.provider.ListProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Optional; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +public abstract class LinuxTaskConfig { + @Input + @Optional + public abstract ListProperty getCategories(); + @Input + @Optional + public abstract Property isGenerateDeb(); + @Input + @Optional + public abstract Property isGenerateRpm(); + @Input + @Optional + public abstract Property isGenerateAppImage();; + @Input + @Optional + public abstract RegularFileProperty getPngFile(); + @Input + @Optional + public abstract Property isWrapJar(); + + public LinuxConfig buildConfig() { + LinuxConfig ret = new LinuxConfig(); + ret.setCategories(getCategories().getOrElse(new ArrayList())); + ret.setGenerateDeb(isGenerateDeb().getOrElse(true)); + ret.setGenerateRpm(isGenerateRpm().getOrElse(true)); + ret.setGenerateAppImage(isGenerateAppImage().getOrElse(true)); + ret.setPngFile(getPngFile().getAsFile().getOrNull()); + ret.setWrapJar(isWrapJar().getOrElse(true)); + return ret; + } +} diff --git a/src/main/java/io/github/fvarrui/javapackager/model/MacConfig.java b/src/main/java/io/github/fvarrui/javapackager/model/MacConfig.java index 0a3c9076..2f28aec5 100644 --- a/src/main/java/io/github/fvarrui/javapackager/model/MacConfig.java +++ b/src/main/java/io/github/fvarrui/javapackager/model/MacConfig.java @@ -36,7 +36,10 @@ public class MacConfig implements Serializable { private File provisionProfile; private File customLauncher; private File customInfoPlist; + private File customRuntimeInfoPlist; private boolean codesignApp = true; + private boolean notarizeApp = false; + private String keyChainProfile; private InfoPlist infoPlist = new InfoPlist(); private boolean hardenedCodesign = true; private MacStartup macStartup = MacStartup.SCRIPT; @@ -209,6 +212,14 @@ public void setCustomInfoPlist(File customInfoPlist) { this.customInfoPlist = customInfoPlist; } + public File getCustomRuntimeInfoPlist() { + return customRuntimeInfoPlist; + } + + public void setCustomRuntimeInfoPlist(File customRuntimeInfoPlist) { + this.customRuntimeInfoPlist = customRuntimeInfoPlist; + } + public File getProvisionProfile() { return provisionProfile; } @@ -233,6 +244,22 @@ public void setCodesignApp(boolean codesignApp) { this.codesignApp = codesignApp; } + public boolean isNotarizeApp() { + return notarizeApp; + } + + public void setNotarizeApp(boolean notarizeApp) { + this.notarizeApp = notarizeApp; + } + + public String getKeyChainProfile() { + return keyChainProfile; + } + + public void setKeyChainProfile(String keyChainProfile) { + this.keyChainProfile = keyChainProfile; + } + public InfoPlist getInfoPlist() { return infoPlist; } @@ -266,9 +293,10 @@ public String toString() { + ", volumeName=" + volumeName + ", generateDmg=" + generateDmg + ", generatePkg=" + generatePkg + ", relocateJar=" + relocateJar + ", appId=" + appId + ", developerId=" + developerId + ", entitlements=" + entitlements + ", provisionProfile=" + provisionProfile + ", customLauncher=" - + customLauncher + ", customInfoPlist=" + customInfoPlist + ", codesignApp=" + codesignApp - + ", infoPlist=" + infoPlist + ", hardenedCodesign=" + hardenedCodesign + ", macStartup=" + macStartup - + "]"; + + customLauncher + ", customInfoPlist=" + customInfoPlist + ", customRuntimeInfoPlist=" + + customRuntimeInfoPlist + ", codesignApp=" + codesignApp + ", notarizeApp=" + notarizeApp + + ", keyChainProfile=" + keyChainProfile + ", infoPlist=" + infoPlist + ", hardenedCodesign=" + + hardenedCodesign + ", macStartup=" + macStartup + "]"; } /** diff --git a/src/main/java/io/github/fvarrui/javapackager/packagers/MacPackager.java b/src/main/java/io/github/fvarrui/javapackager/packagers/MacPackager.java index 5173f4f1..3b805ec8 100644 --- a/src/main/java/io/github/fvarrui/javapackager/packagers/MacPackager.java +++ b/src/main/java/io/github/fvarrui/javapackager/packagers/MacPackager.java @@ -9,11 +9,19 @@ import java.io.File; import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; /** * Packager for MacOS @@ -25,6 +33,7 @@ public class MacPackager extends Packager { private File resourcesFolder; private File javaFolder; private File macOSFolder; + private File jreBundleFolder; public File getAppFile() { return appFile; @@ -73,7 +82,8 @@ protected void doCreateAppStructure() throws Exception { // sets common folders this.executableDestinationFolder = macOSFolder; this.jarFileDestinationFolder = javaFolder; - this.jreDestinationFolder = new File(contentsFolder, "PlugIns/" + jreDirectoryName + "/Contents/Home"); + this.jreBundleFolder = new File(contentsFolder, "PlugIns/" + jreDirectoryName + ".jre"); + this.jreDestinationFolder = new File(jreBundleFolder, "Contents/Home"); this.resourcesDestinationFolder = resourcesFolder; } @@ -83,6 +93,9 @@ protected void doCreateAppStructure() throws Exception { */ @Override public File doCreateApp() throws Exception { + if(bundleJre) { + processRuntimeInfoPlistFile(); + } // copies jarfile to Java folder FileUtils.copyFileToFolder(jarFile, javaFolder); @@ -97,6 +110,8 @@ public File doCreateApp() throws Exception { codesign(); + notarize(); + return appFile; } @@ -157,6 +172,21 @@ private void processInfoPlistFile() throws Exception { Logger.info("Info.plist file created in " + infoPlistFile.getAbsolutePath()); } + /** + * Creates and writes the Info.plist inside the JRE if no custom file is specified. + * @throws Exception if anything goes wrong + */ + private void processRuntimeInfoPlistFile() throws Exception { + File infoPlistFile = new File(jreBundleFolder, "Contents/Info.plist"); + if(macConfig.getCustomRuntimeInfoPlist() != null && macConfig.getCustomRuntimeInfoPlist().isFile() && macConfig.getCustomRuntimeInfoPlist().canRead()){ + FileUtils.copyFileToFile(macConfig.getCustomRuntimeInfoPlist(), infoPlistFile); + } else { + VelocityUtils.render("mac/RuntimeInfo.plist.vtl", infoPlistFile, this); + XMLUtils.prettify(infoPlistFile); + } + Logger.info("RuntimeInfo.plist file created in " + infoPlistFile.getAbsolutePath()); + } + private void codesign() throws Exception { if (!Platform.mac.isCurrentPlatform()) { Logger.warn("Generated app could not be signed due to current platform is " + Platform.getCurrentPlatform()); @@ -167,6 +197,18 @@ private void codesign() throws Exception { } } + private void notarize() throws Exception { + if (!Platform.mac.isCurrentPlatform()) { + Logger.warn("Generated app could not be notarized due to current platform is " + Platform.getCurrentPlatform()); + } else if (!getMacConfig().isCodesignApp()) { + Logger.warn("App codesigning disabled. Cannot notarize unsigned app"); + } else if (!getMacConfig().isNotarizeApp()) { + Logger.warn("App notarization disabled"); + } else { + notarize(this.macConfig.getKeyChainProfile(), this.appFile); + } + } + private void processProvisionProfileFile() throws Exception { if (macConfig.getProvisionProfile() != null && macConfig.getProvisionProfile().isFile() && macConfig.getProvisionProfile().canRead()) { // file name must be 'embedded.provisionprofile' @@ -195,13 +237,13 @@ private File preparePrecompiledStartupStub() throws Exception { private void codesign(String developerId, File entitlements, File appFile) throws Exception { - prepareEntitlementFile(entitlements); + entitlements = prepareEntitlementFile(entitlements); - manualDeepSign(appFile, developerId, entitlements); + signAppBundle(appFile, developerId, entitlements); } - private void prepareEntitlementFile(File entitlements) throws Exception { + private File prepareEntitlementFile(File entitlements) throws Exception { // if entitlements.plist file not specified, use a default one if (entitlements == null) { Logger.warn("Entitlements file not specified. Using defaults!"); @@ -210,45 +252,56 @@ private void prepareEntitlementFile(File entitlements) throws Exception { } else if (!entitlements.exists()) { throw new Exception("Entitlements file doesn't exist: " + entitlements); } + return entitlements; } - private void manualDeepSign(File appFolder, String developerCertificateName, File entitlements) throws IOException, CommandLineException { - - // codesign each file in app - List findCommandArgs = new ArrayList(); - findCommandArgs.add(appFolder); - findCommandArgs.add("-depth"); // execute 'codesign' in 'reverse order', i.e., deepest files first - findCommandArgs.add("-type"); - findCommandArgs.add("f"); // filter for files only - findCommandArgs.add("-exec"); - findCommandArgs.add("codesign"); - findCommandArgs.add("-f"); - addHardenedCodesign(findCommandArgs); - findCommandArgs.add("-s"); - findCommandArgs.add(developerCertificateName); - findCommandArgs.add("--entitlements"); - findCommandArgs.add(entitlements); - findCommandArgs.add("{}"); - findCommandArgs.add("\\;"); - CommandUtils.execute("find", findCommandArgs); + private void signAppBundle(File appFolder, String developerCertificateName, File entitlements) throws IOException, CommandLineException { +// Sign all embedded executables and dynamic libraries +// Structure and order adapted from the JRE's jpackage + try (Stream stream = Files.walk(appFolder.toPath())) { + stream.filter(p -> Files.isRegularFile(p) + && (Files.isExecutable(p) || p.toString().endsWith(".dylib")) + && !(p.toString().contains("dylib.dSYM/Contents")) + && !(p.equals(this.executable.toPath())) + ).forEach(p -> { + if (Files.isSymbolicLink(p)) { + Logger.debug("Skipping signing symlink: " + p); + } else { + try { + codesign(Files.isExecutable(p) ? entitlements : null, developerCertificateName, p.toFile()); + } catch (IOException | CommandLineException e) { + throw new RuntimeException(e); + } + } + }); + } + + // sign the JRE itself after signing all its contents + codesign(developerCertificateName, jreBundleFolder); // make sure the executable is signed last codesign(entitlements, developerCertificateName, this.executable); // finally, sign the top level directory codesign(entitlements, developerCertificateName, appFolder); + } + private void codesign(String developerCertificateName, File file) throws IOException, CommandLineException { + codesign(null, developerCertificateName, file); } private void codesign(File entitlements, String developerCertificateName, File file) throws IOException, CommandLineException { List arguments = new ArrayList(); arguments.add("-f"); - addHardenedCodesign(arguments); - arguments.add("--entitlements"); - arguments.add(entitlements); + if(entitlements != null) { + addHardenedCodesign(arguments); + arguments.add("--entitlements"); + arguments.add(entitlements); + } + arguments.add("--timestamp"); arguments.add("-s"); arguments.add(developerCertificateName); - arguments.add(appFolder); + arguments.add(file); CommandUtils.execute("codesign", arguments); } @@ -263,4 +316,44 @@ private void addHardenedCodesign(Collection args){ } } + private void notarize(String keyChainProfile, File appFile) throws IOException, CommandLineException { + Path zippedApp = null; + try { + zippedApp = zipApp(appFile); + List notarizeArgs = new ArrayList(); + notarizeArgs.add("notarytool"); + notarizeArgs.add("submit"); + notarizeArgs.add(zippedApp.toString()); + notarizeArgs.add("--wait"); + notarizeArgs.add("--keychain-profile"); + notarizeArgs.add(keyChainProfile); + CommandUtils.execute("xcrun", notarizeArgs); + } finally { + if(zippedApp != null) { + Files.deleteIfExists(zippedApp); + } + } + + List stapleArgs = new ArrayList(); + stapleArgs.add("stapler"); + stapleArgs.add("staple"); + stapleArgs.add(appFile); + CommandUtils.execute("xcrun", stapleArgs); + } + + private Path zipApp(File appFile) throws IOException { + Path zipPath = assetsFolder.toPath().resolve(appFile.getName() + "-notarization.zip"); + try(ZipOutputStream zos = new ZipOutputStream(Files.newOutputStream(zipPath))) { + Path sourcePath = appFile.toPath(); + Files.walkFileTree(sourcePath, new SimpleFileVisitor() { + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + zos.putNextEntry(new ZipEntry(sourcePath.getParent().relativize(file).toString())); + Files.copy(file, zos); + zos.closeEntry(); + return FileVisitResult.CONTINUE; + } + }); + } + return zipPath; + } } diff --git a/src/main/resources/mac/Info.plist.vtl b/src/main/resources/mac/Info.plist.vtl index 4475958e..31ca5905 100644 --- a/src/main/resources/mac/Info.plist.vtl +++ b/src/main/resources/mac/Info.plist.vtl @@ -128,7 +128,7 @@ #if ($info.bundleJre) JAVA_HOME - Contents/PlugIns/${info.jreDirectoryName}/Contents/Home + Contents/PlugIns/${info.jreDirectoryName}.jre/Contents/Home #end #if($info.envPath) PATH diff --git a/src/main/resources/mac/RuntimeInfo.plist.vtl b/src/main/resources/mac/RuntimeInfo.plist.vtl new file mode 100644 index 00000000..40cc09ba --- /dev/null +++ b/src/main/resources/mac/RuntimeInfo.plist.vtl @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + + CFBundleIdentifier + ${info.macConfig.appId}.runtime.java + CFBundleInfoDictionaryVersion + 7.0 + CFBundleName + Java Runtime Image + CFBundlePackageType + BNDL + CFBundleShortVersionString + ${info.version} + CFBundleSignature + ???? + CFBundleVersion + ${info.version} + + \ No newline at end of file

AltStyle によって変換されたページ (->オリジナル) /