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 package com.sun.javafx.tools.packager.bundlers;
26
27 import com.oracle.bundlers.*;
28 import com.sun.javafx.tools.packager.Log;
29 import com.sun.javafx.tools.resource.mac.MacResources;
30
31 import java.io.*;
32 import java.net.MalformedURLException;
33 import java.net.URL;
34 import java.util.*;
35
36 import static com.oracle.bundlers.StandardBundlerParam.*;
37 import static com.oracle.bundlers.StandardBundlerParam.USER_JVM_OPTIONS;
38 import static com.oracle.bundlers.StandardBundlerParam.VERSION;
39 import static com.oracle.bundlers.mac.MacBaseInstallerBundler.getPredefinedImage;
40
41 public class MacAppBundler extends AbstractBundler {
42 private File configRoot = null;
43 private Map<String, ? super Object> params;
44
45 public final static String MAC_BUNDLER_PREFIX =
46 BUNDLER_PREFIX + "macosx" + File.separator;
47
48 private static final String EXECUTABLE_NAME = "JavaAppLauncher";
49 private static final String TEMPLATE_BUNDLE_ICON = "GenericApp.icns";
50 private static final String OS_TYPE_CODE = "APPL";
51 private static final String TEMPLATE_INFO_PLIST = "Info.plist.template";
52
53 private static Map<String, String> getMacCategories() {
54 Map<String, String> map = new HashMap<>();
55 map.put("Business", "public.app-category.business");
56 map.put("Developer Tools", "public.app-category.developer-tools");
57 map.put("Education", "public.app-category.education");
58 map.put("Entertainment", "public.app-category.entertainment");
59 map.put("Finance", "public.app-category.finance");
60 map.put("Games", "public.app-category.games");
61 map.put("Graphics & Design", "public.app-category.graphics-design");
62 map.put("Healthcare & Fitness", "public.app-category.healthcare-fitness");
63 map.put("Lifestyle", "public.app-category.lifestyle");
83 map.put("Dice Games", "public.app-category.dice-games");
84 map.put("Educational Games", "public.app-category.educational-games");
85 map.put("Family Games", "public.app-category.family-games");
86 map.put("Kids Games", "public.app-category.kids-games");
87 map.put("Music Games", "public.app-category.music-games");
88 map.put("Puzzle Games", "public.app-category.puzzle-games");
89 map.put("Racing Games", "public.app-category.racing-games");
90 map.put("Role Playing Games", "public.app-category.role-playing-games");
91 map.put("Simulation Games", "public.app-category.simulation-games");
92 map.put("Sports Games", "public.app-category.sports-games");
93 map.put("Strategy Games", "public.app-category.strategy-games");
94 map.put("Trivia Games", "public.app-category.trivia-games");
95 map.put("Word Games", "public.app-category.word-games");
96
97 return map;
98 }
99
100 public static final EnumeratedBundlerParam<String> MAC_CATEGORY =
101 new EnumeratedBundlerParam<>(
102 "Category",
103 "Mac Categories. Note that the key is the string to display to the user and the value is the id of the category",
104 "LSApplicationCategoryType",
105 String.class,
106 null,
107 params -> "Unknown",
108 false,
109 s -> s,
110 getMacCategories(),
111 false //strict - for MacStoreBundler this should be strict
112 );
113
114 public static final BundlerParamInfo<URL> RAW_EXECUTABLE_URL = new StandardBundlerParam<>(
115 "Launcher URL", "Override the packager default launcher with a custom launcher.", "mac.launcher.url",
116 URL.class, null, params -> MacResources.class.getResource(EXECUTABLE_NAME),
117 false, s -> {
118 try {
119 return new URL(s);
120 } catch (MalformedURLException e) {
121 Log.info(e.toString());
122 return null;
123 }
124 });
125
126 private void setBuildRoot(File dir) {
127 configRoot = new File(dir, "macosx");
128 configRoot.mkdirs();
129 }
130
131 public MacAppBundler() {
132 super();
133 baseResourceLoader = MacResources.class;
134 }
135
136 @Override
137 public boolean validate(Map<String, ? super Object> params) throws UnsupportedPlatformException, ConfigException {
138 logParameters(params);
139 return doValidate(params);
140 }
141
142 //to be used by chained bundlers, e.g. by EXE bundler to avoid
143 // skipping validation if p.type does not include "image"
144 public boolean doValidate(Map<String, ? super Object> p) throws UnsupportedPlatformException, ConfigException {
145 if (!System.getProperty("os.name").toLowerCase().contains("os x")) {
146 throw new UnsupportedPlatformException();
147 }
148
149 if (getPredefinedImage(p) != null) {
150 return true;
151 }
152
153 if (StandardBundlerParam.MAIN_JAR.fetchFrom(p) == null) {
154 throw new ConfigException(
155 "Main application jar is missing.",
156 "Make sure to use fx:jar task to create main application jar.");
157 }
158
159 //validate required inputs
160 if (USE_FX_PACKAGING.fetchFrom(p)) {
161 testRuntime(p, new String[] {"Contents/Home/jre/lib/ext/jfxrt.jar", "Contents/Home/jre/lib/jfxrt.jar"});
162 }
163
164 return true;
165 }
166
167
168 private File getConfig_InfoPlist() {
169 return new File(configRoot, "Info.plist");
170 }
171
172 private File getConfig_Icon() {
173 return new File(configRoot, NAME.fetchFrom(params) + ".icns");
174 }
175
176 private void prepareConfigFiles() throws IOException {
177 File infoPlistFile = getConfig_InfoPlist();
178 infoPlistFile.createNewFile();
179 writeInfoPlist(infoPlistFile);
180
181 // Copy icon to Resources folder
182 prepareIcon();
183 }
184
185 public File doBundle(Map<String, ? super Object> p, File outputDirectory, boolean dependentTask) {
186 File rootDirectory = null;
187 try {
188 final File predefinedImage = getPredefinedImage(p);
189 if (predefinedImage != null) {
190 return predefinedImage;
191 }
192 params = p;
193
194 File file = BUILD_ROOT.fetchFrom(p);
195 setBuildRoot(file);
196
197 //prepare config resources (we will copy them to the bundle later)
198 // NB: explicitly saving them to simplify customization
199 prepareConfigFiles();
200
201 // Create directory structure
202 rootDirectory = new File(outputDirectory, NAME.fetchFrom(p) + ".app");
203 IOUtils.deleteRecursive(rootDirectory);
204 rootDirectory.mkdirs();
205
206 if (!dependentTask) {
207 Log.info("Creating app bundle: " + rootDirectory.getAbsolutePath());
208 }
209
210 File contentsDirectory = new File(rootDirectory, "Contents");
211 contentsDirectory.mkdirs();
212
213 File macOSDirectory = new File(contentsDirectory, "MacOS");
214 macOSDirectory.mkdirs();
215
216 File javaDirectory = new File(contentsDirectory, "Java");
217 javaDirectory.mkdirs();
218
219 File plugInsDirectory = new File(contentsDirectory, "PlugIns");
220
221 File resourcesDirectory = new File(contentsDirectory, "Resources");
222 resourcesDirectory.mkdirs();
223
224 // Generate PkgInfo
225 File pkgInfoFile = new File(contentsDirectory, "PkgInfo");
226 pkgInfoFile.createNewFile();
227 writePkgInfo(pkgInfoFile);
228
229 // Copy executable to MacOS folder
230 File executableFile = new File(macOSDirectory, getLauncherName());
231 IOUtils.copyFromURL(
232 RAW_EXECUTABLE_URL.fetchFrom(p),
233 executableFile);
234
235 executableFile.setExecutable(true, false);
236
237 // Copy runtime to PlugIns folder
238 copyRuntime(plugInsDirectory);
239
240 // Copy class path entries to Java folder
241 copyClassPathEntries(javaDirectory);
242
243 //TODO: Need to support adding native libraries.
244 // Copy library path entries to MacOS folder
245 //copyLibraryPathEntries(macOSDirectory);
246
247 /*********** Take care of "config" files *******/
248 // Copy icon to Resources folder
249 IOUtils.copyFile(getConfig_Icon(),
250 new File(resourcesDirectory, getConfig_Icon().getName()));
251 // Generate Info.plist
252 IOUtils.copyFile(getConfig_InfoPlist(),
253 new File(contentsDirectory, "Info.plist"));
254 } catch (ConfigException e) {
255 Log.info("Bundler " + getName() + " skipped because of a configuration problem: " + e.getMessage() + "\nAdvice to fix: " + e.getAdvice());
256 } catch (IOException ex) {
257 Log.verbose(ex);
258 return null;
259 } finally {
260 if (!verbose) {
261 //cleanup
262 cleanupConfigFiles();
263 } else {
264 Log.info("Config files are saved to " +
265 configRoot.getAbsolutePath() +
266 ". Use them to customize package.");
267 }
268 }
269 return rootDirectory;
270 }
271
272 public String getAppName() {
273 return NAME.fetchFrom(params) + ".app";
274 }
275
276 protected void cleanupConfigFiles() {
277 //Since building the app can be bypassed, make sure configRoot was set
278 if (configRoot != null) {
279 if (getConfig_Icon() != null) {
280 getConfig_Icon().delete();
281 }
282 if (getConfig_InfoPlist() != null) {
283 getConfig_InfoPlist().delete();
284 }
285 }
286 }
287
288
289 private void copyClassPathEntries(File javaDirectory) throws IOException {
290 RelativeFileSet classPath = APP_RESOURCES.fetchFrom(params);
291 if (classPath == null) {
292 throw new RuntimeException("Null app resources?");
293 }
294 File srcdir = classPath.getBaseDirectory();
295 for (String fname : classPath.getIncludedFiles()) {
296 IOUtils.copyFile(
297 new File(srcdir, fname), new File(javaDirectory, fname));
298 }
299 }
300
301 private void copyRuntime(File plugInsDirectory) throws IOException {
302 RelativeFileSet runTime = RUNTIME.fetchFrom(params);
303 if (runTime == null) {
304 //request to use system runtime => do not bundle
305 return;
306 }
307 plugInsDirectory.mkdirs();
308
309 File srcdir = runTime.getBaseDirectory();
310 File destDir = new File(plugInsDirectory, srcdir.getName());
311 Set<String> filesToCopy = runTime.getIncludedFiles();
312
313 // We don't need the symlink to libjli or the JRE's info.plist.
314 // We are going to load it directly.
315 filesToCopy.remove("Contents/MacOS/libjli.dylib");
316 filesToCopy.remove("Contents/Info.plist");
317 for (String fname : filesToCopy) {
318 IOUtils.copyFile(
319 new File(srcdir, fname), new File(destDir, fname));
320 }
321 }
322
323
324 // get Name from bundle params
325 private String NAME() {
326 return NAME.fetchFrom(params);
327 }
328
329 private void prepareIcon() throws IOException {
330 File icon = ICON.fetchFrom(params);
331 if (icon == null || !icon.exists()) {
332 fetchResource(MAC_BUNDLER_PREFIX+ NAME() +".icns",
333 "icon",
334 TEMPLATE_BUNDLE_ICON,
335 getConfig_Icon());
336 } else {
337 fetchResource(MAC_BUNDLER_PREFIX+ NAME() +".icns",
338 "icon",
339 icon,
340 getConfig_Icon());
341 }
342 }
343
344 private String getLauncherName() {
345 if (NAME() != null) {
346 return NAME();
347 } else {
348 return MAIN_CLASS.fetchFrom(params);
349 }
350 }
351
352 private String getBundleName() {
353 //TODO: Check to see what rules/limits are in place for CFBundleName
354 if (NAME() != null) {
355 return NAME();
356 } else {
357 String nm = MAIN_CLASS.fetchFrom(params);
358 if (nm.length() > 16) {
359 nm = nm.substring(0, 16);
360 }
361 return nm;
362 }
363 }
364
365 private String getBundleIdentifier() {
366 //TODO: Check to see what rules/limits are in place for CFBundleIdentifier
367 return IDENTIFIER.fetchFrom(params);
368 }
369
370 private void writeInfoPlist(File file) throws IOException {
371 Log.verbose("Preparing Info.plist: "+file.getAbsolutePath());
372
373 //prepare config for exe
374 //Note: do not need CFBundleDisplayName if we do not support localization
375 Map<String, String> data = new HashMap<>();
376 data.put("DEPLOY_ICON_FILE", getConfig_Icon().getName());
377 data.put("DEPLOY_BUNDLE_IDENTIFIER",
378 getBundleIdentifier().toLowerCase());
379 data.put("DEPLOY_BUNDLE_NAME",
380 getBundleName());
381 data.put("DEPLOY_BUNDLE_COPYRIGHT",
382 COPYRIGHT.fetchFrom(params) != null ? COPYRIGHT.fetchFrom(params) : "Unknown");
383 data.put("DEPLOY_LAUNCHER_NAME", getLauncherName());
384 if (RUNTIME.fetchFrom(params) != null) {
385 data.put("DEPLOY_JAVA_RUNTIME_NAME",
386 RUNTIME.fetchFrom(params).getBaseDirectory().getName());
387 } else {
388 data.put("DEPLOY_JAVA_RUNTIME_NAME", "");
389 }
390 data.put("DEPLOY_BUNDLE_SHORT_VERSION",
391 VERSION.fetchFrom(params) != null ? VERSION.fetchFrom(params) : "1.0.0");
392 data.put("DEPLOY_BUNDLE_CATEGORY",
393 //TODO parameters should provide set of values for IDEs
394 CATEGORY.fetchFrom(params) != null ?
395 CATEGORY.fetchFrom(params) : "unknown");
396
397 //TODO NOT THE WAY TODO THIS but good enough for first pass
398 data.put("DEPLOY_MAIN_JAR_NAME", new BundleParams(params).getMainApplicationJar());
399 // data.put("DEPLOY_MAIN_JAR_NAME", MAIN_JAR.fetchFrom(params).toString());
400
401 data.put("DEPLOY_PREFERENCES_ID", PREFERENCES_ID.fetchFrom(params).toLowerCase());
402
403 StringBuilder sb = new StringBuilder();
404 List<String> jvmOptions = JVM_OPTIONS.fetchFrom(params);
405
406 String newline = ""; //So we don't add unneccessary extra line after last append
407 for (String o : jvmOptions) {
408 sb.append(newline).append(" <string>").append(o).append("</string>");
409 newline = "\n";
410 }
411 data.put("DEPLOY_JVM_OPTIONS", sb.toString());
412
413 newline = "";
414 sb = new StringBuilder();
415 Map<String, String> overridableJVMOptions = USER_JVM_OPTIONS.fetchFrom(params);
418 sb.append(" <key>").append(arg.getKey()).append("</key>\n");
419 sb.append(" <string>").append(arg.getValue()).append("</string>");
420 newline = "\n";
421 }
422 data.put("DEPLOY_JVM_USER_OPTIONS", sb.toString());
423
424
425 //TODO UNLESS we are supporting building for jre7, this is unnecessary
426 // if (params.useJavaFXPackaging()) {
427 // data.put("DEPLOY_LAUNCHER_CLASS", JAVAFX_LAUNCHER_CLASS);
428 // } else {
429 data.put("DEPLOY_LAUNCHER_CLASS", MAIN_CLASS.fetchFrom(params));
430 // }
431 // This will be an empty string for correctly packaged JavaFX apps
432 data.put("DEPLOY_APP_CLASSPATH", MAIN_JAR_CLASSPATH.fetchFrom(params));
433
434 //TODO: Add remainder of the classpath
435
436 Writer w = new BufferedWriter(new FileWriter(file));
437 w.write(preprocessTextResource(
438 MAC_BUNDLER_PREFIX + getConfig_InfoPlist().getName(),
439 "Bundle config file", TEMPLATE_INFO_PLIST, data));
440 w.close();
441
442 }
443
444 private void writePkgInfo(File file) throws IOException {
445
446 //hardcoded as it does not seem we need to change it ever
447 String signature = "????";
448
449 try (Writer out = new BufferedWriter(new FileWriter(file))) {
450 out.write(OS_TYPE_CODE + signature);
451 out.flush();
452 }
453 }
454
455 //////////////////////////////////////////////////////////////////////////////////
456 // Implement Bundler
457 //////////////////////////////////////////////////////////////////////////////////
458
459 @Override
475 public BundleType getBundleType() {
476 return BundleType.IMAGE;
477 }
478
479 @Override
480 public Collection<BundlerParamInfo<?>> getBundleParameters() {
481 return getAppBundleParameters();
482 }
483
484 public static Collection<BundlerParamInfo<?>> getAppBundleParameters() {
485 return Arrays.asList(
486 APP_NAME,
487 APP_RESOURCES,
488 BUILD_ROOT,
489 JVM_OPTIONS,
490 MAIN_CLASS,
491 MAIN_JAR,
492 MAIN_JAR_CLASSPATH,
493 PREFERENCES_ID,
494 RAW_EXECUTABLE_URL,
495 RUNTIME,
496 USE_FX_PACKAGING,
497 USER_JVM_OPTIONS,
498 VERSION,
499 ICON,
500 MAC_CATEGORY
501 );
502 }
503
504
505 @Override
506 public File execute(Map<String, ? super Object> params, File outputParentDir) {
507 return doBundle(params, outputParentDir, false);
508 }
509 }
|
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 package com.sun.javafx.tools.packager.bundlers;
26
27 import com.oracle.bundlers.*;
28 import com.oracle.bundlers.JreUtils.Rule;
29 import com.sun.javafx.tools.packager.Log;
30 import com.sun.javafx.tools.resource.mac.MacResources;
31
32 import java.io.*;
33 import java.net.MalformedURLException;
34 import java.net.URL;
35 import java.text.MessageFormat;
36 import java.util.*;
37
38 import static com.oracle.bundlers.StandardBundlerParam.*;
39 import static com.oracle.bundlers.mac.MacBaseInstallerBundler.getPredefinedImage;
40
41 public class MacAppBundler extends AbstractBundler {
42
43 private static final ResourceBundle I18N =
44 ResourceBundle.getBundle("com.oracle.bundlers.mac.MacAppBundler");
45
46 public final static String MAC_BUNDLER_PREFIX =
47 BUNDLER_PREFIX + "macosx" + File.separator;
48
49 private static final String EXECUTABLE_NAME = "JavaAppLauncher";
50 private static final String TEMPLATE_BUNDLE_ICON = "GenericApp.icns";
51 private static final String OS_TYPE_CODE = "APPL";
52 private static final String TEMPLATE_INFO_PLIST = "Info.plist.template";
53
54 private static Map<String, String> getMacCategories() {
55 Map<String, String> map = new HashMap<>();
56 map.put("Business", "public.app-category.business");
57 map.put("Developer Tools", "public.app-category.developer-tools");
58 map.put("Education", "public.app-category.education");
59 map.put("Entertainment", "public.app-category.entertainment");
60 map.put("Finance", "public.app-category.finance");
61 map.put("Games", "public.app-category.games");
62 map.put("Graphics & Design", "public.app-category.graphics-design");
63 map.put("Healthcare & Fitness", "public.app-category.healthcare-fitness");
64 map.put("Lifestyle", "public.app-category.lifestyle");
84 map.put("Dice Games", "public.app-category.dice-games");
85 map.put("Educational Games", "public.app-category.educational-games");
86 map.put("Family Games", "public.app-category.family-games");
87 map.put("Kids Games", "public.app-category.kids-games");
88 map.put("Music Games", "public.app-category.music-games");
89 map.put("Puzzle Games", "public.app-category.puzzle-games");
90 map.put("Racing Games", "public.app-category.racing-games");
91 map.put("Role Playing Games", "public.app-category.role-playing-games");
92 map.put("Simulation Games", "public.app-category.simulation-games");
93 map.put("Sports Games", "public.app-category.sports-games");
94 map.put("Strategy Games", "public.app-category.strategy-games");
95 map.put("Trivia Games", "public.app-category.trivia-games");
96 map.put("Word Games", "public.app-category.word-games");
97
98 return map;
99 }
100
101 public static final EnumeratedBundlerParam<String> MAC_CATEGORY =
102 new EnumeratedBundlerParam<>(
103 "Category",
104 "Mac App Store Categories. Note that the key is the string to display to the user and the value is the id of the category",
105 "mac.category",
106 String.class,
107 new String[] {CATEGORY.getID()},
108 params -> "Unknown",
109 false,
110 (s, p) -> s,
111 getMacCategories(),
112 false //strict - for MacStoreBundler this should be strict
113 );
114
115 public static final BundlerParamInfo<String> MAC_CF_BUNDLE_NAME =
116 new StandardBundlerParam<>(
117 "CFBundleName",
118 "The name of the app as it appears in the Menu Bar. This can be different from the application name. This name should be less than 16 characters long and be suitable for displaying in the menu bar and the app’s Info window.",
119 "mac.CFBundleName",
120 String.class,
121 null,
122 params -> null,
123 false,
124 (s, p) -> s);
125
126 public static final BundlerParamInfo<File> CONFIG_ROOT = new StandardBundlerParam<>(
127 I18N.getString("param.config-root.name"),
128 I18N.getString("param.config-root.description"),
129 "configRoot",
130 File.class,
131 null,
132 params -> {
133 File configRoot = new File(BUILD_ROOT.fetchFrom(params), "macosx");
134 configRoot.mkdirs();
135 return configRoot;
136 },
137 false,
138 (s, p) -> new File(s));
139
140 public static final BundlerParamInfo<URL> RAW_EXECUTABLE_URL = new StandardBundlerParam<>(
141 "Launcher URL",
142 "Override the packager default launcher with a custom launcher.",
143 "mac.launcher.url",
144 URL.class,
145 null,
146 params -> MacResources.class.getResource(EXECUTABLE_NAME),
147 false,
148 (s, p) -> {
149 try {
150 return new URL(s);
151 } catch (MalformedURLException e) {
152 Log.info(e.toString());
153 return null;
154 }
155 });
156
157 public static final BundlerParamInfo<String> DEFAULT_ICNS_ICON = new StandardBundlerParam<>(
158 "Default Icon",
159 "The Default Icon for when a user does not specify an icns file.",
160 ".mac.default.icns",
161 String.class,
162 null,
163 params -> TEMPLATE_BUNDLE_ICON,
164 false,
165 (s, p) -> s);
166
167 //Subsetting of JRE is restricted.
168 //JRE README defines what is allowed to strip:
169 // http://www.oracle.com/technetwork/java/javase/jre-7-readme-430162.html //TODO update when 8 goes GA
170 //
171 public static final BundlerParamInfo<Rule[]> MAC_JDK_RULES = new StandardBundlerParam<>(
172 "",
173 "",
174 ".mac-jdk.runtime.rules",
175 Rule[].class,
176 null,
177 params -> new Rule[]{
178 Rule.suffixNeg("macos/libjli.dylib"),
179 Rule.suffixNeg("resources"),
180 Rule.suffixNeg("home/bin"),
181 Rule.suffixNeg("home/db"),
182 Rule.suffixNeg("home/demo"),
183 Rule.suffixNeg("home/include"),
184 Rule.suffixNeg("home/lib"),
185 Rule.suffixNeg("home/man"),
186 Rule.suffixNeg("home/release"),
187 Rule.suffixNeg("home/sample"),
188 Rule.suffixNeg("home/src.zip"),
189 //"home/rt" is not part of the official builds
190 // but we may be creating this symlink to make older NB projects
191 // happy. Make sure to not include it into final artifact
192 Rule.suffixNeg("home/rt"),
193 Rule.suffixNeg("jre/bin"),
194 Rule.suffixNeg("jre/bin/rmiregistry"),
195 Rule.suffixNeg("jre/bin/tnameserv"),
196 Rule.suffixNeg("jre/bin/keytool"),
197 Rule.suffixNeg("jre/bin/klist"),
198 Rule.suffixNeg("jre/bin/ktab"),
199 Rule.suffixNeg("jre/bin/policytool"),
200 Rule.suffixNeg("jre/bin/orbd"),
201 Rule.suffixNeg("jre/bin/servertool"),
202 Rule.suffixNeg("jre/bin/javaws"),
203 Rule.suffixNeg("jre/bin/java"),
204 //Rule.suffixNeg("jre/lib/ext"), //need some of jars there for https to work
205 Rule.suffixNeg("jre/lib/nibs"),
206 //keep core deploy APIs but strip plugin dll
207 //Rule.suffixNeg("jre/lib/deploy"),
208 //Rule.suffixNeg("jre/lib/deploy.jar"),
209 //Rule.suffixNeg("jre/lib/javaws.jar"),
210 //Rule.suffixNeg("jre/lib/libdeploy.dylib"),
211 //Rule.suffixNeg("jre/lib/plugin.jar"),
212 Rule.suffixNeg("jre/lib/libnpjp2.dylib"),
213 Rule.suffixNeg("jre/lib/security/javaws.policy"),
214 Rule.substrNeg("Contents/Info.plist")
215 },
216 false,
217 (s, p) -> null
218 );
219
220 public static final BundlerParamInfo<RelativeFileSet> MAC_RUNTIME = new StandardBundlerParam<>(
221 RUNTIME.getName(),
222 RUNTIME.getDescription(),
223 RUNTIME.getID(),
224 RelativeFileSet.class,
225 null,
226 params -> extractMacRuntime(System.getProperty("java.home"), params),
227 false,
228 MacAppBundler::extractMacRuntime
229 );
230
231 public static RelativeFileSet extractMacRuntime(String base, Map<String, ? super Object> params) {
232 if (base.endsWith("/Home")) {
233 throw new IllegalArgumentException("Currently Macs require a JDK to package");
234 } else if (base.endsWith("/Home/jre")) {
235 File baseDir = new File(base).getParentFile().getParentFile().getParentFile();
236 return JreUtils.extractJreAsRelativeFileSet(baseDir.toString(),
237 MAC_JDK_RULES.fetchFrom(params));
238 } else {
239 // for now presume we are pointed to the top of a JDK
240 return JreUtils.extractJreAsRelativeFileSet(base,
241 MAC_JDK_RULES.fetchFrom(params));
242 }
243 }
244
245 public MacAppBundler() {
246 super();
247 baseResourceLoader = MacResources.class;
248 }
249
250 @Override
251 public boolean validate(Map<String, ? super Object> params) throws UnsupportedPlatformException, ConfigException {
252 try {
253 logParameters(params);
254 return doValidate(params);
255 } catch (RuntimeException re) {
256 throw new ConfigException(re);
257 }
258 }
259
260 //to be used by chained bundlers, e.g. by EXE bundler to avoid
261 // skipping validation if p.type does not include "image"
262 public boolean doValidate(Map<String, ? super Object> p) throws UnsupportedPlatformException, ConfigException {
263 if (!System.getProperty("os.name").toLowerCase().contains("os x")) {
264 throw new UnsupportedPlatformException();
265 }
266
267 if (getPredefinedImage(p) != null) {
268 return true;
269 }
270
271 if (MAIN_JAR.fetchFrom(p) == null) {
272 throw new ConfigException(
273 "Main application jar is missing.",
274 "Make sure to use fx:jar task to create main application jar.");
275 }
276
277 //validate required inputs
278 if (USE_FX_PACKAGING.fetchFrom(p)) {
279 testRuntime(p, new String[] {"Contents/Home/jre/lib/ext/jfxrt.jar", "Contents/Home/jre/lib/jfxrt.jar"});
280 }
281
282 return true;
283 }
284
285
286 private File getConfig_InfoPlist(Map<String, ? super Object> params) {
287 return new File(CONFIG_ROOT.fetchFrom(params), "Info.plist");
288 }
289
290 private File getConfig_Icon(Map<String, ? super Object> params) {
291 return new File(CONFIG_ROOT.fetchFrom(params), APP_NAME.fetchFrom(params) + ".icns");
292 }
293
294 private void prepareConfigFiles(Map<String, ? super Object> params) throws IOException {
295 File infoPlistFile = getConfig_InfoPlist(params);
296 infoPlistFile.createNewFile();
297 writeInfoPlist(infoPlistFile, params);
298
299 // Copy icon to Resources folder
300 prepareIcon(params);
301 }
302
303 public File doBundle(Map<String, ? super Object> p, File outputDirectory, boolean dependentTask) {
304 File rootDirectory = null;
305 try {
306 final File predefinedImage = getPredefinedImage(p);
307 if (predefinedImage != null) {
308 return predefinedImage;
309 }
310
311 File file = BUILD_ROOT.fetchFrom(p);
312
313 //prepare config resources (we will copy them to the bundle later)
314 // NB: explicitly saving them to simplify customization
315 prepareConfigFiles(p);
316
317 // Create directory structure
318 rootDirectory = new File(outputDirectory, APP_NAME.fetchFrom(p) + ".app");
319 IOUtils.deleteRecursive(rootDirectory);
320 rootDirectory.mkdirs();
321
322 if (!dependentTask) {
323 Log.info("Creating app bundle: " + rootDirectory.getAbsolutePath());
324 }
325
326 File contentsDirectory = new File(rootDirectory, "Contents");
327 contentsDirectory.mkdirs();
328
329 File macOSDirectory = new File(contentsDirectory, "MacOS");
330 macOSDirectory.mkdirs();
331
332 File javaDirectory = new File(contentsDirectory, "Java");
333 javaDirectory.mkdirs();
334
335 File plugInsDirectory = new File(contentsDirectory, "PlugIns");
336
337 File resourcesDirectory = new File(contentsDirectory, "Resources");
338 resourcesDirectory.mkdirs();
339
340 // Generate PkgInfo
341 File pkgInfoFile = new File(contentsDirectory, "PkgInfo");
342 pkgInfoFile.createNewFile();
343 writePkgInfo(pkgInfoFile);
344
345 // Copy executable to MacOS folder
346 File executableFile = new File(macOSDirectory, getLauncherName(p));
347 IOUtils.copyFromURL(
348 RAW_EXECUTABLE_URL.fetchFrom(p),
349 executableFile);
350
351 executableFile.setExecutable(true, false);
352
353 // Copy runtime to PlugIns folder
354 copyRuntime(plugInsDirectory, p);
355
356 // Copy class path entries to Java folder
357 copyClassPathEntries(javaDirectory, p);
358
359 //TODO: Need to support adding native libraries.
360 // Copy library path entries to MacOS folder
361 //copyLibraryPathEntries(macOSDirectory);
362
363 /*********** Take care of "config" files *******/
364 // Copy icon to Resources folder
365 IOUtils.copyFile(getConfig_Icon(p),
366 new File(resourcesDirectory, getConfig_Icon(p).getName()));
367 // Generate Info.plist
368 IOUtils.copyFile(getConfig_InfoPlist(p),
369 new File(contentsDirectory, "Info.plist"));
370 } catch (IOException ex) {
371 Log.verbose(ex);
372 return null;
373 } finally {
374 if (!VERBOSE.fetchFrom(p)) {
375 //cleanup
376 cleanupConfigFiles(p);
377 } else {
378 Log.info("Config files are saved to " +
379 CONFIG_ROOT.fetchFrom(p).getAbsolutePath() +
380 ". Use them to customize package.");
381 }
382 }
383 return rootDirectory;
384 }
385
386 public String getAppName(Map<String, ? super Object> params) {
387 return APP_NAME.fetchFrom(params) + ".app";
388 }
389
390 protected void cleanupConfigFiles(Map<String, ? super Object> params) {
391 //Since building the app can be bypassed, make sure configRoot was set
392 if (CONFIG_ROOT.fetchFrom(params) != null) {
393 if (getConfig_Icon(params) != null) {
394 getConfig_Icon(params).delete();
395 }
396 if (getConfig_InfoPlist(params) != null) {
397 getConfig_InfoPlist(params).delete();
398 }
399 }
400 }
401
402
403 private void copyClassPathEntries(File javaDirectory, Map<String, ? super Object> params) throws IOException {
404 RelativeFileSet classPath = APP_RESOURCES.fetchFrom(params);
405 if (classPath == null) {
406 throw new RuntimeException("Null app resources?");
407 }
408 File srcdir = classPath.getBaseDirectory();
409 for (String fname : classPath.getIncludedFiles()) {
410 IOUtils.copyFile(
411 new File(srcdir, fname), new File(javaDirectory, fname));
412 }
413 }
414
415 private void copyRuntime(File plugInsDirectory, Map<String, ? super Object> params) throws IOException {
416 RelativeFileSet runTime = MAC_RUNTIME.fetchFrom(params);
417 if (runTime == null) {
418 //request to use system runtime => do not bundle
419 return;
420 }
421 plugInsDirectory.mkdirs();
422
423 File srcdir = runTime.getBaseDirectory();
424 File destDir = new File(plugInsDirectory, srcdir.getName());
425 Set<String> filesToCopy = runTime.getIncludedFiles();
426
427 for (String fname : filesToCopy) {
428 IOUtils.copyFile(
429 new File(srcdir, fname), new File(destDir, fname));
430 }
431 }
432
433 private void prepareIcon(Map<String, ? super Object> params) throws IOException {
434 File icon = ICON.fetchFrom(params);
435 if (icon == null || !icon.exists()) {
436 fetchResource(MAC_BUNDLER_PREFIX+ APP_NAME.fetchFrom(params) +".icns",
437 "icon",
438 DEFAULT_ICNS_ICON.fetchFrom(params),
439 getConfig_Icon(params),
440 VERBOSE.fetchFrom(params));
441 } else {
442 fetchResource(MAC_BUNDLER_PREFIX+ APP_NAME.fetchFrom(params) +".icns",
443 "icon",
444 icon,
445 getConfig_Icon(params),
446 VERBOSE.fetchFrom(params));
447 }
448 }
449
450 private String getLauncherName(Map<String, ? super Object> params) {
451 if (APP_NAME.fetchFrom(params) != null) {
452 return APP_NAME.fetchFrom(params);
453 } else {
454 return MAIN_CLASS.fetchFrom(params);
455 }
456 }
457
458 private String getBundleName(Map<String, ? super Object> params) {
459 //TODO: Check to see what rules/limits are in place for CFBundleName
460 if (MAC_CF_BUNDLE_NAME.fetchFrom(params) != null) {
461 String bn = MAC_CF_BUNDLE_NAME.fetchFrom(params);
462 if (bn.length() > 16) {
463 Log.info(MessageFormat.format(I18N.getString("message.bundle-name-too-long-warning"), MAC_CF_BUNDLE_NAME.getID(), bn));
464 }
465 return MAC_CF_BUNDLE_NAME.fetchFrom(params);
466 } else if (APP_NAME.fetchFrom(params) != null) {
467 return APP_NAME.fetchFrom(params);
468 } else {
469 String nm = MAIN_CLASS.fetchFrom(params);
470 if (nm.length() > 16) {
471 nm = nm.substring(0, 16);
472 }
473 return nm;
474 }
475 }
476
477 private String getBundleIdentifier(Map<String, ? super Object> params) {
478 //TODO: Check to see what rules/limits are in place for CFBundleIdentifier
479 return IDENTIFIER.fetchFrom(params);
480 }
481
482 private void writeInfoPlist(File file, Map<String, ? super Object> params) throws IOException {
483 Log.verbose("Preparing Info.plist: "+file.getAbsolutePath());
484
485 //prepare config for exe
486 //Note: do not need CFBundleDisplayName if we do not support localization
487 Map<String, String> data = new HashMap<>();
488 data.put("DEPLOY_ICON_FILE", getConfig_Icon(params).getName());
489 data.put("DEPLOY_BUNDLE_IDENTIFIER",
490 getBundleIdentifier(params));
491 data.put("DEPLOY_BUNDLE_NAME",
492 getBundleName(params));
493 data.put("DEPLOY_BUNDLE_COPYRIGHT",
494 COPYRIGHT.fetchFrom(params) != null ? COPYRIGHT.fetchFrom(params) : "Unknown");
495 data.put("DEPLOY_LAUNCHER_NAME", getLauncherName(params));
496 if (MAC_RUNTIME.fetchFrom(params) != null) {
497 data.put("DEPLOY_JAVA_RUNTIME_NAME",
498 MAC_RUNTIME.fetchFrom(params).getBaseDirectory().getName());
499 } else {
500 data.put("DEPLOY_JAVA_RUNTIME_NAME", "");
501 }
502 data.put("DEPLOY_BUNDLE_SHORT_VERSION",
503 VERSION.fetchFrom(params) != null ? VERSION.fetchFrom(params) : "1.0.0");
504 data.put("DEPLOY_BUNDLE_CATEGORY",
505 //TODO parameters should provide set of values for IDEs
506 MAC_CATEGORY.validatedFetchFrom(params));
507
508 //TODO NOT THE WAY TODO THIS but good enough for first pass
509 data.put("DEPLOY_MAIN_JAR_NAME", new BundleParams(params).getMainApplicationJar());
510 // data.put("DEPLOY_MAIN_JAR_NAME", MAIN_JAR.fetchFrom(params).toString());
511
512 data.put("DEPLOY_PREFERENCES_ID", PREFERENCES_ID.fetchFrom(params).toLowerCase());
513
514 StringBuilder sb = new StringBuilder();
515 List<String> jvmOptions = JVM_OPTIONS.fetchFrom(params);
516
517 String newline = ""; //So we don't add unneccessary extra line after last append
518 for (String o : jvmOptions) {
519 sb.append(newline).append(" <string>").append(o).append("</string>");
520 newline = "\n";
521 }
522 data.put("DEPLOY_JVM_OPTIONS", sb.toString());
523
524 newline = "";
525 sb = new StringBuilder();
526 Map<String, String> overridableJVMOptions = USER_JVM_OPTIONS.fetchFrom(params);
529 sb.append(" <key>").append(arg.getKey()).append("</key>\n");
530 sb.append(" <string>").append(arg.getValue()).append("</string>");
531 newline = "\n";
532 }
533 data.put("DEPLOY_JVM_USER_OPTIONS", sb.toString());
534
535
536 //TODO UNLESS we are supporting building for jre7, this is unnecessary
537 // if (params.useJavaFXPackaging()) {
538 // data.put("DEPLOY_LAUNCHER_CLASS", JAVAFX_LAUNCHER_CLASS);
539 // } else {
540 data.put("DEPLOY_LAUNCHER_CLASS", MAIN_CLASS.fetchFrom(params));
541 // }
542 // This will be an empty string for correctly packaged JavaFX apps
543 data.put("DEPLOY_APP_CLASSPATH", MAIN_JAR_CLASSPATH.fetchFrom(params));
544
545 //TODO: Add remainder of the classpath
546
547 Writer w = new BufferedWriter(new FileWriter(file));
548 w.write(preprocessTextResource(
549 MAC_BUNDLER_PREFIX + getConfig_InfoPlist(params).getName(),
550 "Bundle config file", TEMPLATE_INFO_PLIST, data,
551 VERBOSE.fetchFrom(params)));
552 w.close();
553
554 }
555
556 private void writePkgInfo(File file) throws IOException {
557
558 //hardcoded as it does not seem we need to change it ever
559 String signature = "????";
560
561 try (Writer out = new BufferedWriter(new FileWriter(file))) {
562 out.write(OS_TYPE_CODE + signature);
563 out.flush();
564 }
565 }
566
567 //////////////////////////////////////////////////////////////////////////////////
568 // Implement Bundler
569 //////////////////////////////////////////////////////////////////////////////////
570
571 @Override
587 public BundleType getBundleType() {
588 return BundleType.IMAGE;
589 }
590
591 @Override
592 public Collection<BundlerParamInfo<?>> getBundleParameters() {
593 return getAppBundleParameters();
594 }
595
596 public static Collection<BundlerParamInfo<?>> getAppBundleParameters() {
597 return Arrays.asList(
598 APP_NAME,
599 APP_RESOURCES,
600 BUILD_ROOT,
601 JVM_OPTIONS,
602 MAIN_CLASS,
603 MAIN_JAR,
604 MAIN_JAR_CLASSPATH,
605 PREFERENCES_ID,
606 RAW_EXECUTABLE_URL,
607 MAC_RUNTIME,
608 USE_FX_PACKAGING,
609 USER_JVM_OPTIONS,
610 VERSION,
611 ICON,
612 MAC_CATEGORY
613 );
614 }
615
616
617 @Override
618 public File execute(Map<String, ? super Object> params, File outputParentDir) {
619 return doBundle(params, outputParentDir, false);
620 }
621 }
|