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