1 /* 2 * Copyright (c) 2014, 2016, 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.BundlerParamInfo; 29 import com.oracle.tools.packager.StandardBundlerParam; 30 import com.oracle.tools.packager.Log; 31 import com.oracle.tools.packager.ConfigException; 32 import com.oracle.tools.packager.IOUtils; 33 import com.oracle.tools.packager.RelativeFileSet; 34 import com.oracle.tools.packager.UnsupportedPlatformException; 35 36 import java.io.BufferedWriter; 37 import java.io.File; 38 import java.io.FileNotFoundException; 39 import java.io.FileWriter; 40 import java.io.IOException; 41 import java.io.PrintStream; 42 import java.io.Writer; 43 import java.net.URLEncoder; 44 import java.text.MessageFormat; 45 import java.util.ArrayList; 46 import java.util.Arrays; 47 import java.util.Collection; 48 import java.util.HashMap; 49 import java.util.LinkedHashSet; 50 import java.util.List; 51 import java.util.Map; 52 import java.util.Optional; 53 import java.util.ResourceBundle; 54 55 import static com.oracle.tools.packager.StandardBundlerParam.*; 56 import static com.oracle.tools.packager.mac.MacBaseInstallerBundler.SIGNING_KEYCHAIN; 57 import static com.oracle.tools.packager.mac.MacBaseInstallerBundler.SIGNING_KEY_USER; 58 import jdk.packager.internal.legacy.mac.MacCertificate; 59 60 public class MacPkgBundler extends MacBaseInstallerBundler { 61 62 private static final ResourceBundle I18N = 63 ResourceBundle.getBundle(MacPkgBundler.class.getName()); 64 65 public final static String MAC_BUNDLER_PREFIX = 66 BUNDLER_PREFIX + "macosx" + File.separator; 67 68 private static final String DEFAULT_BACKGROUND_IMAGE = "background_pkg.png"; 69 70 private static final String TEMPLATE_PREINSTALL_SCRIPT = "preinstall.template"; 71 private static final String TEMPLATE_POSTINSTALL_SCRIPT = "postinstall.template"; 72 73 private static final BundlerParamInfo<File> PACKAGES_ROOT = new StandardBundlerParam<>( 74 I18N.getString("param.packages-root.name"), 75 I18N.getString("param.packages-root.description"), 76 "mac.pkg.packagesRoot", 77 File.class, 78 params -> { 79 File packagesRoot = new File(BUILD_ROOT.fetchFrom(params), "packages"); 80 packagesRoot.mkdirs(); 81 return packagesRoot; 82 }, 83 (s, p) -> new File(s)); 84 85 86 protected final BundlerParamInfo<File> SCRIPTS_DIR = new StandardBundlerParam<>( 87 I18N.getString("param.scripts-dir.name"), 88 I18N.getString("param.scripts-dir.description"), 89 "mac.pkg.scriptsDir", 90 File.class, 91 params -> { 92 File scriptsDir = new File(CONFIG_ROOT.fetchFrom(params), "scripts"); 93 scriptsDir.mkdirs(); 94 return scriptsDir; 95 }, 96 (s, p) -> new File(s)); 97 98 public static final BundlerParamInfo<String> DEVELOPER_ID_INSTALLER_SIGNING_KEY = new StandardBundlerParam<>( 99 I18N.getString("param.signing-key-developer-id-installer.name"), 100 I18N.getString("param.signing-key-developer-id-installer.description"), 101 "mac.signing-key-developer-id-installer", 102 String.class, 103 params -> { 104 String result = MacBaseInstallerBundler.findKey("Developer ID Installer: " + SIGNING_KEY_USER.fetchFrom(params), 105 SIGNING_KEYCHAIN.fetchFrom(params), 106 VERBOSE.fetchFrom(params)); 107 if (result != null) { 108 MacCertificate certificate = new MacCertificate(result, VERBOSE.fetchFrom(params)); 109 110 if (!certificate.isValid()) { 111 Log.info(MessageFormat.format(I18N.getString("error.certificate.expired"), result)); 112 } 113 } 114 115 return result; 116 }, 117 (s, p) -> s); 118 119 public static final BundlerParamInfo<String> INSTALLER_SUFFIX = new StandardBundlerParam<> ( 120 I18N.getString("param.installer-suffix.name"), 121 I18N.getString("param.installer-suffix.description"), 122 "mac.pkg.installerName.suffix", 123 String.class, 124 params -> "", 125 (s, p) -> s); 126 127 public MacPkgBundler() { 128 super(); 129 baseResourceLoader = MacResources.class; 130 } 131 132 //@Override 133 public File bundle(Map<String, ? super Object> params, File outdir) { 134 Log.info(MessageFormat.format(I18N.getString("message.building-pkg"), APP_NAME.fetchFrom(params))); 135 if (!outdir.isDirectory() && !outdir.mkdirs()) { 136 throw new RuntimeException(MessageFormat.format(I18N.getString("error.cannot-create-output-dir"), outdir.getAbsolutePath())); 137 } 138 if (!outdir.canWrite()) { 139 throw new RuntimeException(MessageFormat.format(I18N.getString("error.cannot-write-to-output-dir"), outdir.getAbsolutePath())); 140 } 141 142 File appImageDir = null; 143 try { 144 appImageDir = prepareAppBundle(params); 145 146 if (appImageDir != null && prepareConfigFiles(params)) { 147 if (SERVICE_HINT.fetchFrom(params)) { 148 File daemonImageDir = DAEMON_IMAGE_BUILD_ROOT.fetchFrom(params); 149 daemonImageDir.mkdirs(); 150 prepareDaemonBundle(params); 151 } 152 153 File configScript = getConfig_Script(params); 154 if (configScript.exists()) { 155 Log.info(MessageFormat.format(I18N.getString("message.running-script"), configScript.getAbsolutePath())); 156 IOUtils.run("bash", configScript, VERBOSE.fetchFrom(params)); 157 } 158 159 return createPKG(params, outdir, appImageDir); 160 } 161 return null; 162 } catch (IOException ex) { 163 Log.verbose(ex); 164 return null; 165 } finally { 166 try { 167 if (appImageDir != null && !Log.isDebug()) { 168 IOUtils.deleteRecursive(appImageDir); 169 } else if (appImageDir != null) { 170 Log.info(MessageFormat.format(I18N.getString("message.intermediate-image-location"), appImageDir.getAbsolutePath())); 171 } 172 if (!VERBOSE.fetchFrom(params)) { 173 //cleanup 174 cleanupConfigFiles(params); 175 } else { 176 Log.info(MessageFormat.format(I18N.getString("message.config-save-location"), CONFIG_ROOT.fetchFrom(params).getAbsolutePath())); 177 } 178 } catch (FileNotFoundException ex) { 179 Log.debug(ex); 180 //noinspection ReturnInsideFinallyBlock 181 return null; 182 } 183 } 184 } 185 186 private File getPackages_AppPackage(Map<String, ? super Object> params) { 187 return new File(PACKAGES_ROOT.fetchFrom(params), APP_FS_NAME.fetchFrom(params) + "-app.pkg"); 188 } 189 190 private File getPackages_DaemonPackage(Map<String, ? super Object> params) { 191 return new File(PACKAGES_ROOT.fetchFrom(params), APP_FS_NAME.fetchFrom(params) + "-daemon.pkg"); 192 } 193 194 private void cleanupPackagesFiles(Map<String, ? super Object> params) { 195 if (getPackages_AppPackage(params) != null) { 196 getPackages_AppPackage(params).delete(); 197 } 198 if (getPackages_DaemonPackage(params) != null) { 199 getPackages_DaemonPackage(params).delete(); 200 } 201 } 202 203 private File getConfig_DistributionXMLFile(Map<String, ? super Object> params) { 204 return new File(CONFIG_ROOT.fetchFrom(params), "distribution.dist"); 205 } 206 207 private File getConfig_BackgroundImage(Map<String, ? super Object> params) { 208 return new File(CONFIG_ROOT.fetchFrom(params), APP_NAME.fetchFrom(params) + "-background.png"); 209 } 210 211 private File getScripts_PreinstallFile(Map<String, ? super Object> params) { 212 return new File(SCRIPTS_DIR.fetchFrom(params), "preinstall"); 213 } 214 215 private File getScripts_PostinstallFile(Map<String, ? super Object> params) { 216 return new File(SCRIPTS_DIR.fetchFrom(params), "postinstall"); 217 } 218 219 private void cleanupPackageScripts(Map<String, ? super Object> params) { 220 if (getScripts_PreinstallFile(params) != null) { 221 getScripts_PreinstallFile(params).delete(); 222 } 223 if (getScripts_PostinstallFile(params) != null) { 224 getScripts_PostinstallFile(params).delete(); 225 } 226 } 227 228 private void cleanupConfigFiles(Map<String, ? super Object> params) { 229 if (getConfig_DistributionXMLFile(params) != null) { 230 getConfig_DistributionXMLFile(params).delete(); 231 } 232 if (getConfig_BackgroundImage(params) != null) { 233 getConfig_BackgroundImage(params).delete(); 234 } 235 } 236 237 private String getAppIdentifier(Map<String, ? super Object> params) { 238 return IDENTIFIER.fetchFrom(params); 239 } 240 241 private String getDaemonIdentifier(Map<String, ? super Object> params) { 242 return IDENTIFIER.fetchFrom(params) + ".daemon"; 243 } 244 245 private void preparePackageScripts(Map<String, ? super Object> params) throws IOException 246 { 247 Log.verbose(I18N.getString("message.preparing-scripts")); 248 249 Map<String, String> data = new HashMap<>(); 250 251 data.put("DEPLOY_DAEMON_IDENTIFIER", getDaemonIdentifier(params)); 252 data.put("DEPLOY_LAUNCHD_PLIST_FILE", 253 IDENTIFIER.fetchFrom(params).toLowerCase() + ".launchd.plist"); 254 255 Writer w = new BufferedWriter(new FileWriter(getScripts_PreinstallFile(params))); 256 String content = preprocessTextResource( 257 MAC_BUNDLER_PREFIX + getScripts_PreinstallFile(params).getName(), 258 I18N.getString("resource.pkg-preinstall-script"), 259 TEMPLATE_PREINSTALL_SCRIPT, 260 data, 261 VERBOSE.fetchFrom(params), 262 DROP_IN_RESOURCES_ROOT.fetchFrom(params)); 263 w.write(content); 264 w.close(); 265 getScripts_PreinstallFile(params).setExecutable(true, false); 266 267 w = new BufferedWriter(new FileWriter(getScripts_PostinstallFile(params))); 268 content = preprocessTextResource( 269 MAC_BUNDLER_PREFIX + getScripts_PostinstallFile(params).getName(), 270 I18N.getString("resource.pkg-postinstall-script"), 271 TEMPLATE_POSTINSTALL_SCRIPT, 272 data, 273 VERBOSE.fetchFrom(params), 274 DROP_IN_RESOURCES_ROOT.fetchFrom(params)); 275 w.write(content); 276 w.close(); 277 getScripts_PostinstallFile(params).setExecutable(true, false); 278 } 279 280 private void prepareDistributionXMLFile(Map<String, ? super Object> params) 281 throws IOException 282 { 283 File f = getConfig_DistributionXMLFile(params); 284 285 Log.verbose(MessageFormat.format(I18N.getString("message.preparing-distribution-dist"), f.getAbsolutePath())); 286 287 PrintStream out = new PrintStream(f); 288 289 out.println("<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"no\"?>"); 290 out.println("<installer-gui-script minSpecVersion=\"1\">"); 291 292 out.println("<title>" + APP_NAME.fetchFrom(params) + "</title>"); 293 out.println("<background" + 294 " file=\"" + getConfig_BackgroundImage(params).getName() + "\"" + 295 " mime-type=\"image/png\"" + 296 " alignment=\"bottomleft\" " + 297 " scaling=\"none\""+ 298 "/>"); 299 300 if (!LICENSE_FILE.fetchFrom(params).isEmpty()) { 301 File licFile = null; 302 303 List<String> licFiles = LICENSE_FILE.fetchFrom(params); 304 if (licFiles.isEmpty()) { 305 return; 306 } 307 String licFileStr = licFiles.get(0); 308 309 for (RelativeFileSet rfs : APP_RESOURCES_LIST.fetchFrom(params)) { 310 if (rfs.contains(licFileStr)) { 311 licFile = new File(rfs.getBaseDirectory(), licFileStr); 312 break; 313 } 314 } 315 316 // this is NPE protection, validate should have caught it's absence 317 // so we don't complain or throw an error 318 if (licFile != null) { 319 out.println("<license" + 320 " file=\"" + licFile.getAbsolutePath() + "\"" + 321 " mime-type=\"text/rtf\"" + 322 "/>"); 323 } 324 } 325 326 /* 327 * Note that the content of the distribution file 328 * below is generated by productbuild --synthesize 329 */ 330 331 String appId = getAppIdentifier(params); 332 String daemonId = getDaemonIdentifier(params); 333 334 out.println("<pkg-ref id=\"" + appId + "\"/>"); 335 if (SERVICE_HINT.fetchFrom(params)) { 336 out.println("<pkg-ref id=\"" + daemonId + "\"/>"); 337 } 338 339 out.println("<options customize=\"never\" require-scripts=\"false\"/>"); 340 out.println("<choices-outline>"); 341 out.println(" <line choice=\"default\">"); 342 out.println(" <line choice=\"" + appId + "\"/>"); 343 if (SERVICE_HINT.fetchFrom(params)) { 344 out.println(" <line choice=\"" + daemonId + "\"/>"); 345 } 346 out.println(" </line>"); 347 out.println("</choices-outline>"); 348 out.println("<choice id=\"default\"/>"); 349 out.println("<choice id=\"" + appId + "\" visible=\"false\">"); 350 out.println(" <pkg-ref id=\"" + appId + "\"/>"); 351 out.println("</choice>"); 352 out.println("<pkg-ref id=\"" + appId + "\" version=\"" + VERSION.fetchFrom(params) + 353 "\" onConclusion=\"none\">" + 354 URLEncoder.encode(getPackages_AppPackage(params).getName(), "UTF-8") + "</pkg-ref>"); 355 356 if (SERVICE_HINT.fetchFrom(params)) { 357 out.println("<choice id=\"" + daemonId + "\" visible=\"false\">"); 358 out.println(" <pkg-ref id=\"" + daemonId + "\"/>"); 359 out.println("</choice>"); 360 out.println("<pkg-ref id=\"" + daemonId + "\" version=\"" + VERSION.fetchFrom(params) + 361 "\" onConclusion=\"none\">" + 362 URLEncoder.encode(getPackages_DaemonPackage(params).getName(), "UTF-8") + "</pkg-ref>"); 363 } 364 365 out.println("</installer-gui-script>"); 366 367 out.close(); 368 } 369 370 private boolean prepareConfigFiles(Map<String, ? super Object> params) throws IOException { 371 File imageTarget = getConfig_BackgroundImage(params); 372 fetchResource(MacAppBundler.MAC_BUNDLER_PREFIX + imageTarget.getName(), 373 I18N.getString("resource.pkg-background-image"), 374 DEFAULT_BACKGROUND_IMAGE, 375 imageTarget, 376 VERBOSE.fetchFrom(params), 377 DROP_IN_RESOURCES_ROOT.fetchFrom(params)); 378 379 prepareDistributionXMLFile(params); 380 381 fetchResource(MacAppBundler.MAC_BUNDLER_PREFIX + getConfig_Script(params).getName(), 382 I18N.getString("resource.post-install-script"), 383 (String) null, 384 getConfig_Script(params), 385 VERBOSE.fetchFrom(params), 386 DROP_IN_RESOURCES_ROOT.fetchFrom(params)); 387 388 return true; 389 } 390 391 //name of post-image script 392 private File getConfig_Script(Map<String, ? super Object> params) { 393 return new File(CONFIG_ROOT.fetchFrom(params), APP_NAME.fetchFrom(params) + "-post-image.sh"); 394 } 395 396 private File createPKG(Map<String, ? super Object> params, File outdir, File appLocation) { 397 //generic find attempt 398 try { 399 String daemonLocation = DAEMON_IMAGE_BUILD_ROOT.fetchFrom(params) + "/" + APP_NAME.fetchFrom(params) + ".daemon"; 400 401 File appPKG = getPackages_AppPackage(params); 402 File daemonPKG = getPackages_DaemonPackage(params); 403 404 // build application package 405 ProcessBuilder pb = new ProcessBuilder("pkgbuild", 406 "--component", 407 appLocation.toString(), 408 "--install-location", 409 "/Applications", 410 appPKG.getAbsolutePath()); 411 IOUtils.exec(pb, VERBOSE.fetchFrom(params)); 412 413 // build daemon package if requested 414 if (SERVICE_HINT.fetchFrom(params)) { 415 preparePackageScripts(params); 416 417 pb = new ProcessBuilder("pkgbuild", 418 "--identifier", 419 APP_NAME.fetchFrom(params) + ".daemon", 420 "--root", 421 daemonLocation, 422 "--scripts", 423 SCRIPTS_DIR.fetchFrom(params).getAbsolutePath(), 424 daemonPKG.getAbsolutePath()); 425 IOUtils.exec(pb, VERBOSE.fetchFrom(params)); 426 } 427 428 // build final package 429 File finalPKG = new File(outdir, INSTALLER_NAME.fetchFrom(params) 430 + INSTALLER_SUFFIX.fetchFrom(params) 431 + ".pkg"); 432 outdir.mkdirs(); 433 434 List<String> commandLine = new ArrayList<>(); 435 commandLine.add("productbuild"); 436 437 commandLine.add("--resources"); 438 commandLine.add(CONFIG_ROOT.fetchFrom(params).getAbsolutePath()); 439 440 // maybe sign 441 if (Optional.ofNullable(SIGN_BUNDLE.fetchFrom(params)).orElse(Boolean.TRUE)) { 442 String signingIdentity = DEVELOPER_ID_INSTALLER_SIGNING_KEY.fetchFrom(params); 443 if (signingIdentity != null) { 444 commandLine.add("--sign"); 445 commandLine.add(signingIdentity); 446 } 447 448 String keychainName = SIGNING_KEYCHAIN.fetchFrom(params); 449 if (keychainName != null && !keychainName.isEmpty()) { 450 commandLine.add("--keychain"); 451 commandLine.add(keychainName); 452 } 453 } 454 455 commandLine.add("--distribution"); 456 commandLine.add(getConfig_DistributionXMLFile(params).getAbsolutePath()); 457 commandLine.add("--package-path"); 458 commandLine.add(PACKAGES_ROOT.fetchFrom(params).getAbsolutePath()); 459 460 commandLine.add(finalPKG.getAbsolutePath()); 461 462 pb = new ProcessBuilder(commandLine); 463 IOUtils.exec(pb, VERBOSE.fetchFrom(params)); 464 465 return finalPKG; 466 } catch (Exception ignored) { 467 Log.verbose(ignored); 468 return null; 469 } finally { 470 if (!VERBOSE.fetchFrom(params)) { 471 cleanupPackagesFiles(params); 472 cleanupConfigFiles(params); 473 474 if (SERVICE_HINT.fetchFrom(params)) { 475 cleanupPackageScripts(params); 476 } 477 } 478 } 479 } 480 481 ////////////////////////////////////////////////////////////////////////////////// 482 // Implement Bundler 483 ////////////////////////////////////////////////////////////////////////////////// 484 485 @Override 486 public String getName() { 487 return I18N.getString("bundler.name"); 488 } 489 490 @Override 491 public String getDescription() { 492 return I18N.getString("bundler.description"); 493 } 494 495 @Override 496 public String getID() { 497 return "pkg"; 498 } 499 500 @Override 501 public Collection<BundlerParamInfo<?>> getBundleParameters() { 502 Collection<BundlerParamInfo<?>> results = new LinkedHashSet<>(); 503 results.addAll(MacAppBundler.getAppBundleParameters()); 504 results.addAll(getPKGBundleParameters()); 505 return results; 506 } 507 508 public Collection<BundlerParamInfo<?>> getPKGBundleParameters() { 509 Collection<BundlerParamInfo<?>> results = new LinkedHashSet<>(); 510 511 results.addAll(MacAppBundler.getAppBundleParameters()); 512 results.addAll(Arrays.asList( 513 DEVELOPER_ID_INSTALLER_SIGNING_KEY, 514 //IDENTIFIER, 515 INSTALLER_SUFFIX, 516 LICENSE_FILE, 517 //SERVICE_HINT, 518 SIGNING_KEYCHAIN)); 519 520 return results; 521 } 522 523 @Override 524 public boolean validate(Map<String, ? super Object> params) throws UnsupportedPlatformException, ConfigException { 525 try { 526 if (params == null) throw new ConfigException( 527 I18N.getString("error.parameters-null"), 528 I18N.getString("error.parameters-null.advice")); 529 530 //run basic validation to ensure requirements are met 531 //we are not interested in return code, only possible exception 532 validateAppImageAndBundeler(params); 533 534 // validate license file, if used, exists in the proper place 535 if (params.containsKey(LICENSE_FILE.getID())) { 536 List<RelativeFileSet> appResourcesList = APP_RESOURCES_LIST.fetchFrom(params); 537 for (String license : LICENSE_FILE.fetchFrom(params)) { 538 boolean found = false; 539 for (RelativeFileSet appResources : appResourcesList) { 540 found = found || appResources.contains(license); 541 } 542 if (!found) { 543 throw new ConfigException( 544 I18N.getString("error.license-missing"), 545 MessageFormat.format(I18N.getString("error.license-missing.advice"), 546 license)); 547 } 548 } 549 } 550 551 // reject explicitly set sign to true and no valid signature key 552 if (Optional.ofNullable(SIGN_BUNDLE.fetchFrom(params)).orElse(Boolean.FALSE)) { 553 String signingIdentity = DEVELOPER_ID_INSTALLER_SIGNING_KEY.fetchFrom(params); 554 if (signingIdentity == null) { 555 throw new ConfigException( 556 I18N.getString("error.explicit-sign-no-cert"), 557 I18N.getString("error.explicit-sign-no-cert.advice")); 558 } 559 } 560 561 // hdiutil is always available so there's no need to test for availability. 562 563 return true; 564 } catch (RuntimeException re) { 565 if (re.getCause() instanceof ConfigException) { 566 throw (ConfigException) re.getCause(); 567 } else { 568 throw new ConfigException(re); 569 } 570 } 571 } 572 573 @Override 574 public File execute(Map<String, ? super Object> params, File outputParentDir) { 575 return bundle(params, outputParentDir); 576 } 577 578 }