68 private static final String LIBRARY_NAME = "libapplauncher.dylib";
69 private static final String TEMPLATE_BUNDLE_ICON = "java.icns";
70 private static final String OS_TYPE_CODE = "APPL";
71 private static final String TEMPLATE_INFO_PLIST_LITE =
72 "Info-lite.plist.template";
73 private static final String TEMPLATE_RUNTIME_INFO_PLIST =
74 "Runtime-Info.plist.template";
75
76 private final Path root;
77 private final Path contentsDir;
78 private final Path appDir;
79 private final Path javaModsDir;
80 private final Path resourcesDir;
81 private final Path macOSDir;
82 private final Path runtimeDir;
83 private final Path runtimeRoot;
84 private final Path mdir;
85
86 private static List<String> keyChains;
87
88 public static final BundlerParamInfo<Boolean>
89 MAC_CONFIGURE_LAUNCHER_IN_PLIST = new StandardBundlerParam<>(
90 "mac.configure-launcher-in-plist",
91 Boolean.class,
92 params -> Boolean.FALSE,
93 (s, p) -> Boolean.valueOf(s));
94
95 public static final BundlerParamInfo<String> MAC_CF_BUNDLE_NAME =
96 new StandardBundlerParam<>(
97 Arguments.CLIOptions.MAC_BUNDLE_NAME.getId(),
98 String.class,
99 params -> null,
100 (s, p) -> s);
101
102 public static final BundlerParamInfo<String> MAC_CF_BUNDLE_IDENTIFIER =
103 new StandardBundlerParam<>(
104 Arguments.CLIOptions.MAC_BUNDLE_IDENTIFIER.getId(),
105 String.class,
106 params -> {
107 // Get identifier from app image if user provided
145 File f = ICON.fetchFrom(params);
146 if (f != null && !f.getName().toLowerCase().endsWith(".icns")) {
147 Log.error(MessageFormat.format(
148 I18N.getString("message.icon-not-icns"), f));
149 return null;
150 }
151 return f;
152 },
153 (s, p) -> new File(s));
154
155 public static final StandardBundlerParam<Boolean> SIGN_BUNDLE =
156 new StandardBundlerParam<>(
157 Arguments.CLIOptions.MAC_SIGN.getId(),
158 Boolean.class,
159 params -> false,
160 // valueOf(null) is false, we actually do want null in some cases
161 (s, p) -> (s == null || "null".equalsIgnoreCase(s)) ?
162 null : Boolean.valueOf(s)
163 );
164
165 public MacAppImageBuilder(Map<String, Object> params, Path imageOutDir)
166 throws IOException {
167 super(params, imageOutDir.resolve(APP_NAME.fetchFrom(params)
168 + ".app/Contents/runtime/Contents/Home"));
169
170 Objects.requireNonNull(imageOutDir);
171
172 this.root = imageOutDir.resolve(APP_NAME.fetchFrom(params) + ".app");
173 this.contentsDir = root.resolve("Contents");
174 this.appDir = contentsDir.resolve("app");
175 this.javaModsDir = appDir.resolve("mods");
176 this.resourcesDir = contentsDir.resolve("Resources");
177 this.macOSDir = contentsDir.resolve("MacOS");
178 this.runtimeDir = contentsDir.resolve("runtime");
179 this.runtimeRoot = runtimeDir.resolve("Contents/Home");
180 this.mdir = runtimeRoot.resolve("lib");
181 Files.createDirectories(appDir);
182 Files.createDirectories(resourcesDir);
183 Files.createDirectories(macOSDir);
184 Files.createDirectories(runtimeDir);
351 // JDK 9, 10, and 11 have extra '/jli/' subdir
352 Path jli = runtimeRoot.resolve("lib/libjli.dylib");
353 if (!Files.exists(jli)) {
354 jli = runtimeRoot.resolve("lib/jli/libjli.dylib");
355 }
356
357 Files.copy(jli, runtimeMacOSDir.resolve("libjli.dylib"));
358 }
359
360 private void sign(Map<String, ? super Object> params) throws IOException {
361 if (Optional.ofNullable(
362 SIGN_BUNDLE.fetchFrom(params)).orElse(Boolean.TRUE)) {
363 try {
364 addNewKeychain(params);
365 } catch (InterruptedException e) {
366 Log.error(e.getMessage());
367 }
368 String signingIdentity =
369 DEVELOPER_ID_APP_SIGNING_KEY.fetchFrom(params);
370 if (signingIdentity != null) {
371 signAppBundle(params, root, signingIdentity,
372 BUNDLE_ID_SIGNING_PREFIX.fetchFrom(params), null, null);
373 }
374 restoreKeychainList(params);
375 }
376 }
377
378 private String getLauncherName(Map<String, ? super Object> params) {
379 if (APP_NAME.fetchFrom(params) != null) {
380 return APP_NAME.fetchFrom(params);
381 } else {
382 return MAIN_CLASS.fetchFrom(params);
383 }
384 }
385
386 public static String getLauncherCfgName(
387 Map<String, ? super Object> params) {
388 return "Contents/app/" + APP_NAME.fetchFrom(params) + ".cfg";
389 }
390
391 private void copyClassPathEntries(Path javaDirectory,
392 Map<String, ? super Object> params) throws IOException {
393 List<RelativeFileSet> resourcesList =
394 APP_RESOURCES_LIST.fetchFrom(params);
395 if (resourcesList == null) {
396 throw new RuntimeException(
397 I18N.getString("message.null-classpath"));
745 String keyChain = SIGNING_KEYCHAIN.fetchFrom(params);
746
747 // sign all dylibs and jars
748 try (Stream<Path> stream = Files.walk(appLocation)) {
749 stream.peek(path -> { // fix permissions
750 try {
751 Set<PosixFilePermission> pfp =
752 Files.getPosixFilePermissions(path);
753 if (!pfp.contains(PosixFilePermission.OWNER_WRITE)) {
754 pfp = EnumSet.copyOf(pfp);
755 pfp.add(PosixFilePermission.OWNER_WRITE);
756 Files.setPosixFilePermissions(path, pfp);
757 }
758 } catch (IOException e) {
759 Log.verbose(e);
760 }
761 }).filter(p -> Files.isRegularFile(p)
762 && !(p.toString().contains("/Contents/MacOS/libjli.dylib")
763 || p.toString().endsWith(appExecutable)
764 || p.toString().contains("/Contents/runtime")
765 || p.toString().contains("/Contents/Frameworks"))).forEach(p -> {
766 //noinspection ThrowableResultOfMethodCallIgnored
767 if (toThrow.get() != null) return;
768
769 // If p is a symlink then skip the signing process.
770 if (Files.isSymbolicLink(p)) {
771 if (VERBOSE.fetchFrom(params)) {
772 Log.verbose(MessageFormat.format(I18N.getString(
773 "message.ignoring.symlink"), p.toString()));
774 }
775 } else {
776 if (p.toString().endsWith(LIBRARY_NAME)) {
777 if (isFileSigned(p)) {
778 return;
779 }
780 }
781
782 List<String> args = new ArrayList<>();
783 args.addAll(Arrays.asList("codesign",
784 "-s", signingIdentity, // sign with this key
785 "--prefix", identifierPrefix,
786 // use the identifier as a prefix
787 "-vvvv"));
788 if (entitlementsFile != null &&
789 (p.toString().endsWith(".jar")
790 || p.toString().endsWith(".dylib"))) {
791 args.add("--entitlements");
792 args.add(entitlementsFile); // entitlements
793 } else if (inheritedEntitlements != null &&
794 Files.isExecutable(p)) {
795 args.add("--entitlements");
796 args.add(inheritedEntitlements);
797 // inherited entitlements for executable processes
798 }
799 if (keyChain != null && !keyChain.isEmpty()) {
800 args.add("--keychain");
801 args.add(keyChain);
802 }
803 args.add(p.toString());
804
805 try {
806 Set<PosixFilePermission> oldPermissions =
819 });
820 }
821 IOException ioe = toThrow.get();
822 if (ioe != null) {
823 throw ioe;
824 }
825
826 // sign all runtime and frameworks
827 Consumer<? super Path> signIdentifiedByPList = path -> {
828 //noinspection ThrowableResultOfMethodCallIgnored
829 if (toThrow.get() != null) return;
830
831 try {
832 List<String> args = new ArrayList<>();
833 args.addAll(Arrays.asList("codesign",
834 "-f",
835 "-s", signingIdentity, // sign with this key
836 "--prefix", identifierPrefix,
837 // use the identifier as a prefix
838 "-vvvv"));
839 if (keyChain != null && !keyChain.isEmpty()) {
840 args.add("--keychain");
841 args.add(keyChain);
842 }
843 args.add(path.toString());
844 ProcessBuilder pb = new ProcessBuilder(args);
845 IOUtils.exec(pb);
846
847 args = new ArrayList<>();
848 args.addAll(Arrays.asList("codesign",
849 "-s", signingIdentity, // sign with this key
850 "--prefix", identifierPrefix,
851 // use the identifier as a prefix
852 "-vvvv"));
853 if (keyChain != null && !keyChain.isEmpty()) {
854 args.add("--keychain");
855 args.add(keyChain);
856 }
857 args.add(path.toString()
858 + "/Contents/_CodeSignature/CodeResources");
859 pb = new ProcessBuilder(args);
860 IOUtils.exec(pb);
861 } catch (IOException e) {
862 toThrow.set(e);
863 }
864 };
865
866 Path javaPath = appLocation.resolve("Contents/runtime");
867 if (Files.isDirectory(javaPath)) {
868 signIdentifiedByPList.accept(javaPath);
869
870 ioe = toThrow.get();
871 if (ioe != null) {
872 throw ioe;
873 }
874 }
875 Path frameworkPath = appLocation.resolve("Contents/Frameworks");
876 if (Files.isDirectory(frameworkPath)) {
877 Files.list(frameworkPath)
878 .forEach(signIdentifiedByPList);
879
880 ioe = toThrow.get();
881 if (ioe != null) {
882 throw ioe;
883 }
884 }
885
886 // sign the app itself
887 List<String> args = new ArrayList<>();
888 args.addAll(Arrays.asList("codesign",
889 "-s", signingIdentity, // sign with this key
890 "-vvvv")); // super verbose output
891 if (entitlementsFile != null) {
892 args.add("--entitlements");
893 args.add(entitlementsFile); // entitlements
894 }
895 if (keyChain != null && !keyChain.isEmpty()) {
896 args.add("--keychain");
897 args.add(keyChain);
898 }
899 args.add(appLocation.toString());
900
901 ProcessBuilder pb =
902 new ProcessBuilder(args.toArray(new String[args.size()]));
903 IOUtils.exec(pb);
904 }
905
906 private static boolean isFileSigned(Path file) {
907 ProcessBuilder pb =
908 new ProcessBuilder("codesign", "--verify", file.toString());
909
910 try {
|
68 private static final String LIBRARY_NAME = "libapplauncher.dylib";
69 private static final String TEMPLATE_BUNDLE_ICON = "java.icns";
70 private static final String OS_TYPE_CODE = "APPL";
71 private static final String TEMPLATE_INFO_PLIST_LITE =
72 "Info-lite.plist.template";
73 private static final String TEMPLATE_RUNTIME_INFO_PLIST =
74 "Runtime-Info.plist.template";
75
76 private final Path root;
77 private final Path contentsDir;
78 private final Path appDir;
79 private final Path javaModsDir;
80 private final Path resourcesDir;
81 private final Path macOSDir;
82 private final Path runtimeDir;
83 private final Path runtimeRoot;
84 private final Path mdir;
85
86 private static List<String> keyChains;
87
88 private final static String DEFAULT_ENTITLEMENTS =
89 "Mac.entitlements";
90 private final static String DEFAULT_INHERIT_ENTITLEMENTS =
91 "Mac_Inherit.entitlements";
92
93 public static final BundlerParamInfo<Boolean>
94 MAC_CONFIGURE_LAUNCHER_IN_PLIST = new StandardBundlerParam<>(
95 "mac.configure-launcher-in-plist",
96 Boolean.class,
97 params -> Boolean.FALSE,
98 (s, p) -> Boolean.valueOf(s));
99
100 public static final BundlerParamInfo<String> MAC_CF_BUNDLE_NAME =
101 new StandardBundlerParam<>(
102 Arguments.CLIOptions.MAC_BUNDLE_NAME.getId(),
103 String.class,
104 params -> null,
105 (s, p) -> s);
106
107 public static final BundlerParamInfo<String> MAC_CF_BUNDLE_IDENTIFIER =
108 new StandardBundlerParam<>(
109 Arguments.CLIOptions.MAC_BUNDLE_IDENTIFIER.getId(),
110 String.class,
111 params -> {
112 // Get identifier from app image if user provided
150 File f = ICON.fetchFrom(params);
151 if (f != null && !f.getName().toLowerCase().endsWith(".icns")) {
152 Log.error(MessageFormat.format(
153 I18N.getString("message.icon-not-icns"), f));
154 return null;
155 }
156 return f;
157 },
158 (s, p) -> new File(s));
159
160 public static final StandardBundlerParam<Boolean> SIGN_BUNDLE =
161 new StandardBundlerParam<>(
162 Arguments.CLIOptions.MAC_SIGN.getId(),
163 Boolean.class,
164 params -> false,
165 // valueOf(null) is false, we actually do want null in some cases
166 (s, p) -> (s == null || "null".equalsIgnoreCase(s)) ?
167 null : Boolean.valueOf(s)
168 );
169
170 /*
171 public static final StandardBundlerParam<File> MAC_ENTITLEMENTS =
172 new StandardBundlerParam<>(
173 Arguments.CLIOptions.MAC_ENTITLEMENTS.getId(),
174 File.class,
175 params -> null,
176 (s, p) -> new File(s));
177
178 public static final StandardBundlerParam<File> MAC_INHERIT_ENTITLEMENTS =
179 new StandardBundlerParam<>(
180 Arguments.CLIOptions.MAC_INHERIT_ENTITLEMENTS.getId(),
181 File.class,
182 params -> null,
183 (s, p) -> new File(s));
184 */
185
186 public MacAppImageBuilder(Map<String, Object> params, Path imageOutDir)
187 throws IOException {
188 super(params, imageOutDir.resolve(APP_NAME.fetchFrom(params)
189 + ".app/Contents/runtime/Contents/Home"));
190
191 Objects.requireNonNull(imageOutDir);
192
193 this.root = imageOutDir.resolve(APP_NAME.fetchFrom(params) + ".app");
194 this.contentsDir = root.resolve("Contents");
195 this.appDir = contentsDir.resolve("app");
196 this.javaModsDir = appDir.resolve("mods");
197 this.resourcesDir = contentsDir.resolve("Resources");
198 this.macOSDir = contentsDir.resolve("MacOS");
199 this.runtimeDir = contentsDir.resolve("runtime");
200 this.runtimeRoot = runtimeDir.resolve("Contents/Home");
201 this.mdir = runtimeRoot.resolve("lib");
202 Files.createDirectories(appDir);
203 Files.createDirectories(resourcesDir);
204 Files.createDirectories(macOSDir);
205 Files.createDirectories(runtimeDir);
372 // JDK 9, 10, and 11 have extra '/jli/' subdir
373 Path jli = runtimeRoot.resolve("lib/libjli.dylib");
374 if (!Files.exists(jli)) {
375 jli = runtimeRoot.resolve("lib/jli/libjli.dylib");
376 }
377
378 Files.copy(jli, runtimeMacOSDir.resolve("libjli.dylib"));
379 }
380
381 private void sign(Map<String, ? super Object> params) throws IOException {
382 if (Optional.ofNullable(
383 SIGN_BUNDLE.fetchFrom(params)).orElse(Boolean.TRUE)) {
384 try {
385 addNewKeychain(params);
386 } catch (InterruptedException e) {
387 Log.error(e.getMessage());
388 }
389 String signingIdentity =
390 DEVELOPER_ID_APP_SIGNING_KEY.fetchFrom(params);
391 if (signingIdentity != null) {
392 prepareEntitlements(params);
393 signAppBundle(params, root, signingIdentity,
394 BUNDLE_ID_SIGNING_PREFIX.fetchFrom(params),
395 getConfig_Entitlements(params).toString(),
396 getConfig_Inherit_Entitlements(params).toString());
397 }
398 restoreKeychainList(params);
399 }
400 }
401
402 private File getConfig_Entitlements(Map<String, ? super Object> params) {
403 return new File(CONFIG_ROOT.fetchFrom(params),
404 getLauncherName(params) + ".entitlements");
405 }
406
407 private File getConfig_Inherit_Entitlements(
408 Map<String, ? super Object> params) {
409 return new File(CONFIG_ROOT.fetchFrom(params),
410 getLauncherName(params) + "_Inherit.entitlements");
411 }
412
413 private void prepareEntitlements(Map<String, ? super Object> params)
414 throws IOException {
415 createResource(DEFAULT_ENTITLEMENTS, params)
416 .setCategory(I18N.getString("resource.mac-entitlements"))
417 // .setExternal(MAC_ENTITLEMENTS.fetchFrom(params))
418 .saveToFile(getConfig_Entitlements(params));
419
420 createResource(DEFAULT_INHERIT_ENTITLEMENTS, params)
421 .setCategory(I18N.getString(
422 "resource.mac-inherit-entitlements"))
423 // .setExternal(MAC_INHERIT_ENTITLEMENTS.fetchFrom(params))
424 .saveToFile(getConfig_Inherit_Entitlements(params));
425 }
426
427
428 private String getLauncherName(Map<String, ? super Object> params) {
429 if (APP_NAME.fetchFrom(params) != null) {
430 return APP_NAME.fetchFrom(params);
431 } else {
432 return MAIN_CLASS.fetchFrom(params);
433 }
434 }
435
436 public static String getLauncherCfgName(
437 Map<String, ? super Object> params) {
438 return "Contents/app/" + APP_NAME.fetchFrom(params) + ".cfg";
439 }
440
441 private void copyClassPathEntries(Path javaDirectory,
442 Map<String, ? super Object> params) throws IOException {
443 List<RelativeFileSet> resourcesList =
444 APP_RESOURCES_LIST.fetchFrom(params);
445 if (resourcesList == null) {
446 throw new RuntimeException(
447 I18N.getString("message.null-classpath"));
795 String keyChain = SIGNING_KEYCHAIN.fetchFrom(params);
796
797 // sign all dylibs and jars
798 try (Stream<Path> stream = Files.walk(appLocation)) {
799 stream.peek(path -> { // fix permissions
800 try {
801 Set<PosixFilePermission> pfp =
802 Files.getPosixFilePermissions(path);
803 if (!pfp.contains(PosixFilePermission.OWNER_WRITE)) {
804 pfp = EnumSet.copyOf(pfp);
805 pfp.add(PosixFilePermission.OWNER_WRITE);
806 Files.setPosixFilePermissions(path, pfp);
807 }
808 } catch (IOException e) {
809 Log.verbose(e);
810 }
811 }).filter(p -> Files.isRegularFile(p)
812 && !(p.toString().contains("/Contents/MacOS/libjli.dylib")
813 || p.toString().endsWith(appExecutable)
814 || p.toString().contains("/Contents/runtime")
815 || p.toString().contains("/Contents/Frameworks"))
816 ).forEach(p -> {
817 //noinspection ThrowableResultOfMethodCallIgnored
818 if (toThrow.get() != null) return;
819
820 // If p is a symlink then skip the signing process.
821 if (Files.isSymbolicLink(p)) {
822 if (VERBOSE.fetchFrom(params)) {
823 Log.verbose(MessageFormat.format(I18N.getString(
824 "message.ignoring.symlink"), p.toString()));
825 }
826 } else {
827 if (p.toString().endsWith(LIBRARY_NAME)) {
828 if (isFileSigned(p)) {
829 return;
830 }
831 }
832 List<String> args = new ArrayList<>();
833 args.addAll(Arrays.asList("codesign",
834 "--timestamp",
835 "--options", "runtime",
836 "--deep",
837 "--force",
838 "-s", signingIdentity,
839 "--prefix", identifierPrefix,
840 "-vvvv"));
841 if (entitlementsFile != null &&
842 (p.toString().endsWith(".jar")
843 || p.toString().endsWith(".dylib"))) {
844 args.add("--entitlements");
845 args.add(entitlementsFile); // entitlements
846 } else if (inheritedEntitlements != null &&
847 Files.isExecutable(p)) {
848 args.add("--entitlements");
849 args.add(inheritedEntitlements);
850 // inherited entitlements for executable processes
851 }
852 if (keyChain != null && !keyChain.isEmpty()) {
853 args.add("--keychain");
854 args.add(keyChain);
855 }
856 args.add(p.toString());
857
858 try {
859 Set<PosixFilePermission> oldPermissions =
872 });
873 }
874 IOException ioe = toThrow.get();
875 if (ioe != null) {
876 throw ioe;
877 }
878
879 // sign all runtime and frameworks
880 Consumer<? super Path> signIdentifiedByPList = path -> {
881 //noinspection ThrowableResultOfMethodCallIgnored
882 if (toThrow.get() != null) return;
883
884 try {
885 List<String> args = new ArrayList<>();
886 args.addAll(Arrays.asList("codesign",
887 "-f",
888 "-s", signingIdentity, // sign with this key
889 "--prefix", identifierPrefix,
890 // use the identifier as a prefix
891 "-vvvv"));
892
893 if (entitlementsFile != null &&
894 (path.toString().endsWith(".jar")
895 || path.toString().endsWith(".dylib"))) {
896 args.add("--entitlements");
897 args.add(entitlementsFile); // entitlements
898 } else if (inheritedEntitlements != null &&
899 Files.isExecutable(path)) {
900 args.add("--entitlements");
901 args.add(inheritedEntitlements);
902 // inherited entitlements for executable processes
903 }
904
905 if (keyChain != null && !keyChain.isEmpty()) {
906 args.add("--keychain");
907 args.add(keyChain);
908 }
909 args.add(path.toString());
910 ProcessBuilder pb = new ProcessBuilder(args);
911 IOUtils.exec(pb);
912
913
914 args = new ArrayList<>();
915 args.addAll(Arrays.asList("codesign",
916 "--timestamp",
917 "--options", "runtime",
918 "--deep",
919 "--force",
920 "-s", signingIdentity,
921 "--prefix", identifierPrefix,
922 "-vvvv"));
923 if (keyChain != null && !keyChain.isEmpty()) {
924 args.add("--keychain");
925 args.add(keyChain);
926 }
927 args.add(path.toString()
928 + "/Contents/_CodeSignature/CodeResources");
929 pb = new ProcessBuilder(args);
930 IOUtils.exec(pb);
931 } catch (IOException e) {
932 toThrow.set(e);
933 }
934 };
935
936 Path javaPath = appLocation.resolve("Contents/runtime");
937 if (Files.isDirectory(javaPath)) {
938 signIdentifiedByPList.accept(javaPath);
939
940 ioe = toThrow.get();
941 if (ioe != null) {
942 throw ioe;
943 }
944 }
945 Path frameworkPath = appLocation.resolve("Contents/Frameworks");
946 if (Files.isDirectory(frameworkPath)) {
947 Files.list(frameworkPath)
948 .forEach(signIdentifiedByPList);
949
950 ioe = toThrow.get();
951 if (ioe != null) {
952 throw ioe;
953 }
954 }
955
956 // sign the app itself
957 List<String> args = new ArrayList<>();
958 args.addAll(Arrays.asList("codesign",
959 "--timestamp",
960 "--options", "runtime",
961 "--deep",
962 "--force",
963 "-s", signingIdentity,
964 "-vvvv"));
965 if (entitlementsFile != null) {
966 args.add("--entitlements");
967 args.add(entitlementsFile); // entitlements
968 }
969 if (keyChain != null && !keyChain.isEmpty()) {
970 args.add("--keychain");
971 args.add(keyChain);
972 }
973 args.add(appLocation.toString());
974
975 ProcessBuilder pb =
976 new ProcessBuilder(args.toArray(new String[args.size()]));
977 IOUtils.exec(pb);
978 }
979
980 private static boolean isFileSigned(Path file) {
981 ProcessBuilder pb =
982 new ProcessBuilder("codesign", "--verify", file.toString());
983
984 try {
|