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