9
9
10
10
import java .io .File ;
11
11
import 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 ;
12
17
import java .util .ArrayList ;
13
18
import java .util .Arrays ;
14
19
import java .util .Collection ;
15
20
import java .util .List ;
16
21
import java .util .stream .Collectors ;
22
+ import java .util .stream .Stream ;
23
+ import java .util .zip .ZipEntry ;
24
+ import java .util .zip .ZipOutputStream ;
17
25
18
26
/**
19
27
* Packager for MacOS
@@ -25,6 +33,7 @@ public class MacPackager extends Packager {
25
33
private File resourcesFolder ;
26
34
private File javaFolder ;
27
35
private File macOSFolder ;
36
+ private File jreBundleFolder ;
28
37
29
38
public File getAppFile () {
30
39
return appFile ;
@@ -73,7 +82,8 @@ protected void doCreateAppStructure() throws Exception {
73
82
// sets common folders
74
83
this .executableDestinationFolder = macOSFolder ;
75
84
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" );
77
87
this .resourcesDestinationFolder = resourcesFolder ;
78
88
79
89
}
@@ -83,6 +93,9 @@ protected void doCreateAppStructure() throws Exception {
83
93
*/
84
94
@ Override
85
95
public File doCreateApp () throws Exception {
96
+ if (bundleJre ) {
97
+ processRuntimeInfoPlistFile ();
98
+ }
86
99
87
100
// copies jarfile to Java folder
88
101
FileUtils .copyFileToFolder (jarFile , javaFolder );
@@ -97,6 +110,8 @@ public File doCreateApp() throws Exception {
97
110
98
111
codesign ();
99
112
113
+ notarize ();
114
+
100
115
return appFile ;
101
116
}
102
117
@@ -157,6 +172,21 @@ private void processInfoPlistFile() throws Exception {
157
172
Logger .info ("Info.plist file created in " + infoPlistFile .getAbsolutePath ());
158
173
}
159
174
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
+
160
190
private void codesign () throws Exception {
161
191
if (!Platform .mac .isCurrentPlatform ()) {
162
192
Logger .warn ("Generated app could not be signed due to current platform is " + Platform .getCurrentPlatform ());
@@ -167,6 +197,18 @@ private void codesign() throws Exception {
167
197
}
168
198
}
169
199
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
+
170
212
private void processProvisionProfileFile () throws Exception {
171
213
if (macConfig .getProvisionProfile () != null && macConfig .getProvisionProfile ().isFile () && macConfig .getProvisionProfile ().canRead ()) {
172
214
// file name must be 'embedded.provisionprofile'
@@ -197,7 +239,7 @@ private void codesign(String developerId, File entitlements, File appFile) throw
197
239
198
240
entitlements = prepareEntitlementFile (entitlements );
199
241
200
- manualDeepSign (appFile , developerId , entitlements );
242
+ signAppBundle (appFile , developerId , entitlements );
201
243
202
244
}
203
245
@@ -213,39 +255,50 @@ private File prepareEntitlementFile(File entitlements) throws Exception {
213
255
return entitlements ;
214
256
}
215
257
216
- private void manualDeepSign (File appFolder , String developerCertificateName , File entitlements ) throws IOException , CommandLineException {
217
-
218
- // codesign each file in app
219
- List <Object > findCommandArgs = new ArrayList <>();
220
- findCommandArgs .add (appFolder );
221
- findCommandArgs .add ("-depth" ); // execute 'codesign' in 'reverse order', i.e., deepest files first
222
- findCommandArgs .add ("-type" );
223
- findCommandArgs .add ("f" ); // filter for files only
224
- findCommandArgs .add ("-exec" );
225
- findCommandArgs .add ("codesign" );
226
- findCommandArgs .add ("-f" );
227
- addHardenedCodesign (findCommandArgs );
228
- findCommandArgs .add ("-s" );
229
- findCommandArgs .add (developerCertificateName );
230
- findCommandArgs .add ("--entitlements" );
231
- findCommandArgs .add (entitlements );
232
- findCommandArgs .add ("{}" );
233
- findCommandArgs .add ("\\ ;" );
234
- 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
+ // sign the JRE itself after signing all its contents
280
+ codesign (developerCertificateName , jreBundleFolder );
235
281
236
282
// make sure the executable is signed last
237
283
codesign (entitlements , developerCertificateName , this .executable );
238
284
239
285
// finally, sign the top level directory
240
286
codesign (entitlements , developerCertificateName , appFolder );
241
287
}
288
+
289
+ private void codesign (String developerCertificateName , File file ) throws IOException , CommandLineException {
290
+ codesign (null , developerCertificateName , file );
291
+ }
242
292
243
293
private void codesign (File entitlements , String developerCertificateName , File file ) throws IOException , CommandLineException {
244
294
List <Object > arguments = new ArrayList <>();
245
295
arguments .add ("-f" );
246
- addHardenedCodesign (arguments );
247
- arguments .add ("--entitlements" );
248
- arguments .add (entitlements );
296
+ if (entitlements != null ) {
297
+ addHardenedCodesign (arguments );
298
+ arguments .add ("--entitlements" );
299
+ arguments .add (entitlements );
300
+ }
301
+ arguments .add ("--timestamp" );
249
302
arguments .add ("-s" );
250
303
arguments .add (developerCertificateName );
251
304
arguments .add (file );
@@ -263,4 +316,44 @@ private void addHardenedCodesign(Collection<Object> args){
263
316
}
264
317
}
265
318
319
+ private void notarize (String keyChainProfile , File appFile ) throws IOException , CommandLineException {
320
+ Path zippedApp = null ;
321
+ try {
322
+ zippedApp = zipApp (appFile );
323
+ List <Object > notarizeArgs = new ArrayList <>();
324
+ notarizeArgs .add ("notarytool" );
325
+ notarizeArgs .add ("submit" );
326
+ notarizeArgs .add (zippedApp .toString ());
327
+ notarizeArgs .add ("--wait" );
328
+ notarizeArgs .add ("--keychain-profile" );
329
+ notarizeArgs .add (keyChainProfile );
330
+ CommandUtils .execute ("xcrun" , notarizeArgs );
331
+ } finally {
332
+ if (zippedApp != null ) {
333
+ Files .deleteIfExists (zippedApp );
334
+ }
335
+ }
336
+
337
+ List <Object > stapleArgs = new ArrayList <>();
338
+ stapleArgs .add ("stapler" );
339
+ stapleArgs .add ("staple" );
340
+ stapleArgs .add (appFile );
341
+ CommandUtils .execute ("xcrun" , stapleArgs );
342
+ }
343
+
344
+ private Path zipApp (File appFile ) throws IOException {
345
+ Path zipPath = assetsFolder .toPath ().resolve (appFile .getName () + "-notarization.zip" );
346
+ try (ZipOutputStream zos = new ZipOutputStream (Files .newOutputStream (zipPath ))) {
347
+ Path sourcePath = appFile .toPath ();
348
+ Files .walkFileTree (sourcePath , new SimpleFileVisitor <Path >() {
349
+ public FileVisitResult visitFile (Path file , BasicFileAttributes attrs ) throws IOException {
350
+ zos .putNextEntry (new ZipEntry (sourcePath .getParent ().relativize (file ).toString ()));
351
+ Files .copy (file , zos );
352
+ zos .closeEntry ();
353
+ return FileVisitResult .CONTINUE ;
354
+ }
355
+ });
356
+ }
357
+ return zipPath ;
358
+ }
266
359
}
0 commit comments