1 /*
2 * Copyright (c) 2015, 2020, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation. Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26 package jdk.incubator.jpackage.internal;
27
28 import java.io.File;
29 import java.io.FileInputStream;
30 import java.io.IOException;
31 import java.io.InputStream;
32 import java.io.Writer;
33 import java.math.BigInteger;
34 import java.nio.file.Files;
35 import java.nio.file.Path;
36 import java.nio.file.StandardCopyOption;
37 import java.nio.file.attribute.PosixFilePermission;
38 import java.text.MessageFormat;
39 import java.util.ArrayList;
40 import java.util.Arrays;
41 import java.util.EnumSet;
42 import java.util.HashMap;
43 import java.util.List;
44 import java.util.Map;
45 import java.util.Objects;
46 import java.util.Optional;
47 import java.util.ResourceBundle;
48 import java.util.Set;
49 import java.util.concurrent.atomic.AtomicReference;
50 import java.util.function.Consumer;
51 import java.util.stream.Stream;
52 import javax.xml.parsers.DocumentBuilder;
53 import javax.xml.parsers.DocumentBuilderFactory;
54 import javax.xml.xpath.XPath;
55 import javax.xml.xpath.XPathConstants;
56 import javax.xml.xpath.XPathFactory;
57
58 import static jdk.incubator.jpackage.internal.StandardBundlerParam.*;
59 import static jdk.incubator.jpackage.internal.MacBaseInstallerBundler.*;
60 import static jdk.incubator.jpackage.internal.MacAppBundler.*;
61 import static jdk.incubator.jpackage.internal.OverridableResource.createResource;
62
63 public class MacAppImageBuilder extends AbstractAppImageBuilder {
64
65 private static final ResourceBundle I18N = ResourceBundle.getBundle(
66 "jdk.incubator.jpackage.internal.resources.MacResources");
67
68 private static final String LIBRARY_NAME = "libapplauncher.dylib";
69 private static final String TEMPLATE_BUNDLE_ICON = "java.icns";
70 private static final String OS_TYPE_CODE = "APPL";
71 private static final String TEMPLATE_INFO_PLIST_LITE =
72 "Info-lite.plist.template";
73 private static final String TEMPLATE_RUNTIME_INFO_PLIST =
74 "Runtime-Info.plist.template";
75
76 private final Path root;
77 private final Path contentsDir;
78 private final Path appDir;
79 private final Path javaModsDir;
80 private final Path resourcesDir;
81 private final Path macOSDir;
82 private final Path runtimeDir;
83 private final Path runtimeRoot;
84 private final Path mdir;
85
86 private static List<String> keyChains;
87
88 public static final BundlerParamInfo<Boolean>
89 MAC_CONFIGURE_LAUNCHER_IN_PLIST = new StandardBundlerParam<>(
90 "mac.configure-launcher-in-plist",
91 Boolean.class,
92 params -> Boolean.FALSE,
93 (s, p) -> Boolean.valueOf(s));
94
95 public static final BundlerParamInfo<String> MAC_CF_BUNDLE_NAME =
96 new StandardBundlerParam<>(
97 Arguments.CLIOptions.MAC_BUNDLE_NAME.getId(),
98 String.class,
99 params -> null,
100 (s, p) -> s);
101
102 public static final BundlerParamInfo<String> MAC_CF_BUNDLE_IDENTIFIER =
103 new StandardBundlerParam<>(
104 Arguments.CLIOptions.MAC_BUNDLE_IDENTIFIER.getId(),
105 String.class,
106 params -> {
107 // Get identifier from app image if user provided
108 // app image and did not provide the identifier via CLI.
109 String identifier = extractBundleIdentifier(params);
110 if (identifier != null) {
111 return identifier;
112 }
113
114 identifier = IDENTIFIER.fetchFrom(params);
115 if (identifier != null) {
116 return identifier;
117 }
118 // the IDENTIFIER (above) will default to derive from
119 // the main-class, in case there is no main-class
120 // (such as runtime installer) revert to the name.
121 // any of these could be invalid, so check later.
122 return APP_NAME.fetchFrom(params);
123 },
124 (s, p) -> s);
125
126 public static final BundlerParamInfo<String> MAC_CF_BUNDLE_VERSION =
127 new StandardBundlerParam<>(
128 "mac.CFBundleVersion",
129 String.class,
130 p -> {
131 String s = VERSION.fetchFrom(p);
132 if (validCFBundleVersion(s)) {
133 return s;
134 } else {
135 return "100";
136 }
137 },
138 (s, p) -> s);
139
140 public static final BundlerParamInfo<File> ICON_ICNS =
141 new StandardBundlerParam<>(
142 "icon.icns",
143 File.class,
144 params -> {
145 File f = ICON.fetchFrom(params);
146 if (f != null && !f.getName().toLowerCase().endsWith(".icns")) {
147 Log.error(MessageFormat.format(
148 I18N.getString("message.icon-not-icns"), f));
149 return null;
150 }
151 return f;
152 },
153 (s, p) -> new File(s));
154
155 public static final StandardBundlerParam<Boolean> SIGN_BUNDLE =
156 new StandardBundlerParam<>(
157 Arguments.CLIOptions.MAC_SIGN.getId(),
158 Boolean.class,
159 params -> false,
160 // valueOf(null) is false, we actually do want null in some cases
161 (s, p) -> (s == null || "null".equalsIgnoreCase(s)) ?
162 null : Boolean.valueOf(s)
163 );
164
165 public MacAppImageBuilder(Map<String, Object> params, Path imageOutDir)
166 throws IOException {
167 super(params, imageOutDir.resolve(APP_NAME.fetchFrom(params)
168 + ".app/Contents/runtime/Contents/Home"));
169
170 Objects.requireNonNull(imageOutDir);
171
172 this.root = imageOutDir.resolve(APP_NAME.fetchFrom(params) + ".app");
173 this.contentsDir = root.resolve("Contents");
174 this.appDir = contentsDir.resolve("app");
175 this.javaModsDir = appDir.resolve("mods");
176 this.resourcesDir = contentsDir.resolve("Resources");
177 this.macOSDir = contentsDir.resolve("MacOS");
178 this.runtimeDir = contentsDir.resolve("runtime");
179 this.runtimeRoot = runtimeDir.resolve("Contents/Home");
180 this.mdir = runtimeRoot.resolve("lib");
181 Files.createDirectories(appDir);
182 Files.createDirectories(resourcesDir);
183 Files.createDirectories(macOSDir);
184 Files.createDirectories(runtimeDir);
185 }
186
187 private void writeEntry(InputStream in, Path dstFile) throws IOException {
188 Files.createDirectories(dstFile.getParent());
189 Files.copy(in, dstFile);
190 }
191
192 public static boolean validCFBundleVersion(String v) {
193 // CFBundleVersion (String - iOS, OS X) specifies the build version
194 // number of the bundle, which identifies an iteration (released or
195 // unreleased) of the bundle. The build version number should be a
196 // string comprised of three non-negative, period-separated integers
197 // with the first integer being greater than zero. The string should
198 // only contain numeric (0-9) and period (.) characters. Leading zeros
199 // are truncated from each integer and will be ignored (that is,
200 // 1.02.3 is equivalent to 1.2.3). This key is not localizable.
201
202 if (v == null) {
203 return false;
204 }
205
206 String p[] = v.split("\\.");
207 if (p.length > 3 || p.length < 1) {
208 Log.verbose(I18N.getString(
209 "message.version-string-too-many-components"));
210 return false;
211 }
212
213 try {
214 BigInteger n = new BigInteger(p[0]);
215 if (BigInteger.ONE.compareTo(n) > 0) {
216 Log.verbose(I18N.getString(
217 "message.version-string-first-number-not-zero"));
218 return false;
219 }
220 if (p.length > 1) {
221 n = new BigInteger(p[1]);
222 if (BigInteger.ZERO.compareTo(n) > 0) {
223 Log.verbose(I18N.getString(
224 "message.version-string-no-negative-numbers"));
225 return false;
226 }
227 }
228 if (p.length > 2) {
229 n = new BigInteger(p[2]);
230 if (BigInteger.ZERO.compareTo(n) > 0) {
231 Log.verbose(I18N.getString(
232 "message.version-string-no-negative-numbers"));
233 return false;
234 }
235 }
236 } catch (NumberFormatException ne) {
237 Log.verbose(I18N.getString("message.version-string-numbers-only"));
238 Log.verbose(ne);
239 return false;
240 }
241
242 return true;
243 }
244
245 @Override
246 public Path getAppDir() {
247 return appDir;
248 }
249
250 @Override
251 public Path getAppModsDir() {
252 return javaModsDir;
253 }
254
255 @Override
256 public void prepareApplicationFiles(Map<String, ? super Object> params)
257 throws IOException {
258 Map<String, ? super Object> originalParams = new HashMap<>(params);
259 // Generate PkgInfo
260 File pkgInfoFile = new File(contentsDir.toFile(), "PkgInfo");
261 pkgInfoFile.createNewFile();
262 writePkgInfo(pkgInfoFile);
263
264 Path executable = macOSDir.resolve(getLauncherName(params));
265
266 // create the main app launcher
267 try (InputStream is_launcher =
268 getResourceAsStream("jpackageapplauncher");
269 InputStream is_lib = getResourceAsStream(LIBRARY_NAME)) {
270 // Copy executable and library to MacOS folder
271 writeEntry(is_launcher, executable);
272 writeEntry(is_lib, macOSDir.resolve(LIBRARY_NAME));
273 }
274 executable.toFile().setExecutable(true, false);
275 // generate main app launcher config file
276 File cfg = new File(root.toFile(), getLauncherCfgName(params));
277 writeCfgFile(params, cfg);
278
279 // create additional app launcher(s) and config file(s)
280 List<Map<String, ? super Object>> entryPoints =
281 StandardBundlerParam.ADD_LAUNCHERS.fetchFrom(params);
282 for (Map<String, ? super Object> entryPoint : entryPoints) {
283 Map<String, ? super Object> tmp =
284 AddLauncherArguments.merge(originalParams, entryPoint);
285
286 // add executable for add launcher
287 Path addExecutable = macOSDir.resolve(getLauncherName(tmp));
288 try (InputStream is = getResourceAsStream("jpackageapplauncher");) {
289 writeEntry(is, addExecutable);
290 }
291 addExecutable.toFile().setExecutable(true, false);
292
293 // add config file for add launcher
294 cfg = new File(root.toFile(), getLauncherCfgName(tmp));
295 writeCfgFile(tmp, cfg);
296 }
297
298 // Copy class path entries to Java folder
299 copyClassPathEntries(appDir, params);
300
301 /*********** Take care of "config" files *******/
302
303 createResource(TEMPLATE_BUNDLE_ICON, params)
304 .setCategory("icon")
305 .setExternal(ICON_ICNS.fetchFrom(params))
306 .saveToFile(resourcesDir.resolve(APP_NAME.fetchFrom(params)
307 + ".icns"));
308
309 // copy file association icons
310 for (Map<String, ?
311 super Object> fa : FILE_ASSOCIATIONS.fetchFrom(params)) {
312 File f = FA_ICON.fetchFrom(fa);
313 if (f != null && f.exists()) {
314 try (InputStream in2 = new FileInputStream(f)) {
315 Files.copy(in2, resourcesDir.resolve(f.getName()));
316 }
317
318 }
319 }
320
321 copyRuntimeFiles(params);
322 sign(params);
323 }
324
325 @Override
326 public void prepareJreFiles(Map<String, ? super Object> params)
327 throws IOException {
328 copyRuntimeFiles(params);
329 sign(params);
330 }
331
332 @Override
333 File getRuntimeImageDir(File runtimeImageTop) {
334 File home = new File(runtimeImageTop, "Contents/Home");
335 return (home.exists() ? home : runtimeImageTop);
336 }
337
338 private void copyRuntimeFiles(Map<String, ? super Object> params)
339 throws IOException {
340 // Generate Info.plist
341 writeInfoPlist(contentsDir.resolve("Info.plist").toFile(), params);
342
343 // generate java runtime info.plist
344 writeRuntimeInfoPlist(
345 runtimeDir.resolve("Contents/Info.plist").toFile(), params);
346
347 // copy library
348 Path runtimeMacOSDir = Files.createDirectories(
349 runtimeDir.resolve("Contents/MacOS"));
350
351 // JDK 9, 10, and 11 have extra '/jli/' subdir
352 Path jli = runtimeRoot.resolve("lib/libjli.dylib");
353 if (!Files.exists(jli)) {
354 jli = runtimeRoot.resolve("lib/jli/libjli.dylib");
355 }
356
357 Files.copy(jli, runtimeMacOSDir.resolve("libjli.dylib"));
358 }
359
360 private void sign(Map<String, ? super Object> params) throws IOException {
361 if (Optional.ofNullable(
362 SIGN_BUNDLE.fetchFrom(params)).orElse(Boolean.TRUE)) {
363 try {
364 addNewKeychain(params);
365 } catch (InterruptedException e) {
366 Log.error(e.getMessage());
367 }
368 String signingIdentity =
369 DEVELOPER_ID_APP_SIGNING_KEY.fetchFrom(params);
370 if (signingIdentity != null) {
371 signAppBundle(params, root, signingIdentity,
372 BUNDLE_ID_SIGNING_PREFIX.fetchFrom(params), null, null);
373 }
374 restoreKeychainList(params);
375 }
376 }
377
378 private String getLauncherName(Map<String, ? super Object> params) {
379 if (APP_NAME.fetchFrom(params) != null) {
380 return APP_NAME.fetchFrom(params);
381 } else {
382 return MAIN_CLASS.fetchFrom(params);
383 }
384 }
385
386 public static String getLauncherCfgName(
387 Map<String, ? super Object> params) {
388 return "Contents/app/" + APP_NAME.fetchFrom(params) + ".cfg";
389 }
390
391 private void copyClassPathEntries(Path javaDirectory,
392 Map<String, ? super Object> params) throws IOException {
393 List<RelativeFileSet> resourcesList =
394 APP_RESOURCES_LIST.fetchFrom(params);
395 if (resourcesList == null) {
396 throw new RuntimeException(
397 I18N.getString("message.null-classpath"));
398 }
399
400 for (RelativeFileSet classPath : resourcesList) {
401 File srcdir = classPath.getBaseDirectory();
402 for (String fname : classPath.getIncludedFiles()) {
403 copyEntry(javaDirectory, srcdir, fname);
404 }
405 }
406 }
407
408 private String getBundleName(Map<String, ? super Object> params) {
409 if (MAC_CF_BUNDLE_NAME.fetchFrom(params) != null) {
410 String bn = MAC_CF_BUNDLE_NAME.fetchFrom(params);
411 if (bn.length() > 16) {
412 Log.error(MessageFormat.format(I18N.getString(
413 "message.bundle-name-too-long-warning"),
414 MAC_CF_BUNDLE_NAME.getID(), bn));
415 }
416 return MAC_CF_BUNDLE_NAME.fetchFrom(params);
417 } else if (APP_NAME.fetchFrom(params) != null) {
418 return APP_NAME.fetchFrom(params);
419 } else {
420 String nm = MAIN_CLASS.fetchFrom(params);
421 if (nm.length() > 16) {
422 nm = nm.substring(0, 16);
423 }
424 return nm;
425 }
426 }
427
428 private void writeRuntimeInfoPlist(File file,
429 Map<String, ? super Object> params) throws IOException {
430 Map<String, String> data = new HashMap<>();
431 String identifier = StandardBundlerParam.isRuntimeInstaller(params) ?
432 MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params) :
433 "com.oracle.java." + MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params);
434 data.put("CF_BUNDLE_IDENTIFIER", identifier);
435 String name = StandardBundlerParam.isRuntimeInstaller(params) ?
436 getBundleName(params): "Java Runtime Image";
437 data.put("CF_BUNDLE_NAME", name);
438 data.put("CF_BUNDLE_VERSION", VERSION.fetchFrom(params));
439 data.put("CF_BUNDLE_SHORT_VERSION_STRING", VERSION.fetchFrom(params));
440
441 createResource(TEMPLATE_RUNTIME_INFO_PLIST, params)
442 .setPublicName("Runtime-Info.plist")
443 .setCategory(I18N.getString("resource.runtime-info-plist"))
444 .setSubstitutionData(data)
445 .saveToFile(file);
446 }
447
448 private void writeInfoPlist(File file, Map<String, ? super Object> params)
449 throws IOException {
450 Log.verbose(MessageFormat.format(I18N.getString(
451 "message.preparing-info-plist"), file.getAbsolutePath()));
452
453 //prepare config for exe
454 //Note: do not need CFBundleDisplayName if we don't support localization
455 Map<String, String> data = new HashMap<>();
456 data.put("DEPLOY_ICON_FILE", APP_NAME.fetchFrom(params) + ".icns");
457 data.put("DEPLOY_BUNDLE_IDENTIFIER",
458 MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params));
459 data.put("DEPLOY_BUNDLE_NAME",
460 getBundleName(params));
461 data.put("DEPLOY_BUNDLE_COPYRIGHT",
462 COPYRIGHT.fetchFrom(params) != null ?
463 COPYRIGHT.fetchFrom(params) : "Unknown");
464 data.put("DEPLOY_LAUNCHER_NAME", getLauncherName(params));
465 data.put("DEPLOY_BUNDLE_SHORT_VERSION",
466 VERSION.fetchFrom(params) != null ?
467 VERSION.fetchFrom(params) : "1.0.0");
468 data.put("DEPLOY_BUNDLE_CFBUNDLE_VERSION",
469 MAC_CF_BUNDLE_VERSION.fetchFrom(params) != null ?
470 MAC_CF_BUNDLE_VERSION.fetchFrom(params) : "100");
471
472 boolean hasMainJar = MAIN_JAR.fetchFrom(params) != null;
473 boolean hasMainModule =
474 StandardBundlerParam.MODULE.fetchFrom(params) != null;
475
476 if (hasMainJar) {
477 data.put("DEPLOY_MAIN_JAR_NAME", MAIN_JAR.fetchFrom(params).
478 getIncludedFiles().iterator().next());
479 }
480 else if (hasMainModule) {
481 data.put("DEPLOY_MODULE_NAME",
482 StandardBundlerParam.MODULE.fetchFrom(params));
483 }
484
485 StringBuilder sb = new StringBuilder();
486 List<String> jvmOptions = JAVA_OPTIONS.fetchFrom(params);
487
488 String newline = ""; //So we don't add extra line after last append
489 for (String o : jvmOptions) {
490 sb.append(newline).append(
491 " <string>").append(o).append("</string>");
492 newline = "\n";
493 }
494
495 data.put("DEPLOY_JAVA_OPTIONS", sb.toString());
496
497 sb = new StringBuilder();
498 List<String> args = ARGUMENTS.fetchFrom(params);
499 newline = "";
500 // So we don't add unneccessary extra line after last append
501
502 for (String o : args) {
503 sb.append(newline).append(" <string>").append(o).append(
504 "</string>");
505 newline = "\n";
506 }
507 data.put("DEPLOY_ARGUMENTS", sb.toString());
508
509 newline = "";
510
511 data.put("DEPLOY_LAUNCHER_CLASS", MAIN_CLASS.fetchFrom(params));
512
513 data.put("DEPLOY_APP_CLASSPATH",
514 getCfgClassPath(CLASSPATH.fetchFrom(params)));
515
516 StringBuilder bundleDocumentTypes = new StringBuilder();
517 StringBuilder exportedTypes = new StringBuilder();
518 for (Map<String, ? super Object>
519 fileAssociation : FILE_ASSOCIATIONS.fetchFrom(params)) {
520
521 List<String> extensions = FA_EXTENSIONS.fetchFrom(fileAssociation);
522
523 if (extensions == null) {
524 Log.verbose(I18N.getString(
525 "message.creating-association-with-null-extension"));
526 }
527
528 List<String> mimeTypes = FA_CONTENT_TYPE.fetchFrom(fileAssociation);
529 String itemContentType = MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params)
530 + "." + ((extensions == null || extensions.isEmpty())
531 ? "mime" : extensions.get(0));
532 String description = FA_DESCRIPTION.fetchFrom(fileAssociation);
533 File icon = FA_ICON.fetchFrom(fileAssociation);
534
535 bundleDocumentTypes.append(" <dict>\n")
536 .append(" <key>LSItemContentTypes</key>\n")
537 .append(" <array>\n")
538 .append(" <string>")
539 .append(itemContentType)
540 .append("</string>\n")
541 .append(" </array>\n")
542 .append("\n")
543 .append(" <key>CFBundleTypeName</key>\n")
544 .append(" <string>")
545 .append(description)
546 .append("</string>\n")
547 .append("\n")
548 .append(" <key>LSHandlerRank</key>\n")
549 .append(" <string>Owner</string>\n")
550 // TODO make a bundler arg
551 .append("\n")
552 .append(" <key>CFBundleTypeRole</key>\n")
553 .append(" <string>Editor</string>\n")
554 // TODO make a bundler arg
555 .append("\n")
556 .append(" <key>LSIsAppleDefaultForType</key>\n")
557 .append(" <true/>\n")
558 // TODO make a bundler arg
559 .append("\n");
560
561 if (icon != null && icon.exists()) {
562 bundleDocumentTypes
563 .append(" <key>CFBundleTypeIconFile</key>\n")
564 .append(" <string>")
565 .append(icon.getName())
566 .append("</string>\n");
567 }
568 bundleDocumentTypes.append(" </dict>\n");
569
570 exportedTypes.append(" <dict>\n")
571 .append(" <key>UTTypeIdentifier</key>\n")
572 .append(" <string>")
573 .append(itemContentType)
574 .append("</string>\n")
575 .append("\n")
576 .append(" <key>UTTypeDescription</key>\n")
577 .append(" <string>")
578 .append(description)
579 .append("</string>\n")
580 .append(" <key>UTTypeConformsTo</key>\n")
581 .append(" <array>\n")
582 .append(" <string>public.data</string>\n")
583 //TODO expose this?
584 .append(" </array>\n")
585 .append("\n");
586
587 if (icon != null && icon.exists()) {
588 exportedTypes.append(" <key>UTTypeIconFile</key>\n")
589 .append(" <string>")
590 .append(icon.getName())
591 .append("</string>\n")
592 .append("\n");
593 }
594
595 exportedTypes.append("\n")
596 .append(" <key>UTTypeTagSpecification</key>\n")
597 .append(" <dict>\n")
598 // TODO expose via param? .append(
599 // " <key>com.apple.ostype</key>\n");
600 // TODO expose via param? .append(
601 // " <string>ABCD</string>\n")
602 .append("\n");
603
604 if (extensions != null && !extensions.isEmpty()) {
605 exportedTypes.append(
606 " <key>public.filename-extension</key>\n")
607 .append(" <array>\n");
608
609 for (String ext : extensions) {
610 exportedTypes.append(" <string>")
611 .append(ext)
612 .append("</string>\n");
613 }
614 exportedTypes.append(" </array>\n");
615 }
616 if (mimeTypes != null && !mimeTypes.isEmpty()) {
617 exportedTypes.append(" <key>public.mime-type</key>\n")
618 .append(" <array>\n");
619
620 for (String mime : mimeTypes) {
621 exportedTypes.append(" <string>")
622 .append(mime)
623 .append("</string>\n");
624 }
625 exportedTypes.append(" </array>\n");
626 }
627 exportedTypes.append(" </dict>\n")
628 .append(" </dict>\n");
629 }
630 String associationData;
631 if (bundleDocumentTypes.length() > 0) {
632 associationData =
633 "\n <key>CFBundleDocumentTypes</key>\n <array>\n"
634 + bundleDocumentTypes.toString()
635 + " </array>\n\n"
636 + " <key>UTExportedTypeDeclarations</key>\n <array>\n"
637 + exportedTypes.toString()
638 + " </array>\n";
639 } else {
640 associationData = "";
641 }
642 data.put("DEPLOY_FILE_ASSOCIATIONS", associationData);
643
644 createResource(TEMPLATE_INFO_PLIST_LITE, params)
645 .setCategory(I18N.getString("resource.app-info-plist"))
646 .setSubstitutionData(data)
647 .setPublicName("Info.plist")
648 .saveToFile(file);
649 }
650
651 private void writePkgInfo(File file) throws IOException {
652 //hardcoded as it does not seem we need to change it ever
653 String signature = "????";
654
655 try (Writer out = Files.newBufferedWriter(file.toPath())) {
656 out.write(OS_TYPE_CODE + signature);
657 out.flush();
658 }
659 }
660
661 public static void addNewKeychain(Map<String, ? super Object> params)
662 throws IOException, InterruptedException {
663 if (Platform.getMajorVersion() < 10 ||
664 (Platform.getMajorVersion() == 10 &&
665 Platform.getMinorVersion() < 12)) {
666 // we need this for OS X 10.12+
667 return;
668 }
669
670 String keyChain = SIGNING_KEYCHAIN.fetchFrom(params);
671 if (keyChain == null || keyChain.isEmpty()) {
672 return;
673 }
674
675 // get current keychain list
676 String keyChainPath = new File (keyChain).getAbsolutePath().toString();
677 List<String> keychainList = new ArrayList<>();
678 int ret = IOUtils.getProcessOutput(
679 keychainList, "security", "list-keychains");
680 if (ret != 0) {
681 Log.error(I18N.getString("message.keychain.error"));
682 return;
683 }
684
685 boolean contains = keychainList.stream().anyMatch(
686 str -> str.trim().equals("\""+keyChainPath.trim()+"\""));
687 if (contains) {
688 // keychain is already added in the search list
689 return;
690 }
691
692 keyChains = new ArrayList<>();
693 // remove "
694 keychainList.forEach((String s) -> {
695 String path = s.trim();
696 if (path.startsWith("\"") && path.endsWith("\"")) {
697 path = path.substring(1, path.length()-1);
698 }
699 keyChains.add(path);
700 });
701
702 List<String> args = new ArrayList<>();
703 args.add("security");
704 args.add("list-keychains");
705 args.add("-s");
706
707 args.addAll(keyChains);
708 args.add(keyChain);
709
710 ProcessBuilder pb = new ProcessBuilder(args);
711 IOUtils.exec(pb);
712 }
713
714 public static void restoreKeychainList(Map<String, ? super Object> params)
715 throws IOException{
716 if (Platform.getMajorVersion() < 10 ||
717 (Platform.getMajorVersion() == 10 &&
718 Platform.getMinorVersion() < 12)) {
719 // we need this for OS X 10.12+
720 return;
721 }
722
723 if (keyChains == null || keyChains.isEmpty()) {
724 return;
725 }
726
727 List<String> args = new ArrayList<>();
728 args.add("security");
729 args.add("list-keychains");
730 args.add("-s");
731
732 args.addAll(keyChains);
733
734 ProcessBuilder pb = new ProcessBuilder(args);
735 IOUtils.exec(pb);
736 }
737
738 public static void signAppBundle(
739 Map<String, ? super Object> params, Path appLocation,
740 String signingIdentity, String identifierPrefix,
741 String entitlementsFile, String inheritedEntitlements)
742 throws IOException {
743 AtomicReference<IOException> toThrow = new AtomicReference<>();
744 String appExecutable = "/Contents/MacOS/" + APP_NAME.fetchFrom(params);
745 String keyChain = SIGNING_KEYCHAIN.fetchFrom(params);
746
747 // sign all dylibs and jars
748 try (Stream<Path> stream = Files.walk(appLocation)) {
749 stream.peek(path -> { // fix permissions
750 try {
751 Set<PosixFilePermission> pfp =
752 Files.getPosixFilePermissions(path);
753 if (!pfp.contains(PosixFilePermission.OWNER_WRITE)) {
754 pfp = EnumSet.copyOf(pfp);
755 pfp.add(PosixFilePermission.OWNER_WRITE);
756 Files.setPosixFilePermissions(path, pfp);
757 }
758 } catch (IOException e) {
759 Log.verbose(e);
760 }
761 }).filter(p -> Files.isRegularFile(p)
762 && !(p.toString().contains("/Contents/MacOS/libjli.dylib")
763 || p.toString().endsWith(appExecutable)
764 || p.toString().contains("/Contents/runtime")
765 || p.toString().contains("/Contents/Frameworks"))).forEach(p -> {
766 //noinspection ThrowableResultOfMethodCallIgnored
767 if (toThrow.get() != null) return;
768
769 // If p is a symlink then skip the signing process.
770 if (Files.isSymbolicLink(p)) {
771 if (VERBOSE.fetchFrom(params)) {
772 Log.verbose(MessageFormat.format(I18N.getString(
773 "message.ignoring.symlink"), p.toString()));
774 }
775 } else {
776 if (p.toString().endsWith(LIBRARY_NAME)) {
777 if (isFileSigned(p)) {
778 return;
779 }
780 }
781
782 List<String> args = new ArrayList<>();
783 args.addAll(Arrays.asList("codesign",
784 "-s", signingIdentity, // sign with this key
785 "--prefix", identifierPrefix,
786 // use the identifier as a prefix
787 "-vvvv"));
788 if (entitlementsFile != null &&
789 (p.toString().endsWith(".jar")
790 || p.toString().endsWith(".dylib"))) {
791 args.add("--entitlements");
792 args.add(entitlementsFile); // entitlements
793 } else if (inheritedEntitlements != null &&
794 Files.isExecutable(p)) {
795 args.add("--entitlements");
796 args.add(inheritedEntitlements);
797 // inherited entitlements for executable processes
798 }
799 if (keyChain != null && !keyChain.isEmpty()) {
800 args.add("--keychain");
801 args.add(keyChain);
802 }
803 args.add(p.toString());
804
805 try {
806 Set<PosixFilePermission> oldPermissions =
807 Files.getPosixFilePermissions(p);
808 File f = p.toFile();
809 f.setWritable(true, true);
810
811 ProcessBuilder pb = new ProcessBuilder(args);
812 IOUtils.exec(pb);
813
814 Files.setPosixFilePermissions(p, oldPermissions);
815 } catch (IOException ioe) {
816 toThrow.set(ioe);
817 }
818 }
819 });
820 }
821 IOException ioe = toThrow.get();
822 if (ioe != null) {
823 throw ioe;
824 }
825
826 // sign all runtime and frameworks
827 Consumer<? super Path> signIdentifiedByPList = path -> {
828 //noinspection ThrowableResultOfMethodCallIgnored
829 if (toThrow.get() != null) return;
830
831 try {
832 List<String> args = new ArrayList<>();
833 args.addAll(Arrays.asList("codesign",
834 "-f",
835 "-s", signingIdentity, // sign with this key
836 "--prefix", identifierPrefix,
837 // use the identifier as a prefix
838 "-vvvv"));
839 if (keyChain != null && !keyChain.isEmpty()) {
840 args.add("--keychain");
841 args.add(keyChain);
842 }
843 args.add(path.toString());
844 ProcessBuilder pb = new ProcessBuilder(args);
845 IOUtils.exec(pb);
846
847 args = new ArrayList<>();
848 args.addAll(Arrays.asList("codesign",
849 "-s", signingIdentity, // sign with this key
850 "--prefix", identifierPrefix,
851 // use the identifier as a prefix
852 "-vvvv"));
853 if (keyChain != null && !keyChain.isEmpty()) {
854 args.add("--keychain");
855 args.add(keyChain);
856 }
857 args.add(path.toString()
858 + "/Contents/_CodeSignature/CodeResources");
859 pb = new ProcessBuilder(args);
860 IOUtils.exec(pb);
861 } catch (IOException e) {
862 toThrow.set(e);
863 }
864 };
865
866 Path javaPath = appLocation.resolve("Contents/runtime");
867 if (Files.isDirectory(javaPath)) {
868 signIdentifiedByPList.accept(javaPath);
869
870 ioe = toThrow.get();
871 if (ioe != null) {
872 throw ioe;
873 }
874 }
875 Path frameworkPath = appLocation.resolve("Contents/Frameworks");
876 if (Files.isDirectory(frameworkPath)) {
877 Files.list(frameworkPath)
878 .forEach(signIdentifiedByPList);
879
880 ioe = toThrow.get();
881 if (ioe != null) {
882 throw ioe;
883 }
884 }
885
886 // sign the app itself
887 List<String> args = new ArrayList<>();
888 args.addAll(Arrays.asList("codesign",
889 "-s", signingIdentity, // sign with this key
890 "-vvvv")); // super verbose output
891 if (entitlementsFile != null) {
892 args.add("--entitlements");
893 args.add(entitlementsFile); // entitlements
894 }
895 if (keyChain != null && !keyChain.isEmpty()) {
896 args.add("--keychain");
897 args.add(keyChain);
898 }
899 args.add(appLocation.toString());
900
901 ProcessBuilder pb =
902 new ProcessBuilder(args.toArray(new String[args.size()]));
903 IOUtils.exec(pb);
904 }
905
906 private static boolean isFileSigned(Path file) {
907 ProcessBuilder pb =
908 new ProcessBuilder("codesign", "--verify", file.toString());
909
910 try {
911 IOUtils.exec(pb);
912 } catch (IOException ex) {
913 return false;
914 }
915
916 return true;
917 }
918
919 private static String extractBundleIdentifier(Map<String, Object> params) {
920 if (PREDEFINED_APP_IMAGE.fetchFrom(params) == null) {
921 return null;
922 }
923
924 try {
925 File infoPList = new File(PREDEFINED_APP_IMAGE.fetchFrom(params) +
926 File.separator + "Contents" +
927 File.separator + "Info.plist");
928
929 DocumentBuilderFactory dbf
930 = DocumentBuilderFactory.newDefaultInstance();
931 dbf.setFeature("http://apache.org/xml/features/" +
932 "nonvalidating/load-external-dtd", false);
933 DocumentBuilder b = dbf.newDocumentBuilder();
934 org.w3c.dom.Document doc = b.parse(new FileInputStream(
935 infoPList.getAbsolutePath()));
936
937 XPath xPath = XPathFactory.newInstance().newXPath();
938 // Query for the value of <string> element preceding <key>
939 // element with value equal to CFBundleIdentifier
940 String v = (String) xPath.evaluate(
941 "//string[preceding-sibling::key = \"CFBundleIdentifier\"][1]",
942 doc, XPathConstants.STRING);
943
944 if (v != null && !v.isEmpty()) {
945 return v;
946 }
947 } catch (Exception ex) {
948 Log.verbose(ex);
949 }
950
951 return null;
952 }
953
954 }