59 import static jdk.jpackage.internal.MacBaseInstallerBundler.*;
60 import static jdk.jpackage.internal.MacAppBundler.*;
61
62 public class MacAppImageBuilder extends AbstractAppImageBuilder {
63
64 private static final ResourceBundle I18N = ResourceBundle.getBundle(
65 "jdk.jpackage.internal.resources.MacResources");
66
67 private static final String LIBRARY_NAME = "libapplauncher.dylib";
68 private static final String TEMPLATE_BUNDLE_ICON = "GenericApp.icns";
69 private static final String OS_TYPE_CODE = "APPL";
70 private static final String TEMPLATE_INFO_PLIST_LITE =
71 "Info-lite.plist.template";
72 private static final String TEMPLATE_RUNTIME_INFO_PLIST =
73 "Runtime-Info.plist.template";
74
75 private final Path root;
76 private final Path contentsDir;
77 private final Path javaDir;
78 private final Path javaModsDir;
79 private final String relativeModsDir;
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 final Map<String, ? super Object> params;
87
88 private static List<String> keyChains;
89
90 public static final BundlerParamInfo<Boolean>
91 MAC_CONFIGURE_LAUNCHER_IN_PLIST = new StandardBundlerParam<>(
92 "mac.configure-launcher-in-plist",
93 Boolean.class,
94 params -> Boolean.FALSE,
95 (s, p) -> Boolean.valueOf(s));
96
97 public static final EnumeratedBundlerParam<String> MAC_CATEGORY =
98 new EnumeratedBundlerParam<>(
99 Arguments.CLIOptions.MAC_APP_STORE_CATEGORY.getId(),
159 Arguments.CLIOptions.MAC_SIGN.getId(),
160 Boolean.class,
161 params -> false,
162 // valueOf(null) is false, we actually do want null in some cases
163 (s, p) -> (s == null || "null".equalsIgnoreCase(s)) ?
164 null : Boolean.valueOf(s)
165 );
166
167 public MacAppImageBuilder(Map<String, Object> config, Path imageOutDir)
168 throws IOException {
169 super(config, imageOutDir.resolve(APP_NAME.fetchFrom(config)
170 + ".app/Contents/runtime/Contents/Home"));
171
172 Objects.requireNonNull(imageOutDir);
173
174 this.params = config;
175 this.root = imageOutDir.resolve(APP_NAME.fetchFrom(params) + ".app");
176 this.contentsDir = root.resolve("Contents");
177 this.javaDir = contentsDir.resolve("Java");
178 this.javaModsDir = javaDir.resolve("mods");
179 this.relativeModsDir = "Java/mods";
180 this.resourcesDir = contentsDir.resolve("Resources");
181 this.macOSDir = contentsDir.resolve("MacOS");
182 this.runtimeDir = contentsDir.resolve("runtime");
183 this.runtimeRoot = runtimeDir.resolve("Contents/Home");
184 this.mdir = runtimeRoot.resolve("lib");
185 Files.createDirectories(javaDir);
186 Files.createDirectories(resourcesDir);
187 Files.createDirectories(macOSDir);
188 Files.createDirectories(runtimeDir);
189 }
190
191 public MacAppImageBuilder(Map<String, Object> config, String jreName,
192 Path imageOutDir) throws IOException {
193 super(null, imageOutDir.resolve(jreName + "/Contents/Home"));
194
195 Objects.requireNonNull(imageOutDir);
196
197 this.params = config;
198 this.root = imageOutDir.resolve(jreName );
199 this.contentsDir = root.resolve("Contents");
200 this.javaDir = null;
201 this.javaModsDir = null;
202 this.relativeModsDir = null;
203 this.resourcesDir = null;
204 this.macOSDir = null;
205 this.runtimeDir = this.root;
206 this.runtimeRoot = runtimeDir.resolve("Contents/Home");
207 this.mdir = runtimeRoot.resolve("lib");
208
209 Files.createDirectories(runtimeDir);
210 }
211
212 private void writeEntry(InputStream in, Path dstFile) throws IOException {
213 Files.createDirectories(dstFile.getParent());
214 Files.copy(in, dstFile);
215 }
216
217 public static boolean validCFBundleVersion(String v) {
218 // CFBundleVersion (String - iOS, OS X) specifies the build version
219 // number of the bundle, which identifies an iteration (released or
220 // unreleased) of the bundle. The build version number should be a
221 // string comprised of three non-negative, period-separated integers
222 // with the first integer being greater than zero. The string should
261 } catch (NumberFormatException ne) {
262 Log.verbose(I18N.getString("message.version-string-numbers-only"));
263 Log.verbose(ne);
264 return false;
265 }
266
267 return true;
268 }
269
270 @Override
271 public Path getAppDir() {
272 return javaDir;
273 }
274
275 @Override
276 public Path getAppModsDir() {
277 return javaModsDir;
278 }
279
280 @Override
281 public String getRelativeModsDir() {
282 return relativeModsDir;
283 }
284
285 @Override
286 public void prepareApplicationFiles() throws IOException {
287 Map<String, ? super Object> originalParams = new HashMap<>(params);
288 // Generate PkgInfo
289 File pkgInfoFile = new File(contentsDir.toFile(), "PkgInfo");
290 pkgInfoFile.createNewFile();
291 writePkgInfo(pkgInfoFile);
292
293 Path executable = macOSDir.resolve(getLauncherName(params));
294
295 // create the main app launcher
296 try (InputStream is_launcher =
297 getResourceAsStream("jpackageapplauncher");
298 InputStream is_lib = getResourceAsStream(LIBRARY_NAME)) {
299 // Copy executable and library to MacOS folder
300 writeEntry(is_launcher, executable);
301 writeEntry(is_lib, macOSDir.resolve(LIBRARY_NAME));
302 }
303 executable.toFile().setExecutable(true, false);
304 // generate main app launcher config file
305 File cfg = new File(root.toFile(), getLauncherCfgName(params));
306 writeCfgFile(params, cfg, "$APPDIR/runtime");
307
308 // create additional app launcher(s) and config file(s)
309 List<Map<String, ? super Object>> entryPoints =
310 StandardBundlerParam.ADD_LAUNCHERS.fetchFrom(params);
311 for (Map<String, ? super Object> entryPoint : entryPoints) {
312 Map<String, ? super Object> tmp =
313 AddLauncherArguments.merge(originalParams, entryPoint);
314
315 // add executable for add launcher
316 Path addExecutable = macOSDir.resolve(getLauncherName(tmp));
317 try (InputStream is = getResourceAsStream("jpackageapplauncher");) {
318 writeEntry(is, addExecutable);
319 }
320 addExecutable.toFile().setExecutable(true, false);
321
322 // add config file for add launcher
323 cfg = new File(root.toFile(), getLauncherCfgName(tmp));
324 writeCfgFile(tmp, cfg, "$APPDIR/runtime");
325 }
326
327 // Copy class path entries to Java folder
328 copyClassPathEntries(javaDir);
329
330 /*********** Take care of "config" files *******/
331 File icon = ICON_ICNS.fetchFrom(params);
332
333 InputStream in = locateResource(
334 APP_NAME.fetchFrom(params) + ".icns",
335 "icon",
336 DEFAULT_ICNS_ICON.fetchFrom(params),
337 icon,
338 VERBOSE.fetchFrom(params),
339 RESOURCE_DIR.fetchFrom(params));
340 Files.copy(in,
341 resourcesDir.resolve(APP_NAME.fetchFrom(params) + ".icns"),
342 StandardCopyOption.REPLACE_EXISTING);
343
344 // copy file association icons
472 RESOURCE_DIR.fetchFrom(params)));
473 }
474 }
475
476 private void writeInfoPlist(File file) throws IOException {
477 Log.verbose(MessageFormat.format(I18N.getString(
478 "message.preparing-info-plist"), file.getAbsolutePath()));
479
480 //prepare config for exe
481 //Note: do not need CFBundleDisplayName if we don't support localization
482 Map<String, String> data = new HashMap<>();
483 data.put("DEPLOY_ICON_FILE", APP_NAME.fetchFrom(params) + ".icns");
484 data.put("DEPLOY_BUNDLE_IDENTIFIER",
485 MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params));
486 data.put("DEPLOY_BUNDLE_NAME",
487 getBundleName(params));
488 data.put("DEPLOY_BUNDLE_COPYRIGHT",
489 COPYRIGHT.fetchFrom(params) != null ?
490 COPYRIGHT.fetchFrom(params) : "Unknown");
491 data.put("DEPLOY_LAUNCHER_NAME", getLauncherName(params));
492 data.put("DEPLOY_JAVA_RUNTIME_NAME", "$APPDIR/runtime");
493 data.put("DEPLOY_BUNDLE_SHORT_VERSION",
494 VERSION.fetchFrom(params) != null ?
495 VERSION.fetchFrom(params) : "1.0.0");
496 data.put("DEPLOY_BUNDLE_CFBUNDLE_VERSION",
497 MAC_CF_BUNDLE_VERSION.fetchFrom(params) != null ?
498 MAC_CF_BUNDLE_VERSION.fetchFrom(params) : "100");
499 data.put("DEPLOY_BUNDLE_CATEGORY", MAC_CATEGORY.fetchFrom(params));
500
501 boolean hasMainJar = MAIN_JAR.fetchFrom(params) != null;
502 boolean hasMainModule =
503 StandardBundlerParam.MODULE.fetchFrom(params) != null;
504
505 if (hasMainJar) {
506 data.put("DEPLOY_MAIN_JAR_NAME", MAIN_JAR.fetchFrom(params).
507 getIncludedFiles().iterator().next());
508 }
509 else if (hasMainModule) {
510 data.put("DEPLOY_MODULE_NAME",
511 StandardBundlerParam.MODULE.fetchFrom(params));
512 }
522 }
523
524 data.put("DEPLOY_JAVA_OPTIONS", sb.toString());
525
526 sb = new StringBuilder();
527 List<String> args = ARGUMENTS.fetchFrom(params);
528 newline = "";
529 // So we don't add unneccessary extra line after last append
530
531 for (String o : args) {
532 sb.append(newline).append(" <string>").append(o).append(
533 "</string>");
534 newline = "\n";
535 }
536 data.put("DEPLOY_ARGUMENTS", sb.toString());
537
538 newline = "";
539
540 data.put("DEPLOY_LAUNCHER_CLASS", MAIN_CLASS.fetchFrom(params));
541
542 StringBuilder macroedPath = new StringBuilder();
543 for (String s : CLASSPATH.fetchFrom(params).split("[ ;:]+")) {
544 macroedPath.append(s);
545 macroedPath.append(":");
546 }
547 macroedPath.deleteCharAt(macroedPath.length() - 1);
548
549 data.put("DEPLOY_APP_CLASSPATH", macroedPath.toString());
550
551 StringBuilder bundleDocumentTypes = new StringBuilder();
552 StringBuilder exportedTypes = new StringBuilder();
553 for (Map<String, ? super Object>
554 fileAssociation : FILE_ASSOCIATIONS.fetchFrom(params)) {
555
556 List<String> extensions = FA_EXTENSIONS.fetchFrom(fileAssociation);
557
558 if (extensions == null) {
559 Log.verbose(I18N.getString(
560 "message.creating-association-with-null-extension"));
561 }
562
563 List<String> mimeTypes = FA_CONTENT_TYPE.fetchFrom(fileAssociation);
564 String itemContentType = MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params)
565 + "." + ((extensions == null || extensions.isEmpty())
566 ? "mime" : extensions.get(0));
567 String description = FA_DESCRIPTION.fetchFrom(fileAssociation);
568 File icon = FA_ICON.fetchFrom(fileAssociation);
569
|
59 import static jdk.jpackage.internal.MacBaseInstallerBundler.*;
60 import static jdk.jpackage.internal.MacAppBundler.*;
61
62 public class MacAppImageBuilder extends AbstractAppImageBuilder {
63
64 private static final ResourceBundle I18N = ResourceBundle.getBundle(
65 "jdk.jpackage.internal.resources.MacResources");
66
67 private static final String LIBRARY_NAME = "libapplauncher.dylib";
68 private static final String TEMPLATE_BUNDLE_ICON = "GenericApp.icns";
69 private static final String OS_TYPE_CODE = "APPL";
70 private static final String TEMPLATE_INFO_PLIST_LITE =
71 "Info-lite.plist.template";
72 private static final String TEMPLATE_RUNTIME_INFO_PLIST =
73 "Runtime-Info.plist.template";
74
75 private final Path root;
76 private final Path contentsDir;
77 private final Path javaDir;
78 private final Path javaModsDir;
79 private final Path resourcesDir;
80 private final Path macOSDir;
81 private final Path runtimeDir;
82 private final Path runtimeRoot;
83 private final Path mdir;
84
85 private final Map<String, ? super Object> params;
86
87 private static List<String> keyChains;
88
89 public static final BundlerParamInfo<Boolean>
90 MAC_CONFIGURE_LAUNCHER_IN_PLIST = new StandardBundlerParam<>(
91 "mac.configure-launcher-in-plist",
92 Boolean.class,
93 params -> Boolean.FALSE,
94 (s, p) -> Boolean.valueOf(s));
95
96 public static final EnumeratedBundlerParam<String> MAC_CATEGORY =
97 new EnumeratedBundlerParam<>(
98 Arguments.CLIOptions.MAC_APP_STORE_CATEGORY.getId(),
158 Arguments.CLIOptions.MAC_SIGN.getId(),
159 Boolean.class,
160 params -> false,
161 // valueOf(null) is false, we actually do want null in some cases
162 (s, p) -> (s == null || "null".equalsIgnoreCase(s)) ?
163 null : Boolean.valueOf(s)
164 );
165
166 public MacAppImageBuilder(Map<String, Object> config, Path imageOutDir)
167 throws IOException {
168 super(config, imageOutDir.resolve(APP_NAME.fetchFrom(config)
169 + ".app/Contents/runtime/Contents/Home"));
170
171 Objects.requireNonNull(imageOutDir);
172
173 this.params = config;
174 this.root = imageOutDir.resolve(APP_NAME.fetchFrom(params) + ".app");
175 this.contentsDir = root.resolve("Contents");
176 this.javaDir = contentsDir.resolve("Java");
177 this.javaModsDir = javaDir.resolve("mods");
178 this.resourcesDir = contentsDir.resolve("Resources");
179 this.macOSDir = contentsDir.resolve("MacOS");
180 this.runtimeDir = contentsDir.resolve("runtime");
181 this.runtimeRoot = runtimeDir.resolve("Contents/Home");
182 this.mdir = runtimeRoot.resolve("lib");
183 Files.createDirectories(javaDir);
184 Files.createDirectories(resourcesDir);
185 Files.createDirectories(macOSDir);
186 Files.createDirectories(runtimeDir);
187 }
188
189 public MacAppImageBuilder(Map<String, Object> config, String jreName,
190 Path imageOutDir) throws IOException {
191 super(null, imageOutDir.resolve(jreName + "/Contents/Home"));
192
193 Objects.requireNonNull(imageOutDir);
194
195 this.params = config;
196 this.root = imageOutDir.resolve(jreName );
197 this.contentsDir = root.resolve("Contents");
198 this.javaDir = null;
199 this.javaModsDir = null;
200 this.resourcesDir = null;
201 this.macOSDir = null;
202 this.runtimeDir = this.root;
203 this.runtimeRoot = runtimeDir.resolve("Contents/Home");
204 this.mdir = runtimeRoot.resolve("lib");
205
206 Files.createDirectories(runtimeDir);
207 }
208
209 private void writeEntry(InputStream in, Path dstFile) throws IOException {
210 Files.createDirectories(dstFile.getParent());
211 Files.copy(in, dstFile);
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
258 } catch (NumberFormatException ne) {
259 Log.verbose(I18N.getString("message.version-string-numbers-only"));
260 Log.verbose(ne);
261 return false;
262 }
263
264 return true;
265 }
266
267 @Override
268 public Path getAppDir() {
269 return javaDir;
270 }
271
272 @Override
273 public Path getAppModsDir() {
274 return javaModsDir;
275 }
276
277 @Override
278 public void prepareApplicationFiles() throws IOException {
279 Map<String, ? super Object> originalParams = new HashMap<>(params);
280 // Generate PkgInfo
281 File pkgInfoFile = new File(contentsDir.toFile(), "PkgInfo");
282 pkgInfoFile.createNewFile();
283 writePkgInfo(pkgInfoFile);
284
285 Path executable = macOSDir.resolve(getLauncherName(params));
286
287 // create the main app launcher
288 try (InputStream is_launcher =
289 getResourceAsStream("jpackageapplauncher");
290 InputStream is_lib = getResourceAsStream(LIBRARY_NAME)) {
291 // Copy executable and library to MacOS folder
292 writeEntry(is_launcher, executable);
293 writeEntry(is_lib, macOSDir.resolve(LIBRARY_NAME));
294 }
295 executable.toFile().setExecutable(true, false);
296 // generate main app launcher config file
297 File cfg = new File(root.toFile(), getLauncherCfgName(params));
298 writeCfgFile(params, cfg);
299
300 // create additional app launcher(s) and config file(s)
301 List<Map<String, ? super Object>> entryPoints =
302 StandardBundlerParam.ADD_LAUNCHERS.fetchFrom(params);
303 for (Map<String, ? super Object> entryPoint : entryPoints) {
304 Map<String, ? super Object> tmp =
305 AddLauncherArguments.merge(originalParams, entryPoint);
306
307 // add executable for add launcher
308 Path addExecutable = macOSDir.resolve(getLauncherName(tmp));
309 try (InputStream is = getResourceAsStream("jpackageapplauncher");) {
310 writeEntry(is, addExecutable);
311 }
312 addExecutable.toFile().setExecutable(true, false);
313
314 // add config file for add launcher
315 cfg = new File(root.toFile(), getLauncherCfgName(tmp));
316 writeCfgFile(tmp, cfg);
317 }
318
319 // Copy class path entries to Java folder
320 copyClassPathEntries(javaDir);
321
322 /*********** Take care of "config" files *******/
323 File icon = ICON_ICNS.fetchFrom(params);
324
325 InputStream in = locateResource(
326 APP_NAME.fetchFrom(params) + ".icns",
327 "icon",
328 DEFAULT_ICNS_ICON.fetchFrom(params),
329 icon,
330 VERBOSE.fetchFrom(params),
331 RESOURCE_DIR.fetchFrom(params));
332 Files.copy(in,
333 resourcesDir.resolve(APP_NAME.fetchFrom(params) + ".icns"),
334 StandardCopyOption.REPLACE_EXISTING);
335
336 // copy file association icons
464 RESOURCE_DIR.fetchFrom(params)));
465 }
466 }
467
468 private void writeInfoPlist(File file) throws IOException {
469 Log.verbose(MessageFormat.format(I18N.getString(
470 "message.preparing-info-plist"), file.getAbsolutePath()));
471
472 //prepare config for exe
473 //Note: do not need CFBundleDisplayName if we don't support localization
474 Map<String, String> data = new HashMap<>();
475 data.put("DEPLOY_ICON_FILE", APP_NAME.fetchFrom(params) + ".icns");
476 data.put("DEPLOY_BUNDLE_IDENTIFIER",
477 MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params));
478 data.put("DEPLOY_BUNDLE_NAME",
479 getBundleName(params));
480 data.put("DEPLOY_BUNDLE_COPYRIGHT",
481 COPYRIGHT.fetchFrom(params) != null ?
482 COPYRIGHT.fetchFrom(params) : "Unknown");
483 data.put("DEPLOY_LAUNCHER_NAME", getLauncherName(params));
484 data.put("DEPLOY_JAVA_RUNTIME_NAME", getCfgRuntimeDir());
485 data.put("DEPLOY_BUNDLE_SHORT_VERSION",
486 VERSION.fetchFrom(params) != null ?
487 VERSION.fetchFrom(params) : "1.0.0");
488 data.put("DEPLOY_BUNDLE_CFBUNDLE_VERSION",
489 MAC_CF_BUNDLE_VERSION.fetchFrom(params) != null ?
490 MAC_CF_BUNDLE_VERSION.fetchFrom(params) : "100");
491 data.put("DEPLOY_BUNDLE_CATEGORY", MAC_CATEGORY.fetchFrom(params));
492
493 boolean hasMainJar = MAIN_JAR.fetchFrom(params) != null;
494 boolean hasMainModule =
495 StandardBundlerParam.MODULE.fetchFrom(params) != null;
496
497 if (hasMainJar) {
498 data.put("DEPLOY_MAIN_JAR_NAME", MAIN_JAR.fetchFrom(params).
499 getIncludedFiles().iterator().next());
500 }
501 else if (hasMainModule) {
502 data.put("DEPLOY_MODULE_NAME",
503 StandardBundlerParam.MODULE.fetchFrom(params));
504 }
514 }
515
516 data.put("DEPLOY_JAVA_OPTIONS", sb.toString());
517
518 sb = new StringBuilder();
519 List<String> args = ARGUMENTS.fetchFrom(params);
520 newline = "";
521 // So we don't add unneccessary extra line after last append
522
523 for (String o : args) {
524 sb.append(newline).append(" <string>").append(o).append(
525 "</string>");
526 newline = "\n";
527 }
528 data.put("DEPLOY_ARGUMENTS", sb.toString());
529
530 newline = "";
531
532 data.put("DEPLOY_LAUNCHER_CLASS", MAIN_CLASS.fetchFrom(params));
533
534 data.put("DEPLOY_APP_CLASSPATH",
535 getCfgClassPath(CLASSPATH.fetchFrom(params)));
536
537 StringBuilder bundleDocumentTypes = new StringBuilder();
538 StringBuilder exportedTypes = new StringBuilder();
539 for (Map<String, ? super Object>
540 fileAssociation : FILE_ASSOCIATIONS.fetchFrom(params)) {
541
542 List<String> extensions = FA_EXTENSIONS.fetchFrom(fileAssociation);
543
544 if (extensions == null) {
545 Log.verbose(I18N.getString(
546 "message.creating-association-with-null-extension"));
547 }
548
549 List<String> mimeTypes = FA_CONTENT_TYPE.fetchFrom(fileAssociation);
550 String itemContentType = MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params)
551 + "." + ((extensions == null || extensions.isEmpty())
552 ? "mime" : extensions.get(0));
553 String description = FA_DESCRIPTION.fetchFrom(fileAssociation);
554 File icon = FA_ICON.fetchFrom(fileAssociation);
555
|