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