Skip to content

8351073: [macos] jpackage produces invalid Java runtime DMG bundles #25314

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
import static jdk.jpackage.internal.StandardBundlerParam.MAIN_CLASS;
import static jdk.jpackage.internal.StandardBundlerParam.PREDEFINED_APP_IMAGE;
import static jdk.jpackage.internal.StandardBundlerParam.VERSION;
import static jdk.jpackage.internal.StandardBundlerParam.VENDOR;
import static jdk.jpackage.internal.StandardBundlerParam.ADD_LAUNCHERS;
import static jdk.jpackage.internal.StandardBundlerParam.SIGN_BUNDLE;
import static jdk.jpackage.internal.StandardBundlerParam.APP_STORE;
Expand All @@ -86,6 +87,8 @@ public class MacAppImageBuilder extends AbstractAppImageBuilder {
"Info-lite.plist.template";
private static final String TEMPLATE_RUNTIME_INFO_PLIST =
"Runtime-Info.plist.template";
private static final String TEMPLATE_RUNTIMEIMAGE_INFO_PLIST =
"RuntimeImage-Info.plist.template";

private final Path root;
private final Path contentsDir;
Expand Down Expand Up @@ -287,7 +290,7 @@ public void prepareApplicationFiles(Map<String, ? super Object> params)
}

try {
doSigning(params);
doSigning(params, root, true);
} catch (Exception ex) {
// Restore original app image file if signing failed
if (appImageFile != null) {
Expand Down Expand Up @@ -366,7 +369,7 @@ public void prepareApplicationFiles(Map<String, ? super Object> params)

copyRuntimeFiles(params);

doSigning(params);
doSigning(params, root, true);
}

private void copyRuntimeFiles(Map<String, ? super Object> params)
Expand All @@ -392,8 +395,8 @@ private void copyRuntimeFiles(Map<String, ? super Object> params)
}
}

private void doSigning(Map<String, ? super Object> params)
throws IOException {
public static void doSigning(Map<String, ? super Object> params, Path root,
boolean isAppImage) throws IOException {

if (Optional.ofNullable(
SIGN_BUNDLE.fetchFrom(params)).orElse(Boolean.TRUE)) {
Expand All @@ -415,24 +418,34 @@ private void doSigning(Map<String, ? super Object> params)
}
}
if (signingIdentity != null) {
signAppBundle(params, root, signingIdentity,
if (isAppImage) {
signAppBundle(params, root, signingIdentity,
BUNDLE_ID_SIGNING_PREFIX.fetchFrom(params),
ENTITLEMENTS.fetchFrom(params));
} else {
signRuntimeOrFrameworkBundle(params, root, signingIdentity,
BUNDLE_ID_SIGNING_PREFIX.fetchFrom(params),
ENTITLEMENTS.fetchFrom(params));
}
} else {
// Case when user requested to sign installer only
signAppBundle(params, root, "-", null, null);
}
restoreKeychainList(params);
} else {
signAppBundle(params, root, "-", null, null);
if (isAppImage) {
signAppBundle(params, root, "-", null, null);
} else {
signRuntimeOrFrameworkBundle(params, root, "-", null, null);
}
}
}

private static String getLauncherName(Map<String, ? super Object> params) {
return APP_NAME.fetchFrom(params);
}

