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