< prev index next >

modules/fxpackager/src/main/java/com/oracle/tools/packager/mac/MacAppBundler.java

Print this page
rev 9619 : imported patch 9-jake-fxpackager.patch


   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 package com.oracle.tools.packager.mac;
  26 
  27 import com.oracle.tools.packager.AbstractImageBundler;
  28 import com.oracle.tools.packager.BundlerParamInfo;
  29 import com.oracle.tools.packager.EnumeratedBundlerParam;
  30 import com.oracle.tools.packager.JreUtils;
  31 import com.oracle.tools.packager.JreUtils.Rule;
  32 import com.oracle.tools.packager.StandardBundlerParam;
  33 import com.oracle.tools.packager.Log;
  34 import com.sun.javafx.tools.packager.bundlers.BundleParams;
  35 import com.oracle.tools.packager.ConfigException;

  36 import com.oracle.tools.packager.IOUtils;
  37 import com.oracle.tools.packager.RelativeFileSet;


  38 import com.oracle.tools.packager.UnsupportedPlatformException;


  39 
  40 import java.io.*;

  41 import java.math.BigInteger;
  42 import java.net.MalformedURLException;
  43 import java.net.URL;
  44 import java.nio.file.Files;
  45 import java.nio.file.Paths;
  46 import java.text.MessageFormat;
  47 import java.util.*;
  48 import java.util.regex.Matcher;
  49 import java.util.regex.Pattern;



  50 
  51 import static com.oracle.tools.packager.StandardBundlerParam.*;
  52 import static com.oracle.tools.packager.mac.MacBaseInstallerBundler.SIGNING_KEYCHAIN;
  53 import static com.oracle.tools.packager.mac.MacBaseInstallerBundler.SIGNING_KEY_USER;
  54 import static com.oracle.tools.packager.mac.MacBaseInstallerBundler.getPredefinedImage;
  55 
  56 public class MacAppBundler extends AbstractImageBundler {
  57 
  58     private static final ResourceBundle I18N =
  59             ResourceBundle.getBundle(MacAppBundler.class.getName());
  60 
  61     public final static String MAC_BUNDLER_PREFIX =
  62             BUNDLER_PREFIX + "macosx" + File.separator;
  63 
  64     private static final String EXECUTABLE_NAME      = "JavaAppLauncher";
  65     private final static String LIBRARY_NAME         = "libpackager.dylib";
  66     private static final String TEMPLATE_BUNDLE_ICON = "GenericApp.icns";
  67     private static final String OS_TYPE_CODE         = "APPL";
  68     private static final String TEMPLATE_INFO_PLIST_LEGACY  = "Info.plist.template";
  69     private static final String TEMPLATE_INFO_PLIST_LITE    = "Info-lite.plist.template";
  70 
  71     private static Map<String, String> getMacCategories() {
  72         Map<String, String> map = new HashMap<>();
  73         map.put("Business", "public.app-category.business");
  74         map.put("Developer Tools", "public.app-category.developer-tools");
  75         map.put("Education", "public.app-category.education");
  76         map.put("Entertainment", "public.app-category.entertainment");
  77         map.put("Finance", "public.app-category.finance");
  78         map.put("Games", "public.app-category.games");
  79         map.put("Graphics & Design", "public.app-category.graphics-design");
  80         map.put("Healthcare & Fitness", "public.app-category.healthcare-fitness");
  81         map.put("Lifestyle", "public.app-category.lifestyle");
  82         map.put("Medical", "public.app-category.medical");
  83         map.put("Music", "public.app-category.music");
  84         map.put("News", "public.app-category.news");
  85         map.put("Photography", "public.app-category.photography");
  86         map.put("Productivity", "public.app-category.productivity");
  87         map.put("Reference", "public.app-category.reference");
  88         map.put("Social Networking", "public.app-category.social-networking");
  89         map.put("Sports", "public.app-category.sports");


  98         map.put("Board Games", "public.app-category.board-games");
  99         map.put("Card Games", "public.app-category.card-games");
 100         map.put("Casino Games", "public.app-category.casino-games");
 101         map.put("Dice Games", "public.app-category.dice-games");
 102         map.put("Educational Games", "public.app-category.educational-games");
 103         map.put("Family Games", "public.app-category.family-games");
 104         map.put("Kids Games", "public.app-category.kids-games");
 105         map.put("Music Games", "public.app-category.music-games");
 106         map.put("Puzzle Games", "public.app-category.puzzle-games");
 107         map.put("Racing Games", "public.app-category.racing-games");
 108         map.put("Role Playing Games", "public.app-category.role-playing-games");
 109         map.put("Simulation Games", "public.app-category.simulation-games");
 110         map.put("Sports Games", "public.app-category.sports-games");
 111         map.put("Strategy Games", "public.app-category.strategy-games");
 112         map.put("Trivia Games", "public.app-category.trivia-games");
 113         map.put("Word Games", "public.app-category.word-games");
 114 
 115         return map;
 116     }
 117 
 118     public static final BundlerParamInfo<Boolean> MAC_CONFIGURE_LAUNCHER_IN_PLIST =
 119             new StandardBundlerParam<>(
 120                     I18N.getString("param.configure-launcher-in-plist"),
 121                     I18N.getString("param.configure-launcher-in-plist.description"),
 122                     "mac.configure-launcher-in-plist",
 123                     Boolean.class,
 124                     params -> Boolean.FALSE,
 125                     (s, p) -> Boolean.valueOf(s));
 126 
 127     public static final EnumeratedBundlerParam<String> MAC_CATEGORY =
 128             new EnumeratedBundlerParam<>(
 129                     I18N.getString("param.category-name"),
 130                     I18N.getString("param.category-name.description"),
 131                     "mac.category",
 132                     String.class,
 133                     params -> params.containsKey(CATEGORY.getID())
 134                             ? CATEGORY.fetchFrom(params)
 135                             : "Unknown",
 136                     (s, p) -> s,
 137                     getMacCategories(),
 138                     false //strict - for MacStoreBundler this should be strict
 139             );
 140 
 141     public static final BundlerParamInfo<String> MAC_CF_BUNDLE_NAME =
 142             new StandardBundlerParam<>(
 143                     I18N.getString("param.cfbundle-name.name"),
 144                     I18N.getString("param.cfbundle-name.description"),
 145                     "mac.CFBundleName",
 146                     String.class,


 167                         if (validCFBundleVersion(s)) {
 168                             return s;
 169                         } else {
 170                             return "100";
 171                         }
 172                     },
 173                     (s, p) -> s);
 174 
 175     public static final BundlerParamInfo<File> CONFIG_ROOT = new StandardBundlerParam<>(
 176             I18N.getString("param.config-root.name"),
 177             I18N.getString("param.config-root.description"),
 178             "configRoot",
 179             File.class,
 180             params -> {
 181                 File configRoot = new File(BUILD_ROOT.fetchFrom(params), "macosx");
 182                 configRoot.mkdirs();
 183                 return configRoot;
 184             },
 185             (s, p) -> new File(s));
 186 
 187     public static final BundlerParamInfo<URL> RAW_EXECUTABLE_URL = new StandardBundlerParam<>(
 188             I18N.getString("param.raw-executable-url.name"),
 189             I18N.getString("param.raw-executable-url.description"),
 190             "mac.launcher.url",
 191             URL.class,
 192             params -> MacResources.class.getResource(EXECUTABLE_NAME),
 193             (s, p) -> {
 194                 try {
 195                     return new URL(s);
 196                 } catch (MalformedURLException e) {
 197                     Log.info(e.toString());
 198                     return null;
 199                 }
 200             });
 201 
 202     public static final BundlerParamInfo<String> DEFAULT_ICNS_ICON = new StandardBundlerParam<>(
 203             I18N.getString("param.default-icon-icns"),
 204             I18N.getString("param.default-icon-icns.description"),
 205             ".mac.default.icns",
 206             String.class,
 207             params -> TEMPLATE_BUNDLE_ICON,
 208             (s, p) -> s);
 209 
 210     public static final BundlerParamInfo<Rule[]> MAC_RULES = new StandardBundlerParam<>(
 211             "",
 212             "",
 213             ".mac.runtime.rules",
 214             Rule[].class,
 215             MacAppBundler::createMacRuntimeRules,
 216             (s, p) -> null
 217     );
 218 
 219     public static final BundlerParamInfo<RelativeFileSet> MAC_RUNTIME = new StandardBundlerParam<>(
 220             I18N.getString("param.runtime.name"),
 221             I18N.getString("param.runtime.description"),
 222             BundleParams.PARAM_RUNTIME,
 223             RelativeFileSet.class,
 224             params -> extractMacRuntime(System.getProperty("java.home"), params),
 225             MacAppBundler::extractMacRuntime
 226     );
 227 
 228     public static final BundlerParamInfo<String> DEVELOPER_ID_APP_SIGNING_KEY = new StandardBundlerParam<>(
 229             I18N.getString("param.signing-key-developer-id-app.name"),
 230             I18N.getString("param.signing-key-developer-id-app.description"),
 231             "mac.signing-key-developer-id-app",
 232             String.class,
 233             params -> MacBaseInstallerBundler.findKey("Developer ID Application: " + SIGNING_KEY_USER.fetchFrom(params), SIGNING_KEYCHAIN.fetchFrom(params), VERBOSE.fetchFrom(params)),
 234             (s, p) -> s);
 235 
 236     public static final BundlerParamInfo<String> BUNDLE_ID_SIGNING_PREFIX = new StandardBundlerParam<>(
 237             I18N.getString("param.bundle-id-signing-prefix.name"),
 238             I18N.getString("param.bundle-id-signing-prefix.description"),
 239             "mac.bundle-id-signing-prefix",
 240             String.class,
 241             params -> IDENTIFIER.fetchFrom(params) + ".",
 242             (s, p) -> s);
 243 
 244     public static final BundlerParamInfo<File> ICON_ICNS = new StandardBundlerParam<>(
 245             I18N.getString("param.icon-icns.name"),
 246             I18N.getString("param.icon-icns.description"),
 247             "icon.icns",
 248             File.class,
 249             params -> {
 250                 File f = ICON.fetchFrom(params);
 251                 if (f != null && !f.getName().toLowerCase().endsWith(".icns")) {
 252                     Log.info(MessageFormat.format(I18N.getString("message.icon-not-icns"), f));
 253                     return null;
 254                 }
 255                 return f;
 256             },
 257             (s, p) -> new File(s));
 258 
 259     public static RelativeFileSet extractMacRuntime(String base, Map<String, ? super Object> params) {
 260         if (base.isEmpty()) {
 261             return null;
 262         }
 263 
 264         File workingBase = new File(base);
 265         workingBase = workingBase.getAbsoluteFile();
 266         try {
 267             workingBase = workingBase.getCanonicalFile();
 268         } catch (IOException ignore) {
 269             // we tried, workingBase will remain absolute and not canonical.
 270         }
 271 
 272         if (workingBase.getName().equals("jre")) {
 273             workingBase = workingBase.getParentFile();
 274         }
 275         if (workingBase.getName().equals("Home")) {
 276             workingBase = workingBase.getParentFile();
 277         }
 278         if (workingBase.getName().equals("Contents")) {
 279             workingBase = workingBase.getParentFile();
 280         }
 281         return JreUtils.extractJreAsRelativeFileSet(workingBase.toString(),
 282                 MAC_RULES.fetchFrom(params), true);
 283     }
 284 
 285     public MacAppBundler() {
 286         super();
 287         baseResourceLoader = MacResources.class;
 288     }
 289 
 290     @Override
 291     protected String getCacheLocation(Map<String, ? super Object> params) {
 292         return "$CACHEDIR/";
 293     }
 294 
 295 
 296     public static boolean validCFBundleVersion(String v) {
 297         // CFBundleVersion (String - iOS, OS X) specifies the build version
 298         // number of the bundle, which identifies an iteration (released or
 299         // unreleased) of the bundle. The build version number should be a
 300         // string comprised of three non-negative, period-separated integers
 301         // with the first integer being greater than zero. The string should
 302         // only contain numeric (0-9) and period (.) characters. Leading zeros
 303         // are truncated from each integer and will be ignored (that is,
 304         // 1.02.3 is equivalent to 1.2.3). This key is not localizable.
 305 
 306         if (v == null) {
 307             return false;
 308         }
 309 
 310         String p[] = v.split("\\.");
 311         if (p.length > 3 || p.length < 1) {
 312             Log.verbose(I18N.getString("message.version-string-too-many-components"));
 313             return false;
 314         }
 315 


 351                 throw (ConfigException) re.getCause();
 352             } else {
 353                 throw new ConfigException(re);
 354             }
 355         }
 356     }
 357 
 358     //to be used by chained bundlers, e.g. by EXE bundler to avoid
 359     // skipping validation if p.type does not include "image"
 360     public boolean doValidate(Map<String, ? super Object> p) throws UnsupportedPlatformException, ConfigException {
 361         if (!System.getProperty("os.name").toLowerCase().contains("os x")) {
 362             throw new UnsupportedPlatformException();
 363         }
 364 
 365         imageBundleValidation(p);
 366 
 367         if (getPredefinedImage(p) != null) {
 368             return true;
 369         }
 370 
 371         // make sure we are pointing at the right JDK.
 372         RelativeFileSet runtime = MAC_RUNTIME.fetchFrom(p);
 373         if (runtime != null) {
 374             runtime = new RelativeFileSet(runtime);
 375             if ("jre".equals(runtime.getBaseDirectory().getName())) {
 376                 runtime.upshift();
 377             }
 378             if ("Home".equals(runtime.getBaseDirectory().getName())) {
 379                 runtime.upshift();
 380             }
 381             if ("Contents".equals(runtime.getBaseDirectory().getName())) {
 382                 runtime.upshift();
 383             }
 384         }
 385 
 386         //validate required inputs
 387         testRuntime(runtime, new String[] {
 388                 "Contents/Home/(jre/)?lib/[^/]+/libjvm.dylib", // most reliable
 389                 "Contents/Home/(jre/)?lib/rt.jar", // fallback canary for JDK 8
 390         });
 391         if (USE_FX_PACKAGING.fetchFrom(p)) {
 392             testRuntime(runtime, new String[] {"Contents/Home/(jre/)?lib/ext/jfxrt.jar", "Contents/Home/(jre/)?lib/jfxrt.jar"});
 393         }
 394 
 395         // validate short version
 396         if (!validCFBundleVersion(MAC_CF_BUNDLE_VERSION.fetchFrom(p))) {
 397             throw new ConfigException(
 398                     I18N.getString("error.invalid-cfbundle-version"),
 399                     I18N.getString("error.invalid-cfbundle-version.advice"));
 400         }
 401 
 402         // reject explicitly set sign to true and no valid signature key
 403         if (Optional.ofNullable(SIGN_BUNDLE.fetchFrom(p)).orElse(Boolean.FALSE)) {
 404             String signingIdentity = DEVELOPER_ID_APP_SIGNING_KEY.fetchFrom(p);
 405             if (signingIdentity == null) {
 406                 throw new ConfigException(
 407                         I18N.getString("error.explicit-sign-no-cert"),
 408                         I18N.getString("error.explicit-sign-no-cert.advice"));
 409             }
 410         }
 411 
 412         return true;
 413     }
 414 
 415     private File getConfig_InfoPlist(Map<String, ? super Object> params) {
 416         return new File(CONFIG_ROOT.fetchFrom(params), "Info.plist");
 417     }
 418 
 419     private File getConfig_Icon(Map<String, ? super Object> params) {
 420         return new File(CONFIG_ROOT.fetchFrom(params), APP_NAME.fetchFrom(params) + ".icns");
 421     }
 422 
 423     private void prepareConfigFiles(Map<String, ? super Object> params) throws IOException {
 424         File infoPlistFile = getConfig_InfoPlist(params);
 425         infoPlistFile.createNewFile();
 426         writeInfoPlist(infoPlistFile, params);
 427 
 428         // Copy icon to Resources folder
 429         prepareIcon(params);
 430     }
 431 
 432     public File doBundle(Map<String, ? super Object> p, File outputDirectory, boolean dependentTask) {
 433         File rootDirectory = null;
 434         Map<String, ? super Object> originalParams = new HashMap<>(p);
 435 


 436         if (!outputDirectory.isDirectory() && !outputDirectory.mkdirs()) {
 437             throw new RuntimeException(MessageFormat.format(I18N.getString("error.cannot-create-output-dir"), outputDirectory.getAbsolutePath()));
 438         }
 439         if (!outputDirectory.canWrite()) {
 440             throw new RuntimeException(MessageFormat.format(I18N.getString("error.cannot-write-to-output-dir"), outputDirectory.getAbsolutePath()));
 441         }
 442 
 443         try {
 444             final File predefinedImage = getPredefinedImage(p);
 445             if (predefinedImage != null) {
 446                 return predefinedImage;
 447             }
 448 
 449             // side effect is temp dir is created if not specified
 450             BUILD_ROOT.fetchFrom(p);
 451 
 452             //prepare config resources (we will copy them to the bundle later)
 453             // NB: explicitly saving them to simplify customization
 454             prepareConfigFiles(p);
 455 
 456             // Create directory structure
 457             rootDirectory = new File(outputDirectory, APP_NAME.fetchFrom(p) + ".app");
 458             IOUtils.deleteRecursive(rootDirectory);
 459             rootDirectory.mkdirs();
 460 
 461             if (!dependentTask) {
 462                 Log.info(MessageFormat.format(I18N.getString("message.creating-app-bundle"), rootDirectory.getAbsolutePath()));
 463             }
 464 
 465             File contentsDirectory = new File(rootDirectory, "Contents");
 466             contentsDirectory.mkdirs();
 467 
 468             File macOSDirectory = new File(contentsDirectory, "MacOS");
 469             macOSDirectory.mkdirs();
 470 
 471             File javaDirectory = new File(contentsDirectory, "Java");
 472             javaDirectory.mkdirs();
 473 
 474             File plugInsDirectory = new File(contentsDirectory, "PlugIns");
 475 
 476             File resourcesDirectory = new File(contentsDirectory, "Resources");
 477             resourcesDirectory.mkdirs();
 478 
 479             // Generate PkgInfo
 480             File pkgInfoFile = new File(contentsDirectory, "PkgInfo");
 481             pkgInfoFile.createNewFile();
 482             writePkgInfo(pkgInfoFile);
 483 
 484             // Copy executable to MacOS folder
 485             File executableFile = new File(macOSDirectory, getLauncherName(p));
 486             IOUtils.copyFromURL(
 487                     RAW_EXECUTABLE_URL.fetchFrom(p),
 488                     executableFile);
 489 
 490             // Copy library to the MacOS folder
 491             IOUtils.copyFromURL(
 492                     MacResources.class.getResource(LIBRARY_NAME),
 493                     new File(macOSDirectory, LIBRARY_NAME));
 494 
 495             // maybe generate launcher config
 496             if (!MAC_CONFIGURE_LAUNCHER_IN_PLIST.fetchFrom(p)) {
 497                 if (LAUNCHER_CFG_FORMAT.fetchFrom(p).equals(CFG_FORMAT_PROPERTIES)) {
 498                     writeCfgFile(p, rootDirectory);
 499                 } else {
 500                     writeCfgFile(p, new File(rootDirectory, getLauncherCfgName(p)), getRuntimeLocation(p));
 501                 }
 502             }
 503 
 504             executableFile.setExecutable(true, false);
 505 
 506             // Copy runtime to PlugIns folder
 507             copyRuntime(plugInsDirectory, p);
 508 
 509             // Copy class path entries to Java folder
 510             copyClassPathEntries(javaDirectory, p);
 511 
 512             //TODO: Need to support adding native libraries.
 513             // Copy library path entries to MacOS folder
 514             //copyLibraryPathEntries(macOSDirectory);
 515 
 516             /*********** Take care of "config" files *******/
 517             // Copy icon to Resources folder
 518             IOUtils.copyFile(getConfig_Icon(p),
 519                     new File(resourcesDirectory, getConfig_Icon(p).getName()));
 520 
 521             // copy file association icons
 522             for (Map<String, ? super Object> fa : FILE_ASSOCIATIONS.fetchFrom(p)) {
 523                 File f = FA_ICON.fetchFrom(fa);
 524                 if (f != null && f.exists()) {
 525                     IOUtils.copyFile(f,
 526                             new File(resourcesDirectory, f.getName()));
 527                 }
 528             }
 529 
 530             // Generate Info.plist
 531             IOUtils.copyFile(getConfig_InfoPlist(p),
 532                     new File(contentsDirectory, "Info.plist"));
 533 
 534             // create the secondary launchers, if any
 535             List<Map<String, ? super Object>> entryPoints = StandardBundlerParam.SECONDARY_LAUNCHERS.fetchFrom(p);
 536             for (Map<String, ? super Object> entryPoint : entryPoints) {
 537                 Map<String, ? super Object> tmp = new HashMap<>(originalParams);
 538                 tmp.putAll(entryPoint);
 539                 createLauncherForEntryPoint(tmp, rootDirectory);
 540             }
 541 
 542             // maybe sign
 543             if (Optional.ofNullable(SIGN_BUNDLE.fetchFrom(p)).orElse(Boolean.TRUE)) {
 544                 String signingIdentity = DEVELOPER_ID_APP_SIGNING_KEY.fetchFrom(p);
 545                 if (signingIdentity != null) {
 546                     MacBaseInstallerBundler.signAppBundle(p, rootDirectory, signingIdentity, BUNDLE_ID_SIGNING_PREFIX.fetchFrom(p));
 547                 }
 548             }
 549         } catch (IOException ex) {
 550             Log.info(ex.toString());
 551             Log.verbose(ex);
 552             return null;
 553         } finally {
 554             if (!VERBOSE.fetchFrom(p)) {
 555                 //cleanup
 556                 cleanupConfigFiles(p);
 557             } else {
 558                 Log.info(MessageFormat.format(I18N.getString("message.config-save-location"), CONFIG_ROOT.fetchFrom(p).getAbsolutePath()));
 559             }
 560         }
 561         return rootDirectory;
 562     }
 563 
 564     public void cleanupConfigFiles(Map<String, ? super Object> params) {
 565         //Since building the app can be bypassed, make sure configRoot was set
 566         if (CONFIG_ROOT.fetchFrom(params) != null) {
 567             getConfig_Icon(params).delete();
 568             getConfig_InfoPlist(params).delete();
 569         }
 570     }
 571 
 572     private void copyClassPathEntries(File javaDirectory, Map<String, ? super Object> params) throws IOException {
 573         List<RelativeFileSet> resourcesList = APP_RESOURCES_LIST.fetchFrom(params);
 574         if (resourcesList == null) {
 575             throw new RuntimeException(I18N.getString("message.null-classpath"));
 576         }
 577 
 578         for (RelativeFileSet classPath : resourcesList) {
 579             File srcdir = classPath.getBaseDirectory();
 580             for (String fname : classPath.getIncludedFiles()) {
 581                 IOUtils.copyFile(
 582                         new File(srcdir, fname), new File(javaDirectory, fname));
 583             }
 584         }
 585     }
 586 
 587     private void copyRuntime(File plugInsDirectory, Map<String, ? super Object> params) throws IOException {
 588         RelativeFileSet runtime = MAC_RUNTIME.fetchFrom(params);
 589         if (runtime == null) {
 590             //request to use system runtime => do not bundle
 591             return;
 592         }
 593         runtime = new RelativeFileSet(runtime);
 594         if ("jre".equals(runtime.getBaseDirectory().getName())) {
 595             runtime.upshift();
 596         }
 597         if ("Home".equals(runtime.getBaseDirectory().getName())) {
 598             runtime.upshift();
 599         }
 600         if ("Contents".equals(runtime.getBaseDirectory().getName())) {
 601             runtime.upshift();
 602         }
 603 
 604 
 605         plugInsDirectory.mkdirs();
 606 
 607         File srcdir = runtime.getBaseDirectory();
 608         // the name in .../Contents/PlugIns/ must have a dot to be verified
 609         // properly by the Mac App Store.
 610         File destDir = new File(plugInsDirectory, "Java.runtime");
 611         Set<String> filesToCopy = runtime.getIncludedFiles();
 612 
 613         for (String fname : filesToCopy) {
 614             IOUtils.copyFile(
 615                     new File(srcdir, fname), new File(destDir, fname));
 616         }
 617     }
 618 
 619     private void prepareIcon(Map<String, ? super Object> params) throws IOException {
 620         File icon = ICON_ICNS.fetchFrom(params);
 621         if (icon == null || !icon.exists()) {
 622             fetchResource(MAC_BUNDLER_PREFIX+ APP_NAME.fetchFrom(params) +".icns",
 623                     "icon",
 624                     DEFAULT_ICNS_ICON.fetchFrom(params),
 625                     getConfig_Icon(params),
 626                     VERBOSE.fetchFrom(params),
 627                     DROP_IN_RESOURCES_ROOT.fetchFrom(params));
 628         } else {
 629             fetchResource(MAC_BUNDLER_PREFIX+ APP_NAME.fetchFrom(params) +".icns",
 630                     "icon",
 631                     icon,
 632                     getConfig_Icon(params),
 633                     VERBOSE.fetchFrom(params),
 634                     DROP_IN_RESOURCES_ROOT.fetchFrom(params));
 635         }
 636     }
 637 
 638     private String getLauncherName(Map<String, ? super Object> params) {
 639         if (APP_NAME.fetchFrom(params) != null) {
 640             return APP_NAME.fetchFrom(params);
 641         } else {
 642             return MAIN_CLASS.fetchFrom(params);
 643         }
 644     }
 645 
 646     private String getBundleName(Map<String, ? super Object> params) {
 647         //TODO: Check to see what rules/limits are in place for CFBundleName
 648         if (MAC_CF_BUNDLE_NAME.fetchFrom(params) != null) {
 649             String bn = MAC_CF_BUNDLE_NAME.fetchFrom(params);
 650             if (bn.length() > 16) {
 651                 Log.info(MessageFormat.format(I18N.getString("message.bundle-name-too-long-warning"), MAC_CF_BUNDLE_NAME.getID(), bn));
 652             }
 653             return MAC_CF_BUNDLE_NAME.fetchFrom(params);
 654         } else if (APP_NAME.fetchFrom(params) != null) {
 655             return APP_NAME.fetchFrom(params);
 656         } else {
 657             String nm = MAIN_CLASS.fetchFrom(params);
 658             if (nm.length() > 16) {
 659                 nm = nm.substring(0, 16);
 660             }
 661             return nm;
 662         }
 663     }
 664 
 665     private String getRuntimeLocation(Map<String, ? super Object> params) {
 666         if (MAC_RUNTIME.fetchFrom(params) == null) {
 667             return "";
 668         } else {
 669             return "$APPDIR/PlugIns/Java.runtime";
 670         }
 671     }
 672 
 673     private void writeInfoPlist(File file, Map<String, ? super Object> params) throws IOException {
 674         Log.verbose(MessageFormat.format(I18N.getString("message.preparing-info-plist"), file.getAbsolutePath()));
 675 
 676         //prepare config for exe
 677         //Note: do not need CFBundleDisplayName if we do not support localization
 678         Map<String, String> data = new HashMap<>();
 679         data.put("DEPLOY_ICON_FILE", getConfig_Icon(params).getName());
 680         data.put("DEPLOY_BUNDLE_IDENTIFIER",
 681                 MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params));
 682         data.put("DEPLOY_BUNDLE_NAME",
 683                 getBundleName(params));
 684         data.put("DEPLOY_BUNDLE_COPYRIGHT",
 685                 COPYRIGHT.fetchFrom(params) != null ? COPYRIGHT.fetchFrom(params) : "Unknown");
 686         data.put("DEPLOY_LAUNCHER_NAME", getLauncherName(params));
 687         if (MAC_RUNTIME.fetchFrom(params) != null) {
 688             data.put("DEPLOY_JAVA_RUNTIME_NAME", "$APPDIR/PlugIns/Java.runtime");
 689         } else {
 690             data.put("DEPLOY_JAVA_RUNTIME_NAME", "");
 691         }
 692         data.put("DEPLOY_BUNDLE_SHORT_VERSION",
 693                 VERSION.fetchFrom(params) != null ? VERSION.fetchFrom(params) : "1.0.0");
 694         data.put("DEPLOY_BUNDLE_CFBUNDLE_VERSION",
 695                 MAC_CF_BUNDLE_VERSION.fetchFrom(params) != null ? MAC_CF_BUNDLE_VERSION.fetchFrom(params) : "100");
 696         data.put("DEPLOY_BUNDLE_CATEGORY",
 697                 //TODO parameters should provide set of values for IDEs
 698                 MAC_CATEGORY.validatedFetchFrom(params));
 699 
 700         data.put("DEPLOY_MAIN_JAR_NAME", MAIN_JAR.fetchFrom(params).getIncludedFiles().iterator().next());
 701 
 702         data.put("DEPLOY_PREFERENCES_ID", PREFERENCES_ID.fetchFrom(params).toLowerCase());
 703 
 704         StringBuilder sb = new StringBuilder();
 705         List<String> jvmOptions = JVM_OPTIONS.fetchFrom(params);
 706 
 707         String newline = ""; //So we don't add unneccessary extra line after last append
 708         for (String o : jvmOptions) {
 709             sb.append(newline).append("    <string>").append(o).append("</string>");
 710             newline = "\n";
 711         }
 712 
 713         Map<String, String> jvmProps = JVM_PROPERTIES.fetchFrom(params);
 714         for (Map.Entry<String, String> entry : jvmProps.entrySet()) {
 715             sb.append(newline)
 716                     .append("    <string>-D")
 717                     .append(entry.getKey())
 718                     .append("=")
 719                     .append(entry.getValue())
 720                     .append("</string>");
 721             newline = "\n";
 722         }
 723 
 724         String preloader = PRELOADER_CLASS.fetchFrom(params);
 725         if (preloader != null) {
 726             sb.append(newline)
 727                     .append("    <string>-Djavafx.preloader=")
 728                     .append(preloader)
 729                     .append("</string>");
 730             //newline = "\n";
 731         }
 732 
 733         data.put("DEPLOY_JVM_OPTIONS", sb.toString());
 734 
 735         sb = new StringBuilder();
 736         List<String> args = ARGUMENTS.fetchFrom(params);
 737         newline = ""; //So we don't add unneccessary extra line after last append
 738         for (String o : args) {
 739             sb.append(newline).append("    <string>").append(o).append("</string>");
 740             newline = "\n";
 741         }
 742         data.put("DEPLOY_ARGUMENTS", sb.toString());
 743 
 744         newline = "";
 745         sb = new StringBuilder();
 746         Map<String, String> overridableJVMOptions = USER_JVM_OPTIONS.fetchFrom(params);
 747         for (Map.Entry<String, String> arg: overridableJVMOptions.entrySet()) {
 748             sb.append(newline)
 749                 .append("      <key>").append(arg.getKey()).append("</key>\n")
 750                 .append("      <string>").append(arg.getValue()).append("</string>");
 751             newline = "\n";
 752         }
 753         data.put("DEPLOY_JVM_USER_OPTIONS", sb.toString());
 754 
 755 
 756         data.put("DEPLOY_LAUNCHER_CLASS", MAIN_CLASS.fetchFrom(params));
 757 
 758         StringBuilder macroedPath = new StringBuilder();
 759         for (String s : CLASSPATH.fetchFrom(params).split("[ ;:]+")) {
 760             macroedPath.append(s);
 761             macroedPath.append(":");
 762         }
 763         macroedPath.deleteCharAt(macroedPath.length() - 1);
 764 
 765         data.put("DEPLOY_APP_CLASSPATH", macroedPath.toString());
 766 
 767         //TODO: Add remainder of the classpath
 768 
 769         StringBuilder bundleDocumentTypes = new StringBuilder();
 770         StringBuilder exportedTypes = new StringBuilder();
 771         for (Map<String, ? super Object> fileAssociation : FILE_ASSOCIATIONS.fetchFrom(params)) {
 772 
 773             List<String> extensions = FA_EXTENSIONS.fetchFrom(fileAssociation);
 774 
 775             if (extensions == null) {
 776                 Log.info(I18N.getString("message.creating-association-with-null-extension"));
 777             }
 778 
 779             List<String> mimeTypes = FA_CONTENT_TYPE.fetchFrom(fileAssociation);
 780             String itemContentType = MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params) + "." + ((extensions == null || extensions.isEmpty())
 781                     ? "mime"
 782                     : extensions.get(0));
 783             String description = FA_DESCRIPTION.fetchFrom(fileAssociation);
 784             File icon = FA_ICON.fetchFrom(fileAssociation); //TODO FA_ICON_ICNS
 785 
 786             bundleDocumentTypes.append("    <dict>\n")
 787                 .append("      <key>LSItemContentTypes</key>\n")
 788                 .append("      <array>\n")
 789                 .append("        <string>")
 790                 .append(itemContentType)
 791                 .append("</string>\n")
 792                 .append("      </array>\n")
 793                 .append("\n")
 794                 .append("      <key>CFBundleTypeName</key>\n")
 795                 .append("      <string>")
 796                 .append(description)
 797                 .append("</string>\n")
 798                 .append("\n")
 799                 .append("      <key>LSHandlerRank</key>\n")
 800                 .append("      <string>Owner</string>\n") //TODO make a bundler arg
 801                 .append("\n")
 802                 .append("      <key>CFBundleTypeRole</key>\n")
 803                 .append("      <string>Editor</string>\n") // TODO make a bundler arg
 804                 .append("\n")
 805                 .append("      <key>LSIsAppleDefaultForType</key>\n")
 806                 .append("      <true/>\n") // TODO make a bundler arg
 807                 .append("\n");
 808 
 809             if (icon != null && icon.exists()) {
 810                 //?
 811                 bundleDocumentTypes.append("      <key>CFBundleTypeIconFile</key>\n")
 812                         .append("      <string>")
 813                         .append(icon.getName())
 814                         .append("</string>\n");
 815             }
 816             bundleDocumentTypes.append("    </dict>\n");
 817 
 818             exportedTypes.append("    <dict>\n")
 819                 .append("      <key>UTTypeIdentifier</key>\n")
 820                 .append("      <string>")
 821                 .append(itemContentType)
 822                 .append("</string>\n")
 823                 .append("\n")
 824                 .append("      <key>UTTypeDescription</key>\n")
 825                 .append("      <string>")
 826                 .append(description)
 827                 .append("</string>\n")
 828                 .append("      <key>UTTypeConformsTo</key>\n")
 829                 .append("      <array>\n")
 830                 .append("          <string>public.data</string>\n") //TODO expose this?
 831                 .append("      </array>\n")
 832                 .append("\n");
 833 
 834             if (icon != null && icon.exists()) {
 835                 exportedTypes.append("      <key>UTTypeIconFile</key>\n")
 836                     .append("      <string>")
 837                     .append(icon.getName())
 838                     .append("</string>\n")
 839                     .append("\n");
 840             }
 841 
 842             exportedTypes.append("\n")
 843                 .append("      <key>UTTypeTagSpecification</key>\n")
 844                 .append("      <dict>\n")
 845             //TODO expose via param? .append("        <key>com.apple.ostype</key>\n");
 846             //TODO expose via param? .append("        <string>ABCD</string>\n")
 847                 .append("\n");
 848 
 849             if (extensions != null && !extensions.isEmpty()) {
 850                 exportedTypes.append("        <key>public.filename-extension</key>\n")
 851                     .append("        <array>\n");
 852 
 853                 for (String ext : extensions) {
 854                     exportedTypes.append("          <string>")
 855                         .append(ext)
 856                         .append("</string>\n");
 857                 }
 858                 exportedTypes.append("        </array>\n");
 859             }
 860             if (mimeTypes != null && !mimeTypes.isEmpty()) {
 861                 exportedTypes.append("        <key>public.mime-type</key>\n")
 862                     .append("        <array>\n");
 863 
 864                 for (String mime : mimeTypes) {
 865                     exportedTypes.append("          <string>")
 866                         .append(mime)
 867                         .append("</string>\n");
 868                 }
 869                 exportedTypes.append("        </array>\n");
 870             }
 871             exportedTypes.append("      </dict>\n")
 872                     .append("    </dict>\n");
 873         }
 874         String associationData;
 875         if (bundleDocumentTypes.length() > 0) {
 876             associationData = "\n  <key>CFBundleDocumentTypes</key>\n  <array>\n"
 877                     + bundleDocumentTypes.toString()
 878                     + "  </array>\n\n  <key>UTExportedTypeDeclarations</key>\n  <array>\n"
 879                     + exportedTypes.toString()
 880                     + "  </array>\n";
 881         } else {
 882             associationData = "";
 883         }
 884         data.put("DEPLOY_FILE_ASSOCIATIONS", associationData);
 885 
 886 
 887         Writer w = new BufferedWriter(new FileWriter(file));
 888         w.write(preprocessTextResource(
 889                 MAC_BUNDLER_PREFIX + getConfig_InfoPlist(params).getName(),
 890                 I18N.getString("resource.bundle-config-file"),
 891                 MAC_CONFIGURE_LAUNCHER_IN_PLIST.fetchFrom(params)
 892                     ? TEMPLATE_INFO_PLIST_LEGACY
 893                     : TEMPLATE_INFO_PLIST_LITE,
 894                 data, VERBOSE.fetchFrom(params),
 895                 DROP_IN_RESOURCES_ROOT.fetchFrom(params)));
 896         w.close();
 897 
 898     }
 899 
 900     private void writePkgInfo(File file) throws IOException {
 901 
 902         //hardcoded as it does not seem we need to change it ever
 903         String signature = "????";
 904 
 905         try (Writer out = new BufferedWriter(new FileWriter(file))) {
 906             out.write(OS_TYPE_CODE + signature);
 907             out.flush();
 908         }
 909     }
 910 
 911     public static Rule[] createMacRuntimeRules(Map<String, ? super Object> params) {
 912         if (!System.getProperty("os.name").toLowerCase().contains("os x")) {
 913             // we will never get a sensible answer unless we are running on OSX,
 914             // so quit now and return null indicating 'no sensible value'
 915             return null;
 916         }
 917 
 918         //Subsetting of JRE is restricted.
 919         //JRE README defines what is allowed to strip:
 920         //   http://www.oracle.com/technetwork/java/javase/jre-8-readme-2095710.html
 921         //
 922 
 923         List<Rule> rules = new ArrayList<>();
 924 
 925         File baseDir;
 926 
 927         if (params.containsKey(MAC_RUNTIME.getID())) {
 928             Object o = params.get(MAC_RUNTIME.getID());
 929             if (o instanceof RelativeFileSet) {
 930                 baseDir = ((RelativeFileSet)o).getBaseDirectory();
 931             } else {
 932                 baseDir = new File(o.toString());
 933             }
 934         } else {
 935             baseDir = new File(System.getProperty("java.home"));
 936         }
 937 
 938         // we accept either pointing at the directories typically installed at:
 939         // /Libraries/Java/JavaVirtualMachine/jdk1.8.0_40/
 940         //   * .
 941         //   * Contents/Home
 942         //   * Contents/Home/jre
 943         // /Library/Internet\ Plug-Ins/JavaAppletPlugin.plugin/
 944         //   * .
 945         //   * /Contents/Home
 946         // version may change, and if we don't detect any Contents/Home or Contents/Home/jre we will
 947         // presume we are at a root.
 948 
 949         if (!baseDir.exists()) {
 950             throw new RuntimeException(I18N.getString("error.non-existent-runtime"),
 951                 new ConfigException(I18N.getString("error.non-existent-runtime"),
 952                     I18N.getString("error.non-existent-runtime.advice")));
 953         }
 954 
 955         boolean isJRE;
 956         boolean isJDK;
 957 
 958         try {
 959             String path = baseDir.getCanonicalPath();
 960             if (path.endsWith("/Contents/Home/jre")) {
 961                 baseDir = baseDir.getParentFile().getParentFile().getParentFile();
 962             } else if (path.endsWith("/Contents/Home")) {
 963                 baseDir = baseDir.getParentFile().getParentFile();
 964             }
 965 
 966             isJRE = new File(baseDir, "Contents/Home/lib/jli/libjli.dylib").exists();
 967             isJDK = new File(baseDir, "Contents/Home/jre/lib/jli/libjli.dylib").exists();
 968 
 969         } catch (IOException e) {
 970             throw new RuntimeException(e);
 971         }
 972 
 973         if (!(isJRE || isJDK)) {
 974             throw new RuntimeException(I18N.getString("error.cannot-detect-runtime-in-directory"),
 975                     new ConfigException(I18N.getString("error.cannot-detect-runtime-in-directory"),
 976                             I18N.getString("error.cannot-detect-runtime-in-directory.advice")));
 977         }
 978 
 979         // we need the Info.plist for signing
 980         rules.add(Rule.suffix("/contents/info.plist"));
 981 
 982         // Strip some JRE specific stuff
 983         if (isJRE) {
 984             rules.add(Rule.suffixNeg("/contents/disabled.plist"));
 985             rules.add(Rule.suffixNeg("/contents/enabled.plist"));
 986             rules.add(Rule.substrNeg("/contents/frameworks/"));
 987         }
 988 
 989         // strip out command line tools
 990         rules.add(Rule.suffixNeg("home/bin"));
 991         if (isJDK) {
 992             rules.add(Rule.suffixNeg("home/jre/bin"));
 993         }
 994 
 995         // strip out JRE stuff
 996         if (isJRE) {
 997             // update helper
 998             rules.add(Rule.suffixNeg("resources"));
 999             // interfacebuilder files
1000             rules.add(Rule.suffixNeg("lib/nibs"));
1001             // browser integration
1002             rules.add(Rule.suffixNeg("lib/libnpjp2.dylib"));
1003             // java webstart
1004             rules.add(Rule.suffixNeg("lib/security/javaws.policy"));
1005             rules.add(Rule.suffixNeg("lib/shortcuts"));
1006 
1007             // general deploy libraries
1008             rules.add(Rule.suffixNeg("lib/deploy"));
1009             rules.add(Rule.suffixNeg("lib/deploy.jar"));
1010             rules.add(Rule.suffixNeg("lib/javaws.jar"));
1011             rules.add(Rule.suffixNeg("lib/libdeploy.dylib"));
1012             rules.add(Rule.suffixNeg("lib/plugin.jar"));
1013         }
1014 
1015         // strip out man pages
1016         rules.add(Rule.suffixNeg("home/man"));
1017 
1018         // this is the build hashes, strip or keep?
1019         //rules.add(Rule.suffixNeg("home/release"));
1020 
1021         // strip out JDK stuff like JavaDB, JNI Headers, etc
1022         if (isJDK) {
1023             rules.add(Rule.suffixNeg("home/db"));
1024             rules.add(Rule.suffixNeg("home/demo"));
1025             rules.add(Rule.suffixNeg("home/include"));
1026             rules.add(Rule.suffixNeg("home/lib"));
1027             rules.add(Rule.suffixNeg("home/sample"));
1028             rules.add(Rule.suffixNeg("home/src.zip"));
1029             rules.add(Rule.suffixNeg("home/javafx-src.zip"));
1030         }
1031 
1032         //"home/rt" is not part of the official builds
1033         // but we may be creating this symlink to make older NB projects
1034         // happy. Make sure to not include it into final artifact
1035         rules.add(Rule.suffixNeg("home/rt"));
1036 
1037         //rules.add(Rule.suffixNeg("jre/lib/ext")); //need some of jars there for https to work
1038 
1039         // strip out flight recorder
1040         rules.add(Rule.suffixNeg("lib/jfr.jar"));
1041 
1042         return rules.toArray(new Rule[rules.size()]);
1043     }
1044 
1045     //////////////////////////////////////////////////////////////////////////////////
1046     // Implement Bundler
1047     //////////////////////////////////////////////////////////////////////////////////
1048 
1049     @Override
1050     public String getName() {
1051         return I18N.getString("bundler.name");
1052     }
1053 
1054     @Override
1055     public String getDescription() {
1056         return I18N.getString("bundler.description");
1057     }
1058 
1059     @Override
1060     public String getID() {
1061         return "mac.app";
1062     }
1063 
1064     @Override


