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