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

Reviewed-by: asemenyuk
This commit is contained in:
Alexander Matveev 2025-07-18 20:44:20 +00:00
parent a3843e8e6e
commit 03230f8565
18 changed files with 480 additions and 67 deletions

View File

@ -44,7 +44,9 @@ record CodesignConfig(Optional<SigningIdentity> identity, Optional<String> ident
Objects.requireNonNull(keychain);
if (identity.isPresent() != identifierPrefix.isPresent()) {
throw new IllegalArgumentException("Signing identity and identifier prefix mismatch");
throw new IllegalArgumentException(
"Signing identity (" + identity + ") and identifier prefix (" +
identifierPrefix + ") mismatch");
}
identifierPrefix.ifPresent(v -> {

View File

@ -69,7 +69,7 @@ public class MacAppBundler extends AppImageBundler {
}
}
if (StandardBundlerParam.getPredefinedAppImage(params) != null) {
if (StandardBundlerParam.hasPredefinedAppImage(params)) {
if (!Optional.ofNullable(
SIGN_BUNDLE.fetchFrom(params)).orElse(Boolean.FALSE)) {
throw new ConfigException(

View File

@ -0,0 +1,79 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.jpackage.internal;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Objects;
import jdk.jpackage.internal.model.AppImageLayout;
/**
* An abstraction of macOS Application bundle.
*
* @see <a href="https://en.wikipedia.org/wiki/Bundle_(macOS)#Application_bundles">https://en.wikipedia.org/wiki/Bundle_(macOS)#Application_bundles</a>
*/
record MacBundle(Path root) {
MacBundle {
Objects.requireNonNull(root);
}
boolean isValid() {
return Files.isDirectory(contentsDir()) && Files.isDirectory(macOsDir()) && Files.isRegularFile(infoPlistFile());
}
boolean isSigned() {
return Files.isDirectory(contentsDir().resolve("_CodeSignature"));
}
Path contentsDir() {
return root.resolve("Contents");
}
Path homeDir() {
return contentsDir().resolve("Home");
}
Path macOsDir() {
return contentsDir().resolve("MacOS");
}
Path resourcesDir() {
return contentsDir().resolve("Resources");
}
Path infoPlistFile() {
return contentsDir().resolve("Info.plist");
}
static boolean isDirectoryMacBundle(Path dir) {
return new MacBundle(dir).isValid();
}
static MacBundle fromAppImageLayout(AppImageLayout layout) {
return new MacBundle(layout.rootDirectory());
}
}

View File

@ -147,7 +147,15 @@ final class MacFromParams {
signingBuilder.entitlementsResourceName("sandbox.plist");
}
app.mainLauncher().flatMap(Launcher::startupInfo).ifPresent(signingBuilder::signingIdentifierPrefix);
final var bundleIdentifier = appBuilder.create().bundleIdentifier();
app.mainLauncher().flatMap(Launcher::startupInfo).ifPresentOrElse(
signingBuilder::signingIdentifierPrefix,
() -> {
// Runtime installer does not have main launcher, so use
// 'bundleIdentifier' as prefix by default.
signingBuilder.signingIdentifierPrefix(
bundleIdentifier + ".");
});
SIGN_IDENTIFIER_PREFIX.copyInto(params, signingBuilder::signingIdentifierPrefix);
ENTITLEMENTS.copyInto(params, signingBuilder::entitlements);
@ -168,6 +176,12 @@ final class MacFromParams {
.map(MacAppImageFileExtras::signed)
.ifPresent(builder::predefinedAppImageSigned);
PREDEFINED_RUNTIME_IMAGE.findIn(params)
.map(MacBundle::new)
.filter(MacBundle::isValid)
.map(MacBundle::isSigned)
.ifPresent(builder::predefinedAppImageSigned);
return builder;
}

View File

@ -41,6 +41,7 @@ import java.io.IOException;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
@ -72,6 +73,7 @@ import jdk.jpackage.internal.model.MacPackage;
import jdk.jpackage.internal.model.Package;
import jdk.jpackage.internal.model.PackageType;
import jdk.jpackage.internal.model.PackagerException;
import jdk.jpackage.internal.util.FileUtils;
import jdk.jpackage.internal.util.PathUtils;
import jdk.jpackage.internal.util.function.ThrowingConsumer;
@ -91,6 +93,7 @@ final class MacPackagingPipeline {
enum MacCopyAppImageTaskID implements TaskID {
COPY_PACKAGE_FILE,
COPY_RUNTIME_INFO_PLIST,
COPY_RUNTIME_JLILIB,
REPLACE_APP_IMAGE_FILE,
COPY_SIGN
}
@ -115,10 +118,10 @@ final class MacPackagingPipeline {
.task(CopyAppImageTaskID.COPY)
.copyAction(MacPackagingPipeline::copyAppImage).add()
.task(MacBuildApplicationTaskID.RUNTIME_INFO_PLIST)
.applicationAction(MacPackagingPipeline::writeApplicationRuntimeInfoPlist)
.appImageAction(MacPackagingPipeline::writeRuntimeInfoPlist)
.addDependent(BuildApplicationTaskID.CONTENT).add()
.task(MacBuildApplicationTaskID.COPY_JLILIB)
.applicationAction(MacPackagingPipeline::copyJliLib)
.appImageAction(MacPackagingPipeline::copyJliLib)
.addDependency(BuildApplicationTaskID.RUNTIME)
.addDependent(BuildApplicationTaskID.CONTENT).add()
.task(MacBuildApplicationTaskID.APP_ICON)
@ -138,13 +141,18 @@ final class MacPackagingPipeline {
.addDependencies(CopyAppImageTaskID.COPY)
.addDependents(PrimaryTaskID.COPY_APP_IMAGE).add()
.task(MacCopyAppImageTaskID.COPY_RUNTIME_INFO_PLIST)
.appImageAction(MacPackagingPipeline::writeRuntimeInfoPlist)
.addDependencies(CopyAppImageTaskID.COPY)
.addDependents(PrimaryTaskID.COPY_APP_IMAGE).add()
.task(MacCopyAppImageTaskID.COPY_RUNTIME_JLILIB)
.noaction()
.addDependencies(CopyAppImageTaskID.COPY)
.addDependents(PrimaryTaskID.COPY_APP_IMAGE).add()
.task(MacBuildApplicationTaskID.FA_ICONS)
.applicationAction(MacPackagingPipeline::writeFileAssociationIcons)
.addDependent(BuildApplicationTaskID.CONTENT).add()
.task(MacBuildApplicationTaskID.APP_INFO_PLIST)
.applicationAction(MacPackagingPipeline::writeAppInfoPlist)
.applicationAction(MacPackagingPipeline::writeApplicationInfoPlist)
.addDependent(BuildApplicationTaskID.CONTENT).add();
builder.task(MacBuildApplicationTaskID.SIGN)
@ -172,16 +180,38 @@ final class MacPackagingPipeline {
disabledTasks.add(MacCopyAppImageTaskID.COPY_PACKAGE_FILE);
disabledTasks.add(CopyAppImageTaskID.COPY);
disabledTasks.add(PackageTaskID.RUN_POST_IMAGE_USER_SCRIPT);
builder.task(MacCopyAppImageTaskID.REPLACE_APP_IMAGE_FILE).applicationAction(createWriteAppImageFileAction()).add();
builder.task(MacCopyAppImageTaskID.REPLACE_APP_IMAGE_FILE)
.applicationAction(createWriteAppImageFileAction()).add();
builder.appImageLayoutForPackaging(Package::appImageLayout);
} else if (p.isRuntimeInstaller() || ((MacPackage)p).predefinedAppImageSigned().orElse(false)) {
// If this is a runtime package or a signed predefined app image,
// don't create ".package" file and don't sign it.
} else if (p.isRuntimeInstaller()) {
builder.task(MacCopyAppImageTaskID.COPY_RUNTIME_JLILIB)
.appImageAction(MacPackagingPipeline::copyJliLib).add();
final var predefinedRuntimeBundle = Optional.of(
new MacBundle(p.predefinedAppImage().orElseThrow())).filter(MacBundle::isValid);
// Don't create ".package" file.
disabledTasks.add(MacCopyAppImageTaskID.COPY_PACKAGE_FILE);
if (predefinedRuntimeBundle.isPresent()) {
// The predefined app image is a macOS bundle.
// Disable all alterations of the input bundle, but keep the signing enabled.
disabledTasks.addAll(List.of(MacCopyAppImageTaskID.values()));
disabledTasks.remove(MacCopyAppImageTaskID.COPY_SIGN);
}
if (predefinedRuntimeBundle.map(MacBundle::isSigned).orElse(false) && !((MacPackage)p).app().sign()) {
// The predefined app image is a signed bundle; explicit signing is not requested for the package.
// Disable the signing, i.e. don't re-sign the input bundle.
disabledTasks.add(MacCopyAppImageTaskID.COPY_SIGN);
}
} else if (((MacPackage)p).predefinedAppImageSigned().orElse(false)) {
// This is a signed predefined app image.
// Don't create ".package" file.
disabledTasks.add(MacCopyAppImageTaskID.COPY_PACKAGE_FILE);
// Don't sign the image.
disabledTasks.add(MacCopyAppImageTaskID.COPY_SIGN);
// if (p.isRuntimeInstaller()) {
// builder.task(MacCopyAppImageTaskID.COPY_RUNTIME_INFO_PLIST).packageAction(MacPackagingPipeline::writeRuntimeRuntimeInfoPlist).add();
// }
}
for (final var taskId : disabledTasks) {
@ -208,13 +238,27 @@ final class MacPackagingPipeline {
private static void copyAppImage(MacPackage pkg, AppImageDesc srcAppImage,
AppImageDesc dstAppImage) throws IOException {
PackagingPipeline.copyAppImage(srcAppImage, dstAppImage, !pkg.predefinedAppImageSigned().orElse(false));
boolean predefinedAppImageSigned = pkg.predefinedAppImageSigned().orElse(false);
var inputRootDirectory = srcAppImage.resolvedAppImagelayout().rootDirectory();
if (pkg.isRuntimeInstaller() && MacBundle.isDirectoryMacBundle(inputRootDirectory)) {
// Building runtime package from the input runtime bundle.
// Copy the input bundle verbatim.
FileUtils.copyRecursive(
inputRootDirectory,
dstAppImage.resolvedAppImagelayout().rootDirectory(),
LinkOption.NOFOLLOW_LINKS);
} else {
PackagingPipeline.copyAppImage(srcAppImage, dstAppImage, !predefinedAppImageSigned);
}
}
private static void copyJliLib(
AppImageBuildEnv<MacApplication, MacApplicationLayout> env) throws IOException {
AppImageBuildEnv<MacApplication, AppImageLayout> env) throws IOException {
final var runtimeMacOSDir = env.resolvedLayout().runtimeRootDirectory().resolve("Contents/MacOS");
final var runtimeBundle = runtimeBundle(env);
final var jliName = Path.of("libjli.dylib");
@ -223,8 +267,8 @@ final class MacPackagingPipeline {
.filter(file -> file.getFileName().equals(jliName))
.findFirst()
.orElseThrow();
Files.createDirectories(runtimeMacOSDir);
Files.copy(jli, runtimeMacOSDir.resolve(jliName));
Files.createDirectories(runtimeBundle.macOsDir());
Files.copy(jli, runtimeBundle.macOsDir().resolve(jliName));
}
}
@ -247,36 +291,47 @@ final class MacPackagingPipeline {
"APPL????".getBytes(StandardCharsets.ISO_8859_1));
}
private static void writeRuntimeRuntimeInfoPlist(PackageBuildEnv<MacPackage, AppImageLayout> env) throws IOException {
writeRuntimeInfoPlist(env.pkg().app(), env.env(), env.resolvedLayout().rootDirectory());
}
private static void writeRuntimeInfoPlist(
AppImageBuildEnv<MacApplication, AppImageLayout> env) throws IOException {
private static void writeApplicationRuntimeInfoPlist(
AppImageBuildEnv<MacApplication, MacApplicationLayout> env) throws IOException {
writeRuntimeInfoPlist(env.app(), env.env(), env.resolvedLayout().runtimeRootDirectory());
}
private static void writeRuntimeInfoPlist(MacApplication app, BuildEnv env, Path runtimeRootDirectory) throws IOException {
final var app = env.app();
Map<String, String> data = new HashMap<>();
data.put("CF_BUNDLE_IDENTIFIER", app.bundleIdentifier());
data.put("CF_BUNDLE_NAME", app.bundleName());
data.put("CF_BUNDLE_VERSION", app.version());
data.put("CF_BUNDLE_SHORT_VERSION_STRING", app.shortVersion().toString());
if (app.isRuntime()) {
data.put("CF_BUNDLE_VENDOR", app.vendor());
}
env.createResource("Runtime-Info.plist.template")
.setPublicName("Runtime-Info.plist")
.setCategory(I18N.getString("resource.runtime-info-plist"))
final String template;
final String publicName;
final String category;
if (app.isRuntime()) {
template = "Runtime-Info.plist.template";
publicName = "Info.plist";
category = "resource.runtime-info-plist";
} else {
template = "ApplicationRuntime-Info.plist.template";
publicName = "Runtime-Info.plist";
category = "resource.app-runtime-info-plist";
}
env.env().createResource(template)
.setPublicName(publicName)
.setCategory(I18N.getString(category))
.setSubstitutionData(data)
.saveToFile(runtimeRootDirectory.resolve("Contents/Info.plist"));
.saveToFile(runtimeBundle(env).infoPlistFile());
}
private static void writeAppInfoPlist(
private static void writeApplicationInfoPlist(
AppImageBuildEnv<MacApplication, MacApplicationLayout> env) throws IOException {
final var app = env.app();
final var infoPlistFile = env.resolvedLayout().contentDirectory().resolve("Info.plist");
final var infoPlistFile = MacBundle.fromAppImageLayout(env.resolvedLayout()).infoPlistFile();
Log.verbose(I18N.format("message.preparing-info-plist", PathUtils.normalizedAbsolutePathString(infoPlistFile)));
@ -308,7 +363,7 @@ final class MacPackagingPipeline {
.saveToFile(infoPlistFile);
}
private static void sign(AppImageBuildEnv<MacApplication, MacApplicationLayout> env) throws IOException {
private static void sign(AppImageBuildEnv<MacApplication, AppImageLayout> env) throws IOException {
final var app = env.app();
@ -410,6 +465,14 @@ final class MacPackagingPipeline {
}));
}
private static MacBundle runtimeBundle(AppImageBuildEnv<MacApplication, AppImageLayout> env) {
if (env.app().isRuntime()) {
return new MacBundle(env.resolvedLayout().rootDirectory());
} else {
return new MacBundle(((MacApplicationLayout)env.resolvedLayout()).runtimeRootDirectory());
}
}
private static class ApplicationIcon implements ApplicationImageTaskAction<MacApplication, MacApplicationLayout> {
static Path getPath(Application app, ApplicationLayout appLayout) {
return appLayout.desktopIntegrationDirectory().resolve(app.name() + ".icns");

View File

@ -54,11 +54,13 @@ public interface MacApplication extends Application, MacApplicationMixin {
@Override
default Path appImageDirName() {
final String suffix;
if (isRuntime()) {
return Application.super.appImageDirName();
suffix = ".jdk";
} else {
return Path.of(Application.super.appImageDirName().toString() + ".app");
suffix = ".app";
}
return Path.of(Application.super.appImageDirName().toString() + suffix);
}
/**

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleExecutable</key>
<string>libjli.dylib</string>
<key>CFBundleIdentifier</key>
<string>CF_BUNDLE_IDENTIFIER</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>7.0</string>
<key>CFBundleName</key>
<string>CF_BUNDLE_NAME</string>
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>CF_BUNDLE_SHORT_VERSION_STRING</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>CF_BUNDLE_VERSION</string>
</dict>
</plist>

View File

@ -41,6 +41,7 @@ error.app-image.mac-sign.required=Error: --mac-sign option is required with pred
error.tool.failed.with.output=Error: "{0}" failed with following output:
resource.bundle-config-file=Bundle config file
resource.app-info-plist=Application Info.plist
resource.app-runtime-info-plist=Embedded Java Runtime Info.plist
resource.runtime-info-plist=Java Runtime Info.plist
resource.entitlements=Mac Entitlements
resource.dmg-setup-script=DMG setup script

View File

@ -20,5 +20,20 @@
<string>????</string>
<key>CFBundleVersion</key>
<string>CF_BUNDLE_VERSION</string>
<key>NSMicrophoneUsageDescription</key>
<string>The application is requesting access to the microphone.</string>
<key>JavaVM</key>
<dict>
<key>JVMCapabilities</key>
<array>
<string>CommandLine</string>
</array>
<key>JVMPlatformVersion</key>
<string>CF_BUNDLE_VERSION</string>
<key>JVMVendor</key>
<string>CF_BUNDLE_VENDOR</string>
<key>JVMVersion</key>
<string>CF_BUNDLE_VERSION</string>
</dict>
</dict>
</plist>

View File

@ -41,12 +41,12 @@ import static jdk.jpackage.internal.StandardBundlerParam.LICENSE_FILE;
import static jdk.jpackage.internal.StandardBundlerParam.LIMIT_MODULES;
import static jdk.jpackage.internal.StandardBundlerParam.MODULE_PATH;
import static jdk.jpackage.internal.StandardBundlerParam.NAME;
import static jdk.jpackage.internal.StandardBundlerParam.PREDEFINED_APP_IMAGE;
import static jdk.jpackage.internal.StandardBundlerParam.PREDEFINED_APP_IMAGE_FILE;
import static jdk.jpackage.internal.StandardBundlerParam.PREDEFINED_RUNTIME_IMAGE;
import static jdk.jpackage.internal.StandardBundlerParam.SOURCE_DIR;
import static jdk.jpackage.internal.StandardBundlerParam.VENDOR;
import static jdk.jpackage.internal.StandardBundlerParam.VERSION;
import static jdk.jpackage.internal.StandardBundlerParam.getPredefinedAppImage;
import static jdk.jpackage.internal.StandardBundlerParam.hasPredefinedAppImage;
import static jdk.jpackage.internal.StandardBundlerParam.isRuntimeInstaller;
@ -143,7 +143,8 @@ final class FromParams {
VERSION.copyInto(params, builder::version);
ABOUT_URL.copyInto(params, builder::aboutURL);
LICENSE_FILE.findIn(params).map(Path::of).ifPresent(builder::licenseFile);
builder.predefinedAppImage(getPredefinedAppImage(params));
PREDEFINED_APP_IMAGE.findIn(params).ifPresent(builder::predefinedAppImage);
PREDEFINED_RUNTIME_IMAGE.findIn(params).ifPresent(builder::predefinedAppImage);
INSTALL_DIR.findIn(params).map(Path::of).ifPresent(builder::installDir);
return builder;

View File

@ -437,16 +437,8 @@ final class PackagingPipeline {
srcAppImageDesc = new AppImageDesc(appImageLayoutForPackaging, env.appImageDir());
dstAppImageDesc = srcAppImageDesc;
} else {
srcAppImageDesc = new AppImageDesc(pkg.app().imageLayout(), pkg.predefinedAppImage().orElseGet(() -> {
// No predefined app image and no runtime builder.
// This should be runtime packaging.
if (pkg.isRuntimeInstaller()) {
return env.appImageDir();
} else {
// Can't create app image without runtime builder.
throw new UnsupportedOperationException();
}
}));
srcAppImageDesc = new AppImageDesc(pkg.app().imageLayout(),
pkg.predefinedAppImage().orElseThrow(UnsupportedOperationException::new));
if (taskConfig.get(CopyAppImageTaskID.COPY).action().isEmpty()) {
// "copy app image" task action is undefined indicating

View File

@ -148,8 +148,16 @@ public interface Package extends BundleSpec {
Optional<Path> licenseFile();
/**
* Gets the path to a directory with the application app image of this package
* if available or an empty {@link Optional} instance otherwise.
* Gets the path to a directory with the predefined app image of this package if
* available or an empty {@link Optional} instance otherwise.
* <p>
* If {@link #isRuntimeInstaller()} returns {@code true}, the method returns the
* path to a directory with the predefined runtime. The layout of this directory
* should be of {@link RuntimeLayout} type.
* <p>
* If {@link #isRuntimeInstaller()} returns {@code false}, the method returns
* the path to a directory with the predefined application image. The layout of
* this directory should be of {@link ApplicationLayout} type.
*
* @return the path to a directory with the application app image of this
* package

View File

@ -1082,11 +1082,7 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
TKit.assertDirectoryExists(cmd.appRuntimeDirectory());
if (TKit.isOSX()) {
var libjliPath = cmd.appRuntimeDirectory().resolve("Contents/MacOS/libjli.dylib");
if (cmd.isRuntime()) {
TKit.assertPathExists(libjliPath, false);
} else {
TKit.assertFileExists(libjliPath);
}
TKit.assertFileExists(libjliPath);
}
}),
MAC_BUNDLE_STRUCTURE(cmd -> {

View File

@ -334,7 +334,7 @@ public final class MacHelper {
installLocation = cmd.getArgumentValue("--install-dir", () -> defaultInstallLocation, Path::of);
}
return installLocation.resolve(cmd.name() + (cmd.isRuntime() ? "" : ".app"));
return installLocation.resolve(cmd.name() + (cmd.isRuntime() ? ".jdk" : ".app"));
}
static Path getUninstallCommand(JPackageCommand cmd) {
@ -400,22 +400,27 @@ public final class MacHelper {
Executor.of("/usr/bin/xcrun", "--help").executeWithoutExitCodeCheck().getExitCode() == 0;
}
private static Set<Path> createBundleContents(String... customItems) {
return Stream.concat(Stream.of(customItems), Stream.of(
"MacOS",
"Info.plist",
"_CodeSignature"
)).map(Path::of).collect(toSet());
}
static final Set<Path> CRITICAL_RUNTIME_FILES = Set.of(Path.of(
"Contents/Home/lib/server/libjvm.dylib"));
private static final Method getServicePListFileName = initGetServicePListFileName();
private static final Set<Path> APP_BUNDLE_CONTENTS = Stream.of(
"Info.plist",
"MacOS",
private static final Set<Path> APP_BUNDLE_CONTENTS = createBundleContents(
"app",
"runtime",
"Resources",
"PkgInfo",
"_CodeSignature"
).map(Path::of).collect(toSet());
"PkgInfo"
);
private static final Set<Path> RUNTIME_BUNDLE_CONTENTS = Stream.of(
private static final Set<Path> RUNTIME_BUNDLE_CONTENTS = createBundleContents(
"Home"
).map(Path::of).collect(toSet());
);
}

View File

@ -41,7 +41,7 @@ import jdk.jpackage.test.Annotations.Parameter;
* jpackagerTest keychain with always allowed access to this keychain for user
* which runs test.
* note:
* "jpackage.openjdk.java.net" can be over-ridden by systerm property
* "jpackage.openjdk.java.net" can be over-ridden by system property
* "jpackage.mac.signing.key.user.name", and
* "jpackagerTest" can be over-ridden by system property
* "jpackage.mac.signing.keychain"

View File

@ -39,7 +39,7 @@ import jdk.jpackage.test.Annotations.Parameter;
* jpackagerTest keychain with
* always allowed access to this keychain for user which runs test.
* note:
* "jpackage.openjdk.java.net" can be over-ridden by systerm property
* "jpackage.openjdk.java.net" can be over-ridden by system property
* "jpackage.mac.signing.key.user.name", and
* "jpackagerTest" can be over-ridden by system property
* "jpackage.mac.signing.keychain"

View File

@ -42,7 +42,7 @@ import jdk.jpackage.test.Annotations.Parameter;
* jpackagerTest keychain with
* always allowed access to this keychain for user which runs test.
* note:
* "jpackage.openjdk.java.net" can be over-ridden by systerm property
* "jpackage.openjdk.java.net" can be over-ridden by system property
* "jpackage.mac.signing.key.user.name", and
* "jpackagerTest" can be over-ridden by system property
* "jpackage.mac.signing.keychain"

View File

@ -0,0 +1,211 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
import java.io.IOException;
import java.nio.file.Path;
import java.util.function.Predicate;
import java.util.stream.Stream;
import jdk.jpackage.test.Annotations.Parameter;
import jdk.jpackage.test.Annotations.Test;
import jdk.jpackage.test.Executor;
import jdk.jpackage.test.JPackageCommand;
import jdk.jpackage.test.JavaTool;
import jdk.jpackage.test.MacHelper;
import jdk.jpackage.test.PackageTest;
import jdk.jpackage.test.PackageType;
import jdk.jpackage.test.TKit;
/**
* Tests generation of dmg and pkg with --mac-sign and related arguments.
* Test will generate pkg and verifies its signature. It verifies that dmg
* is not signed, but runtime image inside dmg is signed.
*
* Note: Specific UNICODE signing is not tested, since it is shared code
* with app image signing and it will be covered by SigningPackageTest.
*
* Following combinations are tested:
* 1) "--runtime-image" points to unsigned JDK bundle and --mac-sign is not
* provided. Expected result: runtime image ad-hoc signed.
* 2) "--runtime-image" points to unsigned JDK bundle and --mac-sign is
* provided. Expected result: Everything is signed with provided certificate.
* 3) "--runtime-image" points to signed JDK bundle and --mac-sign is not
* provided. Expected result: runtime image is signed with original certificate.
* 4) "--runtime-image" points to signed JDK bundle and --mac-sign is provided.
* Expected result: runtime image is signed with provided certificate.
* 5) "--runtime-image" points to JDK image and --mac-sign is not provided.
* Expected result: runtime image ad-hoc signed.
* 6) "--runtime-image" points to JDK image and --mac-sign is provided.
* Expected result: Everything is signed with provided certificate.
*
* This test requires that the machine is configured with test certificate for
* "Developer ID Installer: jpackage.openjdk.java.net" in
* jpackagerTest keychain with
* always allowed access to this keychain for user which runs test.
* note:
* "jpackage.openjdk.java.net" can be over-ridden by system property
* "jpackage.mac.signing.key.user.name", and
* "jpackagerTest" can be over-ridden by system property
* "jpackage.mac.signing.keychain"
*/
/*
* @test
* @summary jpackage with --type pkg,dmg --runtime-image --mac-sign
* @library /test/jdk/tools/jpackage/helpers
* @library base
* @key jpackagePlatformPackage
* @build SigningBase
* @build jdk.jpackage.test.*
* @build SigningRuntimeImagePackageTest
* @requires (jpackage.test.MacSignTests == "run")
* @run main/othervm/timeout=720 -Xmx512m jdk.jpackage.test.Main
* --jpt-run=SigningRuntimeImagePackageTest
* --jpt-before-run=SigningBase.verifySignTestEnvReady
*/
public class SigningRuntimeImagePackageTest {
private static JPackageCommand addSignOptions(JPackageCommand cmd, int certIndex) {
if (certIndex != SigningBase.CertIndex.INVALID_INDEX.value()) {
cmd.addArguments(
"--mac-sign",
"--mac-signing-keychain", SigningBase.getKeyChain(),
"--mac-signing-key-user-name", SigningBase.getDevName(certIndex));
}
return cmd;
}
private static Path createInputRuntimeImage() throws IOException {
final Path runtimeImageDir;
if (JPackageCommand.DEFAULT_RUNTIME_IMAGE != null) {
runtimeImageDir = JPackageCommand.DEFAULT_RUNTIME_IMAGE;
} else {
runtimeImageDir = TKit.createTempDirectory("runtime-image").resolve("data");
new Executor().setToolProvider(JavaTool.JLINK)
.dumpOutput()
.addArguments(
"--output", runtimeImageDir.toString(),
"--add-modules", "java.desktop",
"--strip-debug",
"--no-header-files",
"--no-man-pages")
.execute();
}
return runtimeImageDir;
}
private static Path createInputRuntimeBundle(int certIndex) throws IOException {
final var runtimeImage = createInputRuntimeImage();
final var runtimeBundleWorkDir = TKit.createTempDirectory("runtime-bundle");
final var unpackadeRuntimeBundleDir = runtimeBundleWorkDir.resolve("unpacked");
var cmd = new JPackageCommand()
.useToolProvider(true)
.ignoreDefaultRuntime(true)
.dumpOutput(true)
.setPackageType(PackageType.MAC_DMG)
.setArgumentValue("--name", "foo")
.addArguments("--runtime-image", runtimeImage)
.addArguments("--dest", runtimeBundleWorkDir);
addSignOptions(cmd, certIndex);
cmd.execute();
MacHelper.withExplodedDmg(cmd, dmgImage -> {
if (dmgImage.endsWith(cmd.appInstallationDirectory().getFileName())) {
Executor.of("cp", "-R")
.addArgument(dmgImage)
.addArgument(unpackadeRuntimeBundleDir)
.execute(0);
}
});
return unpackadeRuntimeBundleDir;
}
@Test
// useJDKBundle - If "true" predefined runtime image will be converted to
// JDK bundle. If "false" JDK image will be used.
// JDKBundleCert - Certificate to sign JDK bundle before calling jpackage.
// signCert - Certificate to sign bundle produced by jpackage.
// 1) unsigned JDK bundle and --mac-sign is not provided
@Parameter({"true", "INVALID_INDEX", "INVALID_INDEX"})
// 2) unsigned JDK bundle and --mac-sign is provided
@Parameter({"true", "INVALID_INDEX", "ASCII_INDEX"})
// 3) signed JDK bundle and --mac-sign is not provided
@Parameter({"true", "UNICODE_INDEX", "INVALID_INDEX"})
// 4) signed JDK bundle and --mac-sign is provided
@Parameter({"true", "UNICODE_INDEX", "ASCII_INDEX"})
// 5) JDK image and --mac-sign is not provided
@Parameter({"false", "INVALID_INDEX", "INVALID_INDEX"})
// 6) JDK image and --mac-sign is provided
@Parameter({"false", "INVALID_INDEX", "ASCII_INDEX"})
public static void test(boolean useJDKBundle,
SigningBase.CertIndex jdkBundleCert,
SigningBase.CertIndex signCert) throws Exception {
final Path inputRuntime[] = new Path[1];
new PackageTest()
.addRunOnceInitializer(() -> {
if (useJDKBundle) {
inputRuntime[0] = createInputRuntimeBundle(jdkBundleCert.value());
} else {
inputRuntime[0] = createInputRuntimeImage();
}
})
.addInitializer(cmd -> {
cmd.addArguments("--runtime-image", inputRuntime[0]);
// Remove --input parameter from jpackage command line as we don't
// create input directory in the test and jpackage fails
// if --input references non existent directory.
cmd.removeArgumentWithValue("--input");
addSignOptions(cmd, signCert.value());
})
.addInstallVerifier(cmd -> {
final var certIndex = Stream.of(signCert, jdkBundleCert)
.filter(Predicate.isEqual(SigningBase.CertIndex.INVALID_INDEX).negate())
.findFirst().orElse(SigningBase.CertIndex.INVALID_INDEX).value();
final var signed = certIndex != SigningBase.CertIndex.INVALID_INDEX.value();
final var unfoldedBundleDir = cmd.appRuntimeDirectory();
final var libjli = unfoldedBundleDir.resolve("Contents/MacOS/libjli.dylib");
SigningBase.verifyCodesign(libjli, signed, certIndex);
SigningBase.verifyCodesign(unfoldedBundleDir, signed, certIndex);
if (signed) {
SigningBase.verifySpctl(unfoldedBundleDir, "exec", certIndex);
}
})
.run();
}
}