1 /* 2 * Copyright (c) 2012, 2015, 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.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"); 82 map.put("Travel", "public.app-category.travel"); 83 map.put("Utilities", "public.app-category.utilities"); 84 map.put("Video", "public.app-category.video"); 85 map.put("Weather", "public.app-category.weather"); 86 87 map.put("Action Games", "public.app-category.action-games"); 88 map.put("Adventure Games", "public.app-category.adventure-games"); 89 map.put("Arcade Games", "public.app-category.arcade-games"); 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, 130 params -> null, 131 (s, p) -> s); 132 133 public static final BundlerParamInfo<String> MAC_CF_BUNDLE_IDENTIFIER = 134 new StandardBundlerParam<>( 135 I18N.getString("param.cfbundle-identifier.name"), 136 I18N.getString("param.cfbundle-identifier.description"), 137 "mac.CFBundleIdentifier", 138 String.class, 139 IDENTIFIER::fetchFrom, 140 (s, p) -> s); 141 142 public static final BundlerParamInfo<String> MAC_CF_BUNDLE_VERSION = 143 new StandardBundlerParam<>( 144 I18N.getString("param.cfbundle-version.name"), 145 I18N.getString("param.cfbundle-version.description"), 146 "mac.CFBundleVersion", 147 String.class, 148 p -> { 149 String s = VERSION.fetchFrom(p); 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 234 try { 235 BigInteger n = new BigInteger(p[0]); 236 if (BigInteger.ONE.compareTo(n) > 0) { 237 Log.verbose(I18N.getString("message.version-string-first-number-not-zero")); 238 return false; 239 } 240 if (p.length > 1) { 241 n = new BigInteger(p[1]); 242 if (BigInteger.ZERO.compareTo(n) > 0) { 243 Log.verbose(I18N.getString("message.version-string-no-negative-numbers")); 244 return false; 245 } 246 } 247 if (p.length > 2) { 248 n = new BigInteger(p[2]); 249 if (BigInteger.ZERO.compareTo(n) > 0) { 250 Log.verbose(I18N.getString("message.version-string-no-negative-numbers")); 251 return false; 252 } 253 } 254 } catch (NumberFormatException ne) { 255 Log.verbose(I18N.getString("message.version-string-numbers-only")); 256 Log.verbose(ne); 257 return false; 258 } 259 260 return true; 261 } 262 263 @Override 264 public boolean validate(Map<String, ? super Object> params) throws UnsupportedPlatformException, ConfigException { 265 try { 266 return doValidate(params); 267 } catch (RuntimeException re) { 268 if (re.getCause() instanceof ConfigException) { 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 380 public String getBundleType() { 381 return "IMAGE"; 382 } 383 384 @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 }