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 Arguments.CLIOptions.MAC_APP_STORE_CATEGORY.getId(), 101 String.class, 102 params -> params.containsKey(CATEGORY.getID()) 103 ? CATEGORY.fetchFrom(params) 104 : "Unknown", 105 (s, p) -> s, 106 getMacCategories(), 107 false //strict - for MacStoreBundler this should be strict 108 ); 109 110 public static final BundlerParamInfo<String> MAC_CF_BUNDLE_NAME = 111 new StandardBundlerParam<>( 112 Arguments.CLIOptions.MAC_BUNDLE_NAME.getId(), 113 String.class, 114 params -> null, 115 (s, p) -> s); 116 117 public static final BundlerParamInfo<String> MAC_CF_BUNDLE_IDENTIFIER = 118 new StandardBundlerParam<>( 119 Arguments.CLIOptions.MAC_BUNDLE_IDENTIFIER.getId(), 120 String.class, 121 IDENTIFIER::fetchFrom, 122 (s, p) -> s); 123 124 public static final BundlerParamInfo<String> MAC_CF_BUNDLE_VERSION = 125 new StandardBundlerParam<>( 126 "mac.CFBundleVersion", 127 String.class, 128 p -> { 129 String s = VERSION.fetchFrom(p); 130 if (validCFBundleVersion(s)) { 131 return s; 132 } else { 133 return "100"; 134 } 135 }, 136 (s, p) -> s); 137 138 public static final BundlerParamInfo<String> DEFAULT_ICNS_ICON = 139 new StandardBundlerParam<>( 140 ".mac.default.icns", 141 String.class, 142 params -> TEMPLATE_BUNDLE_ICON, 143 (s, p) -> s); 144 145 public static final BundlerParamInfo<String> DEVELOPER_ID_APP_SIGNING_KEY = 146 new StandardBundlerParam<>( 147 "mac.signing-key-developer-id-app", 148 String.class, 149 params -> { 150 String result = MacBaseInstallerBundler.findKey( 151 "Developer ID Application: " 152 + SIGNING_KEY_USER.fetchFrom(params), 153 SIGNING_KEYCHAIN.fetchFrom(params), 154 VERBOSE.fetchFrom(params)); 155 if (result != null) { 156 MacCertificate certificate = new MacCertificate(result, 157 VERBOSE.fetchFrom(params)); 158 159 if (!certificate.isValid()) { 160 Log.error(MessageFormat.format(I18N.getString( 161 "error.certificate.expired"), result)); 162 } 163 } 164 165 return result; 166 }, 167 (s, p) -> s); 168 169 public static final BundlerParamInfo<String> BUNDLE_ID_SIGNING_PREFIX = 170 new StandardBundlerParam<>( 171 Arguments.CLIOptions.MAC_BUNDLE_SIGNING_PREFIX.getId(), 172 String.class, 173 params -> IDENTIFIER.fetchFrom(params) + ".", 174 (s, p) -> s); 175 176 public static final BundlerParamInfo<File> ICON_ICNS = 177 new StandardBundlerParam<>( 178 "icon.icns", 179 File.class, 180 params -> { 181 File f = ICON.fetchFrom(params); 182 if (f != null && !f.getName().toLowerCase().endsWith(".icns")) { 183 Log.error(MessageFormat.format( 184 I18N.getString("message.icon-not-icns"), f)); 185 return null; 186 } 187 return f; 188 }, 189 (s, p) -> new File(s)); 190 191 public static boolean validCFBundleVersion(String v) { 192 // CFBundleVersion (String - iOS, OS X) specifies the build version 193 // number of the bundle, which identifies an iteration (released or 194 // unreleased) of the bundle. The build version number should be a 195 // string comprised of three non-negative, period-separated integers 196 // with the first integer being greater than zero. The string should 197 // only contain numeric (0-9) and period (.) characters. Leading zeros 198 // are truncated from each integer and will be ignored (that is, 199 // 1.02.3 is equivalent to 1.2.3). This key is not localizable. 200 201 if (v == null) { 202 return false; 203 } 204 205 String p[] = v.split("\\."); 206 if (p.length > 3 || p.length < 1) { 207 Log.verbose(I18N.getString( 208 "message.version-string-too-many-components")); 209 return false; 210 } 211 212 try { 213 BigInteger n = new BigInteger(p[0]); 214 if (BigInteger.ONE.compareTo(n) > 0) { 215 Log.verbose(I18N.getString( 216 "message.version-string-first-number-not-zero")); 217 return false; 218 } 219 if (p.length > 1) { 220 n = new BigInteger(p[1]); 221 if (BigInteger.ZERO.compareTo(n) > 0) { 222 Log.verbose(I18N.getString( 223 "message.version-string-no-negative-numbers")); 224 return false; 225 } 226 } 227 if (p.length > 2) { 228 n = new BigInteger(p[2]); 229 if (BigInteger.ZERO.compareTo(n) > 0) { 230 Log.verbose(I18N.getString( 231 "message.version-string-no-negative-numbers")); 232 return false; 233 } 234 } 235 } catch (NumberFormatException ne) { 236 Log.verbose(I18N.getString("message.version-string-numbers-only")); 237 Log.verbose(ne); 238 return false; 239 } 240 241 return true; 242 } 243 244 @Override 245 public boolean validate(Map<String, ? super Object> params) 246 throws UnsupportedPlatformException, ConfigException { 247 try { 248 return doValidate(params); 249 } catch (RuntimeException re) { 250 if (re.getCause() instanceof ConfigException) { 251 throw (ConfigException) re.getCause(); 252 } else { 253 throw new ConfigException(re); 254 } 255 } 256 } 257 258 private boolean doValidate(Map<String, ? super Object> p) 259 throws UnsupportedPlatformException, ConfigException { 260 if (Platform.getPlatform() != Platform.MAC) { 261 throw new UnsupportedPlatformException(); 262 } 263 264 imageBundleValidation(p); 265 266 if (StandardBundlerParam.getPredefinedAppImage(p) != null) { 267 return true; 268 } 269 270 // validate short version 271 if (!validCFBundleVersion(MAC_CF_BUNDLE_VERSION.fetchFrom(p))) { 272 throw new ConfigException( 273 I18N.getString("error.invalid-cfbundle-version"), 274 I18N.getString("error.invalid-cfbundle-version.advice")); 275 } 276 277 // reject explicitly set sign to true and no valid signature key 278 if (Optional.ofNullable(MacAppImageBuilder. 279 SIGN_BUNDLE.fetchFrom(p)).orElse(Boolean.FALSE)) { 280 String signingIdentity = DEVELOPER_ID_APP_SIGNING_KEY.fetchFrom(p); 281 if (signingIdentity == null) { 282 throw new ConfigException( 283 I18N.getString("error.explicit-sign-no-cert"), 284 I18N.getString("error.explicit-sign-no-cert.advice")); 285 } 286 } 287 288 return true; 289 } 290 291 File doBundle(Map<String, ? super Object> p, File outputDirectory, 292 boolean dependentTask) throws PackagerException { 293 if (StandardBundlerParam.isRuntimeInstaller(p)) { 294 return PREDEFINED_RUNTIME_IMAGE.fetchFrom(p); 295 } else { 296 return doAppBundle(p, outputDirectory, dependentTask); 297 } 298 } 299 300 File doAppBundle(Map<String, ? super Object> p, File outputDirectory, 301 boolean dependentTask) throws PackagerException { 302 try { 303 File rootDirectory = createRoot(p, outputDirectory, dependentTask, 304 APP_NAME.fetchFrom(p) + ".app"); 305 AbstractAppImageBuilder appBuilder = 306 new MacAppImageBuilder(p, outputDirectory.toPath()); 307 if (PREDEFINED_RUNTIME_IMAGE.fetchFrom(p) == null ) { 308 JLinkBundlerHelper.execute(p, appBuilder); 309 } else { 310 StandardBundlerParam.copyPredefinedRuntimeImage(p, appBuilder); 311 } 312 return rootDirectory; 313 } catch (PackagerException pe) { 314 throw pe; 315 } catch (Exception ex) { 316 Log.verbose(ex); 317 throw new PackagerException(ex); 318 } 319 } 320 321 ///////////////////////////////////////////////////////////////////////// 322 // Implement Bundler 323 ///////////////////////////////////////////////////////////////////////// 324 325 @Override 326 public String getName() { 327 return I18N.getString("app.bundler.name"); 328 } 329 330 @Override 331 public String getDescription() { 332 return I18N.getString("app.bundler.description"); 333 } 334 335 @Override 336 public String getID() { 337 return "mac.app"; 338 } 339 340 @Override 341 public String getBundleType() { 342 return "IMAGE"; 343 } 344 345 @Override 346 public Collection<BundlerParamInfo<?>> getBundleParameters() { 347 return getAppBundleParameters(); 348 } 349 350 public static Collection<BundlerParamInfo<?>> getAppBundleParameters() { 351 return Arrays.asList( 352 APP_NAME, 353 APP_RESOURCES, 354 ARGUMENTS, 355 BUNDLE_ID_SIGNING_PREFIX, 356 CLASSPATH, 357 DEVELOPER_ID_APP_SIGNING_KEY, 358 ICON_ICNS, 359 JVM_OPTIONS, 360 MAC_CATEGORY, 361 MAC_CF_BUNDLE_IDENTIFIER, 362 MAC_CF_BUNDLE_NAME, 363 MAC_CF_BUNDLE_VERSION, 364 MAIN_CLASS, 365 MAIN_JAR, 366 PREFERENCES_ID, 367 SIGNING_KEYCHAIN, 368 VERSION, 369 VERBOSE 370 ); 371 } 372 373 374 @Override 375 public File execute(Map<String, ? super Object> params, 376 File outputParentDir) throws PackagerException { 377 return doBundle(params, outputParentDir, false); 378 } 379 380 @Override 381 public boolean supported(boolean runtimeInstaller) { 382 return Platform.getPlatform() == Platform.MAC; 383 } 384 385 }