Skip to content

Commit 726335a

Browse files
committed
Merge branch 'devel' into pr-310
2 parents b98438c + 63f7579 commit 726335a

File tree

7 files changed

+228
-33
lines changed

7 files changed

+228
-33
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# JavaPackager
22

3-
[![Maven Central](http://img.shields.io/maven-central/v/io.github.fvarrui/javapackager)](https://search.maven.org/artifact/io.github.fvarrui/javapackager)
3+
[![Maven Central](http://img.shields.io/maven-central/v/io.github.fvarrui/javapackager)](https://central.sonatype.com/artifact/io.github.fvarrui/javapackager/1.7.1)
44
[![GPL-3.0](https://img.shields.io/badge/license-GPL--3.0-%250778B9.svg)](https://www.gnu.org/licenses/gpl-3.0.html)
55

66
JavaPackager is a hybrid plugin for **Maven** and **Gradle** which provides an easy way to package Java applications in native Windows, MacOS or GNU/Linux executables, and generate installers for them.

docs/macosx-specific-properties.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
<entitlements>path/to/entitlements.plist</entitlements>
1717
<codesignApp>true|false</codesignApp>
1818
<hardenedCodesign>true|false</hardenedCodesign>
19+
<notarizeApp>true|false</notarizeApp>
20+
<keyChainProfile>xcrun_notarytool_profile_name</keyChainProfile>
1921

2022
<!-- properties used for DMG disk image generation -->
2123
<backgroundImage>path/to/png</backgroundImage>
@@ -62,11 +64,13 @@
6264
## Signing properties
6365

6466
| Property | Mandatory | Default value | Description |
65-
| ------------------ | --------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
67+
|--------------------| --------- |---------------|-----------------------------------------------------------------------------------------------------------------------------------------------------|
6668
| `developerId` | :x: | | Signing identity. |
6769
| `entitlements` | :x: | | Path to [entitlements](https://developer.apple.com/documentation/bundleresources/entitlements) file. |
6870
| `codesignApp` | :x: | `true` | If it is set to `false`, generated app will not be codesigned. |
6971
| `hardenedCodesign` | :x: | `true` | If it is set to `true`, enable [hardened runtime](https://developer.apple.com/documentation/security/hardened_runtime) if MacOS version >= 10.13.6. |
72+
| `notarizeApp` | :x: | `false` | If it is set to `true`, generated app will be submitted to apple for notarization and the ticket will be stapled. |
73+
| `keyChainProfile` | :x: | | Profile name originally provided to `xcrun notarytool store-credentials`. Must be set if `notarizeApp` is `true`.
7074
| `macStartup` | :x: | `SCRIPT` | App startup type, using a `SCRIPT` or a binary (compiled version of the script: `UNIVERSAL`, `X86_64` or `ARM64`). |
7175

7276
## DMG generation properties
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package io.github.fvarrui.javapackager.gradle;
2+
3+
import io.github.fvarrui.javapackager.model.LinuxConfig;
4+
import org.gradle.api.file.RegularFileProperty;
5+
import org.gradle.api.provider.ListProperty;
6+
import org.gradle.api.provider.Property;
7+
import org.gradle.api.tasks.Input;
8+
import org.gradle.api.tasks.Optional;
9+
10+
import java.io.File;
11+
import java.util.ArrayList;
12+
import java.util.List;
13+
14+
public abstract class LinuxTaskConfig {
15+
@Input
16+
@Optional
17+
public abstract ListProperty<String> getCategories();
18+
@Input
19+
@Optional
20+
public abstract Property<Boolean> isGenerateDeb();
21+
@Input
22+
@Optional
23+
public abstract Property<Boolean> isGenerateRpm();
24+
@Input
25+
@Optional
26+
public abstract Property<Boolean> isGenerateAppImage();;
27+
@Input
28+
@Optional
29+
public abstract RegularFileProperty getPngFile();
30+
@Input
31+
@Optional
32+
public abstract Property<Boolean> isWrapJar();
33+
34+
public LinuxConfig buildConfig() {
35+
LinuxConfig ret = new LinuxConfig();
36+
ret.setCategories(getCategories().getOrElse(new ArrayList<>()));
37+
ret.setGenerateDeb(isGenerateDeb().getOrElse(true));
38+
ret.setGenerateRpm(isGenerateRpm().getOrElse(true));
39+
ret.setGenerateAppImage(isGenerateAppImage().getOrElse(true));
40+
ret.setPngFile(getPngFile().getAsFile().getOrNull());
41+
ret.setWrapJar(isWrapJar().getOrElse(true));
42+
return ret;
43+
}
44+
}

src/main/java/io/github/fvarrui/javapackager/model/MacConfig.java

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,10 @@ public class MacConfig implements Serializable {
3636
private File provisionProfile;
3737
private File customLauncher;
3838
private File customInfoPlist;
39+
private File customRuntimeInfoPlist;
3940
private boolean codesignApp = true;
41+
private boolean notarizeApp = false;
42+
private String keyChainProfile;
4043
private InfoPlist infoPlist = new InfoPlist();
4144
private boolean hardenedCodesign = true;
4245
private MacStartup macStartup = MacStartup.SCRIPT;
@@ -209,6 +212,14 @@ public void setCustomInfoPlist(File customInfoPlist) {
209212
this.customInfoPlist = customInfoPlist;
210213
}
211214

215+
public File getCustomRuntimeInfoPlist() {
216+
return customRuntimeInfoPlist;
217+
}
218+
219+
public void setCustomRuntimeInfoPlist(File customRuntimeInfoPlist) {
220+
this.customRuntimeInfoPlist = customRuntimeInfoPlist;
221+
}
222+
212223
public File getProvisionProfile() {
213224
return provisionProfile;
214225
}
@@ -233,6 +244,22 @@ public void setCodesignApp(boolean codesignApp) {
233244
this.codesignApp = codesignApp;
234245
}
235246

247+
public boolean isNotarizeApp() {
248+
return notarizeApp;
249+
}
250+
251+
public void setNotarizeApp(boolean notarizeApp) {
252+
this.notarizeApp = notarizeApp;
253+
}
254+
255+
public String getKeyChainProfile() {
256+
return keyChainProfile;
257+
}
258+
259+
public void setKeyChainProfile(String keyChainProfile) {
260+
this.keyChainProfile = keyChainProfile;
261+
}
262+
236263
public InfoPlist getInfoPlist() {
237264
return infoPlist;
238265
}
@@ -266,9 +293,10 @@ public String toString() {
266293
+ ", volumeName=" + volumeName + ", generateDmg=" + generateDmg + ", generatePkg=" + generatePkg
267294
+ ", relocateJar=" + relocateJar + ", appId=" + appId + ", developerId=" + developerId
268295
+ ", entitlements=" + entitlements + ", provisionProfile=" + provisionProfile + ", customLauncher="
269-
+ customLauncher + ", customInfoPlist=" + customInfoPlist + ", codesignApp=" + codesignApp
270-
+ ", infoPlist=" + infoPlist + ", hardenedCodesign=" + hardenedCodesign + ", macStartup=" + macStartup
271-
+ "]";
296+
+ customLauncher + ", customInfoPlist=" + customInfoPlist + ", customRuntimeInfoPlist="
297+
+ customRuntimeInfoPlist + ", codesignApp=" + codesignApp + ", notarizeApp=" + notarizeApp
298+
+ ", keyChainProfile=" + keyChainProfile + ", infoPlist=" + infoPlist + ", hardenedCodesign="
299+
+ hardenedCodesign + ", macStartup=" + macStartup + "]";
272300
}
273301

274302
/**

src/main/java/io/github/fvarrui/javapackager/packagers/MacPackager.java

Lines changed: 122 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,19 @@
99

1010
import java.io.File;
1111
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;
1217
import java.util.ArrayList;
1318
import java.util.Arrays;
1419
import java.util.Collection;
1520
import java.util.List;
1621
import 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
}

src/main/resources/mac/Info.plist.vtl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@
128128
<dict>
129129
#if ($info.bundleJre)
130130
<key>JAVA_HOME</key>
131-
<string>Contents/PlugIns/${info.jreDirectoryName}/Contents/Home</string>
131+
<string>Contents/PlugIns/${info.jreDirectoryName}.jre/Contents/Home</string>
132132
#end
133133
#if($info.envPath)
134134
<key>PATH</key>

0 commit comments

Comments
 (0)