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 boolean validCFBundleVersion(String v) { 106 // CFBundleVersion (String - iOS, OS X) specifies the build version 107 // number of the bundle, which identifies an iteration (released or 108 // unreleased) of the bundle. The build version number should be a 109 // string comprised of three non-negative, period-separated integers 110 // with the first integer being greater than zero. The string should 111 // only contain numeric (0-9) and period (.) characters. Leading zeros 112 // are truncated from each integer and will be ignored (that is, 113 // 1.02.3 is equivalent to 1.2.3). This key is not localizable. 114 115 if (v == null) { 116 return false; 117 } 118 119 String p[] = v.split("\\."); 120 if (p.length > 3 || p.length < 1) { 121 Log.verbose(I18N.getString( 122 "message.version-string-too-many-components")); 123 return false; 124 } 125 126 try { 127 BigInteger n = new BigInteger(p[0]); 128 if (BigInteger.ONE.compareTo(n) > 0) { 129 Log.verbose(I18N.getString( 130 "message.version-string-first-number-not-zero")); 131 return false; 132 } 133 if (p.length > 1) { 134 n = new BigInteger(p[1]); 135 if (BigInteger.ZERO.compareTo(n) > 0) { 136 Log.verbose(I18N.getString( 137 "message.version-string-no-negative-numbers")); 138 return false; 139 } 140 } 141 if (p.length > 2) { 142 n = new BigInteger(p[2]); 143 if (BigInteger.ZERO.compareTo(n) > 0) { 144 Log.verbose(I18N.getString( 145 "message.version-string-no-negative-numbers")); 146 return false; 147 } 148 } 149 } catch (NumberFormatException ne) { 150 Log.verbose(I18N.getString("message.version-string-numbers-only")); 151 Log.verbose(ne); 152 return false; 153 } 154 155 return true; 156 } 157 158 @Override 159 public boolean validate(Map<String, ? super Object> params) 160 throws ConfigException { 161 try { 162 return doValidate(params); 163 } catch (RuntimeException re) { 164 if (re.getCause() instanceof ConfigException) { 165 throw (ConfigException) re.getCause(); 166 } else { 167 throw new ConfigException(re); 168 } 169 } 170 } 171 172 private boolean doValidate(Map<String, ? super Object> params) 173 throws ConfigException { 174 175 imageBundleValidation(params); 176 177 if (StandardBundlerParam.getPredefinedAppImage(params) != null) { 178 return true; 179 } 180 181 // validate short version 182 if (!validCFBundleVersion(MAC_CF_BUNDLE_VERSION.fetchFrom(params))) { 183 throw new ConfigException( 184 I18N.getString("error.invalid-cfbundle-version"), 185 I18N.getString("error.invalid-cfbundle-version.advice")); 186 } 187 188 // reject explicitly set sign to true and no valid signature key 189 if (Optional.ofNullable(MacAppImageBuilder. 190 SIGN_BUNDLE.fetchFrom(params)).orElse(Boolean.FALSE)) { 191 String signingIdentity = 192 DEVELOPER_ID_APP_SIGNING_KEY.fetchFrom(params); 193 if (signingIdentity == null) { 194 throw new ConfigException( 195 I18N.getString("error.explicit-sign-no-cert"), 196 I18N.getString("error.explicit-sign-no-cert.advice")); 197 } 198 199 // Signing will not work without Xcode with command line developer tools 200 try { 201 ProcessBuilder pb = new ProcessBuilder("xcrun", "--help"); 202 Process p = pb.start(); 203 int code = p.waitFor(); 204 if (code != 0) { 205 throw new ConfigException( 206 I18N.getString("error.no.xcode.signing"), 207 I18N.getString("error.no.xcode.signing.advice")); 208 } 209 } catch (IOException | InterruptedException ex) { 210 throw new ConfigException(ex); 211 } 212 } 213 214 return true; 215 } 216 217 File doBundle(Map<String, ? super Object> params, File outputDirectory, 218 boolean dependentTask) throws PackagerException { 219 if (StandardBundlerParam.isRuntimeInstaller(params)) { 220 return StandardBundlerParam.getPredefinedRuntime(params); 221 } else { 222 return doAppBundle(params, outputDirectory, dependentTask); 223 } 224 } 225 226 File doAppBundle(Map<String, ? super Object> params, File outputDirectory, 227 boolean dependentTask) throws PackagerException { 228 try { 229 File rootDirectory = createRoot(params, outputDirectory, 230 dependentTask, APP_NAME.fetchFrom(params) + ".app"); 231 AbstractAppImageBuilder appBuilder = 232 new MacAppImageBuilder(params, outputDirectory.toPath()); 233 if (PREDEFINED_RUNTIME_IMAGE.fetchFrom(params) == null ) { 234 JLinkBundlerHelper.execute(params, appBuilder); 235 } else { 236 StandardBundlerParam.copyPredefinedRuntimeImage( 237 params, appBuilder); 238 } 239 return rootDirectory; 240 } catch (PackagerException pe) { 241 throw pe; 242 } catch (Exception ex) { 243 Log.verbose(ex); 244 throw new PackagerException(ex); 245 } 246 } 247 248 ///////////////////////////////////////////////////////////////////////// 249 // Implement Bundler 250 ///////////////////////////////////////////////////////////////////////// 251 252 @Override 253 public String getName() { 254 return I18N.getString("app.bundler.name"); 255 } 256 257 @Override 258 public String getID() { 259 return "mac.app"; 260 } 261 262 @Override 263 public String getBundleType() { 264 return "IMAGE"; 265 } 266 267 @Override 268 public File execute(Map<String, ? super Object> params, 269 File outputParentDir) throws PackagerException { 270 return doBundle(params, outputParentDir, false); 271 } 272 273 @Override 274 public boolean supported(boolean runtimeInstaller) { 275 return true; 276 } 277 278 @Override 279 public boolean isDefault() { 280 return false; 281 } 282 283 }