private String getBundleName(Map<String, ? super Object> params) {
private static String getBundleName(Map<String, ? super Object> params) {
if (MAC_CF_BUNDLE_NAME.fetchFrom(params) != null) {
String bn = MAC_CF_BUNDLE_NAME.fetchFrom(params);
if (bn.length() > 16) {
Expand Down Expand Up @@ -481,6 +494,34 @@ private void writeRuntimeInfoPlist(Path file,
.saveToFile(file);
}

static void writeRuntimeImageInfoPlist(Path file,
Map<String, ? super Object> params) throws IOException {
Map<String, String> data = new HashMap<>();
String identifier = MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params);
data.put("CF_BUNDLE_IDENTIFIER", identifier);
String name = getBundleName(params);
data.put("CF_BUNDLE_NAME", name);
String ver = VERSION.fetchFrom(params);
String sver = ver;
int index = ver.indexOf(".");
if (index > 0 && ((index + 1) < ver.length())) {
index = ver.indexOf(".", index + 1);
if (index > 0 ) {
sver = ver.substring(0, index);
}
}
data.put("CF_BUNDLE_VERSION", ver);
data.put("CF_BUNDLE_SHORT_VERSION_STRING", sver);
String ven = VENDOR.fetchFrom(params);
data.put("CF_BUNDLE_VENDOR", ven);

createResource(TEMPLATE_RUNTIMEIMAGE_INFO_PLIST, params)
.setPublicName("RuntimeImage-Info.plist")
.setCategory(I18N.getString("resource.runtime-bundle-info-plist"))
.setSubstitutionData(data)
.saveToFile(file);
}

private void writeStringArrayPlist(StringBuilder sb, String key,
List<String> values) {
if (values != null && !values.isEmpty()) {
Expand Down Expand Up @@ -779,6 +820,36 @@ private static boolean isXcodeDevToolsInstalled() {
return true;
}

public static void signRuntimeOrFrameworkBundle(
Map<String, ? super Object> params, Path pathToSign,
String signingIdentity, String identifierPrefix, Path entitlements)
throws IOException {
AtomicReference<IOException> toThrow = new AtomicReference<>();
String keyChain = SIGNING_KEYCHAIN.fetchFrom(params);

Consumer<? super Path> signIdentifiedByPList = path -> {
// noinspection ThrowableResultOfMethodCallIgnored
if (toThrow.get() != null)
return;

try {
List<String> args = getCodesignArgs(true, path, signingIdentity,
identifierPrefix, entitlements, keyChain);
ProcessBuilder pb = new ProcessBuilder(args);
runCodesign(pb, false, params);
} catch (IOException e) {
toThrow.set(e);
}
};

signIdentifiedByPList.accept(pathToSign);

IOException ioe = toThrow.get();
if (ioe != null) {
throw ioe;
}
}

static void signAppBundle(
Map<String, ? super Object> params, Path appLocation,
String signingIdentity, String identifierPrefix, Path entitlements)
Expand Down Expand Up @@ -864,34 +935,22 @@ static void signAppBundle(
return;
}

// sign all runtime and frameworks
Consumer<? super Path> signIdentifiedByPList = path -> {
//noinspection ThrowableResultOfMethodCallIgnored
if (toThrow.get() != null) return;

try {
List<String> args = getCodesignArgs(true, path, signingIdentity,
identifierPrefix, entitlements, keyChain);
ProcessBuilder pb = new ProcessBuilder(args);
runCodesign(pb, false, params);
} catch (IOException e) {
toThrow.set(e);
}
};

Path javaPath = appLocation.resolve("Contents/runtime");
if (Files.isDirectory(javaPath)) {
signIdentifiedByPList.accept(javaPath);

ioe = toThrow.get();
if (ioe != null) {
throw ioe;
}
signRuntimeOrFrameworkBundle(params, javaPath, signingIdentity,
identifierPrefix, entitlements);
}
Path frameworkPath = appLocation.resolve("Contents/Frameworks");
if (Files.isDirectory(frameworkPath)) {
try (var fileList = Files.list(frameworkPath)) {
fileList.forEach(signIdentifiedByPList);
fileList.forEach(pathToSign -> {
try {
signRuntimeOrFrameworkBundle(params, pathToSign,
signingIdentity, identifierPrefix, entitlements);
} catch (IOException ioe2) {
toThrow.set(ioe2);
}
});
}

ioe = toThrow.get();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,20 @@
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.NoSuchElementException;
import java.util.stream.Stream;

import static jdk.jpackage.internal.StandardBundlerParam.APP_NAME;
import static jdk.jpackage.internal.StandardBundlerParam.INSTALLER_NAME;
import static jdk.jpackage.internal.StandardBundlerParam.INSTALL_DIR;
import static jdk.jpackage.internal.StandardBundlerParam.PREDEFINED_APP_IMAGE;
import static jdk.jpackage.internal.StandardBundlerParam.PREDEFINED_RUNTIME_IMAGE;
import static jdk.jpackage.internal.StandardBundlerParam.VERSION;
import static jdk.jpackage.internal.StandardBundlerParam.SIGN_BUNDLE;
import static jdk.jpackage.internal.StandardBundlerParam.TEMP_ROOT;

import jdk.jpackage.internal.model.ConfigException;
import jdk.jpackage.internal.model.PackagerException;
Expand Down Expand Up @@ -160,6 +166,23 @@ protected void validateAppImageAndBundeler(
"warning.unsigned.app.image"), getID()));
}
}
} else if (StandardBundlerParam.isRuntimeInstaller(params)) {
// Call appImageBundler.validate(params); to validate signing
// requirements.
appImageBundler.validate(params);

Path runtimeImage = PREDEFINED_RUNTIME_IMAGE.fetchFrom(params);

// Make sure we have valid runtime image.
if (!isRuntimeImageJDKBundle(runtimeImage)
&& !isRuntimeImageJDKImage(runtimeImage)) {
throw new ConfigException(
MessageFormat.format(I18N.getString(
"message.runtime-image-invalid"),
runtimeImage.toString()),
I18N.getString(
"message.runtime-image-invalid.advice"));
}
} else {
appImageBundler.validate(params);
}
Expand All @@ -171,6 +194,8 @@ protected Path prepareAppBundle(Map<String, ? super Object> params)
Path appImageRoot = APP_IMAGE_TEMP_ROOT.fetchFrom(params);
Path predefinedImage =
StandardBundlerParam.getPredefinedAppImage(params);
Path runtimeImage =
PREDEFINED_RUNTIME_IMAGE.fetchFrom(params);
if (predefinedImage != null) {
appDir = appImageRoot.resolve(APP_NAME.fetchFrom(params) + ".app");
FileUtils.copyRecursive(predefinedImage, appDir,
Expand All @@ -188,13 +213,95 @@ protected Path prepareAppBundle(Map<String, ? super Object> params)
// need to re-sign it after modification.
MacAppImageBuilder.signAppBundle(params, appDir, "-", null, null);
}
} else if (StandardBundlerParam.isRuntimeInstaller(params)) {
if (isRuntimeImageJDKBundle(runtimeImage)) {
appDir = runtimeImage;
} else {
// It is a valid JDK image, so convert it to JDK bundle.
Path jdkBundleRoot = Files.createTempDirectory(TEMP_ROOT.fetchFrom(params),
"root-");

convertJDKImageToJDKBundle(jdkBundleRoot, runtimeImage, params);

appDir = jdkBundleRoot;
}

// Figure out if we need to sign
boolean signingRequested = Optional.ofNullable(
SIGN_BUNDLE.fetchFrom(params)).orElse(Boolean.FALSE);

// Check if bundle is already signed
Path codeSignature = appDir.resolve("Contents/_CodeSignature");
boolean bundleIsSigned = IOUtils.exists(codeSignature);

// We do signing when it is requested or if bundle is not signed to
// create ad-hoc signature
if (signingRequested || !bundleIsSigned) {
MacAppImageBuilder.doSigning(params, appDir, false);
}
} else {
appDir = appImageBundler.execute(params, appImageRoot);
}

return appDir;
}

