1 /* 2 * Copyright (c) 2012, 2016, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 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.Log; 33 import com.oracle.tools.packager.Platform; 34 import com.oracle.tools.packager.StandardBundlerParam; 35 import com.oracle.tools.packager.UnsupportedPlatformException; 36 import jdk.packager.builders.mac.MacAppImageBuilder; 37 38 import jdk.packager.internal.JLinkBundlerHelper; 39 40 import java.io.File; 41 import java.io.IOException; 42 import java.math.BigInteger; 43 import java.text.MessageFormat; 44 import java.util.Arrays; 45 import java.util.Collection; 46 import java.util.HashMap; 47 import java.util.Map; 48 import java.util.Optional; 49 import java.util.ResourceBundle; 50 51 import static com.oracle.tools.packager.StandardBundlerParam.*; 52 import static com.oracle.tools.packager.mac.MacBaseInstallerBundler.*; 53 import jdk.packager.builders.AbstractAppImageBuilder; 54 55 public class MacAppBundler extends AbstractImageBundler { 56 57 private static final ResourceBundle I18N = 58 ResourceBundle.getBundle(MacAppBundler.class.getName()); 59 60 public final static String MAC_BUNDLER_PREFIX = 61 BUNDLER_PREFIX + "macosx" + File.separator; 62 63 private static final String TEMPLATE_BUNDLE_ICON = "GenericApp.icns"; 64 65 private static Map<String, String> getMacCategories() { 66 Map<String, String> map = new HashMap<>(); 67 map.put("Business", "public.app-category.business"); 68 map.put("Developer Tools", "public.app-category.developer-tools"); 69 map.put("Education", "public.app-category.education"); 70 map.put("Entertainment", "public.app-category.entertainment"); 71 map.put("Finance", "public.app-category.finance"); 72 map.put("Games", "public.app-category.games"); 73 map.put("Graphics & Design", "public.app-category.graphics-design"); 74 map.put("Healthcare & Fitness", "public.app-category.healthcare-fitness"); 75 map.put("Lifestyle", "public.app-category.lifestyle"); 76 map.put("Medical", "public.app-category.medical"); 77 map.put("Music", "public.app-category.music"); 78 map.put("News", "public.app-category.news"); 79 map.put("Photography", "public.app-category.photography"); 80 map.put("Productivity", "public.app-category.productivity"); 81 map.put("Reference", "public.app-category.reference"); 82 map.put("Social Networking", "public.app-category.social-networking"); 83 map.put("Sports", "public.app-category.sports"); 84 map.put("Travel", "public.app-category.travel"); 85 map.put("Utilities", "public.app-category.utilities"); 86 map.put("Video", "public.app-category.video"); 87 map.put("Weather", "public.app-category.weather"); 88 89 map.put("Action Games", "public.app-category.action-games"); 90 map.put("Adventure Games", "public.app-category.adventure-games"); 91 map.put("Arcade Games", "public.app-category.arcade-games"); 92 map.put("Board Games", "public.app-category.board-games"); 93 map.put("Card Games", "public.app-category.card-games"); 94 map.put("Casino Games", "public.app-category.casino-games"); 95 map.put("Dice Games", "public.app-category.dice-games"); 96 map.put("Educational Games", "public.app-category.educational-games"); 97 map.put("Family Games", "public.app-category.family-games"); 98 map.put("Kids Games", "public.app-category.kids-games"); 99 map.put("Music Games", "public.app-category.music-games"); 100 map.put("Puzzle Games", "public.app-category.puzzle-games"); 101 map.put("Racing Games", "public.app-category.racing-games"); 102 map.put("Role Playing Games", "public.app-category.role-playing-games"); 103 map.put("Simulation Games", "public.app-category.simulation-games"); 104 map.put("Sports Games", "public.app-category.sports-games"); 105 map.put("Strategy Games", "public.app-category.strategy-games"); 106 map.put("Trivia Games", "public.app-category.trivia-games"); 107 map.put("Word Games", "public.app-category.word-games"); 108 109 return map; 110 } 111 112 public static final EnumeratedBundlerParam<String> MAC_CATEGORY = 113 new EnumeratedBundlerParam<>( 114 I18N.getString("param.category-name"), 115 I18N.getString("param.category-name.description"), 116 "mac.category", 117 String.class, 118 params -> params.containsKey(CATEGORY.getID()) 119 ? CATEGORY.fetchFrom(params) 120 : "Unknown", 121 (s, p) -> s, 122 getMacCategories(), 123 false //strict - for MacStoreBundler this should be strict 124 ); 125 126 public static final BundlerParamInfo<String> MAC_CF_BUNDLE_NAME = 127 new StandardBundlerParam<>( 128 I18N.getString("param.cfbundle-name.name"), 129 I18N.getString("param.cfbundle-name.description"), 130 "mac.CFBundleName", 131 String.class, 132 params -> null, 133 (s, p) -> s); 134 135 public static final BundlerParamInfo<String> MAC_CF_BUNDLE_IDENTIFIER = 136 new StandardBundlerParam<>( 137 I18N.getString("param.cfbundle-identifier.name"), 138 I18N.getString("param.cfbundle-identifier.description"), 139 "mac.CFBundleIdentifier", 140 String.class, 141 IDENTIFIER::fetchFrom, 142 (s, p) -> s); 143 144 public static final BundlerParamInfo<String> MAC_CF_BUNDLE_VERSION = 145 new StandardBundlerParam<>( 146 I18N.getString("param.cfbundle-version.name"), 147 I18N.getString("param.cfbundle-version.description"), 148 "mac.CFBundleVersion", 149 String.class, 150 p -> { 151 String s = VERSION.fetchFrom(p); 152 if (validCFBundleVersion(s)) { 153 return s; 154 } else { 155 return "100"; 156 } 157 }, 158 (s, p) -> s); 159 160 public static final BundlerParamInfo<File> CONFIG_ROOT = new StandardBundlerParam<>( 161 I18N.getString("param.config-root.name"), 162 I18N.getString("param.config-root.description"), 163 "configRoot", 164 File.class, 165 params -> { 166 File configRoot = new File(BUILD_ROOT.fetchFrom(params), "macosx"); 167 configRoot.mkdirs(); 168 return configRoot; 169 }, 170 (s, p) -> new File(s)); 171 172 public static final BundlerParamInfo<String> DEFAULT_ICNS_ICON = new StandardBundlerParam<>( 173 I18N.getString("param.default-icon-icns"), 174 I18N.getString("param.default-icon-icns.description"), 175 ".mac.default.icns", 176 String.class, 177 params -> TEMPLATE_BUNDLE_ICON, 178 (s, p) -> s); 179 180 public static final BundlerParamInfo<String> DEVELOPER_ID_APP_SIGNING_KEY = new StandardBundlerParam<>( 181 I18N.getString("param.signing-key-developer-id-app.name"), 182 I18N.getString("param.signing-key-developer-id-app.description"), 183 "mac.signing-key-developer-id-app", 184 String.class, 185 params -> MacBaseInstallerBundler.findKey("Developer ID Application: " + SIGNING_KEY_USER.fetchFrom(params), SIGNING_KEYCHAIN.fetchFrom(params), VERBOSE.fetchFrom(params)), 186 (s, p) -> s); 187 188 public static final BundlerParamInfo<String> BUNDLE_ID_SIGNING_PREFIX = new StandardBundlerParam<>( 189 I18N.getString("param.bundle-id-signing-prefix.name"), 190 I18N.getString("param.bundle-id-signing-prefix.description"), 191 "mac.bundle-id-signing-prefix", 192 String.class, 193 params -> IDENTIFIER.fetchFrom(params) + ".", 194 (s, p) -> s); 195 196 public static final BundlerParamInfo<File> ICON_ICNS = new StandardBundlerParam<>( 197 I18N.getString("param.icon-icns.name"), 198 I18N.getString("param.icon-icns.description"), 199 "icon.icns", 200 File.class, 201 params -> { 202 File f = ICON.fetchFrom(params); 203 if (f != null && !f.getName().toLowerCase().endsWith(".icns")) { 204 Log.info(MessageFormat.format(I18N.getString("message.icon-not-icns"), f)); 205 return null; 206 } 207 return f; 208 }, 209 (s, p) -> new File(s)); 210 211 public MacAppBundler() { 212 super(); 213 baseResourceLoader = MacResources.class; 214 } 215 216 public static boolean validCFBundleVersion(String v) { 217 // CFBundleVersion (String - iOS, OS X) specifies the build version 218 // number of the bundle, which identifies an iteration (released or 219 // unreleased) of the bundle. The build version number should be a 220 // string comprised of three non-negative, period-separated integers 221 // with the first integer being greater than zero. The string should 222 // only contain numeric (0-9) and period (.) characters. Leading zeros 223 // are truncated from each integer and will be ignored (that is, 224 // 1.02.3 is equivalent to 1.2.3). This key is not localizable. 225 226 if (v == null) { 227 return false; 228 } 229 230 String p[] = v.split("\\."); 231 if (p.length > 3 || p.length < 1) { 232 Log.verbose(I18N.getString("message.version-string-too-many-components")); 233 return false; 234 } 235 236 try { 237 BigInteger n = new BigInteger(p[0]); 238 if (BigInteger.ONE.compareTo(n) > 0) { 239 Log.verbose(I18N.getString("message.version-string-first-number-not-zero")); 240 return false; 241 } 242 if (p.length > 1) { 243 n = new BigInteger(p[1]); 244 if (BigInteger.ZERO.compareTo(n) > 0) { 245 Log.verbose(I18N.getString("message.version-string-no-negative-numbers")); 246 return false; 247 } 248 } 249 if (p.length > 2) { 250 n = new BigInteger(p[2]); 251 if (BigInteger.ZERO.compareTo(n) > 0) { 252 Log.verbose(I18N.getString("message.version-string-no-negative-numbers")); 253 return false; 254 } 255 } 256 } catch (NumberFormatException ne) { 257 Log.verbose(I18N.getString("message.version-string-numbers-only")); 258 Log.verbose(ne); 259 return false; 260 } 261 262 return true; 263 } 264 265 @Override 266 public boolean validate(Map<String, ? super Object> params) throws UnsupportedPlatformException, ConfigException { 267 try { 268 return doValidate(params); 269 } catch (RuntimeException re) { 270 if (re.getCause() instanceof ConfigException) { 271 throw (ConfigException) re.getCause(); 272 } else { 273 throw new ConfigException(re); 274 } 275 } 276 } 277 278 //to be used by chained bundlers, e.g. by EXE bundler to avoid 279 // skipping validation if p.type does not include "image" 280 public boolean doValidate(Map<String, ? super Object> p) throws UnsupportedPlatformException, ConfigException { 281 if (Platform.getPlatform() != Platform.MAC) { 282 throw new UnsupportedPlatformException(); 283 } 284 285 imageBundleValidation(p); 286 287 if (getPredefinedImage(p) != null) { 288 return true; 289 } 290 291 //TODO warn if MAC_RUNTIME is set 292 293 // validate short version 294 if (!validCFBundleVersion(MAC_CF_BUNDLE_VERSION.fetchFrom(p))) { 295 throw new ConfigException( 296 I18N.getString("error.invalid-cfbundle-version"), 297 I18N.getString("error.invalid-cfbundle-version.advice")); 298 } 299 300 // reject explicitly set sign to true and no valid signature key 301 if (Optional.ofNullable(SIGN_BUNDLE.fetchFrom(p)).orElse(Boolean.FALSE)) { 302 String signingIdentity = DEVELOPER_ID_APP_SIGNING_KEY.fetchFrom(p); 303 if (signingIdentity == null) { 304 throw new ConfigException( 305 I18N.getString("error.explicit-sign-no-cert"), 306 I18N.getString("error.explicit-sign-no-cert.advice")); 307 } 308 } 309 310 return true; 311 } 312 313 private File getConfig_InfoPlist(Map<String, ? super Object> params) { 314 return new File(CONFIG_ROOT.fetchFrom(params), "Info.plist"); 315 } 316 317 private File getConfig_Icon(Map<String, ? super Object> params) { 318 return new File(CONFIG_ROOT.fetchFrom(params), APP_NAME.fetchFrom(params) + ".icns"); 319 } 320 321 322 File doBundle(Map<String, ? super Object> p, File outputDirectory, boolean dependentTask) { 323 try { 324 if (!outputDirectory.isDirectory() && !outputDirectory.mkdirs()) { 325 throw new RuntimeException(MessageFormat.format(I18N.getString("error.cannot-create-output-dir"), outputDirectory.getAbsolutePath())); 326 } 327 if (!outputDirectory.canWrite()) { 328 throw new RuntimeException(MessageFormat.format(I18N.getString("error.cannot-write-to-output-dir"), outputDirectory.getAbsolutePath())); 329 } 330 331 // Create directory structure 332 File rootDirectory = new File(outputDirectory, APP_NAME.fetchFrom(p) + ".app"); 333 IOUtils.deleteRecursive(rootDirectory); 334 rootDirectory.mkdirs(); 335 336 if (!dependentTask) { 337 Log.info(MessageFormat.format(I18N.getString("message.creating-app-bundle"), rootDirectory.getAbsolutePath())); 338 } 339 340 if (!p.containsKey(JLinkBundlerHelper.JLINK_BUILDER.getID())) { 341 p.put(JLinkBundlerHelper.JLINK_BUILDER.getID(), "macapp-image-builder"); 342 } 343 344 AbstractAppImageBuilder appBuilder = new MacAppImageBuilder(p, outputDirectory.toPath()); 345 JLinkBundlerHelper.execute(p, appBuilder); 346 return rootDirectory; 347 } catch (IOException ex) { 348 Log.info(ex.toString()); 349 Log.verbose(ex); 350 return null; 351 } catch (Exception ex) { 352 Log.info("Exception: "+ex); 353 Log.debug(ex); 354 return null; 355 } 356 } 357 358 public void cleanupConfigFiles(Map<String, ? super Object> params) { 359 //Since building the app can be bypassed, make sure configRoot was set 360 if (CONFIG_ROOT.fetchFrom(params) != null) { 361 getConfig_Icon(params).delete(); 362 getConfig_InfoPlist(params).delete(); 363 } 364 } 365 366 ////////////////////////////////////////////////////////////////////////////////// 367 // Implement Bundler 368 ////////////////////////////////////////////////////////////////////////////////// 369 370 @Override 371 public String getName() { 372 return I18N.getString("bundler.name"); 373 } 374 375 @Override 376 public String getDescription() { 377 return I18N.getString("bundler.description"); 378 } 379 380 @Override 381 public String getID() { 382 return "mac.app"; 383 } 384 385 @Override 386 public String getBundleType() { 387 return "IMAGE"; 388 } 389 390 @Override 391 public Collection<BundlerParamInfo<?>> getBundleParameters() { 392 return getAppBundleParameters(); 393 } 394 395 public static Collection<BundlerParamInfo<?>> getAppBundleParameters() { 396 return Arrays.asList( 397 APP_NAME, 398 APP_RESOURCES, 399 ARGUMENTS, 400 BUNDLE_ID_SIGNING_PREFIX, 401 CLASSPATH, 402 DEVELOPER_ID_APP_SIGNING_KEY, 403 ICON_ICNS, 404 JVM_OPTIONS, 405 JVM_PROPERTIES, 406 MAC_CATEGORY, 407 MAC_CF_BUNDLE_IDENTIFIER, 408 MAC_CF_BUNDLE_NAME, 409 MAC_CF_BUNDLE_VERSION, 410 MAIN_CLASS, 411 MAIN_JAR, 412 PREFERENCES_ID, 413 PRELOADER_CLASS, 414 SIGNING_KEYCHAIN, 415 USER_JVM_OPTIONS, 416 VERSION 417 ); 418 } 419 420 421 @Override 422 public File execute(Map<String, ? super Object> params, File outputParentDir) { 423 return doBundle(params, outputParentDir, false); 424 } 425 426 // private void createLauncherForEntryPoint(Map<String, ? super Object> p, File rootDirectory) throws IOException { 427 // prepareConfigFiles(p); 428 // 429 // if (LAUNCHER_CFG_FORMAT.fetchFrom(p).equals(CFG_FORMAT_PROPERTIES)) { 430 // writeCfgFile(p, rootDirectory); 431 // } else { 432 // writeCfgFile(p, new File(rootDirectory, getLauncherCfgName(p)), "$APPDIR/PlugIns/Java.runtime"); 433 // } 434 // 435 // // Copy executable root folder 436 // File executableFile = new File(rootDirectory, "Contents/MacOS/" + getLauncherName(p)); 437 // IOUtils.copyFromURL( 438 // RAW_EXECUTABLE_URL.fetchFrom(p), 439 // executableFile); 440 // executableFile.setExecutable(true, false); 441 // 442 // } 443 // 444 445 }