/* * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package jdk.packager.internal.legacy.builders.mac; import com.oracle.tools.packager.BundlerParamInfo; import com.oracle.tools.packager.IOUtils; import com.oracle.tools.packager.Log; import com.oracle.tools.packager.RelativeFileSet; import com.oracle.tools.packager.StandardBundlerParam; import com.oracle.tools.packager.mac.MacResources; import jdk.packager.internal.legacy.builders.AbstractAppImageBuilder; import jdk.packager.internal.legacy.JLinkBundlerHelper; import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.UncheckedIOException; import java.io.Writer; import java.math.BigInteger; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.attribute.PosixFilePermission; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.EnumSet; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.ResourceBundle; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import static com.oracle.tools.packager.StandardBundlerParam.*; import static com.oracle.tools.packager.mac.MacBaseInstallerBundler.*; import static com.oracle.tools.packager.mac.MacAppBundler.*; public class MacAppImageBuilder extends AbstractAppImageBuilder { private static final ResourceBundle I18N = ResourceBundle.getBundle(MacAppImageBuilder.class.getName()); private static final String EXECUTABLE_NAME = "JavaAppLauncher"; private static final String LIBRARY_NAME = "libpackager.dylib"; private static final String TEMPLATE_BUNDLE_ICON = "GenericApp.icns"; private static final String OS_TYPE_CODE = "APPL"; private static final String TEMPLATE_INFO_PLIST_LITE = "Info-lite.plist.template"; private static final String TEMPLATE_RUNTIME_INFO_PLIST = "Runtime-Info.plist.template"; private final Path root; private final Path contentsDir; private final Path javaDir; private final Path resourcesDir; private final Path macOSDir; private final Path runtimeDir; private final Path runtimeRoot; private final Path mdir; private final Map params; private static Map getMacCategories() { Map map = new HashMap<>(); map.put("Business", "public.app-category.business"); map.put("Developer Tools", "public.app-category.developer-tools"); map.put("Education", "public.app-category.education"); map.put("Entertainment", "public.app-category.entertainment"); map.put("Finance", "public.app-category.finance"); map.put("Games", "public.app-category.games"); map.put("Graphics & Design", "public.app-category.graphics-design"); map.put("Healthcare & Fitness", "public.app-category.healthcare-fitness"); map.put("Lifestyle", "public.app-category.lifestyle"); map.put("Medical", "public.app-category.medical"); map.put("Music", "public.app-category.music"); map.put("News", "public.app-category.news"); map.put("Photography", "public.app-category.photography"); map.put("Productivity", "public.app-category.productivity"); map.put("Reference", "public.app-category.reference"); map.put("Social Networking", "public.app-category.social-networking"); map.put("Sports", "public.app-category.sports"); map.put("Travel", "public.app-category.travel"); map.put("Utilities", "public.app-category.utilities"); map.put("Video", "public.app-category.video"); map.put("Weather", "public.app-category.weather"); map.put("Action Games", "public.app-category.action-games"); map.put("Adventure Games", "public.app-category.adventure-games"); map.put("Arcade Games", "public.app-category.arcade-games"); map.put("Board Games", "public.app-category.board-games"); map.put("Card Games", "public.app-category.card-games"); map.put("Casino Games", "public.app-category.casino-games"); map.put("Dice Games", "public.app-category.dice-games"); map.put("Educational Games", "public.app-category.educational-games"); map.put("Family Games", "public.app-category.family-games"); map.put("Kids Games", "public.app-category.kids-games"); map.put("Music Games", "public.app-category.music-games"); map.put("Puzzle Games", "public.app-category.puzzle-games"); map.put("Racing Games", "public.app-category.racing-games"); map.put("Role Playing Games", "public.app-category.role-playing-games"); map.put("Simulation Games", "public.app-category.simulation-games"); map.put("Sports Games", "public.app-category.sports-games"); map.put("Strategy Games", "public.app-category.strategy-games"); map.put("Trivia Games", "public.app-category.trivia-games"); map.put("Word Games", "public.app-category.word-games"); return map; } public static final BundlerParamInfo MAC_CONFIGURE_LAUNCHER_IN_PLIST = new StandardBundlerParam<>( I18N.getString("param.configure-launcher-in-plist"), I18N.getString("param.configure-launcher-in-plist.description"), "mac.configure-launcher-in-plist", Boolean.class, params -> Boolean.FALSE, (s, p) -> Boolean.valueOf(s)); public static final BundlerParamInfo MAC_CATEGORY = new StandardBundlerParam<>( I18N.getString("param.category-name"), I18N.getString("param.category-name.description"), "mac.category", String.class, CATEGORY::fetchFrom, (s, p) -> s ); public static final BundlerParamInfo MAC_CF_BUNDLE_NAME = new StandardBundlerParam<>( I18N.getString("param.cfbundle-name.name"), I18N.getString("param.cfbundle-name.description"), "mac.CFBundleName", String.class, params -> null, (s, p) -> s); public static final BundlerParamInfo MAC_CF_BUNDLE_IDENTIFIER = new StandardBundlerParam<>( I18N.getString("param.cfbundle-identifier.name"), I18N.getString("param.cfbundle-identifier.description"), "mac.CFBundleIdentifier", String.class, IDENTIFIER::fetchFrom, (s, p) -> s); public static final BundlerParamInfo MAC_CF_BUNDLE_VERSION = new StandardBundlerParam<>( I18N.getString("param.cfbundle-version.name"), I18N.getString("param.cfbundle-version.description"), "mac.CFBundleVersion", String.class, p -> { String s = VERSION.fetchFrom(p); if (validCFBundleVersion(s)) { return s; } else { return "100"; } }, (s, p) -> s); public static final BundlerParamInfo CONFIG_ROOT = new StandardBundlerParam<>( I18N.getString("param.config-root.name"), I18N.getString("param.config-root.description"), "configRoot", File.class, params -> { File configRoot = new File(BUILD_ROOT.fetchFrom(params), "macosx"); configRoot.mkdirs(); return configRoot; }, (s, p) -> new File(s)); public static final BundlerParamInfo DEFAULT_ICNS_ICON = new StandardBundlerParam<>( I18N.getString("param.default-icon-icns"), I18N.getString("param.default-icon-icns.description"), ".mac.default.icns", String.class, params -> TEMPLATE_BUNDLE_ICON, (s, p) -> s); // public static final BundlerParamInfo DEVELOPER_ID_APP_SIGNING_KEY = new StandardBundlerParam<>( // I18N.getString("param.signing-key-developer-id-app.name"), // I18N.getString("param.signing-key-developer-id-app.description"), // "mac.signing-key-developer-id-app", // String.class, // params -> MacBaseInstallerBundler.findKey("Developer ID Application: " + SIGNING_KEY_USER.fetchFrom(params), SIGNING_KEYCHAIN.fetchFrom(params), VERBOSE.fetchFrom(params)), // (s, p) -> s); // public static final BundlerParamInfo BUNDLE_ID_SIGNING_PREFIX = new StandardBundlerParam<>( // I18N.getString("param.bundle-id-signing-prefix.name"), // I18N.getString("param.bundle-id-signing-prefix.description"), // "mac.bundle-id-signing-prefix", // String.class, // params -> IDENTIFIER.fetchFrom(params) + ".", // (s, p) -> s); // public static final BundlerParamInfo ICON_ICNS = new StandardBundlerParam<>( I18N.getString("param.icon-icns.name"), I18N.getString("param.icon-icns.description"), "icon.icns", File.class, params -> { File f = ICON.fetchFrom(params); if (f != null && !f.getName().toLowerCase().endsWith(".icns")) { Log.info(MessageFormat.format(I18N.getString("message.icon-not-icns"), f)); return null; } return f; }, (s, p) -> new File(s)); public MacAppImageBuilder(Map config, Path imageOutDir) throws IOException { super(config, imageOutDir.resolve(APP_NAME.fetchFrom(config) + ".app/Contents/PlugIns/Java.runtime/Contents/Home")); Objects.requireNonNull(imageOutDir); this.params = config; this.root = imageOutDir.resolve(APP_NAME.fetchFrom(params) + ".app"); this.contentsDir = root.resolve("Contents"); this.javaDir = contentsDir.resolve("Java"); this.resourcesDir = contentsDir.resolve("Resources"); this.macOSDir = contentsDir.resolve("MacOS"); this.runtimeDir = contentsDir.resolve("PlugIns/Java.runtime"); this.runtimeRoot = runtimeDir.resolve("Contents/Home"); this.mdir = runtimeRoot.resolve("lib"); Files.createDirectories(javaDir); Files.createDirectories(resourcesDir); Files.createDirectories(macOSDir); Files.createDirectories(runtimeDir); } private static String extractAppName() { return ""; } private void writeEntry(InputStream in, Path dstFile) throws IOException { Files.createDirectories(dstFile.getParent()); Files.copy(in, dstFile); } /** * chmod ugo+x file */ private void setExecutable(Path file) { try { Set perms = Files.getPosixFilePermissions(file); perms.add(PosixFilePermission.OWNER_EXECUTE); perms.add(PosixFilePermission.GROUP_EXECUTE); perms.add(PosixFilePermission.OTHERS_EXECUTE); Files.setPosixFilePermissions(file, perms); } catch (IOException ioe) { throw new UncheckedIOException(ioe); } } private static void createUtf8File(File file, String content) throws IOException { try (OutputStream fout = new FileOutputStream(file); Writer output = new OutputStreamWriter(fout, "UTF-8")) { output.write(content); } } @Override protected String getCacheLocation(Map params) { return "$CACHEDIR/"; } public static boolean validCFBundleVersion(String v) { // CFBundleVersion (String - iOS, OS X) specifies the build version // number of the bundle, which identifies an iteration (released or // unreleased) of the bundle. The build version number should be a // string comprised of three non-negative, period-separated integers // with the first integer being greater than zero. The string should // only contain numeric (0-9) and period (.) characters. Leading zeros // are truncated from each integer and will be ignored (that is, // 1.02.3 is equivalent to 1.2.3). This key is not localizable. if (v == null) { return false; } String p[] = v.split("\\."); if (p.length > 3 || p.length < 1) { Log.verbose(I18N.getString("message.version-string-too-many-components")); return false; } try { BigInteger n = new BigInteger(p[0]); if (BigInteger.ONE.compareTo(n) > 0) { Log.verbose(I18N.getString("message.version-string-first-number-not-zero")); return false; } if (p.length > 1) { n = new BigInteger(p[1]); if (BigInteger.ZERO.compareTo(n) > 0) { Log.verbose(I18N.getString("message.version-string-no-negative-numbers")); return false; } } if (p.length > 2) { n = new BigInteger(p[2]); if (BigInteger.ZERO.compareTo(n) > 0) { Log.verbose(I18N.getString("message.version-string-no-negative-numbers")); return false; } } } catch (NumberFormatException ne) { Log.verbose(I18N.getString("message.version-string-numbers-only")); Log.verbose(ne); return false; } return true; } @Override public InputStream getResourceAsStream(String name) { return MacResources.class.getResourceAsStream(name); } @Override public void prepareApplicationFiles() throws IOException { File f; // Generate PkgInfo File pkgInfoFile = new File(contentsDir.toFile(), "PkgInfo"); pkgInfoFile.createNewFile(); writePkgInfo(pkgInfoFile); // Copy executable to MacOS folder Path executable = macOSDir.resolve(getLauncherName(params)); writeEntry(MacResources.class.getResourceAsStream(EXECUTABLE_NAME), executable); executable.toFile().setExecutable(true, false); // Copy library to the MacOS folder writeEntry( MacResources.class.getResourceAsStream(LIBRARY_NAME), macOSDir.resolve(LIBRARY_NAME) ); // generate launcher config writeCfgFile(params, new File(root.toFile(), getLauncherCfgName(params)), "$APPDIR/PlugIns/Java.runtime"); // Copy class path entries to Java folder copyClassPathEntries(javaDir); /*********** Take care of "config" files *******/ // Copy icon to Resources folder File icon = ICON_ICNS.fetchFrom(params); InputStream in = locateResource("package/macosx/" + APP_NAME.fetchFrom(params) + ".icns", "icon", DEFAULT_ICNS_ICON.fetchFrom(params), icon, VERBOSE.fetchFrom(params), DROP_IN_RESOURCES_ROOT.fetchFrom(params)); Files.copy(in, resourcesDir.resolve(APP_NAME.fetchFrom(params) + ".icns")); // copy file association icons for (Map fa : FILE_ASSOCIATIONS.fetchFrom(params)) { f = FA_ICON.fetchFrom(fa); if (f != null && f.exists()) { try (InputStream in2 = new FileInputStream(f)) { Files.copy(in2, resourcesDir.resolve(f.getName())); } } } // Generate Info.plist writeInfoPlist(contentsDir.resolve("Info.plist").toFile()); // generate java runtime info.plist writeRuntimeInfoPlist(runtimeDir.resolve("Contents/Info.plist").toFile()); // copy library Path runtimeMacOSDir = Files.createDirectories(runtimeDir.resolve("Contents/MacOS")); Files.copy(runtimeRoot.resolve("lib/jli/libjli.dylib"), runtimeMacOSDir.resolve("libjli.dylib")); // maybe sign if (Optional.ofNullable(SIGN_BUNDLE.fetchFrom(params)).orElse(Boolean.TRUE)) { String signingIdentity = DEVELOPER_ID_APP_SIGNING_KEY.fetchFrom(params); if (signingIdentity != null) { signAppBundle(params, root, signingIdentity, BUNDLE_ID_SIGNING_PREFIX.fetchFrom(params), null, null); } } } private String getLauncherName(Map params) { if (APP_NAME.fetchFrom(params) != null) { return APP_NAME.fetchFrom(params); } else { return MAIN_CLASS.fetchFrom(params); } } public static String getLauncherCfgName(Map p) { return "Contents/Java/" + APP_NAME.fetchFrom(p) + ".cfg"; } private void copyClassPathEntries(Path javaDirectory) throws IOException { List resourcesList = APP_RESOURCES_LIST.fetchFrom(params); if (resourcesList == null) { throw new RuntimeException(I18N.getString("message.null-classpath")); } for (RelativeFileSet classPath : resourcesList) { File srcdir = classPath.getBaseDirectory(); for (String fname : classPath.getIncludedFiles()) { // use new File since fname can have file separators Files.copy(new File(srcdir, fname).toPath(), new File(javaDirectory.toFile(), fname).toPath()); } } } private String getBundleName(Map params) { if (MAC_CF_BUNDLE_NAME.fetchFrom(params) != null) { String bn = MAC_CF_BUNDLE_NAME.fetchFrom(params); if (bn.length() > 16) { Log.info(MessageFormat.format(I18N.getString("message.bundle-name-too-long-warning"), MAC_CF_BUNDLE_NAME.getID(), bn)); } return MAC_CF_BUNDLE_NAME.fetchFrom(params); } else if (APP_NAME.fetchFrom(params) != null) { return APP_NAME.fetchFrom(params); } else { String nm = MAIN_CLASS.fetchFrom(params); if (nm.length() > 16) { nm = nm.substring(0, 16); } return nm; } } private void writeRuntimeInfoPlist(File file) throws IOException { Map data = new HashMap<>(); data.put("CF_BUNDLE_IDENTIFIER", "com.oracle.java." + MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params)); data.put("CF_BUNDLE_NAME", "Java Runtime Image"); data.put("CF_BUNDLE_VERSION", VERSION.fetchFrom(params)); data.put("CF_BUDNEL_SHORT_VERSION_STRING", VERSION.fetchFrom(params)); Writer w = new BufferedWriter(new FileWriter(file)); w.write(preprocessTextResource( "package/macosx/Runtime-Info.plist", I18N.getString("resource.runtime-info-plist"), TEMPLATE_RUNTIME_INFO_PLIST, data, VERBOSE.fetchFrom(params), DROP_IN_RESOURCES_ROOT.fetchFrom(params))); w.close(); } private void writeInfoPlist(File file) throws IOException { Log.verbose(MessageFormat.format(I18N.getString("message.preparing-info-plist"), file.getAbsolutePath())); //prepare config for exe //Note: do not need CFBundleDisplayName if we do not support localization Map data = new HashMap<>(); data.put("DEPLOY_ICON_FILE", APP_NAME.fetchFrom(params) + ".icns"); data.put("DEPLOY_BUNDLE_IDENTIFIER", MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params)); data.put("DEPLOY_BUNDLE_NAME", getBundleName(params)); data.put("DEPLOY_BUNDLE_COPYRIGHT", COPYRIGHT.fetchFrom(params) != null ? COPYRIGHT.fetchFrom(params) : "Unknown"); data.put("DEPLOY_LAUNCHER_NAME", getLauncherName(params)); data.put("DEPLOY_JAVA_RUNTIME_NAME", "$APPDIR/PlugIns/Java.runtime"); data.put("DEPLOY_BUNDLE_SHORT_VERSION", VERSION.fetchFrom(params) != null ? VERSION.fetchFrom(params) : "1.0.0"); data.put("DEPLOY_BUNDLE_CFBUNDLE_VERSION", MAC_CF_BUNDLE_VERSION.fetchFrom(params) != null ? MAC_CF_BUNDLE_VERSION.fetchFrom(params) : "100"); data.put("DEPLOY_BUNDLE_CATEGORY", MAC_CATEGORY.fetchFrom(params)); boolean hasMainJar = MAIN_JAR.fetchFrom(params) != null; boolean hasMainModule = StandardBundlerParam.MODULE.fetchFrom(params) != null; if (hasMainJar) { data.put("DEPLOY_MAIN_JAR_NAME", MAIN_JAR.fetchFrom(params).getIncludedFiles().iterator().next()); } else if (hasMainModule) { data.put("DEPLOY_MODULE_NAME", StandardBundlerParam.MODULE.fetchFrom(params)); } data.put("DEPLOY_PREFERENCES_ID", PREFERENCES_ID.fetchFrom(params).toLowerCase()); StringBuilder sb = new StringBuilder(); List jvmOptions = JVM_OPTIONS.fetchFrom(params); String newline = ""; //So we don't add unneccessary extra line after last append for (String o : jvmOptions) { sb.append(newline).append(" ").append(o).append(""); newline = "\n"; } Map jvmProps = JVM_PROPERTIES.fetchFrom(params); for (Map.Entry entry : jvmProps.entrySet()) { sb.append(newline) .append(" -D") .append(entry.getKey()) .append("=") .append(entry.getValue()) .append(""); newline = "\n"; } String preloader = PRELOADER_CLASS.fetchFrom(params); if (preloader != null) { sb.append(newline) .append(" -Djavafx.preloader=") .append(preloader) .append(""); } data.put("DEPLOY_JVM_OPTIONS", sb.toString()); sb = new StringBuilder(); List args = ARGUMENTS.fetchFrom(params); newline = ""; //So we don't add unneccessary extra line after last append for (String o : args) { sb.append(newline).append(" ").append(o).append(""); newline = "\n"; } data.put("DEPLOY_ARGUMENTS", sb.toString()); newline = ""; sb = new StringBuilder(); Map overridableJVMOptions = USER_JVM_OPTIONS.fetchFrom(params); for (Map.Entry arg : overridableJVMOptions.entrySet()) { sb.append(newline) .append(" ").append(arg.getKey()).append("\n") .append(" ").append(arg.getValue()).append(""); newline = "\n"; } data.put("DEPLOY_JVM_USER_OPTIONS", sb.toString()); data.put("DEPLOY_LAUNCHER_CLASS", MAIN_CLASS.fetchFrom(params)); StringBuilder macroedPath = new StringBuilder(); for (String s : CLASSPATH.fetchFrom(params).split("[ ;:]+")) { macroedPath.append(s); macroedPath.append(":"); } macroedPath.deleteCharAt(macroedPath.length() - 1); data.put("DEPLOY_APP_CLASSPATH", macroedPath.toString()); StringBuilder bundleDocumentTypes = new StringBuilder(); StringBuilder exportedTypes = new StringBuilder(); for (Map fileAssociation : FILE_ASSOCIATIONS.fetchFrom(params)) { List extensions = FA_EXTENSIONS.fetchFrom(fileAssociation); if (extensions == null) { Log.info(I18N.getString("message.creating-association-with-null-extension")); } List mimeTypes = FA_CONTENT_TYPE.fetchFrom(fileAssociation); String itemContentType = MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params) + "." + ((extensions == null || extensions.isEmpty()) ? "mime" : extensions.get(0)); String description = FA_DESCRIPTION.fetchFrom(fileAssociation); File icon = FA_ICON.fetchFrom(fileAssociation); //TODO FA_ICON_ICNS bundleDocumentTypes.append(" \n") .append(" LSItemContentTypes\n") .append(" \n") .append(" ") .append(itemContentType) .append("\n") .append(" \n") .append("\n") .append(" CFBundleTypeName\n") .append(" ") .append(description) .append("\n") .append("\n") .append(" LSHandlerRank\n") .append(" Owner\n") //TODO make a bundler arg .append("\n") .append(" CFBundleTypeRole\n") .append(" Editor\n") // TODO make a bundler arg .append("\n") .append(" LSIsAppleDefaultForType\n") .append(" \n") // TODO make a bundler arg .append("\n"); if (icon != null && icon.exists()) { //? bundleDocumentTypes.append(" CFBundleTypeIconFile\n") .append(" ") .append(icon.getName()) .append("\n"); } bundleDocumentTypes.append(" \n"); exportedTypes.append(" \n") .append(" UTTypeIdentifier\n") .append(" ") .append(itemContentType) .append("\n") .append("\n") .append(" UTTypeDescription\n") .append(" ") .append(description) .append("\n") .append(" UTTypeConformsTo\n") .append(" \n") .append(" public.data\n") //TODO expose this? .append(" \n") .append("\n"); if (icon != null && icon.exists()) { exportedTypes.append(" UTTypeIconFile\n") .append(" ") .append(icon.getName()) .append("\n") .append("\n"); } exportedTypes.append("\n") .append(" UTTypeTagSpecification\n") .append(" \n") //TODO expose via param? .append(" com.apple.ostype\n"); //TODO expose via param? .append(" ABCD\n") .append("\n"); if (extensions != null && !extensions.isEmpty()) { exportedTypes.append(" public.filename-extension\n") .append(" \n"); for (String ext : extensions) { exportedTypes.append(" ") .append(ext) .append("\n"); } exportedTypes.append(" \n"); } if (mimeTypes != null && !mimeTypes.isEmpty()) { exportedTypes.append(" public.mime-type\n") .append(" \n"); for (String mime : mimeTypes) { exportedTypes.append(" ") .append(mime) .append("\n"); } exportedTypes.append(" \n"); } exportedTypes.append(" \n") .append(" \n"); } String associationData; if (bundleDocumentTypes.length() > 0) { associationData = "\n CFBundleDocumentTypes\n \n" + bundleDocumentTypes.toString() + " \n\n UTExportedTypeDeclarations\n \n" + exportedTypes.toString() + " \n"; } else { associationData = ""; } data.put("DEPLOY_FILE_ASSOCIATIONS", associationData); Writer w = new BufferedWriter(new FileWriter(file)); w.write(preprocessTextResource( //MAC_BUNDLER_PREFIX + getConfig_InfoPlist(params).getName(), "package/macosx/Info.plist", I18N.getString("resource.app-info-plist"), TEMPLATE_INFO_PLIST_LITE, data, VERBOSE.fetchFrom(params), DROP_IN_RESOURCES_ROOT.fetchFrom(params))); w.close(); } private void writePkgInfo(File file) throws IOException { //hardcoded as it does not seem we need to change it ever String signature = "????"; try (Writer out = new BufferedWriter(new FileWriter(file))) { out.write(OS_TYPE_CODE + signature); out.flush(); } } public static void signAppBundle(Map params, Path appLocation, String signingIdentity, String identifierPrefix, String entitlementsFile, String inheritedEntitlements) throws IOException { AtomicReference toThrow = new AtomicReference<>(); String appExecutable = "/Contents/MacOS/" + APP_NAME.fetchFrom(params); String keyChain = SIGNING_KEYCHAIN.fetchFrom(params); // sign all dylibs and jars Files.walk(appLocation) // fix permissions .peek(path -> { try { Set pfp = Files.getPosixFilePermissions(path); if (!pfp.contains(PosixFilePermission.OWNER_WRITE)) { pfp = EnumSet.copyOf(pfp); pfp.add(PosixFilePermission.OWNER_WRITE); Files.setPosixFilePermissions(path, pfp); } } catch (IOException e) { Log.debug(e); } }) .filter(p -> Files.isRegularFile(p) && !(p.toString().contains("/Contents/MacOS/libjli.dylib") || p.toString().contains("/Contents/MacOS/JavaAppletPlugin") || p.toString().endsWith(appExecutable)) ).forEach(p -> { //noinspection ThrowableResultOfMethodCallIgnored if (toThrow.get() != null) return; // If p is a symlink then skip the signing process. if (Files.isSymbolicLink(p)) { if (VERBOSE.fetchFrom(params)) { Log.verbose(MessageFormat.format(I18N.getString("message.ignoring.symlink"), p.toString())); } } else { List args = new ArrayList<>(); args.addAll(Arrays.asList("codesign", "-s", signingIdentity, // sign with this key "--prefix", identifierPrefix, // use the identifier as a prefix "-vvvv")); if (entitlementsFile != null && (p.toString().endsWith(".jar") || p.toString().endsWith(".dylib"))) { args.add("--entitlements"); args.add(entitlementsFile); // entitlements } else if (inheritedEntitlements != null && Files.isExecutable(p)) { args.add("--entitlements"); args.add(inheritedEntitlements); // inherited entitlements for executable processes } if (keyChain != null && !keyChain.isEmpty()) { args.add("--keychain"); args.add(keyChain); } args.add(p.toString()); try { Set oldPermissions = Files.getPosixFilePermissions(p); File f = p.toFile(); f.setWritable(true, true); ProcessBuilder pb = new ProcessBuilder(args); IOUtils.exec(pb, VERBOSE.fetchFrom(params)); Files.setPosixFilePermissions(p, oldPermissions); } catch (IOException ioe) { toThrow.set(ioe); } } }); IOException ioe = toThrow.get(); if (ioe != null) { throw ioe; } // sign all plugins and frameworks Consumer signIdentifiedByPList = path -> { //noinspection ThrowableResultOfMethodCallIgnored if (toThrow.get() != null) return; try { List args = new ArrayList<>(); args.addAll(Arrays.asList("codesign", "-s", signingIdentity, // sign with this key "--prefix", identifierPrefix, // use the identifier as a prefix "-vvvv")); if (keyChain != null && !keyChain.isEmpty()) { args.add("--keychain"); args.add(keyChain); } args.add(path.toString()); ProcessBuilder pb = new ProcessBuilder(args); IOUtils.exec(pb, VERBOSE.fetchFrom(params)); args = new ArrayList<>(); args.addAll(Arrays.asList("codesign", "-s", signingIdentity, // sign with this key "--prefix", identifierPrefix, // use the identifier as a prefix "-vvvv")); if (keyChain != null && !keyChain.isEmpty()) { args.add("--keychain"); args.add(keyChain); } args.add(path.toString() + "/Contents/_CodeSignature/CodeResources"); pb = new ProcessBuilder(args); IOUtils.exec(pb, VERBOSE.fetchFrom(params)); } catch (IOException e) { toThrow.set(e); } }; Path pluginsPath = appLocation.resolve("Contents/PlugIns"); if (Files.isDirectory(pluginsPath)) { Files.list(pluginsPath) .forEach(signIdentifiedByPList); ioe = toThrow.get(); if (ioe != null) { throw ioe; } } Path frameworkPath = appLocation.resolve("Contents/Frameworks"); if (Files.isDirectory(frameworkPath)) { Files.list(frameworkPath) .forEach(signIdentifiedByPList); ioe = toThrow.get(); if (ioe != null) { throw ioe; } } // sign the app itself List args = new ArrayList<>(); args.addAll(Arrays.asList("codesign", "-s", signingIdentity, // sign with this key "-vvvv")); // super verbose output if (entitlementsFile != null) { args.add("--entitlements"); args.add(entitlementsFile); // entitlements } if (keyChain != null && !keyChain.isEmpty()) { args.add("--keychain"); args.add(keyChain); } args.add(appLocation.toString()); ProcessBuilder pb = new ProcessBuilder(args.toArray(new String[args.size()])); IOUtils.exec(pb, VERBOSE.fetchFrom(params)); } }