public static void convertJDKImageToJDKBundle(Path jdkBundleRoot,
Path runtimeImage,
Map<String, ? super Object> params) throws IOException {
Path path1 = jdkBundleRoot.resolve("Contents/Home");
Files.createDirectories(path1);
FileUtils.copyRecursive(runtimeImage, path1);

// Copy libjli.dylib library
Path path2 = Files.createDirectories(
jdkBundleRoot.resolve("Contents/MacOS"));

final Path jliName = Path.of("libjli.dylib");
try (Stream<Path> walk = Files.walk(runtimeImage.resolve("lib"))) {
final Path jli = walk
.filter(file -> file.getFileName().equals(jliName))
.findFirst()
.get();
Files.copy(jli, path2.resolve(jliName));
}

MacAppImageBuilder.writeRuntimeImageInfoPlist(
jdkBundleRoot.resolve("Contents/Info.plist"), params);
}

// JDK bundle: "Contents/Home", "Contents/MacOS/libjli.dylib"
// and "Contents/Info.plist"
private boolean isRuntimeImageJDKBundle(Path runtimeImage) {
Path path1 = runtimeImage.resolve("Contents/Home");
Path path2 = runtimeImage.resolve("Contents/MacOS/libjli.dylib");
Path path3 = runtimeImage.resolve("Contents/Info.plist");
if (IOUtils.exists(path1)
&& path1.toFile().list() != null
&& path1.toFile().list().length > 0
&& IOUtils.exists(path2)
&& IOUtils.exists(path3)) {
return true;
}

return false;
}

