1 /*
2  * Copyright (c) 2014, 2021, 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.jpackage.internal;
27 
28 import java.io.IOException;
29 import java.io.PrintWriter;
30 import java.net.URI;
31 import java.net.URISyntaxException;
32 import java.nio.file.Files;
33 import java.nio.file.Path;
34 import java.text.MessageFormat;
35 import java.util.ArrayList;
36 import java.util.HashMap;
37 import java.util.List;
38 import java.util.Map;
39 import java.util.Objects;
40 import java.util.Optional;
41 import java.util.ResourceBundle;
42 
43 import static jdk.jpackage.internal.StandardBundlerParam.CONFIG_ROOT;
44 import static jdk.jpackage.internal.StandardBundlerParam.TEMP_ROOT;
45 import static jdk.jpackage.internal.StandardBundlerParam.VERBOSE;
46 import static jdk.jpackage.internal.StandardBundlerParam.APP_NAME;
47 import static jdk.jpackage.internal.StandardBundlerParam.LICENSE_FILE;
48 import static jdk.jpackage.internal.StandardBundlerParam.VERSION;
49 import static jdk.jpackage.internal.StandardBundlerParam.SIGN_BUNDLE;
50 import static jdk.jpackage.internal.MacBaseInstallerBundler.SIGNING_KEYCHAIN;
51 import static jdk.jpackage.internal.MacBaseInstallerBundler.SIGNING_KEY_USER;
52 import static jdk.jpackage.internal.MacAppImageBuilder.APP_STORE;
53 import static jdk.jpackage.internal.MacAppImageBuilder.MAC_CF_BUNDLE_IDENTIFIER;
54 import static jdk.jpackage.internal.OverridableResource.createResource;
55 
56 public class MacPkgBundler extends MacBaseInstallerBundler {
57 
58     private static final ResourceBundle I18N = ResourceBundle.getBundle(
59             "jdk.jpackage.internal.resources.MacResources");
60 
61     private static final String DEFAULT_BACKGROUND_IMAGE = "background_pkg.png";
62     private static final String DEFAULT_PDF = "product-def.plist";
63 
64     private static final String TEMPLATE_PREINSTALL_SCRIPT =
65             "preinstall.template";
66     private static final String TEMPLATE_POSTINSTALL_SCRIPT =
67             "postinstall.template";
68 
69     private static final BundlerParamInfo<Path> PACKAGES_ROOT =
70             new StandardBundlerParam<>(
71             "mac.pkg.packagesRoot",
72             Path.class,
73             params -> {
74                 Path packagesRoot =
75                         TEMP_ROOT.fetchFrom(params).resolve("packages");
76                 try {
77                     Files.createDirectories(packagesRoot);
78                 } catch (IOException ioe) {
79                     return null;
80                 }
81                 return packagesRoot;
82             },
83             (s, p) -> Path.of(s));
84 
85 
86     protected final BundlerParamInfo<Path> SCRIPTS_DIR =
87             new StandardBundlerParam<>(
88             "mac.pkg.scriptsDir",
89             Path.class,
90             params -> {
91                 Path scriptsDir =
92                         CONFIG_ROOT.fetchFrom(params).resolve("scripts");
93                 try {
94                     Files.createDirectories(scriptsDir);
95                 } catch (IOException ioe) {
96                     return null;
97                 }
98                 return scriptsDir;
99             },
100             (s, p) -> Path.of(s));
101 
102     public static final
103             BundlerParamInfo<String> DEVELOPER_ID_INSTALLER_SIGNING_KEY =
104             new StandardBundlerParam<>(
105             "mac.signing-key-developer-id-installer",
106             String.class,
107             params -> {
108                     String user = SIGNING_KEY_USER.fetchFrom(params);
109                     String keychain = SIGNING_KEYCHAIN.fetchFrom(params);
110                     String result = null;
111                     if (APP_STORE.fetchFrom(params)) {
112                         result = MacBaseInstallerBundler.findKey(
113                             "3rd Party Mac Developer Installer: ",
114                             user, keychain);
115                     }
116                     // if either not signing for app store or couldn't find
117                     if (result == null) {
118                         result = MacBaseInstallerBundler.findKey(
119                             "Developer ID Installer: ", user, keychain);
120                     }
121 
122                     if (result != null) {
123                         MacCertificate certificate = new MacCertificate(result);
124 
125                         if (!certificate.isValid()) {
126                             Log.error(MessageFormat.format(
127                                     I18N.getString("error.certificate.expired"),
128                                     result));
129                         }
130                     }
131 
132                     return result;
133                 },
134             (s, p) -> s);
135 
136     public static final BundlerParamInfo<String> INSTALLER_SUFFIX =
137             new StandardBundlerParam<> (
138             "mac.pkg.installerName.suffix",
139             String.class,
140             params -> "",
141             (s, p) -> s);
142 
bundle(Map<String, ? super Object> params, Path outdir)143     public Path bundle(Map<String, ? super Object> params,
144             Path outdir) throws PackagerException {
145         Log.verbose(MessageFormat.format(I18N.getString("message.building-pkg"),
146                 APP_NAME.fetchFrom(params)));
147 
148         IOUtils.writableOutputDir(outdir);
149 
150         try {
151             Path appImageDir = prepareAppBundle(params);
152 
153             if (appImageDir != null && prepareConfigFiles(params)) {
154 
155                 Path configScript = getConfig_Script(params);
156                 if (IOUtils.exists(configScript)) {
157                     IOUtils.run("bash", configScript);
158                 }
159 
160                 return createPKG(params, outdir, appImageDir);
161             }
162             return null;
163         } catch (IOException ex) {
164             Log.verbose(ex);
165             throw new PackagerException(ex);
166         }
167     }
168 
getPackages_AppPackage(Map<String, ? super Object> params)169     private Path getPackages_AppPackage(Map<String, ? super Object> params) {
170         return PACKAGES_ROOT.fetchFrom(params).resolve(
171                 APP_NAME.fetchFrom(params) + "-app.pkg");
172     }
173 
getConfig_DistributionXMLFile( Map<String, ? super Object> params)174     private Path getConfig_DistributionXMLFile(
175             Map<String, ? super Object> params) {
176         return CONFIG_ROOT.fetchFrom(params).resolve("distribution.dist");
177     }
178 
getConfig_PDF(Map<String, ? super Object> params)179     private Path getConfig_PDF(Map<String, ? super Object> params) {
180         return CONFIG_ROOT.fetchFrom(params).resolve("product-def.plist");
181     }
182 
getConfig_BackgroundImage(Map<String, ? super Object> params)183     private Path getConfig_BackgroundImage(Map<String, ? super Object> params) {
184         return CONFIG_ROOT.fetchFrom(params).resolve(
185                 APP_NAME.fetchFrom(params) + "-background.png");
186     }
187 
getConfig_BackgroundImageDarkAqua(Map<String, ? super Object> params)188     private Path getConfig_BackgroundImageDarkAqua(Map<String, ? super Object> params) {
189         return CONFIG_ROOT.fetchFrom(params).resolve(
190                 APP_NAME.fetchFrom(params) + "-background-darkAqua.png");
191     }
192 
getScripts_PreinstallFile(Map<String, ? super Object> params)193     private Path getScripts_PreinstallFile(Map<String, ? super Object> params) {
194         return SCRIPTS_DIR.fetchFrom(params).resolve("preinstall");
195     }
196 
getScripts_PostinstallFile( Map<String, ? super Object> params)197     private Path getScripts_PostinstallFile(
198             Map<String, ? super Object> params) {
199         return SCRIPTS_DIR.fetchFrom(params).resolve("postinstall");
200     }
201 
getAppIdentifier(Map<String, ? super Object> params)202     private String getAppIdentifier(Map<String, ? super Object> params) {
203         return MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params);
204     }
205 
preparePackageScripts(Map<String, ? super Object> params)206     private void preparePackageScripts(Map<String, ? super Object> params)
207             throws IOException {
208         Log.verbose(I18N.getString("message.preparing-scripts"));
209 
210         Map<String, String> data = new HashMap<>();
211 
212         Path appLocation = Path.of(getInstallDir(params, false),
213                          APP_NAME.fetchFrom(params) + ".app", "Contents", "app");
214 
215         data.put("INSTALL_LOCATION", getInstallDir(params, false));
216         data.put("APP_LOCATION", appLocation.toString());
217 
218         createResource(TEMPLATE_PREINSTALL_SCRIPT, params)
219                 .setCategory(I18N.getString("resource.pkg-preinstall-script"))
220                 .setSubstitutionData(data)
221                 .saveToFile(getScripts_PreinstallFile(params));
222         getScripts_PreinstallFile(params).toFile().setExecutable(true, false);
223 
224         createResource(TEMPLATE_POSTINSTALL_SCRIPT, params)
225                 .setCategory(I18N.getString("resource.pkg-postinstall-script"))
226                 .setSubstitutionData(data)
227                 .saveToFile(getScripts_PostinstallFile(params));
228         getScripts_PostinstallFile(params).toFile().setExecutable(true, false);
229     }
230 
URLEncoding(String pkgName)231     private static String URLEncoding(String pkgName) throws URISyntaxException {
232         URI uri = new URI(null, null, pkgName, null);
233         return uri.toASCIIString();
234     }
235 
prepareDistributionXMLFile(Map<String, ? super Object> params)236     private void prepareDistributionXMLFile(Map<String, ? super Object> params)
237             throws IOException {
238         Path f = getConfig_DistributionXMLFile(params);
239 
240         Log.verbose(MessageFormat.format(I18N.getString(
241                 "message.preparing-distribution-dist"), f.toAbsolutePath().toString()));
242 
243         IOUtils.createXml(f, xml -> {
244             xml.writeStartElement("installer-gui-script");
245             xml.writeAttribute("minSpecVersion", "1");
246 
247             xml.writeStartElement("title");
248             xml.writeCharacters(APP_NAME.fetchFrom(params));
249             xml.writeEndElement();
250 
251             xml.writeStartElement("background");
252             xml.writeAttribute("file",
253                     getConfig_BackgroundImage(params).getFileName().toString());
254             xml.writeAttribute("mime-type", "image/png");
255             xml.writeAttribute("alignment", "bottomleft");
256             xml.writeAttribute("scaling", "none");
257             xml.writeEndElement();
258 
259             xml.writeStartElement("background-darkAqua");
260             xml.writeAttribute("file",
261                     getConfig_BackgroundImageDarkAqua(params).getFileName().toString());
262             xml.writeAttribute("mime-type", "image/png");
263             xml.writeAttribute("alignment", "bottomleft");
264             xml.writeAttribute("scaling", "none");
265             xml.writeEndElement();
266 
267             String licFileStr = LICENSE_FILE.fetchFrom(params);
268             if (licFileStr != null) {
269                 Path licFile = Path.of(licFileStr);
270                 xml.writeStartElement("license");
271                 xml.writeAttribute("file", licFile.toAbsolutePath().toString());
272                 xml.writeAttribute("mime-type", "text/rtf");
273                 xml.writeEndElement();
274             }
275 
276             /*
277              * Note that the content of the distribution file
278              * below is generated by productbuild --synthesize
279              */
280             String appId = getAppIdentifier(params);
281 
282             xml.writeStartElement("pkg-ref");
283             xml.writeAttribute("id", appId);
284             xml.writeEndElement(); // </pkg-ref>
285             xml.writeStartElement("options");
286             xml.writeAttribute("customize", "never");
287             xml.writeAttribute("require-scripts", "false");
288             xml.writeAttribute("hostArchitectures",
289                     Platform.isArmMac() ? "arm64" : "x86_64");
290             xml.writeEndElement(); // </options>
291             xml.writeStartElement("choices-outline");
292             xml.writeStartElement("line");
293             xml.writeAttribute("choice", "default");
294             xml.writeStartElement("line");
295             xml.writeAttribute("choice", appId);
296             xml.writeEndElement(); // </line>
297             xml.writeEndElement(); // </line>
298             xml.writeEndElement(); // </choices-outline>
299             xml.writeStartElement("choice");
300             xml.writeAttribute("id", "default");
301             xml.writeEndElement(); // </choice>
302             xml.writeStartElement("choice");
303             xml.writeAttribute("id", appId);
304             xml.writeAttribute("visible", "false");
305             xml.writeStartElement("pkg-ref");
306             xml.writeAttribute("id", appId);
307             xml.writeEndElement(); // </pkg-ref>
308             xml.writeEndElement(); // </choice>
309             xml.writeStartElement("pkg-ref");
310             xml.writeAttribute("id", appId);
311             xml.writeAttribute("version", VERSION.fetchFrom(params));
312             xml.writeAttribute("onConclusion", "none");
313             try {
314                 xml.writeCharacters(URLEncoding(
315                         getPackages_AppPackage(params).getFileName().toString()));
316             } catch (URISyntaxException ex) {
317                 throw new IOException(ex);
318             }
319             xml.writeEndElement(); // </pkg-ref>
320 
321             xml.writeEndElement(); // </installer-gui-script>
322         });
323     }
324 
prepareConfigFiles(Map<String, ? super Object> params)325     private boolean prepareConfigFiles(Map<String, ? super Object> params)
326             throws IOException {
327 
328         createResource(DEFAULT_BACKGROUND_IMAGE, params)
329                 .setCategory(I18N.getString("resource.pkg-background-image"))
330                 .saveToFile(getConfig_BackgroundImage(params));
331 
332         createResource(DEFAULT_BACKGROUND_IMAGE, params)
333                 .setCategory(I18N.getString("resource.pkg-background-image"))
334                 .saveToFile(getConfig_BackgroundImageDarkAqua(params));
335 
336         createResource(DEFAULT_PDF, params)
337                 .setCategory(I18N.getString("resource.pkg-pdf"))
338                 .saveToFile(getConfig_PDF(params));
339 
340         prepareDistributionXMLFile(params);
341 
342         createResource(null, params)
343                 .setCategory(I18N.getString("resource.post-install-script"))
344                 .saveToFile(getConfig_Script(params));
345 
346         return true;
347     }
348 
349     // name of post-image script
getConfig_Script(Map<String, ? super Object> params)350     private Path getConfig_Script(Map<String, ? super Object> params) {
351         return CONFIG_ROOT.fetchFrom(params).resolve(
352                 APP_NAME.fetchFrom(params) + "-post-image.sh");
353     }
354 
patchCPLFile(Path cpl)355     private void patchCPLFile(Path cpl) throws IOException {
356         String cplData = Files.readString(cpl);
357         String[] lines = cplData.split("\n");
358         try (PrintWriter out = new PrintWriter(Files.newBufferedWriter(cpl))) {
359             int skip = 0;
360             // Used to skip Java.runtime bundle, since
361             // pkgbuild with --root will find two bundles app and Java runtime.
362             // We cannot generate component proprty list when using
363             // --component argument.
364             for (int i = 0; i < lines.length; i++) {
365                 if (lines[i].trim().equals("<key>BundleIsRelocatable</key>")) {
366                     out.println(lines[i]);
367                     out.println("<false/>");
368                     i++;
369                 } else if (lines[i].trim().equals("<key>ChildBundles</key>")) {
370                     ++skip;
371                 } else if ((skip > 0) && lines[i].trim().equals("</array>")) {
372                     --skip;
373                 } else {
374                     if (skip == 0) {
375                         out.println(lines[i]);
376                     }
377                 }
378             }
379         }
380     }
381 
382     // pkgbuild includes all components from "--root" and subfolders,
383     // so if we have app image in folder which contains other images, then they
384     // will be included as well. It does have "--filter" option which use regex
385     // to exclude files/folder, but it will overwrite default one which excludes
386     // based on doc "any .svn or CVS directories, and any .DS_Store files".
387     // So easy aproach will be to copy user provided app-image into temp folder
388     // if root path contains other files.
getRoot(Map<String, ? super Object> params, Path appLocation)389     private String getRoot(Map<String, ? super Object> params,
390             Path appLocation) throws IOException {
391         Path rootDir = appLocation.getParent() == null ?
392                 Path.of(".") : appLocation.getParent();
393 
394         // Not needed for runtime installer and it might break runtime installer
395         // if parent does not have any other files
396         if (!StandardBundlerParam.isRuntimeInstaller(params)) {
397             try (var fileList = Files.list(rootDir)) {
398                 Path[] list = fileList.toArray(Path[]::new);
399                 // We should only have app image and/or .DS_Store
400                 if (list.length == 1) {
401                     return rootDir.toString();
402                 } else if (list.length == 2) {
403                     // Check case with app image and .DS_Store
404                     if (list[0].toString().toLowerCase().endsWith(".ds_store") ||
405                         list[1].toString().toLowerCase().endsWith(".ds_store")) {
406                         return rootDir.toString(); // Only app image and .DS_Store
407                     }
408                 }
409             }
410         }
411 
412         // Copy to new root
413         Path newRoot = Files.createTempDirectory(
414                 TEMP_ROOT.fetchFrom(params), "root-");
415 
416         Path source, dest;
417 
418         if (StandardBundlerParam.isRuntimeInstaller(params)) {
419             // firs, is this already a runtime with
420             // <runtime>/Contents/Home - if so we need the Home dir
421             Path original = appLocation;
422             Path home = original.resolve("Contents/Home");
423             source = (Files.exists(home)) ? home : original;
424 
425             // Then we need to put back the <NAME>/Content/Home
426             dest = newRoot.resolve(
427                 MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params) + "/Contents/Home");
428         } else {
429             source = appLocation;
430             dest = newRoot.resolve(appLocation.getFileName());
431         }
432         IOUtils.copyRecursive(source, dest);
433 
434         return newRoot.toString();
435     }
436 
createPKG(Map<String, ? super Object> params, Path outdir, Path appLocation)437     private Path createPKG(Map<String, ? super Object> params,
438             Path outdir, Path appLocation) {
439         // generic find attempt
440         try {
441             Path appPKG = getPackages_AppPackage(params);
442 
443             String root = getRoot(params, appLocation);
444 
445             // Generate default CPL file
446             Path cpl = CONFIG_ROOT.fetchFrom(params).resolve("cpl.plist");
447             ProcessBuilder pb = new ProcessBuilder("/usr/bin/pkgbuild",
448                     "--root",
449                     root,
450                     "--install-location",
451                     getInstallDir(params, false),
452                     "--analyze",
453                     cpl.toAbsolutePath().toString());
454 
455             IOUtils.exec(pb, false, null, true, Executor.INFINITE_TIMEOUT);
456 
457             patchCPLFile(cpl);
458 
459             // build application package
460             if (APP_STORE.fetchFrom(params)) {
461                 pb = new ProcessBuilder("/usr/bin/pkgbuild",
462                         "--root",
463                         root,
464                         "--install-location",
465                         getInstallDir(params, false),
466                         "--component-plist",
467                         cpl.toAbsolutePath().toString(),
468                         "--identifier",
469                          MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params),
470                         appPKG.toAbsolutePath().toString());
471                 IOUtils.exec(pb, false, null, true, Executor.INFINITE_TIMEOUT);
472             } else {
473                 preparePackageScripts(params);
474                 pb = new ProcessBuilder("/usr/bin/pkgbuild",
475                         "--root",
476                         root,
477                         "--install-location",
478                         getInstallDir(params, false),
479                         "--component-plist",
480                         cpl.toAbsolutePath().toString(),
481                         "--scripts",
482                         SCRIPTS_DIR.fetchFrom(params)
483                         .toAbsolutePath().toString(),
484                         "--identifier",
485                          MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params),
486                         appPKG.toAbsolutePath().toString());
487                 IOUtils.exec(pb, false, null, true, Executor.INFINITE_TIMEOUT);
488             }
489 
490             // build final package
491             Path finalPKG = outdir.resolve(MAC_INSTALLER_NAME.fetchFrom(params)
492                     + INSTALLER_SUFFIX.fetchFrom(params)
493                     + ".pkg");
494             Files.createDirectories(outdir);
495 
496             List<String> commandLine = new ArrayList<>();
497             commandLine.add("/usr/bin/productbuild");
498 
499             commandLine.add("--resources");
500             commandLine.add(CONFIG_ROOT.fetchFrom(params).toAbsolutePath().toString());
501 
502             // maybe sign
503             if (Optional.ofNullable(
504                     SIGN_BUNDLE.fetchFrom(params)).orElse(Boolean.TRUE)) {
505                 if (Platform.getMajorVersion() > 10 ||
506                     (Platform.getMajorVersion() == 10 &&
507                     Platform.getMinorVersion() >= 12)) {
508                     // we need this for OS X 10.12+
509                     Log.verbose(I18N.getString("message.signing.pkg"));
510                 }
511 
512                 String signingIdentity =
513                         DEVELOPER_ID_INSTALLER_SIGNING_KEY.fetchFrom(params);
514                 if (signingIdentity != null) {
515                     commandLine.add("--sign");
516                     commandLine.add(signingIdentity);
517                 }
518 
519                 String keychainName = SIGNING_KEYCHAIN.fetchFrom(params);
520                 if (keychainName != null && !keychainName.isEmpty()) {
521                     commandLine.add("--keychain");
522                     commandLine.add(keychainName);
523                 }
524             }
525 
526             if (APP_STORE.fetchFrom(params)) {
527                 commandLine.add("--product");
528                 commandLine.add(getConfig_PDF(params)
529                         .toAbsolutePath().toString());
530                 commandLine.add("--component");
531                 Path p = Path.of(root, APP_NAME.fetchFrom(params) + ".app");
532                 commandLine.add(p.toAbsolutePath().toString());
533                 commandLine.add(getInstallDir(params, false));
534             } else {
535                 commandLine.add("--distribution");
536                 commandLine.add(getConfig_DistributionXMLFile(params)
537                         .toAbsolutePath().toString());
538                 commandLine.add("--package-path");
539                 commandLine.add(PACKAGES_ROOT.fetchFrom(params)
540                         .toAbsolutePath().toString());
541             }
542             commandLine.add(finalPKG.toAbsolutePath().toString());
543 
544             pb = new ProcessBuilder(commandLine);
545             IOUtils.exec(pb, false, null, true, Executor.INFINITE_TIMEOUT);
546 
547             return finalPKG;
548         } catch (Exception ignored) {
549             Log.verbose(ignored);
550             return null;
551         }
552     }
553 
554     //////////////////////////////////////////////////////////////////////////
555     // Implement Bundler
556     //////////////////////////////////////////////////////////////////////////
557 
558     @Override
getName()559     public String getName() {
560         return I18N.getString("pkg.bundler.name");
561     }
562 
563     @Override
getID()564     public String getID() {
565         return "pkg";
566     }
567 
isValidBundleIdentifier(String id)568     private static boolean isValidBundleIdentifier(String id) {
569         for (int i = 0; i < id.length(); i++) {
570             char a = id.charAt(i);
571             // We check for ASCII codes first which we accept. If check fails,
572             // check if it is acceptable extended ASCII or unicode character.
573             if ((a >= 'A' && a <= 'Z') || (a >= 'a' && a <= 'z')
574                     || (a >= '0' && a <= '9') || (a == '-' || a == '.')) {
575                 continue;
576             }
577             return false;
578         }
579         return true;
580     }
581 
582     @Override
validate(Map<String, ? super Object> params)583     public boolean validate(Map<String, ? super Object> params)
584             throws ConfigException {
585         try {
586             Objects.requireNonNull(params);
587 
588             // run basic validation to ensure requirements are met
589             // we are not interested in return code, only possible exception
590             validateAppImageAndBundeler(params);
591 
592             String identifier = MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params);
593             if (identifier == null) {
594                 throw new ConfigException(
595                         I18N.getString("message.app-image-requires-identifier"),
596                         I18N.getString(
597                             "message.app-image-requires-identifier.advice"));
598             }
599             if (!isValidBundleIdentifier(identifier)) {
600                 throw new ConfigException(
601                         MessageFormat.format(I18N.getString(
602                         "message.invalid-identifier"), identifier),
603                         I18N.getString("message.invalid-identifier.advice"));
604             }
605 
606             // reject explicitly set sign to true and no valid signature key
607             if (Optional.ofNullable(
608                     SIGN_BUNDLE.fetchFrom(params)).orElse(Boolean.FALSE)) {
609                 String signingIdentity =
610                         DEVELOPER_ID_INSTALLER_SIGNING_KEY.fetchFrom(params);
611                 if (signingIdentity == null) {
612                     throw new ConfigException(
613                             I18N.getString("error.explicit-sign-no-cert"),
614                             I18N.getString(
615                             "error.explicit-sign-no-cert.advice"));
616                 }
617             }
618 
619             // hdiutil is always available so there's no need
620             // to test for availability.
621 
622             return true;
623         } catch (RuntimeException re) {
624             if (re.getCause() instanceof ConfigException) {
625                 throw (ConfigException) re.getCause();
626             } else {
627                 throw new ConfigException(re);
628             }
629         }
630     }
631 
632     @Override
execute(Map<String, ? super Object> params, Path outputParentDir)633     public Path execute(Map<String, ? super Object> params,
634             Path outputParentDir) throws PackagerException {
635         return bundle(params, outputParentDir);
636     }
637 
638     @Override
supported(boolean runtimeInstaller)639     public boolean supported(boolean runtimeInstaller) {
640         return true;
641     }
642 
643     @Override
isDefault()644     public boolean isDefault() {
645         return false;
646     }
647 
648 }
649