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.incubator.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.HashMap; 33 import java.util.Map; 34 import java.util.Optional; 35 import java.util.ResourceBundle; 36 37 import static jdk.incubator.jpackage.internal.StandardBundlerParam.*; 38 import static jdk.incubator.jpackage.internal.MacBaseInstallerBundler.*; 39 40 public class MacAppBundler extends AbstractImageBundler { 41 42 private static final ResourceBundle I18N = ResourceBundle.getBundle( 43 "jdk.incubator.jpackage.internal.resources.MacResources"); 44 45 private static final String TEMPLATE_BUNDLE_ICON = "java.icns"; 46 47 public static final BundlerParamInfo<String> MAC_CF_BUNDLE_NAME = 48 new StandardBundlerParam<>( 49 Arguments.CLIOptions.MAC_BUNDLE_NAME.getId(), 50 String.class, 51 params -> null, 52 (s, p) -> s); 53 54 public static final BundlerParamInfo<String> MAC_CF_BUNDLE_VERSION = 55 new StandardBundlerParam<>( 56 "mac.CFBundleVersion", 57 String.class, 58 p -> { 59 String s = VERSION.fetchFrom(p); 60 if (validCFBundleVersion(s)) { 61 return s; 62 } else { 63 return "100"; 64 } 65 }, 66 (s, p) -> s); 67 68 public static final BundlerParamInfo<String> DEFAULT_ICNS_ICON = 69 new StandardBundlerParam<>( 70 ".mac.default.icns", 71 String.class, 72 params -> TEMPLATE_BUNDLE_ICON, 73 (s, p) -> s); 74 75 public static final BundlerParamInfo<String> DEVELOPER_ID_APP_SIGNING_KEY = 76 new StandardBundlerParam<>( 77 "mac.signing-key-developer-id-app", 78 String.class, 79 params -> { 80 String result = MacBaseInstallerBundler.findKey( 81 "Developer ID Application: " 82 + SIGNING_KEY_USER.fetchFrom(params), 83 SIGNING_KEYCHAIN.fetchFrom(params), 84 VERBOSE.fetchFrom(params)); 85 if (result != null) { 86 MacCertificate certificate = new MacCertificate(result); 87 88 if (!certificate.isValid()) { 89 Log.error(MessageFormat.format(I18N.getString( 90 "error.certificate.expired"), result)); 91 } 92 } 93 94 return result; 95 }, 96 (s, p) -> s); 97 98 public static final BundlerParamInfo<String> BUNDLE_ID_SIGNING_PREFIX = 99 new StandardBundlerParam<>( 100 Arguments.CLIOptions.MAC_BUNDLE_SIGNING_PREFIX.getId(), 101 String.class, 102 params -> IDENTIFIER.fetchFrom(params) + ".", 103 (s, p) -> s); 104 105 public static final BundlerParamInfo<File> ICON_ICNS = 106 new StandardBundlerParam<>( 107 "icon.icns", 108 File.class, 109 params -> { 110 File f = ICON.fetchFrom(params); 111 if (f != null && !f.getName().toLowerCase().endsWith(".icns")) { 112 Log.error(MessageFormat.format( 113 I18N.getString("message.icon-not-icns"), f)); 114 return null; 115 } 116 return f; 117 }, 118 (s, p) -> new File(s)); 119 120 public static boolean validCFBundleVersion(String v) { 121 // CFBundleVersion (String - iOS, OS X) specifies the build version 122 // number of the bundle, which identifies an iteration (released or 123 // unreleased) of the bundle. The build version number should be a 124 // string comprised of three non-negative, period-separated integers 125 // with the first integer being greater than zero. The string should 126 // only contain numeric (0-9) and period (.) characters. Leading zeros 127 // are truncated from each integer and will be ignored (that is, 128 // 1.02.3 is equivalent to 1.2.3). This key is not localizable. 129 130 if (v == null) { 131 return false; 132 } 133 134 String p[] = v.split("\\."); 135 if (p.length > 3 || p.length < 1) { 136 Log.verbose(I18N.getString( 137 "message.version-string-too-many-components")); 138 return false; 139 } 140 141 try { 142 BigInteger n = new BigInteger(p[0]); 143 if (BigInteger.ONE.compareTo(n) > 0) { 144 Log.verbose(I18N.getString( 145 "message.version-string-first-number-not-zero")); 146 return false; 147 } 148 if (p.length > 1) { 149 n = new BigInteger(p[1]); 150 if (BigInteger.ZERO.compareTo(n) > 0) { 151 Log.verbose(I18N.getString( 152 "message.version-string-no-negative-numbers")); 153 return false; 154 } 155 } 156 if (p.length > 2) { 157 n = new BigInteger(p[2]); 158 if (BigInteger.ZERO.compareTo(n) > 0) { 159 Log.verbose(I18N.getString( 160 "message.version-string-no-negative-numbers")); 161 return false; 162 } 163 } 164 } catch (NumberFormatException ne) { 165 Log.verbose(I18N.getString("message.version-string-numbers-only")); 166 Log.verbose(ne); 167 return false; 168 } 169 170 return true; 171 } 172 173 @Override 174 public boolean validate(Map<String, ? super Object> params) 175 throws ConfigException { 176 try { 177 return doValidate(params); 178 } catch (RuntimeException re) { 179 if (re.getCause() instanceof ConfigException) { 180 throw (ConfigException) re.getCause(); 181 } else { 182 throw new ConfigException(re); 183 } 184 } 185 } 186 187 private boolean doValidate(Map<String, ? super Object> params) 188 throws ConfigException { 189 190 imageBundleValidation(params); 191 192 if (StandardBundlerParam.getPredefinedAppImage(params) != null) { 193 return true; 194 } 195 196 // validate short version 197 if (!validCFBundleVersion(MAC_CF_BUNDLE_VERSION.fetchFrom(params))) { 198 throw new ConfigException( 199 I18N.getString("error.invalid-cfbundle-version"), 200 I18N.getString("error.invalid-cfbundle-version.advice")); 201 } 202 203 // reject explicitly set sign to true and no valid signature key 204 if (Optional.ofNullable(MacAppImageBuilder. 205 SIGN_BUNDLE.fetchFrom(params)).orElse(Boolean.FALSE)) { 206 String signingIdentity = 207 DEVELOPER_ID_APP_SIGNING_KEY.fetchFrom(params); 208 if (signingIdentity == null) { 209 throw new ConfigException( 210 I18N.getString("error.explicit-sign-no-cert"), 211 I18N.getString("error.explicit-sign-no-cert.advice")); 212 } 213 214 // Signing will not work without Xcode with command line developer tools 215 try { 216 ProcessBuilder pb = new ProcessBuilder("xcrun", "--help"); 217 Process p = pb.start(); 218 int code = p.waitFor(); 219 if (code != 0) { 220 throw new ConfigException( 221 I18N.getString("error.no.xcode.signing"), 222 I18N.getString("error.no.xcode.signing.advice")); 223 } 224 } catch (IOException | InterruptedException ex) { 225 throw new ConfigException(ex); 226 } 227 } 228 229 return true; 230 } 231 232 File doBundle(Map<String, ? super Object> params, File outputDirectory, 233 boolean dependentTask) throws PackagerException { 234 if (StandardBundlerParam.isRuntimeInstaller(params)) { 235 return PREDEFINED_RUNTIME_IMAGE.fetchFrom(params); 236 } else { 237 return doAppBundle(params, outputDirectory, dependentTask); 238 } 239 } 240 241 File doAppBundle(Map<String, ? super Object> params, File outputDirectory, 242 boolean dependentTask) throws PackagerException { 243 try { 244 File rootDirectory = createRoot(params, outputDirectory, 245 dependentTask, APP_NAME.fetchFrom(params) + ".app"); 246 AbstractAppImageBuilder appBuilder = 247 new MacAppImageBuilder(params, outputDirectory.toPath()); 248 if (PREDEFINED_RUNTIME_IMAGE.fetchFrom(params) == null ) { 249 JLinkBundlerHelper.execute(params, appBuilder); 250 } else { 251 StandardBundlerParam.copyPredefinedRuntimeImage( 252 params, appBuilder); 253 } 254 return rootDirectory; 255 } catch (PackagerException pe) { 256 throw pe; 257 } catch (Exception ex) { 258 Log.verbose(ex); 259 throw new PackagerException(ex); 260 } 261 } 262 263 ///////////////////////////////////////////////////////////////////////// 264 // Implement Bundler 265 ///////////////////////////////////////////////////////////////////////// 266 267 @Override 268 public String getName() { 269 return I18N.getString("app.bundler.name"); 270 } 271 272 @Override 273 public String getID() { 274 return "mac.app"; 275 } 276 277 @Override 278 public String getBundleType() { 279 return "IMAGE"; 280 } 281 282 @Override 283 public File execute(Map<String, ? super Object> params, 284 File outputParentDir) throws PackagerException { 285 return doBundle(params, outputParentDir, false); 286 } 287 288 @Override 289 public boolean supported(boolean runtimeInstaller) { 290 return true; 291 } 292 293 @Override 294 public boolean isDefault() { 295 return false; 296 } 297 298 }