// JDK image: "lib/*/libjli.dylib"
private boolean isRuntimeImageJDKImage(Path runtimeImage) {
final Path jliName = Path.of("libjli.dylib");
try (Stream<Path> walk = Files.walk(runtimeImage.resolve("lib"))) {
final Path jli = walk
.filter(file -> file.getFileName().equals(jliName))
.findFirst()
.get();
return IOUtils.exists(jli);
} catch (IOException | NoSuchElementException ex) {
Log.verbose(ex);
return false;
}
}

@Override
public String getBundleType() {
return "INSTALLER";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,16 @@ private void prepareDMGSetupScript(Path appLocation,
data.put("DEPLOY_BG_FILE", bgFile.toString());
data.put("DEPLOY_VOLUME_PATH", volumePath.toString());
data.put("DEPLOY_APPLICATION_NAME", APP_NAME.fetchFrom(params));
String targetItem = (StandardBundlerParam.isRuntimeInstaller(params)) ?
APP_NAME.fetchFrom(params) : appLocation.getFileName().toString();
String targetItem = null;
if (StandardBundlerParam.isRuntimeInstaller(params)) {
if (APP_NAME.fetchFrom(params).endsWith(".jdk")) {
targetItem = APP_NAME.fetchFrom(params);
} else {
targetItem = APP_NAME.fetchFrom(params).concat(".jdk");
}
} else {
targetItem = appLocation.getFileName().toString();
}
data.put("DEPLOY_TARGET", targetItem);
data.put("DEPLOY_INSTALL_LOCATION", getInstallDir(params, true));
data.put("DEPLOY_INSTALL_LOCATION_DISPLAY_NAME",
Expand Down Expand Up @@ -275,17 +283,16 @@ private Path buildDMG( Map<String, ? super Object> params,
Path newRoot = Files.createTempDirectory(TEMP_ROOT.fetchFrom(params),
"root-");

// first, is this already a runtime with
// <runtime>/Contents/Home - if so we need the Home dir
Path home = appLocation.resolve("Contents/Home");
Path source = (Files.exists(home)) ? home : appLocation;

// Then we need to put back the <NAME>/Content/Home
Path root = newRoot.resolve(
// We need to copy entire runtime folder as provided to include
// .plist and signing files.
Path dest = newRoot.resolve(
MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params));
Path dest = root.resolve("Contents/Home");
// Add .jdk if needed.
if (!dest.getFileName().endsWith(".jdk")) {
dest = dest.resolveSibling(dest.getFileName() + ".jdk");
}

FileUtils.copyRecursive(source, dest);
FileUtils.copyRecursive(appLocation, dest);

srcFolder = newRoot;
}
Expand Down
Loading