1070     public Collection<BundlerParamInfo<?>> getBundleParameters() {
1071         return getAppBundleParameters();
1072     }
1073 
1074     public static Collection<BundlerParamInfo<?>> getAppBundleParameters() {
1075         return Arrays.asList(
1076                 APP_NAME,
1077                 APP_RESOURCES,
1078                 // APP_RESOURCES_LIST, // ??
1079                 ARGUMENTS,
1080                 BUNDLE_ID_SIGNING_PREFIX,
1081                 CLASSPATH,
1082                 DEVELOPER_ID_APP_SIGNING_KEY,
1083                 ICON_ICNS,
1084                 JVM_OPTIONS,
1085                 JVM_PROPERTIES,
1086                 MAC_CATEGORY,
1087                 MAC_CF_BUNDLE_IDENTIFIER,
1088                 MAC_CF_BUNDLE_NAME,
1089                 MAC_CF_BUNDLE_VERSION,
1090                 MAC_RUNTIME,
1091                 MAIN_CLASS,
1092                 MAIN_JAR,
1093                 PREFERENCES_ID,
1094                 PRELOADER_CLASS,
1095                 SIGNING_KEYCHAIN,
1096                 USER_JVM_OPTIONS,
1097                 VERSION
1098         );
1099     }
1100 
1101 
1102     @Override
1103     public File execute(Map<String, ? super Object> params, File outputParentDir) {
1104         return doBundle(params, outputParentDir, false);
1105     }
1106 
1107     private void createLauncherForEntryPoint(Map<String, ? super Object> p, File rootDirectory) throws IOException {
1108         prepareConfigFiles(p);
1109 
1110         if (LAUNCHER_CFG_FORMAT.fetchFrom(p).equals(CFG_FORMAT_PROPERTIES)) {
1111             writeCfgFile(p, rootDirectory);
1112         } else {
1113             writeCfgFile(p, new File(rootDirectory, getLauncherCfgName(p)), "$APPDIR/PlugIns/Java.runtime");
1114         }
1115 
1116         // Copy executable root folder
1117         File executableFile = new File(rootDirectory, "Contents/MacOS/" + getLauncherName(p));
1118         IOUtils.copyFromURL(
1119                 RAW_EXECUTABLE_URL.fetchFrom(p),
1120                 executableFile);
1121         executableFile.setExecutable(true, false);
1122 
1123     }
1124 
1125     public static String getLauncherCfgName(Map<String, ? super Object> p) {
1126         return "Contents/Java/" + APP_NAME.fetchFrom(p) +".cfg";
1127     }
1128 
1129     private void writeCfgFile(Map<String, ? super Object> params, File rootDir) throws FileNotFoundException {
1130         File pkgInfoFile = new File(rootDir, getLauncherCfgName(params));
1131 
1132         pkgInfoFile.delete();
1133 
1134         PrintStream out = new PrintStream(pkgInfoFile);
1135         out.println("app.runtime=" + getRuntimeLocation(params));
1136         out.println("app.mainjar=" + MAIN_JAR.fetchFrom(params).getIncludedFiles().iterator().next());
1137         out.println("app.version=" + VERSION.fetchFrom(params));
1138         //for future AU support (to be able to find app in the registry)
1139         out.println("app.id=" + IDENTIFIER.fetchFrom(params));
1140         out.println("app.preferences.id=" + PREFERENCES_ID.fetchFrom(params));
1141         out.println("app.identifier=" + IDENTIFIER.fetchFrom(params));
1142 
1143         out.println("app.mainclass=" +
1144                 MAIN_CLASS.fetchFrom(params).replaceAll("\\.", "/"));
1145         out.println("app.classpath=" + CLASSPATH.fetchFrom(params));
1146 
1147         List<String> jvmargs = JVM_OPTIONS.fetchFrom(params);
1148         int idx = 1;
1149         for (String a : jvmargs) {
1150             out.println("jvmarg."+idx+"="+a);
1151             idx++;
1152         }
1153         Map<String, String> jvmProps = JVM_PROPERTIES.fetchFrom(params);
1154         for (Map.Entry<String, String> entry : jvmProps.entrySet()) {
1155             out.println("jvmarg."+idx+"=-D"+entry.getKey()+"="+entry.getValue());
1156             idx++;
1157         }
1158 
1159         String preloader = PRELOADER_CLASS.fetchFrom(params);
1160         if (preloader != null) {
1161             out.println("jvmarg."+idx+"=-Djavafx.preloader="+preloader);
1162         }
1163 
1164         Map<String, String> overridableJVMOptions = USER_JVM_OPTIONS.fetchFrom(params);
1165         idx = 1;
1166         for (Map.Entry<String, String> arg: overridableJVMOptions.entrySet()) {
1167             if (arg.getKey() == null || arg.getValue() == null) {
1168                 Log.info(I18N.getString("message.jvm-user-arg-is-null"));
1169             }
1170             else {
1171                 out.println("jvmuserarg."+idx+".name="+arg.getKey());
1172                 out.println("jvmuserarg."+idx+".value="+arg.getValue());
1173             }
1174             idx++;
1175         }
1176 
1177         // add command line args
1178         List<String> args = ARGUMENTS.fetchFrom(params);
1179         idx = 1;
1180         for (String a : args) {
1181             out.println("arg."+idx+"="+a);
1182             idx++;
1183         }
1184 
1185         out.close();
1186     }
1187     @Override
1188     public void extractRuntimeFlags(Map<String, ? super Object> params) {
1189         if (params.containsKey(".runtime.autodetect")) return;
1190 
1191         params.put(".runtime.autodetect", "attempted");
1192         RelativeFileSet runtime = MAC_RUNTIME.fetchFrom(params);
1193         String commandline;
1194         if (runtime == null) {
1195             //System JRE, report nothing useful
1196             params.put(".runtime.autodetect", "systemjre");
1197         } else {
1198             File workingBase = runtime.getBaseDirectory();
1199             if (workingBase.getName().equals("jre")) {
1200                 workingBase = workingBase.getParentFile();
1201             }
1202             if (workingBase.getName().equals("Home")) {
1203                 workingBase = workingBase.getParentFile();
1204             }
1205             if (workingBase.getName().equals("Contents")) {
1206                 workingBase = workingBase.getParentFile();
1207             }
1208 
1209 
1210             try {
1211                 byte[] infoPlistBytes = Files.readAllBytes(workingBase.toPath().resolve(Paths.get("Contents", "Info.plist")));
1212                 String infoPlist = new String(infoPlistBytes);
1213 
1214                 Pattern cfBundleVersionMatcher = Pattern.compile("<key>CFBundleVersion</key>\\s*<string>([^<]+)</string>");
1215                 Matcher m = cfBundleVersionMatcher.matcher(infoPlist);
1216                 if (m.find()) {
1217                     AbstractImageBundler.extractFlagsFromVersion(params, "java version \"" + m.group(1) + "\"\n");
1218                     params.put(".runtime.autodetect", "succeeded");
1219                 } else {
1220                     params.put(".runtime.autodetect", "failed");
1221                 }
1222             } catch (IOException e) {
1223                 e.printStackTrace();
1224                 params.put(".runtime.autodetect", "failed");
1225             }
1226         }
1227     }
1228 }


   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 package com.oracle.tools.packager.mac;
  26 
  27 import com.oracle.tools.packager.AbstractImageBundler;
  28 import com.oracle.tools.packager.BundlerParamInfo;






  29 import com.oracle.tools.packager.ConfigException;
  30 import com.oracle.tools.packager.EnumeratedBundlerParam;
  31 import com.oracle.tools.packager.IOUtils;
  32 import com.oracle.tools.packager.JLinkBundlerHelper;
  33 import com.oracle.tools.packager.Log;
  34 import com.oracle.tools.packager.StandardBundlerParam;
  35 import com.oracle.tools.packager.UnsupportedPlatformException;
  36 import jdk.tools.jlink.builder.ImageBuilder;
  37 import jdk.packager.builders.mac.MacAppImageBuilder;
  38 
  39 import java.io.File;
  40 import java.io.IOException;
  41 import java.math.BigInteger;




  42 import java.text.MessageFormat;
  43 import java.util.Arrays;
  44 import java.util.Collection;
  45 import java.util.HashMap;
  46 import java.util.Map;
  47 import java.util.Optional;
  48 import java.util.ResourceBundle;
  49 
  50 import static com.oracle.tools.packager.StandardBundlerParam.*;
  51 import static com.oracle.tools.packager.mac.MacBaseInstallerBundler.*;


  52 
  53 public class MacAppBundler extends AbstractImageBundler {
  54 
  55     private static final ResourceBundle I18N =
  56             ResourceBundle.getBundle(MacAppBundler.class.getName());
  57 
  58     public final static String MAC_BUNDLER_PREFIX =
  59             BUNDLER_PREFIX + "macosx" + File.separator;
  60 


  61     private static final String TEMPLATE_BUNDLE_ICON = "GenericApp.icns";



  62 
  63     private static Map<String, String> getMacCategories() {
  64         Map<String, String> map = new HashMap<>();
  65         map.put("Business", "public.app-category.business");
  66         map.put("Developer Tools", "public.app-category.developer-tools");
  67         map.put("Education", "public.app-category.education");
  68         map.put("Entertainment", "public.app-category.entertainment");
  69         map.put("Finance", "public.app-category.finance");
  70         map.put("Games", "public.app-category.games");
  71         map.put("Graphics & Design", "public.app-category.graphics-design");
  72         map.put("Healthcare & Fitness", "public.app-category.healthcare-fitness");
  73         map.put("Lifestyle", "public.app-category.lifestyle");
  74         map.put("Medical", "public.app-category.medical");
  75         map.put("Music", "public.app-category.music");
  76         map.put("News", "public.app-category.news");
  77         map.put("Photography", "public.app-category.photography");
  78         map.put("Productivity", "public.app-category.productivity");
  79         map.put("Reference", "public.app-category.reference");
  80         map.put("Social Networking", "public.app-category.social-networking");
  81         map.put("Sports", "public.app-category.sports");


  90         map.put("Board Games", "public.app-category.board-games");
  91         map.put("Card Games", "public.app-category.card-games");
  92         map.put("Casino Games", "public.app-category.casino-games");
  93         map.put("Dice Games", "public.app-category.dice-games");
  94         map.put("Educational Games", "public.app-category.educational-games");
  95         map.put("Family Games", "public.app-category.family-games");
  96         map.put("Kids Games", "public.app-category.kids-games");
  97         map.put("Music Games", "public.app-category.music-games");
  98         map.put("Puzzle Games", "public.app-category.puzzle-games");
  99         map.put("Racing Games", "public.app-category.racing-games");
 100         map.put("Role Playing Games", "public.app-category.role-playing-games");
 101         map.put("Simulation Games", "public.app-category.simulation-games");
 102         map.put("Sports Games", "public.app-category.sports-games");
 103         map.put("Strategy Games", "public.app-category.strategy-games");
 104         map.put("Trivia Games", "public.app-category.trivia-games");
 105         map.put("Word Games", "public.app-category.word-games");
 106 
 107         return map;
 108     }
 109 









 110     public static final EnumeratedBundlerParam<String> MAC_CATEGORY =
 111             new EnumeratedBundlerParam<>(
 112                     I18N.getString("param.category-name"),
 113                     I18N.getString("param.category-name.description"),
 114                     "mac.category",
 115                     String.class,
 116                     params -> params.containsKey(CATEGORY.getID())
 117                             ? CATEGORY.fetchFrom(params)
 118                             : "Unknown",
 119                     (s, p) -> s,
 120                     getMacCategories(),
 121                     false //strict - for MacStoreBundler this should be strict
 122             );
 123 
 124     public static final BundlerParamInfo<String> MAC_CF_BUNDLE_NAME =
 125             new StandardBundlerParam<>(
 126                     I18N.getString("param.cfbundle-name.name"),
 127                     I18N.getString("param.cfbundle-name.description"),
 128                     "mac.CFBundleName",
 129                     String.class,


 150                         if (validCFBundleVersion(s)) {
 151                             return s;
 152                         } else {
 153                             return "100";
 154                         }
 155                     },
 156                     (s, p) -> s);
 157 
 158     public static final BundlerParamInfo<File> CONFIG_ROOT = new StandardBundlerParam<>(
 159             I18N.getString("param.config-root.name"),
 160             I18N.getString("param.config-root.description"),
 161             "configRoot",
 162             File.class,
 163             params -> {
 164                 File configRoot = new File(BUILD_ROOT.fetchFrom(params), "macosx");
 165                 configRoot.mkdirs();
 166                 return configRoot;
 167             },
 168             (s, p) -> new File(s));
 169 















 170     public static final BundlerParamInfo<String> DEFAULT_ICNS_ICON = new StandardBundlerParam<>(
 171             I18N.getString("param.default-icon-icns"),
 172             I18N.getString("param.default-icon-icns.description"),
 173             ".mac.default.icns",
 174             String.class,
 175             params -> TEMPLATE_BUNDLE_ICON,
 176             (s, p) -> s);
 177 


















 178     public static final BundlerParamInfo<String> DEVELOPER_ID_APP_SIGNING_KEY = new StandardBundlerParam<>(
 179             I18N.getString("param.signing-key-developer-id-app.name"),
 180             I18N.getString("param.signing-key-developer-id-app.description"),
 181             "mac.signing-key-developer-id-app",
 182             String.class,
 183             params -> MacBaseInstallerBundler.findKey("Developer ID Application: " + SIGNING_KEY_USER.fetchFrom(params), SIGNING_KEYCHAIN.fetchFrom(params), VERBOSE.fetchFrom(params)),
 184             (s, p) -> s);
 185 
 186     public static final BundlerParamInfo<String> BUNDLE_ID_SIGNING_PREFIX = new StandardBundlerParam<>(
 187             I18N.getString("param.bundle-id-signing-prefix.name"),
 188             I18N.getString("param.bundle-id-signing-prefix.description"),
 189             "mac.bundle-id-signing-prefix",
 190             String.class,
 191             params -> IDENTIFIER.fetchFrom(params) + ".",
 192             (s, p) -> s);
 193 
 194     public static final BundlerParamInfo<File> ICON_ICNS = new StandardBundlerParam<>(
 195             I18N.getString("param.icon-icns.name"),
 196             I18N.getString("param.icon-icns.description"),
 197             "icon.icns",
 198             File.class,
 199             params -> {
 200                 File f = ICON.fetchFrom(params);
 201                 if (f != null && !f.getName().toLowerCase().endsWith(".icns")) {
 202                     Log.info(MessageFormat.format(I18N.getString("message.icon-not-icns"), f));
 203                     return null;
 204                 }
 205                 return f;
 206             },
 207             (s, p) -> new File(s));
 208 


























 209     public MacAppBundler() {
 210         super();
 211         baseResourceLoader = MacResources.class;
 212     }
 213 






 214     public static boolean validCFBundleVersion(String v) {
 215         // CFBundleVersion (String - iOS, OS X) specifies the build version
 216         // number of the bundle, which identifies an iteration (released or
 217         // unreleased) of the bundle. The build version number should be a
 218         // string comprised of three non-negative, period-separated integers
 219         // with the first integer being greater than zero. The string should
 220         // only contain numeric (0-9) and period (.) characters. Leading zeros
 221         // are truncated from each integer and will be ignored (that is,
 222         // 1.02.3 is equivalent to 1.2.3). This key is not localizable.
 223 
 224         if (v == null) {
 225             return false;
 226         }
 227 
 228         String p[] = v.split("\\.");
 229         if (p.length > 3 || p.length < 1) {
 230             Log.verbose(I18N.getString("message.version-string-too-many-components"));
 231             return false;
 232         }
 233 


 269                 throw (ConfigException) re.getCause();
 270             } else {
 271                 throw new ConfigException(re);
 272             }
 273         }
 274     }
 275 
 276     //to be used by chained bundlers, e.g. by EXE bundler to avoid
 277     // skipping validation if p.type does not include "image"
 278     public boolean doValidate(Map<String, ? super Object> p) throws UnsupportedPlatformException, ConfigException {
 279         if (!System.getProperty("os.name").toLowerCase().contains("os x")) {
 280             throw new UnsupportedPlatformException();
 281         }
 282 
 283         imageBundleValidation(p);
 284 
 285         if (getPredefinedImage(p) != null) {
 286             return true;
 287         }
 288 
 289         //TODO warn if MAC_RUNTIME is set






















 290 
 291         // validate short version
 292         if (!validCFBundleVersion(MAC_CF_BUNDLE_VERSION.fetchFrom(p))) {
 293             throw new ConfigException(
 294                     I18N.getString("error.invalid-cfbundle-version"),
 295                     I18N.getString("error.invalid-cfbundle-version.advice"));
 296         }
 297 
 298         // reject explicitly set sign to true and no valid signature key
 299         if (Optional.ofNullable(SIGN_BUNDLE.fetchFrom(p)).orElse(Boolean.FALSE)) {
 300             String signingIdentity = DEVELOPER_ID_APP_SIGNING_KEY.fetchFrom(p);
 301             if (signingIdentity == null) {
 302                 throw new ConfigException(
 303                         I18N.getString("error.explicit-sign-no-cert"),
 304                         I18N.getString("error.explicit-sign-no-cert.advice"));
 305             }
 306         }
 307 
 308         return true;
 309     }
 310 
 311     private File getConfig_InfoPlist(Map<String, ? super Object> params) {
 312         return new File(CONFIG_ROOT.fetchFrom(params), "Info.plist");
 313     }
 314 
 315     private File getConfig_Icon(Map<String, ? super Object> params) {
 316         return new File(CONFIG_ROOT.fetchFrom(params), APP_NAME.fetchFrom(params) + ".icns");
 317     }
 318 












 319 
 320     File doBundle(Map<String, ? super Object> p, File outputDirectory, boolean dependentTask) {
 321         try {
 322             if (!outputDirectory.isDirectory() && !outputDirectory.mkdirs()) {
 323                 throw new RuntimeException(MessageFormat.format(I18N.getString("error.cannot-create-output-dir"), outputDirectory.getAbsolutePath()));
 324             }
 325             if (!outputDirectory.canWrite()) {
 326                 throw new RuntimeException(MessageFormat.format(I18N.getString("error.cannot-write-to-output-dir"), outputDirectory.getAbsolutePath()));
 327             }
 328 













 329             // Create directory structure
 330             File rootDirectory = new File(outputDirectory, APP_NAME.fetchFrom(p) + ".app");
 331             IOUtils.deleteRecursive(rootDirectory);
 332             rootDirectory.mkdirs();
 333 
 334             if (!dependentTask) {
 335                 Log.info(MessageFormat.format(I18N.getString("message.creating-app-bundle"), rootDirectory.getAbsolutePath()));
 336             }
 337 
 338             if (!p.containsKey(JLinkBundlerHelper.JLINK_BUILDER.getID())) {
 339                 p.put(JLinkBundlerHelper.JLINK_BUILDER.getID(), "macapp-image-builder");









































































 340             }
 341 
 342             ImageBuilder imageBuilder = new MacAppImageBuilder(p, outputDirectory.toPath());
 343             JLinkBundlerHelper.execute(p, outputDirectory, imageBuilder);
 344             return rootDirectory;




 345         } catch (IOException ex) {
 346             Log.info(ex.toString());
 347             Log.verbose(ex);
 348             return null;







 349         }

 350     }
 351 
 352     public void cleanupConfigFiles(Map<String, ? super Object> params) {
 353         //Since building the app can be bypassed, make sure configRoot was set
 354         if (CONFIG_ROOT.fetchFrom(params) != null) {
 355             getConfig_Icon(params).delete();
 356             getConfig_InfoPlist(params).delete();
 357         }
 358     }
 359 

























































































































































































































































































































































































































































































 360     //////////////////////////////////////////////////////////////////////////////////
 361     // Implement Bundler
 362     //////////////////////////////////////////////////////////////////////////////////
 363 
 364     @Override
 365     public String getName() {
 366         return I18N.getString("bundler.name");
 367     }
 368 
 369     @Override
 370     public String getDescription() {
 371         return I18N.getString("bundler.description");
 372     }
 373 
 374     @Override
 375     public String getID() {
 376         return "mac.app";
 377     }
 378 
 379     @Override


 385     public Collection<BundlerParamInfo<?>> getBundleParameters() {
 386         return getAppBundleParameters();
 387     }
 388 
 389     public static Collection<BundlerParamInfo<?>> getAppBundleParameters() {
 390         return Arrays.asList(
 391                 APP_NAME,
 392                 APP_RESOURCES,
 393                 // APP_RESOURCES_LIST, // ??
 394                 ARGUMENTS,
 395                 BUNDLE_ID_SIGNING_PREFIX,
 396                 CLASSPATH,
 397                 DEVELOPER_ID_APP_SIGNING_KEY,
 398                 ICON_ICNS,
 399                 JVM_OPTIONS,
 400                 JVM_PROPERTIES,
 401                 MAC_CATEGORY,
 402                 MAC_CF_BUNDLE_IDENTIFIER,
 403                 MAC_CF_BUNDLE_NAME,
 404                 MAC_CF_BUNDLE_VERSION,
 405 //                MAC_RUNTIME,
 406                 MAIN_CLASS,
 407                 MAIN_JAR,
 408                 PREFERENCES_ID,
 409                 PRELOADER_CLASS,
 410                 SIGNING_KEYCHAIN,
 411                 USER_JVM_OPTIONS,
 412                 VERSION
 413         );
 414     }
 415 
 416 
 417     @Override
 418     public File execute(Map<String, ? super Object> params, File outputParentDir) {
 419         return doBundle(params, outputParentDir, false);
 420     }
 421 
 422 //    private void createLauncherForEntryPoint(Map<String, ? super Object> p, File rootDirectory) throws IOException {
 423 //        prepareConfigFiles(p);
 424 //
 425 //        if (LAUNCHER_CFG_FORMAT.fetchFrom(p).equals(CFG_FORMAT_PROPERTIES)) {
 426 //            writeCfgFile(p, rootDirectory);
 427 //        } else {
 428 //            writeCfgFile(p, new File(rootDirectory, getLauncherCfgName(p)), "$APPDIR/PlugIns/Java.runtime");
 429 //        }
 430 //
 431 //        // Copy executable root folder
 432 //        File executableFile = new File(rootDirectory, "Contents/MacOS/" + getLauncherName(p));
 433 //        IOUtils.copyFromURL(
 434 //                RAW_EXECUTABLE_URL.fetchFrom(p),
 435 //                executableFile);
 436 //        executableFile.setExecutable(true, false);
 437 //
 438 //    }
 439 //
























































































 440 














 441 }
< prev index next >