1 /* 2 * Copyright (c) 2014, 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 com.oracle.tools.packager.mac; 27 28 import com.oracle.tools.packager.AbstractBundler; 29 import com.oracle.tools.packager.BundlerParamInfo; 30 import com.oracle.tools.packager.StandardBundlerParam; 31 import com.oracle.tools.packager.Log; 32 import com.oracle.tools.packager.ConfigException; 33 import com.oracle.tools.packager.IOUtils; 34 import com.oracle.tools.packager.UnsupportedPlatformException; 35 36 import java.io.ByteArrayOutputStream; 37 import java.io.File; 38 import java.io.IOException; 39 import java.io.PrintStream; 40 import java.nio.file.Files; 41 import java.nio.file.Path; 42 import java.nio.file.attribute.PosixFilePermission; 43 import java.text.MessageFormat; 44 import java.util.ArrayList; 45 import java.util.Arrays; 46 import java.util.Collection; 47 import java.util.EnumSet; 48 import java.util.LinkedHashSet; 49 import java.util.List; 50 import java.util.Map; 51 import java.util.ResourceBundle; 52 import java.util.Set; 53 import java.util.concurrent.atomic.AtomicReference; 54 import java.util.function.Consumer; 55 import java.util.regex.Matcher; 56 import java.util.regex.Pattern; 57 58 import static com.oracle.tools.packager.StandardBundlerParam.*; 59 60 public abstract class MacBaseInstallerBundler extends AbstractBundler { 61 62 private static final ResourceBundle I18N = 63 ResourceBundle.getBundle(MacBaseInstallerBundler.class.getName()); 64 65 //This could be generalized more to be for any type of Image Bundler 66 public static final BundlerParamInfo<MacAppBundler> APP_BUNDLER = new StandardBundlerParam<>( 67 I18N.getString("param.app-bundler.name"), 68 I18N.getString("param.app-bundle.description"), 69 "mac.app.bundler", 70 MacAppBundler.class, 71 params -> new MacAppBundler(), 72 (s, p) -> null); 73 74 public final BundlerParamInfo<File> APP_IMAGE_BUILD_ROOT = new StandardBundlerParam<>( 75 I18N.getString("param.app-image-build-root.name"), 76 I18N.getString("param.app-image-build-root.description"), 77 "mac.app.imageRoot", 78 File.class, 79 params -> { 80 File imageDir = IMAGES_ROOT.fetchFrom(params); 81 if (!imageDir.exists()) imageDir.mkdirs(); 82 try { 83 return Files.createTempDirectory(imageDir.toPath(), "image-").toFile(); 84 } catch (IOException e) { 85 return new File(imageDir, getID()+ ".image"); 86 } 87 }, 88 (s, p) -> new File(s)); 89 90 public static final StandardBundlerParam<File> MAC_APP_IMAGE = new StandardBundlerParam<>( 91 I18N.getString("param.app-image.name"), 92 I18N.getString("param.app-image.description"), 93 "mac.app.image", 94 File.class, 95 params -> null, 96 (s, p) -> new File(s)); 97 98 99 public static final BundlerParamInfo<MacDaemonBundler> DAEMON_BUNDLER = new StandardBundlerParam<>( 100 I18N.getString("param.daemon-bundler.name"), 101 I18N.getString("param.daemon-bundler.description"), 102 "mac.daemon.bundler", 103 MacDaemonBundler.class, 104 params -> new MacDaemonBundler(), 105 (s, p) -> null); 106 107 108 public final BundlerParamInfo<File> DAEMON_IMAGE_BUILD_ROOT = new StandardBundlerParam<>( 109 I18N.getString("param.daemon-image-build-root.name"), 110 I18N.getString("param.daemon-image-build-root.description"), 111 "mac.daemon.image", 112 File.class, 113 params -> { 114 File imageDir = IMAGES_ROOT.fetchFrom(params); 115 if (!imageDir.exists()) imageDir.mkdirs(); 116 return new File(imageDir, getID()+ ".daemon"); 117 }, 118 (s, p) -> new File(s)); 119 120 121 public static final BundlerParamInfo<File> CONFIG_ROOT = new StandardBundlerParam<>( 122 I18N.getString("param.config-root.name"), 123 I18N.getString("param.config-root.description"), 124 "configRoot", 125 File.class, 126 params -> { 127 File imagesRoot = new File(BUILD_ROOT.fetchFrom(params), "macosx"); 128 imagesRoot.mkdirs(); 129 return imagesRoot; 130 }, 131 (s, p) -> null); 132 133 public static final BundlerParamInfo<String> SIGNING_KEY_USER = new StandardBundlerParam<>( 134 I18N.getString("param.signing-key-name.name"), 135 I18N.getString("param.signing-key-name.description"), 136 "mac.signing-key-user-name", 137 String.class, 138 params -> "", 139 null); 140 141 public static final BundlerParamInfo<String> SIGNING_KEYCHAIN = new StandardBundlerParam<>( 142 I18N.getString("param.signing-keychain.name"), 143 I18N.getString("param.signing-keychain.description"), 144 "mac.signing-keychain", 145 String.class, 146 params -> "", 147 null); 148 149 public static final BundlerParamInfo<String> INSTALLER_NAME = new StandardBundlerParam<> ( 150 I18N.getString("param.installer-name.name"), 151 I18N.getString("param.installer-name.description"), 152 "mac.installerName", 153 String.class, 154 params -> { 155 String nm = APP_NAME.fetchFrom(params); 156 if (nm == null) return null; 157 158 String version = VERSION.fetchFrom(params); 159 if (version == null) { 160 return nm; 161 } else { 162 return nm + "-" + version; 163 } 164 }, 165 (s, p) -> s); 166 167 public static File getPredefinedImage(Map<String, ? super Object> p) { 168 File applicationImage = null; 169 if (MAC_APP_IMAGE.fetchFrom(p) != null) { 170 applicationImage = MAC_APP_IMAGE.fetchFrom(p); 171 Log.debug("Using App Image from " + applicationImage); 172 if (!applicationImage.exists()) { 173 throw new RuntimeException( 174 MessageFormat.format(I18N.getString("message.app-image-dir-does-not-exist"), MAC_APP_IMAGE.getID(), applicationImage.toString())); 175 } 176 } 177 return applicationImage; 178 } 179 180 protected void validateAppImageAndBundeler(Map<String, ? super Object> params) throws ConfigException, UnsupportedPlatformException { 181 if (MAC_APP_IMAGE.fetchFrom(params) != null) { 182 File applicationImage = MAC_APP_IMAGE.fetchFrom(params); 183 if (!applicationImage.exists()) { 184 throw new ConfigException( 185 MessageFormat.format(I18N.getString("message.app-image-dir-does-not-exist"), MAC_APP_IMAGE.getID(), applicationImage.toString()), 186 MessageFormat.format(I18N.getString("message.app-image-dir-does-not-exist.advice"), MAC_APP_IMAGE.getID())); 187 } 188 if (APP_NAME.fetchFrom(params) == null) { 189 throw new ConfigException( 190 I18N.getString("message.app-image-requires-app-name"), 191 I18N.getString("message.app-image-requires-app-name.advice")); 192 } 193 if (IDENTIFIER.fetchFrom(params) == null) { 194 throw new ConfigException( 195 I18N.getString("message.app-image-requires-identifier"), 196 I18N.getString("message.app-image-requires-identifier.advice")); 197 } 198 } else { 199 APP_BUNDLER.fetchFrom(params).doValidate(params); 200 } 201 } 202 203 protected File prepareAppBundle(Map<String, ? super Object> p) { 204 File predefinedImage = getPredefinedImage(p); 205 if (predefinedImage != null) { 206 return predefinedImage; 207 } 208 209 File appImageRoot = APP_IMAGE_BUILD_ROOT.fetchFrom(p); 210 return APP_BUNDLER.fetchFrom(p).doBundle(p, appImageRoot, true); 211 } 212 213 protected File prepareDaemonBundle(Map<String, ? super Object> p) { 214 File daemonImageRoot = DAEMON_IMAGE_BUILD_ROOT.fetchFrom(p); 215 return DAEMON_BUNDLER.fetchFrom(p).doBundle(p, daemonImageRoot, true); 216 } 217 218 public static void signAppBundle(Map<String, ? super Object> params, File appLocation, String signingIdentity, String identifierPrefix) throws IOException { 219 signAppBundle(params, appLocation, signingIdentity, identifierPrefix, null, null); 220 } 221 222 public static void signAppBundle(Map<String, ? super Object> params, File appLocation, String signingIdentity, String identifierPrefix, String entitlementsFile, String inheritedEntitlements) throws IOException { 223 AtomicReference<IOException> toThrow = new AtomicReference<>(); 224 String appExecutable = "/Contents/MacOS/" + APP_NAME.fetchFrom(params); 225 String keyChain = SIGNING_KEYCHAIN.fetchFrom(params); 226 227 // sign all dylibs and jars 228 Files.walk(appLocation.toPath()) 229 // while we are searching let's fix permissions 230 .peek(path -> { 231 try { 232 Set<PosixFilePermission> pfp = Files.getPosixFilePermissions(path); 233 if (!pfp.contains(PosixFilePermission.OWNER_WRITE)) { 234 pfp = EnumSet.copyOf(pfp); 235 pfp.add(PosixFilePermission.OWNER_WRITE); 236 Files.setPosixFilePermissions(path, pfp); 237 } 238 } catch (IOException e) { 239 Log.debug(e); 240 } 241 }) 242 .filter(p -> Files.isRegularFile(p) && 243 !(p.toString().contains("/Contents/MacOS/libjli.dylib") 244 || p.toString().contains("/Contents/MacOS/JavaAppletPlugin") 245 || p.toString().endsWith(appExecutable)) 246 ).forEach(p -> { 247 //noinspection ThrowableResultOfMethodCallIgnored 248 if (toThrow.get() != null) return; 249 250 List<String> args = new ArrayList<>(); 251 args.addAll(Arrays.asList("codesign", 252 "-s", signingIdentity, // sign with this key 253 "--prefix", identifierPrefix, // use the identifier as a prefix 254 "-vvvv")); 255 if (entitlementsFile != null && 256 (p.toString().endsWith(".jar") 257 || p.toString().endsWith(".dylib"))) 258 { 259 args.add("--entitlements"); 260 args.add(entitlementsFile); // entitlements 261 } else if (inheritedEntitlements != null && Files.isExecutable(p)) { 262 args.add("--entitlements"); 263 args.add(inheritedEntitlements); // inherited entitlements for executable processes 264 } 265 if (keyChain != null && !keyChain.isEmpty()) { 266 args.add("--keychain"); 267 args.add(keyChain); 268 } 269 args.add(p.toString()); 270 271 try { 272 Set<PosixFilePermission> oldPermissions = Files.getPosixFilePermissions(p); 273 File f = p.toFile(); 274 f.setWritable(true, true); 275 276 ProcessBuilder pb = new ProcessBuilder(args); 277 IOUtils.exec(pb, VERBOSE.fetchFrom(params)); 278 279 Files.setPosixFilePermissions(p, oldPermissions); 280 } catch (IOException ioe) { 281 toThrow.set(ioe); 282 } 283 }); 284 285 IOException ioe = toThrow.get(); 286 if (ioe != null) { 287 throw ioe; 288 } 289 290 // sign all plugins and frameworks 291 Consumer<? super Path> signIdentifiedByPList = path -> { 292 //noinspection ThrowableResultOfMethodCallIgnored 293 if (toThrow.get() != null) return; 294 295 try { 296 List<String> args = new ArrayList<>(); 297 args.addAll(Arrays.asList("codesign", 298 "-s", signingIdentity, // sign with this key 299 "--prefix", identifierPrefix, // use the identifier as a prefix 300 "-vvvv")); 301 if (keyChain != null && !keyChain.isEmpty()) { 302 args.add("--keychain"); 303 args.add(keyChain); 304 } 305 args.add(path.toString()); 306 ProcessBuilder pb = new ProcessBuilder(args); 307 IOUtils.exec(pb, VERBOSE.fetchFrom(params)); 308 309 args = new ArrayList<>(); 310 args.addAll(Arrays.asList("codesign", 311 "-s", signingIdentity, // sign with this key 312 "--prefix", identifierPrefix, // use the identifier as a prefix 313 "-vvvv")); 314 if (keyChain != null && !keyChain.isEmpty()) { 315 args.add("--keychain"); 316 args.add(keyChain); 317 } 318 args.add(path.toString() + "/Contents/_CodeSignature/CodeResources"); 319 pb = new ProcessBuilder(args); 320 IOUtils.exec(pb, VERBOSE.fetchFrom(params)); 321 } catch (IOException e) { 322 toThrow.set(e); 323 } 324 }; 325 326 Path pluginsPath = appLocation.toPath().resolve("Contents/PlugIns"); 327 if (Files.isDirectory(pluginsPath)) { 328 Files.list(pluginsPath) 329 .forEach(signIdentifiedByPList); 330 331 ioe = toThrow.get(); 332 if (ioe != null) { 333 throw ioe; 334 } 335 } 336 Path frameworkPath = appLocation.toPath().resolve("Contents/Frameworks"); 337 if (Files.isDirectory(frameworkPath)) { 338 Files.list(frameworkPath) 339 .forEach(signIdentifiedByPList); 340 341 ioe = toThrow.get(); 342 if (ioe != null) { 343 throw ioe; 344 } 345 } 346 347 // sign the app itself 348 List<String> args = new ArrayList<>(); 349 args.addAll(Arrays.asList("codesign", 350 "-s", signingIdentity, // sign with this key 351 "-vvvv")); // super verbose output 352 if (entitlementsFile != null) { 353 args.add("--entitlements"); 354 args.add(entitlementsFile); // entitlements 355 } 356 if (keyChain != null && !keyChain.isEmpty()) { 357 args.add("--keychain"); 358 args.add(keyChain); 359 } 360 args.add(appLocation.toString()); 361 362 ProcessBuilder pb = new ProcessBuilder(args.toArray(new String[args.size()])); 363 IOUtils.exec(pb, VERBOSE.fetchFrom(params)); 364 } 365 366 @Override 367 public Collection<BundlerParamInfo<?>> getBundleParameters() { 368 Collection<BundlerParamInfo<?>> results = new LinkedHashSet<>(); 369 370 results.addAll(MacAppBundler.getAppBundleParameters()); 371 results.addAll(Arrays.asList( 372 APP_BUNDLER, 373 CONFIG_ROOT, 374 APP_IMAGE_BUILD_ROOT, 375 MAC_APP_IMAGE 376 )); 377 378 return results; 379 } 380 381 @Override 382 public String getBundleType() { 383 return "INSTALLER"; 384 } 385 386 public static String findKey(String key, String keychainName, boolean verbose) { 387 388 try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); PrintStream ps = new PrintStream(baos)) { 389 List<String> searchOptions = new ArrayList<>(); 390 searchOptions.add("security"); 391 searchOptions.add("find-certificate"); 392 searchOptions.add("-c"); 393 searchOptions.add(key); 394 searchOptions.add("-a"); 395 if (keychainName != null && !keychainName.isEmpty()) { 396 searchOptions.add(keychainName); 397 } 398 399 ProcessBuilder pb = new ProcessBuilder(searchOptions); 400 401 IOUtils.exec(pb, verbose, false, ps); 402 Pattern p = Pattern.compile("\"alis\"<blob>=\"([^\"]+)\""); 403 Matcher m = p.matcher(baos.toString()); 404 if (!m.find()) { 405 Log.info("Did not find a key matching '" + key + "'"); 406 return null; 407 } 408 String matchedKey = m.group(1); 409 if (m.find()) { 410 Log.info("Found more than one key matching '" + key + "'"); 411 return null; 412 } 413 Log.debug("Using key '" + matchedKey + "'"); 414 return matchedKey; 415 } catch (IOException ioe) { 416 ioe.printStackTrace(); 417 Log.verbose(ioe); 418 return null; 419 } 420 } 421 }