99
1010import java .io .File ;
1111import java .io .IOException ;
12+ import java .nio .file .FileVisitResult ;
13+ import java .nio .file .Files ;
14+ import java .nio .file .Path ;
15+ import java .nio .file .SimpleFileVisitor ;
16+ import java .nio .file .attribute .BasicFileAttributes ;
1217import java .util .ArrayList ;
1318import java .util .Arrays ;
1419import java .util .Collection ;
1520import java .util .List ;
1621import java .util .stream .Collectors ;
22+ import java .util .stream .Stream ;
23+ import java .util .zip .ZipEntry ;
24+ import java .util .zip .ZipOutputStream ;
1725
1826/**
1927 * Packager for MacOS
@@ -25,6 +33,7 @@ public class MacPackager extends Packager {
2533 private File resourcesFolder ;
2634 private File javaFolder ;
2735 private File macOSFolder ;
36+ private File jreBundleFolder ;
2837
2938 public File getAppFile () {
3039 return appFile ;
@@ -73,7 +82,8 @@ protected void doCreateAppStructure() throws Exception {
7382 // sets common folders
7483 this .executableDestinationFolder = macOSFolder ;
7584 this .jarFileDestinationFolder = javaFolder ;
76- this .jreDestinationFolder = new File (contentsFolder , "PlugIns/" + jreDirectoryName + "/Contents/Home" );
85+ this .jreBundleFolder = new File (contentsFolder , "PlugIns/" + jreDirectoryName + ".jre" );
86+ this .jreDestinationFolder = new File (jreBundleFolder , "Contents/Home" );
7787 this .resourcesDestinationFolder = resourcesFolder ;
7888
7989 }
@@ -83,6 +93,9 @@ protected void doCreateAppStructure() throws Exception {
8393 */
8494 @ Override
8595 public File doCreateApp () throws Exception {
96+ if (bundleJre ) {
97+ processRuntimeInfoPlistFile ();
98+ }
8699
87100 // copies jarfile to Java folder
88101 FileUtils .copyFileToFolder (jarFile , javaFolder );
@@ -97,6 +110,8 @@ public File doCreateApp() throws Exception {
97110
98111 codesign ();
99112
113+ notarize ();
114+
100115 return appFile ;
101116 }
102117
@@ -157,6 +172,21 @@ private void processInfoPlistFile() throws Exception {
157172 Logger .info ("Info.plist file created in " + infoPlistFile .getAbsolutePath ());
158173 }
159174
175+ /**
176+ * Creates and writes the Info.plist inside the JRE if no custom file is specified.
177+ * @throws Exception if anything goes wrong
178+ */
179+ private void processRuntimeInfoPlistFile () throws Exception {
180+ File infoPlistFile = new File (jreBundleFolder , "Contents/Info.plist" );
181+ if (macConfig .getCustomRuntimeInfoPlist () != null && macConfig .getCustomRuntimeInfoPlist ().isFile () && macConfig .getCustomRuntimeInfoPlist ().canRead ()){
182+ FileUtils .copyFileToFile (macConfig .getCustomRuntimeInfoPlist (), infoPlistFile );
183+ } else {
184+ VelocityUtils .render ("mac/RuntimeInfo.plist.vtl" , infoPlistFile , this );
185+ XMLUtils .prettify (infoPlistFile );
186+ }
187+ Logger .info ("RuntimeInfo.plist file created in " + infoPlistFile .getAbsolutePath ());
188+ }
189+
160190 private void codesign () throws Exception {
161191 if (!Platform .mac .isCurrentPlatform ()) {
162192 Logger .warn ("Generated app could not be signed due to current platform is " + Platform .getCurrentPlatform ());
@@ -167,6 +197,18 @@ private void codesign() throws Exception {
167197 }
168198 }
169199
200+ private void notarize () throws Exception {
201+ if (!Platform .mac .isCurrentPlatform ()) {
202+ Logger .warn ("Generated app could not be notarized due to current platform is " + Platform .getCurrentPlatform ());
203+ } else if (!getMacConfig ().isCodesignApp ()) {
204+ Logger .warn ("App codesigning disabled. Cannot notarize unsigned app" );
205+ } else if (!getMacConfig ().isNotarizeApp ()) {
206+ Logger .warn ("App notarization disabled" );
207+ } else {
208+ notarize (this .macConfig .getKeyChainProfile (), this .appFile );
209+ }
210+ }
211+
170212 private void processProvisionProfileFile () throws Exception {
171213 if (macConfig .getProvisionProfile () != null && macConfig .getProvisionProfile ().isFile () && macConfig .getProvisionProfile ().canRead ()) {
172214 // file name must be 'embedded.provisionprofile'
@@ -195,13 +237,13 @@ private File preparePrecompiledStartupStub() throws Exception {
195237
196238 private void codesign (String developerId , File entitlements , File appFile ) throws Exception {
197239
198- prepareEntitlementFile (entitlements );
240+ entitlements = prepareEntitlementFile (entitlements );
199241
200- manualDeepSign (appFile , developerId , entitlements );
242+ signAppBundle (appFile , developerId , entitlements );
201243
202244 }
203245
204- private void prepareEntitlementFile (File entitlements ) throws Exception {
246+ private File prepareEntitlementFile (File entitlements ) throws Exception {
205247 // if entitlements.plist file not specified, use a default one
206248 if (entitlements == null ) {
207249 Logger .warn ("Entitlements file not specified. Using defaults!" );
@@ -210,45 +252,58 @@ private void prepareEntitlementFile(File entitlements) throws Exception {
210252 } else if (!entitlements .exists ()) {
211253 throw new Exception ("Entitlements file doesn't exist: " + entitlements );
212254 }
255+ return entitlements ;
213256 }
214257
215- private void manualDeepSign (File appFolder , String developerCertificateName , File entitlements ) throws IOException , CommandLineException {
216-
217- // codesign each file in app
218- List <Object > findCommandArgs = new ArrayList <>();
219- findCommandArgs .add (appFolder );
220- findCommandArgs .add ("-depth" ); // execute 'codesign' in 'reverse order', i.e., deepest files first
221- findCommandArgs .add ("-type" );
222- findCommandArgs .add ("f" ); // filter for files only
223- findCommandArgs .add ("-exec" );
224- findCommandArgs .add ("codesign" );
225- findCommandArgs .add ("-f" );
226- addHardenedCodesign (findCommandArgs );
227- findCommandArgs .add ("-s" );
228- findCommandArgs .add (developerCertificateName );
229- findCommandArgs .add ("--entitlements" );
230- findCommandArgs .add (entitlements );
231- findCommandArgs .add ("{}" );
232- findCommandArgs .add ("\\ ;" );
233- CommandUtils .execute ("find" , findCommandArgs );
258+ private void signAppBundle (File appFolder , String developerCertificateName , File entitlements ) throws IOException , CommandLineException {
259+ // Sign all embedded executables and dynamic libraries
260+ // Structure and order adapted from the JRE's jpackage
261+ try (Stream <Path > stream = Files .walk (appFolder .toPath ())) {
262+ stream .filter (p -> Files .isRegularFile (p )
263+ && (Files .isExecutable (p ) || p .toString ().endsWith (".dylib" ))
264+ && !(p .toString ().contains ("dylib.dSYM/Contents" ))
265+ && !(p .equals (this .executable .toPath ()))
266+ ).forEach (p -> {
267+ if (Files .isSymbolicLink (p )) {
268+ Logger .debug ("Skipping signing symlink: " + p );
269+ } else {
270+ try {
271+ codesign (Files .isExecutable (p ) ? entitlements : null , developerCertificateName , p .toFile ());
272+ } catch (IOException | CommandLineException e ) {
273+ throw new RuntimeException (e );
274+ }
275+ }
276+ });
277+ }
278+
279+ if (bundleJre ) {
280+ // sign the JRE itself after signing all its contents
281+ codesign (developerCertificateName , jreBundleFolder );
282+ }
234283
235284 // make sure the executable is signed last
236285 codesign (entitlements , developerCertificateName , this .executable );
237286
238287 // finally, sign the top level directory
239288 codesign (entitlements , developerCertificateName , appFolder );
289+ }
240290
291+ private void codesign (String developerCertificateName , File file ) throws IOException , CommandLineException {
292+ codesign (null , developerCertificateName , file );
241293 }
242294
243295 private void codesign (File entitlements , String developerCertificateName , File file ) throws IOException , CommandLineException {
244296 List <Object > arguments = new ArrayList <>();
245297 arguments .add ("-f" );
246- addHardenedCodesign (arguments );
247- arguments .add ("--entitlements" );
248- arguments .add (entitlements );
298+ if (entitlements != null ) {
299+ addHardenedCodesign (arguments );
300+ arguments .add ("--entitlements" );
301+ arguments .add (entitlements );
302+ }
303+ arguments .add ("--timestamp" );
249304 arguments .add ("-s" );
250305 arguments .add (developerCertificateName );
251- arguments .add (appFolder );
306+ arguments .add (file );
252307 CommandUtils .execute ("codesign" , arguments );
253308 }
254309
@@ -263,4 +318,44 @@ private void addHardenedCodesign(Collection<Object> args){
263318 }
264319 }
265320
321+ private void notarize (String keyChainProfile , File appFile ) throws IOException , CommandLineException {
322+ Path zippedApp = null ;
323+ try {
324+ zippedApp = zipApp (appFile );
325+ List <Object > notarizeArgs = new ArrayList <>();
326+ notarizeArgs .add ("notarytool" );
327+ notarizeArgs .add ("submit" );
328+ notarizeArgs .add (zippedApp .toString ());
329+ notarizeArgs .add ("--wait" );
330+ notarizeArgs .add ("--keychain-profile" );
331+ notarizeArgs .add (keyChainProfile );
332+ CommandUtils .execute ("xcrun" , notarizeArgs );
333+ } finally {
334+ if (zippedApp != null ) {
335+ Files .deleteIfExists (zippedApp );
336+ }
337+ }
338+
339+ List <Object > stapleArgs = new ArrayList <>();
340+ stapleArgs .add ("stapler" );
341+ stapleArgs .add ("staple" );
342+ stapleArgs .add (appFile );
343+ CommandUtils .execute ("xcrun" , stapleArgs );
344+ }
345+
346+ private Path zipApp (File appFile ) throws IOException {
347+ Path zipPath = assetsFolder .toPath ().resolve (appFile .getName () + "-notarization.zip" );
348+ try (ZipOutputStream zos = new ZipOutputStream (Files .newOutputStream (zipPath ))) {
349+ Path sourcePath = appFile .toPath ();
350+ Files .walkFileTree (sourcePath , new SimpleFileVisitor <Path >() {
351+ public FileVisitResult visitFile (Path file , BasicFileAttributes attrs ) throws IOException {
352+ zos .putNextEntry (new ZipEntry (sourcePath .getParent ().relativize (file ).toString ()));
353+ Files .copy (file , zos );
354+ zos .closeEntry ();
355+ return FileVisitResult .CONTINUE ;
356+ }
357+ });
358+ }
359+ return zipPath ;
360+ }
266361}
0 commit comments