1 /* 2 * Copyright (c) 2012, 2019, 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 26 package jdk.jpackage.internal; 27 28 import java.io.File; 29 import java.io.IOException; 30 import java.math.BigInteger; 31 import java.text.MessageFormat; 32 import java.util.Arrays; 33 import java.util.Collection; 34 import java.util.HashMap; 35 import java.util.Map; 36 import java.util.Optional; 37 import java.util.ResourceBundle; 38 39 import static jdk.jpackage.internal.StandardBundlerParam.*; 40 import static jdk.jpackage.internal.MacBaseInstallerBundler.*; 41 import jdk.jpackage.internal.AbstractAppImageBuilder; 42 43 public class MacAppBundler extends AbstractImageBundler { 44 45 private static final ResourceBundle I18N = ResourceBundle.getBundle( 46 "jdk.jpackage.internal.resources.MacResources"); 47 48 private static final String TEMPLATE_BUNDLE_ICON = "GenericApp.icns"; 49 50 private static Map<String, String> getMacCategories() { 51 Map<String, String> map = new HashMap<>(); 52 map.put("Business", "public.app-category.business"); 53 map.put("Developer Tools", "public.app-category.developer-tools"); 54 map.put("Education", "public.app-category.education"); 55 map.put("Entertainment", "public.app-category.entertainment"); 56 map.put("Finance", "public.app-category.finance"); 57 map.put("Games", "public.app-category.games"); 58 map.put("Graphics & Design", "public.app-category.graphics-design"); 59 map.put("Healthcare & Fitness", 60 "public.app-category.healthcare-fitness"); 61 map.put("Lifestyle", "public.app-category.lifestyle"); 62 map.put("Medical", "public.app-category.medical"); 63 map.put("Music", "public.app-category.music"); 64 map.put("News", "public.app-category.news"); 65 map.put("Photography", "public.app-category.photography"); 66 map.put("Productivity", "public.app-category.productivity"); 67 map.put("Reference", "public.app-category.reference"); 68 map.put("Social Networking", "public.app-category.social-networking"); 69 map.put("Sports", "public.app-category.sports"); 70 map.put("Travel", "public.app-category.travel"); 71 map.put("Utilities", "public.app-category.utilities"); 72 map.put("Video", "public.app-category.video"); 73 map.put("Weather", "public.app-category.weather"); 74 75 map.put("Action Games", "public.app-category.action-games"); 76 map.put("Adventure Games", "public.app-category.adventure-games"); 77 map.put("Arcade Games", "public.app-category.arcade-games"); 78 map.put("Board Games", "public.app-category.board-games"); 79 map.put("Card Games", "public.app-category.card-games"); 80 map.put("Casino Games", "public.app-category.casino-games"); 81 map.put("Dice Games", "public.app-category.dice-games"); 82 map.put("Educational Games", "public.app-category.educational-games"); 83 map.put("Family Games", "public.app-category.family-games"); 84 map.put("Kids Games", "public.app-category.kids-games"); 85 map.put("Music Games", "public.app-category.music-games"); 86 map.put("Puzzle Games", "public.app-category.puzzle-games"); 87 map.put("Racing Games", "public.app-category.racing-games"); 88 map.put("Role Playing Games", "public.app-category.role-playing-games"); 89 map.put("Simulation Games", "public.app-category.simulation-games"); 90 map.put("Sports Games", "public.app-category.sports-games"); 91 map.put("Strategy Games", "public.app-category.strategy-games"); 92 map.put("Trivia Games", "public.app-category.trivia-games"); 93 map.put("Word Games", "public.app-category.word-games"); 94 95 return map; 96 } 97 98 public static final EnumeratedBundlerParam<String> MAC_CATEGORY = 99 new EnumeratedBundlerParam<>( 100 I18N.getString("param.category-name"), 101 I18N.getString("param.category-name.description"), 102 Arguments.CLIOptions.MAC_APP_STORE_CATEGORY.getId(), 103 String.class, 104 params -> params.containsKey(CATEGORY.getID()) 105 ? CATEGORY.fetchFrom(params) 106 : "Unknown", 107 (s, p) -> s, 108 getMacCategories(), 109 false //strict - for MacStoreBundler this should be strict 110 ); 111 112 public static final BundlerParamInfo<String> MAC_CF_BUNDLE_NAME = 113 new StandardBundlerParam<>( 114 I18N.getString("param.cfbundle-name.name"), 115 I18N.getString("param.cfbundle-name.description"), 116 Arguments.CLIOptions.MAC_BUNDLE_NAME.getId(), 117 String.class, 118 params -> null, 119 (s, p) -> s); 120 121 public static final BundlerParamInfo<String> MAC_CF_BUNDLE_IDENTIFIER = 122 new StandardBundlerParam<>( 123 I18N.getString("param.cfbundle-identifier.name"), 124 I18N.getString("param.cfbundle-identifier.description"), 125 Arguments.CLIOptions.MAC_BUNDLE_IDENTIFIER.getId(), 126 String.class, 127 IDENTIFIER::fetchFrom, 128 (s, p) -> s); 129 130 public static final BundlerParamInfo<String> MAC_CF_BUNDLE_VERSION = 131 new StandardBundlerParam<>( 132 I18N.getString("param.cfbundle-version.name"), 133 I18N.getString("param.cfbundle-version.description"), 134 "mac.CFBundleVersion", 135 String.class, 136 p -> { 137 String s = VERSION.fetchFrom(p); 138 if (validCFBundleVersion(s)) { 139 return s; 140 } else { 141 return "100"; 142 } 143 }, 144 (s, p) -> s); 145 146 public static final BundlerParamInfo<String> DEFAULT_ICNS_ICON = 147 new StandardBundlerParam<>( 148 I18N.getString("param.default-icon-icns"), 149 I18N.getString("param.default-icon-icns.description"), 150 ".mac.default.icns", 151 String.class, 152 params -> TEMPLATE_BUNDLE_ICON, 153 (s, p) -> s); 154 155 public static final BundlerParamInfo<String> DEVELOPER_ID_APP_SIGNING_KEY = 156 new StandardBundlerParam<>( 157 I18N.getString("param.signing-key-developer-id-app.name"), 158 I18N.getString("param.signing-key-developer-id-app.description"), 159 "mac.signing-key-developer-id-app", 160 String.class, 161 params -> { 162 String result = MacBaseInstallerBundler.findKey( 163 "Developer ID Application: " 164 + SIGNING_KEY_USER.fetchFrom(params), 165 SIGNING_KEYCHAIN.fetchFrom(params), 166 VERBOSE.fetchFrom(params)); 167 if (result != null) { 168 MacCertificate certificate = new MacCertificate(result, 169 VERBOSE.fetchFrom(params)); 170 171 if (!certificate.isValid()) { 172 Log.error(MessageFormat.format(I18N.getString( 173 "error.certificate.expired"), result)); 174 } 175 } 176 177 return result; 178 }, 179 (s, p) -> s); 180 181 public static final BundlerParamInfo<String> BUNDLE_ID_SIGNING_PREFIX = 182 new StandardBundlerParam<>( 183 I18N.getString("param.bundle-id-signing-prefix.name"), 184 I18N.getString("param.bundle-id-signing-prefix.description"), 185 Arguments.CLIOptions.MAC_BUNDLE_SIGNING_PREFIX.getId(), 186 String.class, 187 params -> IDENTIFIER.fetchFrom(params) + ".", 188 (s, p) -> s); 189 190 public static final BundlerParamInfo<File> ICON_ICNS = 191 new StandardBundlerParam<>( 192 I18N.getString("param.icon-icns.name"), 193 I18N.getString("param.icon-icns.description"), 194 "icon.icns", 195 File.class, 196 params -> { 197 File f = ICON.fetchFrom(params); 198 if (f != null && !f.getName().toLowerCase().endsWith(".icns")) { 199 Log.error(MessageFormat.format( 200 I18N.getString("message.icon-not-icns"), f)); 201 return null; 202 } 203 return f; 204 }, 205 (s, p) -> new File(s)); 206 207 public static boolean validCFBundleVersion(String v) { 208 // CFBundleVersion (String - iOS, OS X) specifies the build version 209 // number of the bundle, which identifies an iteration (released or 210 // unreleased) of the bundle. The build version number should be a 211 // string comprised of three non-negative, period-separated integers 212 // with the first integer being greater than zero. The string should 213 // only contain numeric (0-9) and period (.) characters. Leading zeros 214 // are truncated from each integer and will be ignored (that is, 215 // 1.02.3 is equivalent to 1.2.3). This key is not localizable. 216 217 if (v == null) { 218 return false; 219 } 220 221 String p[] = v.split("\\."); 222 if (p.length > 3 || p.length < 1) { 223 Log.verbose(I18N.getString( 224 "message.version-string-too-many-components")); 225 return false; 226 } 227 228 try { 229 BigInteger n = new BigInteger(p[0]); 230 if (BigInteger.ONE.compareTo(n) > 0) { 231 Log.verbose(I18N.getString( 232 "message.version-string-first-number-not-zero")); 233 return false; 234 } 235 if (p.length > 1) { 236 n = new BigInteger(p[1]); 237 if (BigInteger.ZERO.compareTo(n) > 0) { 238 Log.verbose(I18N.getString( 239 "message.version-string-no-negative-numbers")); 240 return false; 241 } 242 } 243 if (p.length > 2) { 244 n = new BigInteger(p[2]); 245 if (BigInteger.ZERO.compareTo(n) > 0) { 246 Log.verbose(I18N.getString( 247 "message.version-string-no-negative-numbers")); 248 return false; 249 } 250 } 251 } catch (NumberFormatException ne) { 252 Log.verbose(I18N.getString("message.version-string-numbers-only")); 253 Log.verbose(ne); 254 return false; 255 } 256 257 return true; 258 } 259 260 @Override 261 public boolean validate(Map<String, ? super Object> params) 262 throws UnsupportedPlatformException, ConfigException { 263 try { 264 return doValidate(params); 265 } catch (RuntimeException re) { 266 if (re.getCause() instanceof ConfigException) { 267 throw (ConfigException) re.getCause(); 268 } else { 269 throw new ConfigException(re); 270 } 271 } 272 } 273 274 private boolean doValidate(Map<String, ? super Object> p) 275 throws UnsupportedPlatformException, ConfigException { 276 if (Platform.getPlatform() != Platform.MAC) { 277 throw new UnsupportedPlatformException(); 278 } 279 280 imageBundleValidation(p); 281 282 if (StandardBundlerParam.getPredefinedAppImage(p) != null) { 283 return true; 284 } 285 286 // validate short version 287 if (!validCFBundleVersion(MAC_CF_BUNDLE_VERSION.fetchFrom(p))) { 288 throw new ConfigException( 289 I18N.getString("error.invalid-cfbundle-version"), 290 I18N.getString("error.invalid-cfbundle-version.advice")); 291 } 292 293 // reject explicitly set sign to true and no valid signature key 294 if (Optional.ofNullable(MacAppImageBuilder. 295 SIGN_BUNDLE.fetchFrom(p)).orElse(Boolean.FALSE)) { 296 String signingIdentity = DEVELOPER_ID_APP_SIGNING_KEY.fetchFrom(p); 297 if (signingIdentity == null) { 298 throw new ConfigException( 299 I18N.getString("error.explicit-sign-no-cert"), 300 I18N.getString("error.explicit-sign-no-cert.advice")); 301 } 302 } 303 304 return true; 305 } 306 307 File doBundle(Map<String, ? super Object> p, File outputDirectory, 308 boolean dependentTask) throws PackagerException { 309 if (RUNTIME_INSTALLER.fetchFrom(p)) { 310 return doJreBundle(p, outputDirectory, dependentTask); 311 } else { 312 return doAppBundle(p, outputDirectory, dependentTask); 313 } 314 } 315 316 File doJreBundle(Map<String, ? super Object> p, File outputDirectory, 317 boolean dependentTask) throws PackagerException { 318 try { 319 File rootDirectory = createRoot(p, outputDirectory, dependentTask, 320 APP_NAME.fetchFrom(p), "macapp-image-builder"); 321 AbstractAppImageBuilder appBuilder = new MacAppImageBuilder(p, 322 APP_NAME.fetchFrom(p), outputDirectory.toPath()); 323 File predefined = PREDEFINED_RUNTIME_IMAGE.fetchFrom(p); 324 if (predefined == null ) { 325 JLinkBundlerHelper.generateJre(p, appBuilder); 326 } else { 327 return predefined; 328 } 329 return rootDirectory; 330 } catch (PackagerException pe) { 331 throw pe; 332 } catch (Exception ex) { 333 Log.verbose(ex); 334 throw new PackagerException(ex); 335 } 336 } 337 338 File doAppBundle(Map<String, ? super Object> p, File outputDirectory, 339 boolean dependentTask) throws PackagerException { 340 try { 341 File rootDirectory = createRoot(p, outputDirectory, dependentTask, 342 APP_NAME.fetchFrom(p) + ".app", "macapp-image-builder"); 343 AbstractAppImageBuilder appBuilder = 344 new MacAppImageBuilder(p, outputDirectory.toPath()); 345 if (PREDEFINED_RUNTIME_IMAGE.fetchFrom(p) == null ) { 346 JLinkBundlerHelper.execute(p, appBuilder); 347 } else { 348 StandardBundlerParam.copyPredefinedRuntimeImage(p, appBuilder); 349 } 350 return rootDirectory; 351 } catch (PackagerException pe) { 352 throw pe; 353 } catch (Exception ex) { 354 Log.verbose(ex); 355 throw new PackagerException(ex); 356 } 357 } 358 359 ///////////////////////////////////////////////////////////////////////// 360 // Implement Bundler 361 ///////////////////////////////////////////////////////////////////////// 362 363 @Override 364 public String getName() { 365 return I18N.getString("app.bundler.name"); 366 } 367 368 @Override 369 public String getDescription() { 370 return I18N.getString("app.bundler.description"); 371 } 372 373 @Override 374 public String getID() { 375 return "mac.app"; 376 } 377 378 @Override 379 public String getBundleType() { 380 return "IMAGE"; 381 } 382 383 @Override 384 public Collection<BundlerParamInfo<?>> getBundleParameters() { 385 return getAppBundleParameters(); 386 } 387 388 public static Collection<BundlerParamInfo<?>> getAppBundleParameters() { 389 return Arrays.asList( 390 APP_NAME, 391 APP_RESOURCES, 392 ARGUMENTS, 393 BUNDLE_ID_SIGNING_PREFIX, 394 CLASSPATH, 395 DEVELOPER_ID_APP_SIGNING_KEY, 396 ICON_ICNS, 397 JVM_OPTIONS, 398 MAC_CATEGORY, 399 MAC_CF_BUNDLE_IDENTIFIER, 400 MAC_CF_BUNDLE_NAME, 401 MAC_CF_BUNDLE_VERSION, 402 MAIN_CLASS, 403 MAIN_JAR, 404 PREFERENCES_ID, 405 SIGNING_KEYCHAIN, 406 VERSION, 407 VERBOSE 408 ); 409 } 410 411 412 @Override 413 public File execute(Map<String, ? super Object> params, 414 File outputParentDir) throws PackagerException { 415 return doBundle(params, outputParentDir, false); 416 } 417 418 @Override 419 public boolean supported(boolean runtimeInstaller) { 420 return Platform.getPlatform() == Platform.MAC; 421 } 422 423 }