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 package com.oracle.tools.packager.mac;
26
27 import com.oracle.tools.packager.AbstractImageBundler;
28 import com.oracle.tools.packager.BundlerParamInfo;
29 import com.oracle.tools.packager.EnumeratedBundlerParam;
30 import com.oracle.tools.packager.JreUtils;
31 import com.oracle.tools.packager.JreUtils.Rule;
32 import com.oracle.tools.packager.StandardBundlerParam;
33 import com.oracle.tools.packager.Log;
34 import com.sun.javafx.tools.packager.bundlers.BundleParams;
35 import com.oracle.tools.packager.ConfigException;
36 import com.oracle.tools.packager.IOUtils;
37 import com.oracle.tools.packager.RelativeFileSet;
38 import com.oracle.tools.packager.UnsupportedPlatformException;
39
40 import java.io.*;
41 import java.math.BigInteger;
42 import java.net.MalformedURLException;
43 import java.net.URL;
44 import java.nio.file.Files;
45 import java.nio.file.Paths;
46 import java.text.MessageFormat;
47 import java.util.*;
48 import java.util.regex.Matcher;
49 import java.util.regex.Pattern;
50
51 import static com.oracle.tools.packager.StandardBundlerParam.*;
52 import static com.oracle.tools.packager.mac.MacBaseInstallerBundler.SIGNING_KEYCHAIN;
53 import static com.oracle.tools.packager.mac.MacBaseInstallerBundler.SIGNING_KEY_USER;
54 import static com.oracle.tools.packager.mac.MacBaseInstallerBundler.getPredefinedImage;
55
56 public class MacAppBundler extends AbstractImageBundler {
57
58 private static final ResourceBundle I18N =
59 ResourceBundle.getBundle(MacAppBundler.class.getName());
60
61 public final static String MAC_BUNDLER_PREFIX =
62 BUNDLER_PREFIX + "macosx" + File.separator;
63
64 private static final String EXECUTABLE_NAME = "JavaAppLauncher";
65 private final static String LIBRARY_NAME = "libpackager.dylib";
66 private static final String TEMPLATE_BUNDLE_ICON = "GenericApp.icns";
67 private static final String OS_TYPE_CODE = "APPL";
68 private static final String TEMPLATE_INFO_PLIST_LEGACY = "Info.plist.template";
69 private static final String TEMPLATE_INFO_PLIST_LITE = "Info-lite.plist.template";
70
71 private static Map<String, String> getMacCategories() {
72 Map<String, String> map = new HashMap<>();
73 map.put("Business", "public.app-category.business");
74 map.put("Developer Tools", "public.app-category.developer-tools");
75 map.put("Education", "public.app-category.education");
76 map.put("Entertainment", "public.app-category.entertainment");
77 map.put("Finance", "public.app-category.finance");
78 map.put("Games", "public.app-category.games");
79 map.put("Graphics & Design", "public.app-category.graphics-design");
80 map.put("Healthcare & Fitness", "public.app-category.healthcare-fitness");
81 map.put("Lifestyle", "public.app-category.lifestyle");
82 map.put("Medical", "public.app-category.medical");
83 map.put("Music", "public.app-category.music");
84 map.put("News", "public.app-category.news");
85 map.put("Photography", "public.app-category.photography");
86 map.put("Productivity", "public.app-category.productivity");
87 map.put("Reference", "public.app-category.reference");
88 map.put("Social Networking", "public.app-category.social-networking");
89 map.put("Sports", "public.app-category.sports");
98 map.put("Board Games", "public.app-category.board-games");
99 map.put("Card Games", "public.app-category.card-games");
100 map.put("Casino Games", "public.app-category.casino-games");
101 map.put("Dice Games", "public.app-category.dice-games");
102 map.put("Educational Games", "public.app-category.educational-games");
103 map.put("Family Games", "public.app-category.family-games");
104 map.put("Kids Games", "public.app-category.kids-games");
105 map.put("Music Games", "public.app-category.music-games");
106 map.put("Puzzle Games", "public.app-category.puzzle-games");
107 map.put("Racing Games", "public.app-category.racing-games");
108 map.put("Role Playing Games", "public.app-category.role-playing-games");
109 map.put("Simulation Games", "public.app-category.simulation-games");
110 map.put("Sports Games", "public.app-category.sports-games");
111 map.put("Strategy Games", "public.app-category.strategy-games");
112 map.put("Trivia Games", "public.app-category.trivia-games");
113 map.put("Word Games", "public.app-category.word-games");
114
115 return map;
116 }
117
118 public static final BundlerParamInfo<Boolean> MAC_CONFIGURE_LAUNCHER_IN_PLIST =
119 new StandardBundlerParam<>(
120 I18N.getString("param.configure-launcher-in-plist"),
121 I18N.getString("param.configure-launcher-in-plist.description"),
122 "mac.configure-launcher-in-plist",
123 Boolean.class,
124 params -> Boolean.FALSE,
125 (s, p) -> Boolean.valueOf(s));
126
127 public static final EnumeratedBundlerParam<String> MAC_CATEGORY =
128 new EnumeratedBundlerParam<>(
129 I18N.getString("param.category-name"),
130 I18N.getString("param.category-name.description"),
131 "mac.category",
132 String.class,
133 params -> params.containsKey(CATEGORY.getID())
134 ? CATEGORY.fetchFrom(params)
135 : "Unknown",
136 (s, p) -> s,
137 getMacCategories(),
138 false //strict - for MacStoreBundler this should be strict
139 );
140
141 public static final BundlerParamInfo<String> MAC_CF_BUNDLE_NAME =
142 new StandardBundlerParam<>(
143 I18N.getString("param.cfbundle-name.name"),
144 I18N.getString("param.cfbundle-name.description"),
145 "mac.CFBundleName",
146 String.class,
167 if (validCFBundleVersion(s)) {
168 return s;
169 } else {
170 return "100";
171 }
172 },
173 (s, p) -> s);
174
175 public static final BundlerParamInfo<File> CONFIG_ROOT = new StandardBundlerParam<>(
176 I18N.getString("param.config-root.name"),
177 I18N.getString("param.config-root.description"),
178 "configRoot",
179 File.class,
180 params -> {
181 File configRoot = new File(BUILD_ROOT.fetchFrom(params), "macosx");
182 configRoot.mkdirs();
183 return configRoot;
184 },
185 (s, p) -> new File(s));
186
187 public static final BundlerParamInfo<URL> RAW_EXECUTABLE_URL = new StandardBundlerParam<>(
188 I18N.getString("param.raw-executable-url.name"),
189 I18N.getString("param.raw-executable-url.description"),
190 "mac.launcher.url",
191 URL.class,
192 params -> MacResources.class.getResource(EXECUTABLE_NAME),
193 (s, p) -> {
194 try {
195 return new URL(s);
196 } catch (MalformedURLException e) {
197 Log.info(e.toString());
198 return null;
199 }
200 });
201
202 public static final BundlerParamInfo<String> DEFAULT_ICNS_ICON = new StandardBundlerParam<>(
203 I18N.getString("param.default-icon-icns"),
204 I18N.getString("param.default-icon-icns.description"),
205 ".mac.default.icns",
206 String.class,
207 params -> TEMPLATE_BUNDLE_ICON,
208 (s, p) -> s);
209
210 public static final BundlerParamInfo<Rule[]> MAC_RULES = new StandardBundlerParam<>(
211 "",
212 "",
213 ".mac.runtime.rules",
214 Rule[].class,
215 MacAppBundler::createMacRuntimeRules,
216 (s, p) -> null
217 );
218
219 public static final BundlerParamInfo<RelativeFileSet> MAC_RUNTIME = new StandardBundlerParam<>(
220 I18N.getString("param.runtime.name"),
221 I18N.getString("param.runtime.description"),
222 BundleParams.PARAM_RUNTIME,
223 RelativeFileSet.class,
224 params -> extractMacRuntime(System.getProperty("java.home"), params),
225 MacAppBundler::extractMacRuntime
226 );
227
228 public static final BundlerParamInfo<String> DEVELOPER_ID_APP_SIGNING_KEY = new StandardBundlerParam<>(
229 I18N.getString("param.signing-key-developer-id-app.name"),
230 I18N.getString("param.signing-key-developer-id-app.description"),
231 "mac.signing-key-developer-id-app",
232 String.class,
233 params -> MacBaseInstallerBundler.findKey("Developer ID Application: " + SIGNING_KEY_USER.fetchFrom(params), SIGNING_KEYCHAIN.fetchFrom(params), VERBOSE.fetchFrom(params)),
234 (s, p) -> s);
235
236 public static final BundlerParamInfo<String> BUNDLE_ID_SIGNING_PREFIX = new StandardBundlerParam<>(
237 I18N.getString("param.bundle-id-signing-prefix.name"),
238 I18N.getString("param.bundle-id-signing-prefix.description"),
239 "mac.bundle-id-signing-prefix",
240 String.class,
241 params -> IDENTIFIER.fetchFrom(params) + ".",
242 (s, p) -> s);
243
244 public static final BundlerParamInfo<File> ICON_ICNS = new StandardBundlerParam<>(
245 I18N.getString("param.icon-icns.name"),
246 I18N.getString("param.icon-icns.description"),
247 "icon.icns",
248 File.class,
249 params -> {
250 File f = ICON.fetchFrom(params);
251 if (f != null && !f.getName().toLowerCase().endsWith(".icns")) {
252 Log.info(MessageFormat.format(I18N.getString("message.icon-not-icns"), f));
253 return null;
254 }
255 return f;
256 },
257 (s, p) -> new File(s));
258
259 public static RelativeFileSet extractMacRuntime(String base, Map<String, ? super Object> params) {
260 if (base.isEmpty()) {
261 return null;
262 }
263
264 File workingBase = new File(base);
265 workingBase = workingBase.getAbsoluteFile();
266 try {
267 workingBase = workingBase.getCanonicalFile();
268 } catch (IOException ignore) {
269 // we tried, workingBase will remain absolute and not canonical.
270 }
271
272 if (workingBase.getName().equals("jre")) {
273 workingBase = workingBase.getParentFile();
274 }
275 if (workingBase.getName().equals("Home")) {
276 workingBase = workingBase.getParentFile();
277 }
278 if (workingBase.getName().equals("Contents")) {
279 workingBase = workingBase.getParentFile();
280 }
281 return JreUtils.extractJreAsRelativeFileSet(workingBase.toString(),
282 MAC_RULES.fetchFrom(params), true);
283 }
284
285 public MacAppBundler() {
286 super();
287 baseResourceLoader = MacResources.class;
288 }
289
290 @Override
291 protected String getCacheLocation(Map<String, ? super Object> params) {
292 return "$CACHEDIR/";
293 }
294
295
296 public static boolean validCFBundleVersion(String v) {
297 // CFBundleVersion (String - iOS, OS X) specifies the build version
298 // number of the bundle, which identifies an iteration (released or
299 // unreleased) of the bundle. The build version number should be a
300 // string comprised of three non-negative, period-separated integers
301 // with the first integer being greater than zero. The string should
302 // only contain numeric (0-9) and period (.) characters. Leading zeros
303 // are truncated from each integer and will be ignored (that is,
304 // 1.02.3 is equivalent to 1.2.3). This key is not localizable.
305
306 if (v == null) {
307 return false;
308 }
309
310 String p[] = v.split("\\.");
311 if (p.length > 3 || p.length < 1) {
312 Log.verbose(I18N.getString("message.version-string-too-many-components"));
313 return false;
314 }
315
351 throw (ConfigException) re.getCause();
352 } else {
353 throw new ConfigException(re);
354 }
355 }
356 }
357
358 //to be used by chained bundlers, e.g. by EXE bundler to avoid
359 // skipping validation if p.type does not include "image"
360 public boolean doValidate(Map<String, ? super Object> p) throws UnsupportedPlatformException, ConfigException {
361 if (!System.getProperty("os.name").toLowerCase().contains("os x")) {
362 throw new UnsupportedPlatformException();
363 }
364
365 imageBundleValidation(p);
366
367 if (getPredefinedImage(p) != null) {
368 return true;
369 }
370
371 // make sure we are pointing at the right JDK.
372 RelativeFileSet runtime = MAC_RUNTIME.fetchFrom(p);
373 if (runtime != null) {
374 runtime = new RelativeFileSet(runtime);
375 if ("jre".equals(runtime.getBaseDirectory().getName())) {
376 runtime.upshift();
377 }
378 if ("Home".equals(runtime.getBaseDirectory().getName())) {
379 runtime.upshift();
380 }
381 if ("Contents".equals(runtime.getBaseDirectory().getName())) {
382 runtime.upshift();
383 }
384 }
385
386 //validate required inputs
387 testRuntime(runtime, new String[] {
388 "Contents/Home/(jre/)?lib/[^/]+/libjvm.dylib", // most reliable
389 "Contents/Home/(jre/)?lib/rt.jar", // fallback canary for JDK 8
390 });
391 if (USE_FX_PACKAGING.fetchFrom(p)) {
392 testRuntime(runtime, new String[] {"Contents/Home/(jre/)?lib/ext/jfxrt.jar", "Contents/Home/(jre/)?lib/jfxrt.jar"});
393 }
394
395 // validate short version
396 if (!validCFBundleVersion(MAC_CF_BUNDLE_VERSION.fetchFrom(p))) {
397 throw new ConfigException(
398 I18N.getString("error.invalid-cfbundle-version"),
399 I18N.getString("error.invalid-cfbundle-version.advice"));
400 }
401
402 // reject explicitly set sign to true and no valid signature key
403 if (Optional.ofNullable(SIGN_BUNDLE.fetchFrom(p)).orElse(Boolean.FALSE)) {
404 String signingIdentity = DEVELOPER_ID_APP_SIGNING_KEY.fetchFrom(p);
405 if (signingIdentity == null) {
406 throw new ConfigException(
407 I18N.getString("error.explicit-sign-no-cert"),
408 I18N.getString("error.explicit-sign-no-cert.advice"));
409 }
410 }
411
412 return true;
413 }
414
415 private File getConfig_InfoPlist(Map<String, ? super Object> params) {
416 return new File(CONFIG_ROOT.fetchFrom(params), "Info.plist");
417 }
418
419 private File getConfig_Icon(Map<String, ? super Object> params) {
420 return new File(CONFIG_ROOT.fetchFrom(params), APP_NAME.fetchFrom(params) + ".icns");
421 }
422
423 private void prepareConfigFiles(Map<String, ? super Object> params) throws IOException {
424 File infoPlistFile = getConfig_InfoPlist(params);
425 infoPlistFile.createNewFile();
426 writeInfoPlist(infoPlistFile, params);
427
428 // Copy icon to Resources folder
429 prepareIcon(params);
430 }
431
432 public File doBundle(Map<String, ? super Object> p, File outputDirectory, boolean dependentTask) {
433 File rootDirectory = null;
434 Map<String, ? super Object> originalParams = new HashMap<>(p);
435
436 if (!outputDirectory.isDirectory() && !outputDirectory.mkdirs()) {
437 throw new RuntimeException(MessageFormat.format(I18N.getString("error.cannot-create-output-dir"), outputDirectory.getAbsolutePath()));
438 }
439 if (!outputDirectory.canWrite()) {
440 throw new RuntimeException(MessageFormat.format(I18N.getString("error.cannot-write-to-output-dir"), outputDirectory.getAbsolutePath()));
441 }
442
443 try {
444 final File predefinedImage = getPredefinedImage(p);
445 if (predefinedImage != null) {
446 return predefinedImage;
447 }
448
449 // side effect is temp dir is created if not specified
450 BUILD_ROOT.fetchFrom(p);
451
452 //prepare config resources (we will copy them to the bundle later)
453 // NB: explicitly saving them to simplify customization
454 prepareConfigFiles(p);
455
456 // Create directory structure
457 rootDirectory = new File(outputDirectory, APP_NAME.fetchFrom(p) + ".app");
458 IOUtils.deleteRecursive(rootDirectory);
459 rootDirectory.mkdirs();
460
461 if (!dependentTask) {
462 Log.info(MessageFormat.format(I18N.getString("message.creating-app-bundle"), rootDirectory.getAbsolutePath()));
463 }
464
465 File contentsDirectory = new File(rootDirectory, "Contents");
466 contentsDirectory.mkdirs();
467
468 File macOSDirectory = new File(contentsDirectory, "MacOS");
469 macOSDirectory.mkdirs();
470
471 File javaDirectory = new File(contentsDirectory, "Java");
472 javaDirectory.mkdirs();
473
474 File plugInsDirectory = new File(contentsDirectory, "PlugIns");
475
476 File resourcesDirectory = new File(contentsDirectory, "Resources");
477 resourcesDirectory.mkdirs();
478
479 // Generate PkgInfo
480 File pkgInfoFile = new File(contentsDirectory, "PkgInfo");
481 pkgInfoFile.createNewFile();
482 writePkgInfo(pkgInfoFile);
483
484 // Copy executable to MacOS folder
485 File executableFile = new File(macOSDirectory, getLauncherName(p));
486 IOUtils.copyFromURL(
487 RAW_EXECUTABLE_URL.fetchFrom(p),
488 executableFile);
489
490 // Copy library to the MacOS folder
491 IOUtils.copyFromURL(
492 MacResources.class.getResource(LIBRARY_NAME),
493 new File(macOSDirectory, LIBRARY_NAME));
494
495 // maybe generate launcher config
496 if (!MAC_CONFIGURE_LAUNCHER_IN_PLIST.fetchFrom(p)) {
497 if (LAUNCHER_CFG_FORMAT.fetchFrom(p).equals(CFG_FORMAT_PROPERTIES)) {
498 writeCfgFile(p, rootDirectory);
499 } else {
500 writeCfgFile(p, new File(rootDirectory, getLauncherCfgName(p)), getRuntimeLocation(p));
501 }
502 }
503
504 executableFile.setExecutable(true, false);
505
506 // Copy runtime to PlugIns folder
507 copyRuntime(plugInsDirectory, p);
508
509 // Copy class path entries to Java folder
510 copyClassPathEntries(javaDirectory, p);
511
512 //TODO: Need to support adding native libraries.
513 // Copy library path entries to MacOS folder
514 //copyLibraryPathEntries(macOSDirectory);
515
516 /*********** Take care of "config" files *******/
517 // Copy icon to Resources folder
518 IOUtils.copyFile(getConfig_Icon(p),
519 new File(resourcesDirectory, getConfig_Icon(p).getName()));
520
521 // copy file association icons
522 for (Map<String, ? super Object> fa : FILE_ASSOCIATIONS.fetchFrom(p)) {
523 File f = FA_ICON.fetchFrom(fa);
524 if (f != null && f.exists()) {
525 IOUtils.copyFile(f,
526 new File(resourcesDirectory, f.getName()));
527 }
528 }
529
530 // Generate Info.plist
531 IOUtils.copyFile(getConfig_InfoPlist(p),
532 new File(contentsDirectory, "Info.plist"));
533
534 // create the secondary launchers, if any
535 List<Map<String, ? super Object>> entryPoints = StandardBundlerParam.SECONDARY_LAUNCHERS.fetchFrom(p);
536 for (Map<String, ? super Object> entryPoint : entryPoints) {
537 Map<String, ? super Object> tmp = new HashMap<>(originalParams);
538 tmp.putAll(entryPoint);
539 createLauncherForEntryPoint(tmp, rootDirectory);
540 }
541
542 // maybe sign
543 if (Optional.ofNullable(SIGN_BUNDLE.fetchFrom(p)).orElse(Boolean.TRUE)) {
544 String signingIdentity = DEVELOPER_ID_APP_SIGNING_KEY.fetchFrom(p);
545 if (signingIdentity != null) {
546 MacBaseInstallerBundler.signAppBundle(p, rootDirectory, signingIdentity, BUNDLE_ID_SIGNING_PREFIX.fetchFrom(p));
547 }
548 }
549 } catch (IOException ex) {
550 Log.info(ex.toString());
551 Log.verbose(ex);
552 return null;
553 } finally {
554 if (!VERBOSE.fetchFrom(p)) {
555 //cleanup
556 cleanupConfigFiles(p);
557 } else {
558 Log.info(MessageFormat.format(I18N.getString("message.config-save-location"), CONFIG_ROOT.fetchFrom(p).getAbsolutePath()));
559 }
560 }
561 return rootDirectory;
562 }
563
564 public void cleanupConfigFiles(Map<String, ? super Object> params) {
565 //Since building the app can be bypassed, make sure configRoot was set
566 if (CONFIG_ROOT.fetchFrom(params) != null) {
567 getConfig_Icon(params).delete();
568 getConfig_InfoPlist(params).delete();
569 }
570 }
571
572 private void copyClassPathEntries(File javaDirectory, Map<String, ? super Object> params) throws IOException {
573 List<RelativeFileSet> resourcesList = APP_RESOURCES_LIST.fetchFrom(params);
574 if (resourcesList == null) {
575 throw new RuntimeException(I18N.getString("message.null-classpath"));
576 }
577
578 for (RelativeFileSet classPath : resourcesList) {
579 File srcdir = classPath.getBaseDirectory();
580 for (String fname : classPath.getIncludedFiles()) {
581 IOUtils.copyFile(
582 new File(srcdir, fname), new File(javaDirectory, fname));
583 }
584 }
585 }
586
587 private void copyRuntime(File plugInsDirectory, Map<String, ? super Object> params) throws IOException {
588 RelativeFileSet runtime = MAC_RUNTIME.fetchFrom(params);
589 if (runtime == null) {
590 //request to use system runtime => do not bundle
591 return;
592 }
593 runtime = new RelativeFileSet(runtime);
594 if ("jre".equals(runtime.getBaseDirectory().getName())) {
595 runtime.upshift();
596 }
597 if ("Home".equals(runtime.getBaseDirectory().getName())) {
598 runtime.upshift();
599 }
600 if ("Contents".equals(runtime.getBaseDirectory().getName())) {
601 runtime.upshift();
602 }
603
604
605 plugInsDirectory.mkdirs();
606
607 File srcdir = runtime.getBaseDirectory();
608 // the name in .../Contents/PlugIns/ must have a dot to be verified
609 // properly by the Mac App Store.
610 File destDir = new File(plugInsDirectory, "Java.runtime");
611 Set<String> filesToCopy = runtime.getIncludedFiles();
612
613 for (String fname : filesToCopy) {
614 IOUtils.copyFile(
615 new File(srcdir, fname), new File(destDir, fname));
616 }
617 }
618
619 private void prepareIcon(Map<String, ? super Object> params) throws IOException {
620 File icon = ICON_ICNS.fetchFrom(params);
621 if (icon == null || !icon.exists()) {
622 fetchResource(MAC_BUNDLER_PREFIX+ APP_NAME.fetchFrom(params) +".icns",
623 "icon",
624 DEFAULT_ICNS_ICON.fetchFrom(params),
625 getConfig_Icon(params),
626 VERBOSE.fetchFrom(params),
627 DROP_IN_RESOURCES_ROOT.fetchFrom(params));
628 } else {
629 fetchResource(MAC_BUNDLER_PREFIX+ APP_NAME.fetchFrom(params) +".icns",
630 "icon",
631 icon,
632 getConfig_Icon(params),
633 VERBOSE.fetchFrom(params),
634 DROP_IN_RESOURCES_ROOT.fetchFrom(params));
635 }
636 }
637
638 private String getLauncherName(Map<String, ? super Object> params) {
639 if (APP_NAME.fetchFrom(params) != null) {
640 return APP_NAME.fetchFrom(params);
641 } else {
642 return MAIN_CLASS.fetchFrom(params);
643 }
644 }
645
646 private String getBundleName(Map<String, ? super Object> params) {
647 //TODO: Check to see what rules/limits are in place for CFBundleName
648 if (MAC_CF_BUNDLE_NAME.fetchFrom(params) != null) {
649 String bn = MAC_CF_BUNDLE_NAME.fetchFrom(params);
650 if (bn.length() > 16) {
651 Log.info(MessageFormat.format(I18N.getString("message.bundle-name-too-long-warning"), MAC_CF_BUNDLE_NAME.getID(), bn));
652 }
653 return MAC_CF_BUNDLE_NAME.fetchFrom(params);
654 } else if (APP_NAME.fetchFrom(params) != null) {
655 return APP_NAME.fetchFrom(params);
656 } else {
657 String nm = MAIN_CLASS.fetchFrom(params);
658 if (nm.length() > 16) {
659 nm = nm.substring(0, 16);
660 }
661 return nm;
662 }
663 }
664
665 private String getRuntimeLocation(Map<String, ? super Object> params) {
666 if (MAC_RUNTIME.fetchFrom(params) == null) {
667 return "";
668 } else {
669 return "$APPDIR/PlugIns/Java.runtime";
670 }
671 }
672
673 private void writeInfoPlist(File file, Map<String, ? super Object> params) throws IOException {
674 Log.verbose(MessageFormat.format(I18N.getString("message.preparing-info-plist"), file.getAbsolutePath()));
675
676 //prepare config for exe
677 //Note: do not need CFBundleDisplayName if we do not support localization
678 Map<String, String> data = new HashMap<>();
679 data.put("DEPLOY_ICON_FILE", getConfig_Icon(params).getName());
680 data.put("DEPLOY_BUNDLE_IDENTIFIER",
681 MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params));
682 data.put("DEPLOY_BUNDLE_NAME",
683 getBundleName(params));
684 data.put("DEPLOY_BUNDLE_COPYRIGHT",
685 COPYRIGHT.fetchFrom(params) != null ? COPYRIGHT.fetchFrom(params) : "Unknown");
686 data.put("DEPLOY_LAUNCHER_NAME", getLauncherName(params));
687 if (MAC_RUNTIME.fetchFrom(params) != null) {
688 data.put("DEPLOY_JAVA_RUNTIME_NAME", "$APPDIR/PlugIns/Java.runtime");
689 } else {
690 data.put("DEPLOY_JAVA_RUNTIME_NAME", "");
691 }
692 data.put("DEPLOY_BUNDLE_SHORT_VERSION",
693 VERSION.fetchFrom(params) != null ? VERSION.fetchFrom(params) : "1.0.0");
694 data.put("DEPLOY_BUNDLE_CFBUNDLE_VERSION",
695 MAC_CF_BUNDLE_VERSION.fetchFrom(params) != null ? MAC_CF_BUNDLE_VERSION.fetchFrom(params) : "100");
696 data.put("DEPLOY_BUNDLE_CATEGORY",
697 //TODO parameters should provide set of values for IDEs
698 MAC_CATEGORY.validatedFetchFrom(params));
699
700 data.put("DEPLOY_MAIN_JAR_NAME", MAIN_JAR.fetchFrom(params).getIncludedFiles().iterator().next());
701
702 data.put("DEPLOY_PREFERENCES_ID", PREFERENCES_ID.fetchFrom(params).toLowerCase());
703
704 StringBuilder sb = new StringBuilder();
705 List<String> jvmOptions = JVM_OPTIONS.fetchFrom(params);
706
707 String newline = ""; //So we don't add unneccessary extra line after last append
708 for (String o : jvmOptions) {
709 sb.append(newline).append(" <string>").append(o).append("</string>");
710 newline = "\n";
711 }
712
713 Map<String, String> jvmProps = JVM_PROPERTIES.fetchFrom(params);
714 for (Map.Entry<String, String> entry : jvmProps.entrySet()) {
715 sb.append(newline)
716 .append(" <string>-D")
717 .append(entry.getKey())
718 .append("=")
719 .append(entry.getValue())
720 .append("</string>");
721 newline = "\n";
722 }
723
724 String preloader = PRELOADER_CLASS.fetchFrom(params);
725 if (preloader != null) {
726 sb.append(newline)
727 .append(" <string>-Djavafx.preloader=")
728 .append(preloader)
729 .append("</string>");
730 //newline = "\n";
731 }
732
733 data.put("DEPLOY_JVM_OPTIONS", sb.toString());
734
735 sb = new StringBuilder();
736 List<String> args = ARGUMENTS.fetchFrom(params);
737 newline = ""; //So we don't add unneccessary extra line after last append
738 for (String o : args) {
739 sb.append(newline).append(" <string>").append(o).append("</string>");
740 newline = "\n";
741 }
742 data.put("DEPLOY_ARGUMENTS", sb.toString());
743
744 newline = "";
745 sb = new StringBuilder();
746 Map<String, String> overridableJVMOptions = USER_JVM_OPTIONS.fetchFrom(params);
747 for (Map.Entry<String, String> arg: overridableJVMOptions.entrySet()) {
748 sb.append(newline)
749 .append(" <key>").append(arg.getKey()).append("</key>\n")
750 .append(" <string>").append(arg.getValue()).append("</string>");
751 newline = "\n";
752 }
753 data.put("DEPLOY_JVM_USER_OPTIONS", sb.toString());
754
755
756 data.put("DEPLOY_LAUNCHER_CLASS", MAIN_CLASS.fetchFrom(params));
757
758 StringBuilder macroedPath = new StringBuilder();
759 for (String s : CLASSPATH.fetchFrom(params).split("[ ;:]+")) {
760 macroedPath.append(s);
761 macroedPath.append(":");
762 }
763 macroedPath.deleteCharAt(macroedPath.length() - 1);
764
765 data.put("DEPLOY_APP_CLASSPATH", macroedPath.toString());
766
767 //TODO: Add remainder of the classpath
768
769 StringBuilder bundleDocumentTypes = new StringBuilder();
770 StringBuilder exportedTypes = new StringBuilder();
771 for (Map<String, ? super Object> fileAssociation : FILE_ASSOCIATIONS.fetchFrom(params)) {
772
773 List<String> extensions = FA_EXTENSIONS.fetchFrom(fileAssociation);
774
775 if (extensions == null) {
776 Log.info(I18N.getString("message.creating-association-with-null-extension"));
777 }
778
779 List<String> mimeTypes = FA_CONTENT_TYPE.fetchFrom(fileAssociation);
780 String itemContentType = MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params) + "." + ((extensions == null || extensions.isEmpty())
781 ? "mime"
782 : extensions.get(0));
783 String description = FA_DESCRIPTION.fetchFrom(fileAssociation);
784 File icon = FA_ICON.fetchFrom(fileAssociation); //TODO FA_ICON_ICNS
785
786 bundleDocumentTypes.append(" <dict>\n")
787 .append(" <key>LSItemContentTypes</key>\n")
788 .append(" <array>\n")
789 .append(" <string>")
790 .append(itemContentType)
791 .append("</string>\n")
792 .append(" </array>\n")
793 .append("\n")
794 .append(" <key>CFBundleTypeName</key>\n")
795 .append(" <string>")
796 .append(description)
797 .append("</string>\n")
798 .append("\n")
799 .append(" <key>LSHandlerRank</key>\n")
800 .append(" <string>Owner</string>\n") //TODO make a bundler arg
801 .append("\n")
802 .append(" <key>CFBundleTypeRole</key>\n")
803 .append(" <string>Editor</string>\n") // TODO make a bundler arg
804 .append("\n")
805 .append(" <key>LSIsAppleDefaultForType</key>\n")
806 .append(" <true/>\n") // TODO make a bundler arg
807 .append("\n");
808
809 if (icon != null && icon.exists()) {
810 //?
811 bundleDocumentTypes.append(" <key>CFBundleTypeIconFile</key>\n")
812 .append(" <string>")
813 .append(icon.getName())
814 .append("</string>\n");
815 }
816 bundleDocumentTypes.append(" </dict>\n");
817
818 exportedTypes.append(" <dict>\n")
819 .append(" <key>UTTypeIdentifier</key>\n")
820 .append(" <string>")
821 .append(itemContentType)
822 .append("</string>\n")
823 .append("\n")
824 .append(" <key>UTTypeDescription</key>\n")
825 .append(" <string>")
826 .append(description)
827 .append("</string>\n")
828 .append(" <key>UTTypeConformsTo</key>\n")
829 .append(" <array>\n")
830 .append(" <string>public.data</string>\n") //TODO expose this?
831 .append(" </array>\n")
832 .append("\n");
833
834 if (icon != null && icon.exists()) {
835 exportedTypes.append(" <key>UTTypeIconFile</key>\n")
836 .append(" <string>")
837 .append(icon.getName())
838 .append("</string>\n")
839 .append("\n");
840 }
841
842 exportedTypes.append("\n")
843 .append(" <key>UTTypeTagSpecification</key>\n")
844 .append(" <dict>\n")
845 //TODO expose via param? .append(" <key>com.apple.ostype</key>\n");
846 //TODO expose via param? .append(" <string>ABCD</string>\n")
847 .append("\n");
848
849 if (extensions != null && !extensions.isEmpty()) {
850 exportedTypes.append(" <key>public.filename-extension</key>\n")
851 .append(" <array>\n");
852
853 for (String ext : extensions) {
854 exportedTypes.append(" <string>")
855 .append(ext)
856 .append("</string>\n");
857 }
858 exportedTypes.append(" </array>\n");
859 }
860 if (mimeTypes != null && !mimeTypes.isEmpty()) {
861 exportedTypes.append(" <key>public.mime-type</key>\n")
862 .append(" <array>\n");
863
864 for (String mime : mimeTypes) {
865 exportedTypes.append(" <string>")
866 .append(mime)
867 .append("</string>\n");
868 }
869 exportedTypes.append(" </array>\n");
870 }
871 exportedTypes.append(" </dict>\n")
872 .append(" </dict>\n");
873 }
874 String associationData;
875 if (bundleDocumentTypes.length() > 0) {
876 associationData = "\n <key>CFBundleDocumentTypes</key>\n <array>\n"
877 + bundleDocumentTypes.toString()
878 + " </array>\n\n <key>UTExportedTypeDeclarations</key>\n <array>\n"
879 + exportedTypes.toString()
880 + " </array>\n";
881 } else {
882 associationData = "";
883 }
884 data.put("DEPLOY_FILE_ASSOCIATIONS", associationData);
885
886
887 Writer w = new BufferedWriter(new FileWriter(file));
888 w.write(preprocessTextResource(
889 MAC_BUNDLER_PREFIX + getConfig_InfoPlist(params).getName(),
890 I18N.getString("resource.bundle-config-file"),
891 MAC_CONFIGURE_LAUNCHER_IN_PLIST.fetchFrom(params)
892 ? TEMPLATE_INFO_PLIST_LEGACY
893 : TEMPLATE_INFO_PLIST_LITE,
894 data, VERBOSE.fetchFrom(params),
895 DROP_IN_RESOURCES_ROOT.fetchFrom(params)));
896 w.close();
897
898 }
899
900 private void writePkgInfo(File file) throws IOException {
901
902 //hardcoded as it does not seem we need to change it ever
903 String signature = "????";
904
905 try (Writer out = new BufferedWriter(new FileWriter(file))) {
906 out.write(OS_TYPE_CODE + signature);
907 out.flush();
908 }
909 }
910
911 public static Rule[] createMacRuntimeRules(Map<String, ? super Object> params) {
912 if (!System.getProperty("os.name").toLowerCase().contains("os x")) {
913 // we will never get a sensible answer unless we are running on OSX,
914 // so quit now and return null indicating 'no sensible value'
915 return null;
916 }
917
918 //Subsetting of JRE is restricted.
919 //JRE README defines what is allowed to strip:
920 // http://www.oracle.com/technetwork/java/javase/jre-8-readme-2095710.html
921 //
922
923 List<Rule> rules = new ArrayList<>();
924
925 File baseDir;
926
927 if (params.containsKey(MAC_RUNTIME.getID())) {
928 Object o = params.get(MAC_RUNTIME.getID());
929 if (o instanceof RelativeFileSet) {
930 baseDir = ((RelativeFileSet)o).getBaseDirectory();
931 } else {
932 baseDir = new File(o.toString());
933 }
934 } else {
935 baseDir = new File(System.getProperty("java.home"));
936 }
937
938 // we accept either pointing at the directories typically installed at:
939 // /Libraries/Java/JavaVirtualMachine/jdk1.8.0_40/
940 // * .
941 // * Contents/Home
942 // * Contents/Home/jre
943 // /Library/Internet\ Plug-Ins/JavaAppletPlugin.plugin/
944 // * .
945 // * /Contents/Home
946 // version may change, and if we don't detect any Contents/Home or Contents/Home/jre we will
947 // presume we are at a root.
948
949 if (!baseDir.exists()) {
950 throw new RuntimeException(I18N.getString("error.non-existent-runtime"),
951 new ConfigException(I18N.getString("error.non-existent-runtime"),
952 I18N.getString("error.non-existent-runtime.advice")));
953 }
954
955 boolean isJRE;
956 boolean isJDK;
957
958 try {
959 String path = baseDir.getCanonicalPath();
960 if (path.endsWith("/Contents/Home/jre")) {
961 baseDir = baseDir.getParentFile().getParentFile().getParentFile();
962 } else if (path.endsWith("/Contents/Home")) {
963 baseDir = baseDir.getParentFile().getParentFile();
964 }
965
966 isJRE = new File(baseDir, "Contents/Home/lib/jli/libjli.dylib").exists();
967 isJDK = new File(baseDir, "Contents/Home/jre/lib/jli/libjli.dylib").exists();
968
969 } catch (IOException e) {
970 throw new RuntimeException(e);
971 }
972
973 if (!(isJRE || isJDK)) {
974 throw new RuntimeException(I18N.getString("error.cannot-detect-runtime-in-directory"),
975 new ConfigException(I18N.getString("error.cannot-detect-runtime-in-directory"),
976 I18N.getString("error.cannot-detect-runtime-in-directory.advice")));
977 }
978
979 // we need the Info.plist for signing
980 rules.add(Rule.suffix("/contents/info.plist"));
981
982 // Strip some JRE specific stuff
983 if (isJRE) {
984 rules.add(Rule.suffixNeg("/contents/disabled.plist"));
985 rules.add(Rule.suffixNeg("/contents/enabled.plist"));
986 rules.add(Rule.substrNeg("/contents/frameworks/"));
987 }
988
989 // strip out command line tools
990 rules.add(Rule.suffixNeg("home/bin"));
991 if (isJDK) {
992 rules.add(Rule.suffixNeg("home/jre/bin"));
993 }
994
995 // strip out JRE stuff
996 if (isJRE) {
997 // update helper
998 rules.add(Rule.suffixNeg("resources"));
999 // interfacebuilder files
1000 rules.add(Rule.suffixNeg("lib/nibs"));
1001 // browser integration
1002 rules.add(Rule.suffixNeg("lib/libnpjp2.dylib"));
1003 // java webstart
1004 rules.add(Rule.suffixNeg("lib/security/javaws.policy"));
1005 rules.add(Rule.suffixNeg("lib/shortcuts"));
1006
1007 // general deploy libraries
1008 rules.add(Rule.suffixNeg("lib/deploy"));
1009 rules.add(Rule.suffixNeg("lib/deploy.jar"));
1010 rules.add(Rule.suffixNeg("lib/javaws.jar"));
1011 rules.add(Rule.suffixNeg("lib/libdeploy.dylib"));
1012 rules.add(Rule.suffixNeg("lib/plugin.jar"));
1013 }
1014
1015 // strip out man pages
1016 rules.add(Rule.suffixNeg("home/man"));
1017
1018 // this is the build hashes, strip or keep?
1019 //rules.add(Rule.suffixNeg("home/release"));
1020
1021 // strip out JDK stuff like JavaDB, JNI Headers, etc
1022 if (isJDK) {
1023 rules.add(Rule.suffixNeg("home/db"));
1024 rules.add(Rule.suffixNeg("home/demo"));
1025 rules.add(Rule.suffixNeg("home/include"));
1026 rules.add(Rule.suffixNeg("home/lib"));
1027 rules.add(Rule.suffixNeg("home/sample"));
1028 rules.add(Rule.suffixNeg("home/src.zip"));
1029 rules.add(Rule.suffixNeg("home/javafx-src.zip"));
1030 }
1031
1032 //"home/rt" is not part of the official builds
1033 // but we may be creating this symlink to make older NB projects
1034 // happy. Make sure to not include it into final artifact
1035 rules.add(Rule.suffixNeg("home/rt"));
1036
1037 //rules.add(Rule.suffixNeg("jre/lib/ext")); //need some of jars there for https to work
1038
1039 // strip out flight recorder
1040 rules.add(Rule.suffixNeg("lib/jfr.jar"));
1041
1042 return rules.toArray(new Rule[rules.size()]);
1043 }
1044
1045 //////////////////////////////////////////////////////////////////////////////////
1046 // Implement Bundler
1047 //////////////////////////////////////////////////////////////////////////////////
1048
1049 @Override
1050 public String getName() {
1051 return I18N.getString("bundler.name");
1052 }
1053
1054 @Override
1055 public String getDescription() {
1056 return I18N.getString("bundler.description");
1057 }
1058
1059 @Override
1060 public String getID() {
1061 return "mac.app";
1062 }
1063
1064 @Override
1070 public Collection<BundlerParamInfo<?>> getBundleParameters() {
1071 return getAppBundleParameters();
1072 }
1073
1074 public static Collection<BundlerParamInfo<?>> getAppBundleParameters() {
1075 return Arrays.asList(
1076 APP_NAME,
1077 APP_RESOURCES,
1078 // APP_RESOURCES_LIST, // ??
1079 ARGUMENTS,
1080 BUNDLE_ID_SIGNING_PREFIX,
1081 CLASSPATH,
1082 DEVELOPER_ID_APP_SIGNING_KEY,
1083 ICON_ICNS,
1084 JVM_OPTIONS,
1085 JVM_PROPERTIES,
1086 MAC_CATEGORY,
1087 MAC_CF_BUNDLE_IDENTIFIER,
1088 MAC_CF_BUNDLE_NAME,
1089 MAC_CF_BUNDLE_VERSION,
1090 MAC_RUNTIME,
1091 MAIN_CLASS,
1092 MAIN_JAR,
1093 PREFERENCES_ID,
1094 PRELOADER_CLASS,
1095 SIGNING_KEYCHAIN,
1096 USER_JVM_OPTIONS,
1097 VERSION
1098 );
1099 }
1100
1101
1102 @Override
1103 public File execute(Map<String, ? super Object> params, File outputParentDir) {
1104 return doBundle(params, outputParentDir, false);
1105 }
1106
1107 private void createLauncherForEntryPoint(Map<String, ? super Object> p, File rootDirectory) throws IOException {
1108 prepareConfigFiles(p);
1109
1110 if (LAUNCHER_CFG_FORMAT.fetchFrom(p).equals(CFG_FORMAT_PROPERTIES)) {
1111 writeCfgFile(p, rootDirectory);
1112 } else {
1113 writeCfgFile(p, new File(rootDirectory, getLauncherCfgName(p)), "$APPDIR/PlugIns/Java.runtime");
1114 }
1115
1116 // Copy executable root folder
1117 File executableFile = new File(rootDirectory, "Contents/MacOS/" + getLauncherName(p));
1118 IOUtils.copyFromURL(
1119 RAW_EXECUTABLE_URL.fetchFrom(p),
1120 executableFile);
1121 executableFile.setExecutable(true, false);
1122
1123 }
1124
1125 public static String getLauncherCfgName(Map<String, ? super Object> p) {
1126 return "Contents/Java/" + APP_NAME.fetchFrom(p) +".cfg";
1127 }
1128
1129 private void writeCfgFile(Map<String, ? super Object> params, File rootDir) throws FileNotFoundException {
1130 File pkgInfoFile = new File(rootDir, getLauncherCfgName(params));
1131
1132 pkgInfoFile.delete();
1133
1134 PrintStream out = new PrintStream(pkgInfoFile);
1135 out.println("app.runtime=" + getRuntimeLocation(params));
1136 out.println("app.mainjar=" + MAIN_JAR.fetchFrom(params).getIncludedFiles().iterator().next());
1137 out.println("app.version=" + VERSION.fetchFrom(params));
1138 //for future AU support (to be able to find app in the registry)
1139 out.println("app.id=" + IDENTIFIER.fetchFrom(params));
1140 out.println("app.preferences.id=" + PREFERENCES_ID.fetchFrom(params));
1141 out.println("app.identifier=" + IDENTIFIER.fetchFrom(params));
1142
1143 out.println("app.mainclass=" +
1144 MAIN_CLASS.fetchFrom(params).replaceAll("\\.", "/"));
1145 out.println("app.classpath=" + CLASSPATH.fetchFrom(params));
1146
1147 List<String> jvmargs = JVM_OPTIONS.fetchFrom(params);
1148 int idx = 1;
1149 for (String a : jvmargs) {
1150 out.println("jvmarg."+idx+"="+a);
1151 idx++;
1152 }
1153 Map<String, String> jvmProps = JVM_PROPERTIES.fetchFrom(params);
1154 for (Map.Entry<String, String> entry : jvmProps.entrySet()) {
1155 out.println("jvmarg."+idx+"=-D"+entry.getKey()+"="+entry.getValue());
1156 idx++;
1157 }
1158
1159 String preloader = PRELOADER_CLASS.fetchFrom(params);
1160 if (preloader != null) {
1161 out.println("jvmarg."+idx+"=-Djavafx.preloader="+preloader);
1162 }
1163
1164 Map<String, String> overridableJVMOptions = USER_JVM_OPTIONS.fetchFrom(params);
1165 idx = 1;
1166 for (Map.Entry<String, String> arg: overridableJVMOptions.entrySet()) {
1167 if (arg.getKey() == null || arg.getValue() == null) {
1168 Log.info(I18N.getString("message.jvm-user-arg-is-null"));
1169 }
1170 else {
1171 out.println("jvmuserarg."+idx+".name="+arg.getKey());
1172 out.println("jvmuserarg."+idx+".value="+arg.getValue());
1173 }
1174 idx++;
1175 }
1176
1177 // add command line args
1178 List<String> args = ARGUMENTS.fetchFrom(params);
1179 idx = 1;
1180 for (String a : args) {
1181 out.println("arg."+idx+"="+a);
1182 idx++;
1183 }
1184
1185 out.close();
1186 }
1187 @Override
1188 public void extractRuntimeFlags(Map<String, ? super Object> params) {
1189 if (params.containsKey(".runtime.autodetect")) return;
1190
1191 params.put(".runtime.autodetect", "attempted");
1192 RelativeFileSet runtime = MAC_RUNTIME.fetchFrom(params);
1193 String commandline;
1194 if (runtime == null) {
1195 //System JRE, report nothing useful
1196 params.put(".runtime.autodetect", "systemjre");
1197 } else {
1198 File workingBase = runtime.getBaseDirectory();
1199 if (workingBase.getName().equals("jre")) {
1200 workingBase = workingBase.getParentFile();
1201 }
1202 if (workingBase.getName().equals("Home")) {
1203 workingBase = workingBase.getParentFile();
1204 }
1205 if (workingBase.getName().equals("Contents")) {
1206 workingBase = workingBase.getParentFile();
1207 }
1208
1209
1210 try {
1211 byte[] infoPlistBytes = Files.readAllBytes(workingBase.toPath().resolve(Paths.get("Contents", "Info.plist")));
1212 String infoPlist = new String(infoPlistBytes);
1213
1214 Pattern cfBundleVersionMatcher = Pattern.compile("<key>CFBundleVersion</key>\\s*<string>([^<]+)</string>");
1215 Matcher m = cfBundleVersionMatcher.matcher(infoPlist);
1216 if (m.find()) {
1217 AbstractImageBundler.extractFlagsFromVersion(params, "java version \"" + m.group(1) + "\"\n");
1218 params.put(".runtime.autodetect", "succeeded");
1219 } else {
1220 params.put(".runtime.autodetect", "failed");
1221 }
1222 } catch (IOException e) {
1223 e.printStackTrace();
1224 params.put(".runtime.autodetect", "failed");
1225 }
1226 }
1227 }
1228 }
|
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 package com.oracle.tools.packager.mac;
26
27 import com.oracle.tools.packager.AbstractImageBundler;
28 import com.oracle.tools.packager.BundlerParamInfo;
29 import com.oracle.tools.packager.ConfigException;
30 import com.oracle.tools.packager.EnumeratedBundlerParam;
31 import com.oracle.tools.packager.IOUtils;
32 import com.oracle.tools.packager.JLinkBundlerHelper;
33 import com.oracle.tools.packager.Log;
34 import com.oracle.tools.packager.StandardBundlerParam;
35 import com.oracle.tools.packager.UnsupportedPlatformException;
36 import jdk.tools.jlink.builder.ImageBuilder;
37 import jdk.packager.builders.mac.MacAppImageBuilder;
38
39 import java.io.File;
40 import java.io.IOException;
41 import java.math.BigInteger;
42 import java.text.MessageFormat;
43 import java.util.Arrays;
44 import java.util.Collection;
45 import java.util.HashMap;
46 import java.util.Map;
47 import java.util.Optional;
48 import java.util.ResourceBundle;
49
50 import static com.oracle.tools.packager.StandardBundlerParam.*;
51 import static com.oracle.tools.packager.mac.MacBaseInstallerBundler.*;
52
53 public class MacAppBundler extends AbstractImageBundler {
54
55 private static final ResourceBundle I18N =
56 ResourceBundle.getBundle(MacAppBundler.class.getName());
57
58 public final static String MAC_BUNDLER_PREFIX =
59 BUNDLER_PREFIX + "macosx" + File.separator;
60
61 private static final String TEMPLATE_BUNDLE_ICON = "GenericApp.icns";
62
63 private static Map<String, String> getMacCategories() {
64 Map<String, String> map = new HashMap<>();
65 map.put("Business", "public.app-category.business");
66 map.put("Developer Tools", "public.app-category.developer-tools");
67 map.put("Education", "public.app-category.education");
68 map.put("Entertainment", "public.app-category.entertainment");
69 map.put("Finance", "public.app-category.finance");
70 map.put("Games", "public.app-category.games");
71 map.put("Graphics & Design", "public.app-category.graphics-design");
72 map.put("Healthcare & Fitness", "public.app-category.healthcare-fitness");
73 map.put("Lifestyle", "public.app-category.lifestyle");
74 map.put("Medical", "public.app-category.medical");
75 map.put("Music", "public.app-category.music");
76 map.put("News", "public.app-category.news");
77 map.put("Photography", "public.app-category.photography");
78 map.put("Productivity", "public.app-category.productivity");
79 map.put("Reference", "public.app-category.reference");
80 map.put("Social Networking", "public.app-category.social-networking");
81 map.put("Sports", "public.app-category.sports");
90 map.put("Board Games", "public.app-category.board-games");
91 map.put("Card Games", "public.app-category.card-games");
92 map.put("Casino Games", "public.app-category.casino-games");
93 map.put("Dice Games", "public.app-category.dice-games");
94 map.put("Educational Games", "public.app-category.educational-games");
95 map.put("Family Games", "public.app-category.family-games");
96 map.put("Kids Games", "public.app-category.kids-games");
97 map.put("Music Games", "public.app-category.music-games");
98 map.put("Puzzle Games", "public.app-category.puzzle-games");
99 map.put("Racing Games", "public.app-category.racing-games");
100 map.put("Role Playing Games", "public.app-category.role-playing-games");
101 map.put("Simulation Games", "public.app-category.simulation-games");
102 map.put("Sports Games", "public.app-category.sports-games");
103 map.put("Strategy Games", "public.app-category.strategy-games");
104 map.put("Trivia Games", "public.app-category.trivia-games");
105 map.put("Word Games", "public.app-category.word-games");
106
107 return map;
108 }
109
110 public static final EnumeratedBundlerParam<String> MAC_CATEGORY =
111 new EnumeratedBundlerParam<>(
112 I18N.getString("param.category-name"),
113 I18N.getString("param.category-name.description"),
114 "mac.category",
115 String.class,
116 params -> params.containsKey(CATEGORY.getID())
117 ? CATEGORY.fetchFrom(params)
118 : "Unknown",
119 (s, p) -> s,
120 getMacCategories(),
121 false //strict - for MacStoreBundler this should be strict
122 );
123
124 public static final BundlerParamInfo<String> MAC_CF_BUNDLE_NAME =
125 new StandardBundlerParam<>(
126 I18N.getString("param.cfbundle-name.name"),
127 I18N.getString("param.cfbundle-name.description"),
128 "mac.CFBundleName",
129 String.class,
150 if (validCFBundleVersion(s)) {
151 return s;
152 } else {
153 return "100";
154 }
155 },
156 (s, p) -> s);
157
158 public static final BundlerParamInfo<File> CONFIG_ROOT = new StandardBundlerParam<>(
159 I18N.getString("param.config-root.name"),
160 I18N.getString("param.config-root.description"),
161 "configRoot",
162 File.class,
163 params -> {
164 File configRoot = new File(BUILD_ROOT.fetchFrom(params), "macosx");
165 configRoot.mkdirs();
166 return configRoot;
167 },
168 (s, p) -> new File(s));
169
170 public static final BundlerParamInfo<String> DEFAULT_ICNS_ICON = new StandardBundlerParam<>(
171 I18N.getString("param.default-icon-icns"),
172 I18N.getString("param.default-icon-icns.description"),
173 ".mac.default.icns",
174 String.class,
175 params -> TEMPLATE_BUNDLE_ICON,
176 (s, p) -> s);
177
178 public static final BundlerParamInfo<String> DEVELOPER_ID_APP_SIGNING_KEY = new StandardBundlerParam<>(
179 I18N.getString("param.signing-key-developer-id-app.name"),
180 I18N.getString("param.signing-key-developer-id-app.description"),
181 "mac.signing-key-developer-id-app",
182 String.class,
183 params -> MacBaseInstallerBundler.findKey("Developer ID Application: " + SIGNING_KEY_USER.fetchFrom(params), SIGNING_KEYCHAIN.fetchFrom(params), VERBOSE.fetchFrom(params)),
184 (s, p) -> s);
185
186 public static final BundlerParamInfo<String> BUNDLE_ID_SIGNING_PREFIX = new StandardBundlerParam<>(
187 I18N.getString("param.bundle-id-signing-prefix.name"),
188 I18N.getString("param.bundle-id-signing-prefix.description"),
189 "mac.bundle-id-signing-prefix",
190 String.class,
191 params -> IDENTIFIER.fetchFrom(params) + ".",
192 (s, p) -> s);
193
194 public static final BundlerParamInfo<File> ICON_ICNS = new StandardBundlerParam<>(
195 I18N.getString("param.icon-icns.name"),
196 I18N.getString("param.icon-icns.description"),
197 "icon.icns",
198 File.class,
199 params -> {
200 File f = ICON.fetchFrom(params);
201 if (f != null && !f.getName().toLowerCase().endsWith(".icns")) {
202 Log.info(MessageFormat.format(I18N.getString("message.icon-not-icns"), f));
203 return null;
204 }
205 return f;
206 },
207 (s, p) -> new File(s));
208
209 public MacAppBundler() {
210 super();
211 baseResourceLoader = MacResources.class;
212 }
213
214 public static boolean validCFBundleVersion(String v) {
215 // CFBundleVersion (String - iOS, OS X) specifies the build version
216 // number of the bundle, which identifies an iteration (released or
217 // unreleased) of the bundle. The build version number should be a
218 // string comprised of three non-negative, period-separated integers
219 // with the first integer being greater than zero. The string should
220 // only contain numeric (0-9) and period (.) characters. Leading zeros
221 // are truncated from each integer and will be ignored (that is,
222 // 1.02.3 is equivalent to 1.2.3). This key is not localizable.
223
224 if (v == null) {
225 return false;
226 }
227
228 String p[] = v.split("\\.");
229 if (p.length > 3 || p.length < 1) {
230 Log.verbose(I18N.getString("message.version-string-too-many-components"));
231 return false;
232 }
233
269 throw (ConfigException) re.getCause();
270 } else {
271 throw new ConfigException(re);
272 }
273 }
274 }
275
276 //to be used by chained bundlers, e.g. by EXE bundler to avoid
277 // skipping validation if p.type does not include "image"
278 public boolean doValidate(Map<String, ? super Object> p) throws UnsupportedPlatformException, ConfigException {
279 if (!System.getProperty("os.name").toLowerCase().contains("os x")) {
280 throw new UnsupportedPlatformException();
281 }
282
283 imageBundleValidation(p);
284
285 if (getPredefinedImage(p) != null) {
286 return true;
287 }
288
289 //TODO warn if MAC_RUNTIME is set
290
291 // validate short version
292 if (!validCFBundleVersion(MAC_CF_BUNDLE_VERSION.fetchFrom(p))) {
293 throw new ConfigException(
294 I18N.getString("error.invalid-cfbundle-version"),
295 I18N.getString("error.invalid-cfbundle-version.advice"));
296 }
297
298 // reject explicitly set sign to true and no valid signature key
299 if (Optional.ofNullable(SIGN_BUNDLE.fetchFrom(p)).orElse(Boolean.FALSE)) {
300 String signingIdentity = DEVELOPER_ID_APP_SIGNING_KEY.fetchFrom(p);
301 if (signingIdentity == null) {
302 throw new ConfigException(
303 I18N.getString("error.explicit-sign-no-cert"),
304 I18N.getString("error.explicit-sign-no-cert.advice"));
305 }
306 }
307
308 return true;
309 }
310
311 private File getConfig_InfoPlist(Map<String, ? super Object> params) {
312 return new File(CONFIG_ROOT.fetchFrom(params), "Info.plist");
313 }
314
315 private File getConfig_Icon(Map<String, ? super Object> params) {
316 return new File(CONFIG_ROOT.fetchFrom(params), APP_NAME.fetchFrom(params) + ".icns");
317 }
318
319
320 File doBundle(Map<String, ? super Object> p, File outputDirectory, boolean dependentTask) {
321 try {
322 if (!outputDirectory.isDirectory() && !outputDirectory.mkdirs()) {
323 throw new RuntimeException(MessageFormat.format(I18N.getString("error.cannot-create-output-dir"), outputDirectory.getAbsolutePath()));
324 }
325 if (!outputDirectory.canWrite()) {
326 throw new RuntimeException(MessageFormat.format(I18N.getString("error.cannot-write-to-output-dir"), outputDirectory.getAbsolutePath()));
327 }
328
329 // Create directory structure
330 File rootDirectory = new File(outputDirectory, APP_NAME.fetchFrom(p) + ".app");
331 IOUtils.deleteRecursive(rootDirectory);
332 rootDirectory.mkdirs();
333
334 if (!dependentTask) {
335 Log.info(MessageFormat.format(I18N.getString("message.creating-app-bundle"), rootDirectory.getAbsolutePath()));
336 }
337
338 if (!p.containsKey(JLinkBundlerHelper.JLINK_BUILDER.getID())) {
339 p.put(JLinkBundlerHelper.JLINK_BUILDER.getID(), "macapp-image-builder");
340 }
341
342 ImageBuilder imageBuilder = new MacAppImageBuilder(p, outputDirectory.toPath());
343 JLinkBundlerHelper.execute(p, outputDirectory, imageBuilder);
344 return rootDirectory;
345 } catch (IOException ex) {
346 Log.info(ex.toString());
347 Log.verbose(ex);
348 return null;
349 }
350 }
351
352 public void cleanupConfigFiles(Map<String, ? super Object> params) {
353 //Since building the app can be bypassed, make sure configRoot was set
354 if (CONFIG_ROOT.fetchFrom(params) != null) {
355 getConfig_Icon(params).delete();
356 getConfig_InfoPlist(params).delete();
357 }
358 }
359
360 //////////////////////////////////////////////////////////////////////////////////
361 // Implement Bundler
362 //////////////////////////////////////////////////////////////////////////////////
363
364 @Override
365 public String getName() {
366 return I18N.getString("bundler.name");
367 }
368
369 @Override
370 public String getDescription() {
371 return I18N.getString("bundler.description");
372 }
373
374 @Override
375 public String getID() {
376 return "mac.app";
377 }
378
379 @Override
385 public Collection<BundlerParamInfo<?>> getBundleParameters() {
386 return getAppBundleParameters();
387 }
388
389 public static Collection<BundlerParamInfo<?>> getAppBundleParameters() {
390 return Arrays.asList(
391 APP_NAME,
392 APP_RESOURCES,
393 // APP_RESOURCES_LIST, // ??
394 ARGUMENTS,
395 BUNDLE_ID_SIGNING_PREFIX,
396 CLASSPATH,
397 DEVELOPER_ID_APP_SIGNING_KEY,
398 ICON_ICNS,
399 JVM_OPTIONS,
400 JVM_PROPERTIES,
401 MAC_CATEGORY,
402 MAC_CF_BUNDLE_IDENTIFIER,
403 MAC_CF_BUNDLE_NAME,
404 MAC_CF_BUNDLE_VERSION,
405 // MAC_RUNTIME,
406 MAIN_CLASS,
407 MAIN_JAR,
408 PREFERENCES_ID,
409 PRELOADER_CLASS,
410 SIGNING_KEYCHAIN,
411 USER_JVM_OPTIONS,
412 VERSION
413 );
414 }
415
416
417 @Override
418 public File execute(Map<String, ? super Object> params, File outputParentDir) {
419 return doBundle(params, outputParentDir, false);
420 }
421
422 // private void createLauncherForEntryPoint(Map<String, ? super Object> p, File rootDirectory) throws IOException {
423 // prepareConfigFiles(p);
424 //
425 // if (LAUNCHER_CFG_FORMAT.fetchFrom(p).equals(CFG_FORMAT_PROPERTIES)) {
426 // writeCfgFile(p, rootDirectory);
427 // } else {
428 // writeCfgFile(p, new File(rootDirectory, getLauncherCfgName(p)), "$APPDIR/PlugIns/Java.runtime");
429 // }
430 //
431 // // Copy executable root folder
432 // File executableFile = new File(rootDirectory, "Contents/MacOS/" + getLauncherName(p));
433 // IOUtils.copyFromURL(
434 // RAW_EXECUTABLE_URL.fetchFrom(p),
435 // executableFile);
436 // executableFile.setExecutable(true, false);
437 //
438 // }
439 //
440
441 }
|