From d880fcf73f1c6e7d2722c970b5671726d65e7665 Mon Sep 17 00:00:00 2001 From: alexander_matveev Date: Mon, 7 Jul 2025 15:14:00 -0700 Subject: [PATCH] 8351073: [macos] jpackage produces invalid Java runtime DMG bundles --- .../jdk/jpackage/internal/CodesignConfig.java | 4 +- .../internal/MacApplicationBuilder.java | 2 +- .../internal/MacBaseInstallerBundler.java | 49 ++++ .../jdk/jpackage/internal/MacFromParams.java | 14 +- .../internal/MacPackagingPipeline.java | 95 +++++-- .../internal/model/MacApplication.java | 6 +- .../jpackage/internal/model/MacPackage.java | 33 ++- .../resources/MacResources.properties | 3 + .../RuntimeBundle-Info.plist.template | 39 +++ .../jpackage/internal/ApplicationBuilder.java | 7 + .../jdk/jpackage/internal/FromParams.java | 1 + .../jpackage/internal/model/Application.java | 13 +- .../jdk/jpackage/test/JPackageCommand.java | 13 +- .../helpers/jdk/jpackage/test/MacHelper.java | 7 +- .../jpackage/internal/AppImageFileTest.java | 2 +- .../SigningRuntimeImagePackageTest.java | 263 ++++++++++++++++++ test/jdk/tools/jpackage/share/ErrorTest.java | 41 ++- 17 files changed, 558 insertions(+), 34 deletions(-) create mode 100644 src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/resources/RuntimeBundle-Info.plist.template create mode 100644 test/jdk/tools/jpackage/macosx/SigningRuntimeImagePackageTest.java diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/CodesignConfig.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/CodesignConfig.java index b59e6c8ad0017..7280f49562c85 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/CodesignConfig.java +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/CodesignConfig.java @@ -44,7 +44,9 @@ record CodesignConfig(Optional identity, Optional 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 -> { diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacApplicationBuilder.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacApplicationBuilder.java index ca05be519ff50..b34faaa8c897e 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacApplicationBuilder.java +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacApplicationBuilder.java @@ -181,7 +181,7 @@ private String validatedBundleName() throws ConfigException { return value; } - private String validatedBundleIdentifier() throws ConfigException { + public String validatedBundleIdentifier() throws ConfigException { final var value = Optional.ofNullable(bundleIdentifier).orElseGet(() -> { return app.mainLauncher() .flatMap(Launcher::startupInfo) diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacBaseInstallerBundler.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacBaseInstallerBundler.java index f46b5a328fd85..af3dcbab9ba2e 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacBaseInstallerBundler.java +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacBaseInstallerBundler.java @@ -27,13 +27,17 @@ 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.SIGN_BUNDLE; +import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.text.MessageFormat; import java.util.Map; +import java.util.NoSuchElementException; import java.util.Optional; +import java.util.stream.Stream; import jdk.jpackage.internal.model.ConfigException; public abstract class MacBaseInstallerBundler extends AbstractBundler { @@ -63,6 +67,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); } @@ -73,5 +94,33 @@ public String getBundleType() { return "INSTALLER"; } + // JDK bundle: "Contents/Home", "Contents/MacOS/libjli.dylib" + // and "Contents/Info.plist" + private static 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"); + return IOUtils.exists(path1) + && path1.toFile().list() != null + && path1.toFile().list().length > 0 + && IOUtils.exists(path2) + && IOUtils.exists(path3); + } + + // JDK image: "lib/*/libjli.dylib" + static boolean isRuntimeImageJDKImage(Path runtimeImage) { + final Path jliName = Path.of("libjli.dylib"); + try (Stream 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; + } + } + private final Bundler appImageBundler; } diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacFromParams.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacFromParams.java index c13b9d939df5d..e15fa3991e980 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacFromParams.java +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacFromParams.java @@ -43,6 +43,7 @@ import static jdk.jpackage.internal.model.StandardPackageType.MAC_DMG; import static jdk.jpackage.internal.model.StandardPackageType.MAC_PKG; import static jdk.jpackage.internal.util.function.ThrowingFunction.toFunction; +import static jdk.jpackage.internal.StandardBundlerParam.isRuntimeInstaller; import java.io.IOException; import java.nio.file.Files; @@ -76,7 +77,8 @@ private static MacApplication createMacApplication( Map params) throws ConfigException, IOException { final var predefinedRuntimeLayout = PREDEFINED_RUNTIME_IMAGE.findIn(params).map(predefinedRuntimeImage -> { - if (Files.isDirectory(RUNTIME_PACKAGE_LAYOUT.resolveAt(predefinedRuntimeImage).runtimeDirectory())) { + if (!isRuntimeInstaller(params) && + Files.isDirectory(RUNTIME_PACKAGE_LAYOUT.resolveAt(predefinedRuntimeImage).runtimeDirectory())) { return RUNTIME_PACKAGE_LAYOUT; } else { return RuntimeLayout.DEFAULT; @@ -147,7 +149,15 @@ private static MacApplication createMacApplication( signingBuilder.entitlementsResourceName("sandbox.plist"); } - app.mainLauncher().flatMap(Launcher::startupInfo).ifPresent(signingBuilder::signingIdentifierPrefix); + final var bundleIdentifier = appBuilder.validatedBundleIdentifier(); + 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); diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPackagingPipeline.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPackagingPipeline.java index eea69825a496d..f6841f820dbb0 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPackagingPipeline.java +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPackagingPipeline.java @@ -36,6 +36,7 @@ import static jdk.jpackage.internal.util.XmlUtils.toXmlConsumer; import static jdk.jpackage.internal.util.function.ThrowingBiConsumer.toBiConsumer; import static jdk.jpackage.internal.util.function.ThrowingSupplier.toSupplier; +import static jdk.jpackage.internal.model.MacPackage.RUNTIME_PACKAGE_LAYOUT; import java.io.IOException; import java.io.StringWriter; @@ -71,6 +72,7 @@ import jdk.jpackage.internal.model.MacPackage; import jdk.jpackage.internal.model.Package; import jdk.jpackage.internal.model.PackageType; +import jdk.jpackage.internal.model.RuntimeLayout; import jdk.jpackage.internal.model.PackagerException; import jdk.jpackage.internal.util.PathUtils; import jdk.jpackage.internal.util.function.ThrowingConsumer; @@ -91,12 +93,22 @@ enum MacBuildApplicationTaskID implements TaskID { enum MacCopyAppImageTaskID implements TaskID { COPY_PACKAGE_FILE, COPY_RUNTIME_INFO_PLIST, + COPY_RUNTIME_JLILIB, REPLACE_APP_IMAGE_FILE, - COPY_SIGN + COPY_SIGN, + SIGN_RUNTIME_BUNDLE } static AppImageLayout packagingLayout(Package pkg) { - return pkg.appImageLayout().resolveAt(pkg.relativeInstallDir().getFileName()); + if (pkg.isRuntimeInstaller()) { + if (((MacPackage)pkg).isRuntimeImageJDKImage()) { + return RUNTIME_PACKAGE_LAYOUT.resolveAt(pkg.relativeInstallDir().getFileName()); + } else { + return RuntimeLayout.DEFAULT.resolveAt(pkg.relativeInstallDir().getFileName()); + } + } else { + return pkg.appImageLayout().resolveAt(pkg.relativeInstallDir().getFileName()); + } } static PackagingPipeline.Builder build(Optional pkg) { @@ -118,7 +130,7 @@ static PackagingPipeline.Builder build(Optional pkg) { .applicationAction(MacPackagingPipeline::writeApplicationRuntimeInfoPlist) .addDependent(BuildApplicationTaskID.CONTENT).add() .task(MacBuildApplicationTaskID.COPY_JLILIB) - .applicationAction(MacPackagingPipeline::copyJliLib) + .applicationAction(MacPackagingPipeline::copyApplicationRuntimeJliLib) .addDependency(BuildApplicationTaskID.RUNTIME) .addDependent(BuildApplicationTaskID.CONTENT).add() .task(MacBuildApplicationTaskID.APP_ICON) @@ -140,6 +152,12 @@ static PackagingPipeline.Builder build(Optional pkg) { .task(MacCopyAppImageTaskID.COPY_RUNTIME_INFO_PLIST) .addDependencies(CopyAppImageTaskID.COPY) .addDependents(PrimaryTaskID.COPY_APP_IMAGE).add() + .task(MacCopyAppImageTaskID.COPY_RUNTIME_JLILIB) + .addDependencies(CopyAppImageTaskID.COPY) + .addDependents(PrimaryTaskID.COPY_APP_IMAGE).add() + .task(MacCopyAppImageTaskID.SIGN_RUNTIME_BUNDLE) + .addDependencies(CopyAppImageTaskID.COPY) + .addDependents(PrimaryTaskID.COPY_APP_IMAGE).add() .task(MacBuildApplicationTaskID.FA_ICONS) .applicationAction(MacPackagingPipeline::writeFileAssociationIcons) .addDependent(BuildApplicationTaskID.CONTENT).add() @@ -148,13 +166,13 @@ static PackagingPipeline.Builder build(Optional pkg) { .addDependent(BuildApplicationTaskID.CONTENT).add(); builder.task(MacBuildApplicationTaskID.SIGN) - .appImageAction(MacPackagingPipeline::sign) + .appImageAction(MacPackagingPipeline::signApplicationBundle) .addDependencies(builder.taskGraphSnapshot().getAllTailsOf(PrimaryTaskID.BUILD_APPLICATION_IMAGE)) .addDependent(PrimaryTaskID.BUILD_APPLICATION_IMAGE) .add(); builder.task(MacCopyAppImageTaskID.COPY_SIGN) - .appImageAction(MacPackagingPipeline::sign) + .appImageAction(MacPackagingPipeline::signApplicationBundle) .addDependencies(builder.taskGraphSnapshot().getAllTailsOf(PrimaryTaskID.COPY_APP_IMAGE)) .addDependent(PrimaryTaskID.COPY_APP_IMAGE) .add(); @@ -179,9 +197,20 @@ static PackagingPipeline.Builder build(Optional pkg) { // don't create ".package" file and don't sign it. disabledTasks.add(MacCopyAppImageTaskID.COPY_PACKAGE_FILE); disabledTasks.add(MacCopyAppImageTaskID.COPY_SIGN); -// if (p.isRuntimeInstaller()) { -// builder.task(MacCopyAppImageTaskID.COPY_RUNTIME_INFO_PLIST).packageAction(MacPackagingPipeline::writeRuntimeRuntimeInfoPlist).add(); -// } + if (((MacPackage)p).isRuntimeImageJDKImage()) { + builder.task(MacCopyAppImageTaskID.COPY_RUNTIME_INFO_PLIST) + .packageAction(MacPackagingPipeline::writeRuntimeRuntimeInfoPlist) + .add(); + builder.task(MacCopyAppImageTaskID.COPY_RUNTIME_JLILIB) + .packageAction(MacPackagingPipeline::copyRuntimeRuntimeJliLib) + .add(); + } + + if (((MacPackage)p).isRuntimeJDKBundleNeedSigning()) { + builder.task(MacCopyAppImageTaskID.SIGN_RUNTIME_BUNDLE) + .packageAction(MacPackagingPipeline::signRuntimeBundle) + .add(); + } } for (final var taskId : disabledTasks) { @@ -211,14 +240,24 @@ private static void copyAppImage(MacPackage pkg, AppImageDesc srcAppImage, PackagingPipeline.copyAppImage(srcAppImage, dstAppImage, !pkg.predefinedAppImageSigned().orElse(false)); } - private static void copyJliLib( + private static void copyRuntimeRuntimeJliLib(PackageBuildEnv env) throws IOException { + copyJliLib(env.resolvedLayout().rootDirectory(), + env.resolvedLayout().rootDirectory().resolve("Contents/Home")); + } + + private static void copyApplicationRuntimeJliLib( AppImageBuildEnv env) throws IOException { + copyJliLib(env.resolvedLayout().runtimeRootDirectory(), + env.resolvedLayout().runtimeDirectory()); + } + + private static void copyJliLib(Path runtimeRootDirectory, Path runtimeLibRoot) throws IOException { - final var runtimeMacOSDir = env.resolvedLayout().runtimeRootDirectory().resolve("Contents/MacOS"); + final var runtimeMacOSDir = runtimeRootDirectory.resolve("Contents/MacOS"); final var jliName = Path.of("libjli.dylib"); - try (var walk = Files.walk(env.resolvedLayout().runtimeDirectory().resolve("lib"))) { + try (var walk = Files.walk(runtimeLibRoot.resolve("lib"))) { final var jli = walk .filter(file -> file.getFileName().equals(jliName)) .findFirst() @@ -248,7 +287,7 @@ private static void writePkgInfoFile( } private static void writeRuntimeRuntimeInfoPlist(PackageBuildEnv env) throws IOException { - writeRuntimeInfoPlist(env.pkg().app(), env.env(), env.resolvedLayout().rootDirectory()); + writeRuntimeBundleInfoPlist(env.pkg().app(), env.env(), env.resolvedLayout().rootDirectory()); } private static void writeApplicationRuntimeInfoPlist( @@ -271,6 +310,22 @@ private static void writeRuntimeInfoPlist(MacApplication app, BuildEnv env, Path .saveToFile(runtimeRootDirectory.resolve("Contents/Info.plist")); } + private static void writeRuntimeBundleInfoPlist(MacApplication app, BuildEnv env, Path runtimeRootDirectory) throws IOException { + + Map 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()); + data.put("CF_BUNDLE_VENDOR", app.vendor()); + + env.createResource("RuntimeBundle-Info.plist.template") + .setPublicName("RuntimeBundle-Info.plist") + .setCategory(I18N.getString("resource.runtime-bundle-info-plist")) + .setSubstitutionData(data) + .saveToFile(runtimeRootDirectory.resolve("Contents/Info.plist")); + } + private static void writeAppInfoPlist( AppImageBuildEnv env) throws IOException { @@ -308,9 +363,16 @@ private static void writeAppInfoPlist( .saveToFile(infoPlistFile); } - private static void sign(AppImageBuildEnv env) throws IOException { + private static void signRuntimeBundle(PackageBuildEnv env) throws IOException { + sign(env.pkg().app(), env.env(), env.resolvedLayout().rootDirectory()); + } - final var app = env.app(); + private static void signApplicationBundle(AppImageBuildEnv env) throws IOException { + sign(env.app(), env.env(), env.resolvedLayout().rootDirectory()); + } + + private static void sign(final MacApplication app, + final BuildEnv env, final Path appImageDir) throws IOException { final var codesignConfigBuilder = CodesignConfig.build(); app.signingConfig().ifPresent(codesignConfigBuilder::from); @@ -319,9 +381,9 @@ private static void sign(AppImageBuildEnv final var entitlementsDefaultResource = app.signingConfig().map( AppImageSigningConfig::entitlementsResourceName).orElseThrow(); - final var entitlementsFile = env.env().configDir().resolve(app.name() + ".entitlements"); + final var entitlementsFile = env.configDir().resolve(app.name() + ".entitlements"); - env.env().createResource(entitlementsDefaultResource) + env.createResource(entitlementsDefaultResource) .setCategory(I18N.getString("resource.entitlements")) .saveToFile(entitlementsFile); @@ -329,7 +391,6 @@ private static void sign(AppImageBuildEnv } final Runnable signAction = () -> { - final var appImageDir = env.resolvedLayout().rootDirectory(); AppImageSigner.createSigner(app, codesignConfigBuilder.create()).accept(appImageDir); }; diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/model/MacApplication.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/model/MacApplication.java index a38ac65bbaf6f..181d38e89f153 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/model/MacApplication.java +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/model/MacApplication.java @@ -55,7 +55,11 @@ default DottedVersion shortVersion() { @Override default Path appImageDirName() { if (isRuntime()) { - return Application.super.appImageDirName(); + if (Application.super.appImageDirName().toString().endsWith(".jdk")) { + return Application.super.appImageDirName(); + } else { + return Path.of(Application.super.appImageDirName().toString() + ".jdk"); + } } else { return Path.of(Application.super.appImageDirName().toString() + ".app"); } diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/model/MacPackage.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/model/MacPackage.java index 083fc225581b6..2b3d4718448e0 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/model/MacPackage.java +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/model/MacPackage.java @@ -25,6 +25,7 @@ package jdk.jpackage.internal.model; import java.nio.file.Path; +import java.nio.file.Files; import jdk.jpackage.internal.util.CompositeProxy; public interface MacPackage extends Package, MacPackageMixin { @@ -34,7 +35,7 @@ public interface MacPackage extends Package, MacPackageMixin { @Override default AppImageLayout appImageLayout() { if (isRuntimeInstaller()) { - return RUNTIME_PACKAGE_LAYOUT; + return RuntimeLayout.DEFAULT; } else { return Package.super.appImageLayout(); } @@ -44,6 +45,36 @@ default Path installDir() { return Path.of("/").resolve(relativeInstallDir()); } + default boolean isRuntimeImageJDKImage() { + if (isRuntimeInstaller()) { + Path runtimeImage = app().runtimeImage().orElseThrow(); + Path p = runtimeImage.resolve("Contents/Home"); + return !Files.exists(p); + } + + return false; + } + + // Returns true if signing is requested or JDK bundle is not signed + // or JDK image is provided. + default boolean isRuntimeJDKBundleNeedSigning() { + if (!isRuntimeInstaller()) { + return false; + } + + if (app().sign()) { + return true; + } + + if (isRuntimeImageJDKImage()) { + return true; + } else { + Path runtimeImage = app().runtimeImage().orElseThrow(); + Path p = runtimeImage.resolve("Contents/_CodeSignature"); + return !Files.exists(p); + } + } + public static MacPackage create(Package pkg, MacPackageMixin mixin) { return CompositeProxy.create(MacPackage.class, pkg, mixin); } diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/resources/MacResources.properties b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/resources/MacResources.properties index 1325e3be4f4c7..092ce564c81b7 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/resources/MacResources.properties +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/resources/MacResources.properties @@ -42,6 +42,7 @@ 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.runtime-info-plist=Java Runtime Info.plist +resource.runtime-bundle-info-plist=Java Runtime Bundle Info.plist resource.entitlements=Mac Entitlements resource.dmg-setup-script=DMG setup script resource.license-setup=License setup @@ -82,5 +83,7 @@ message.signing.pkg=Warning: For signing PKG, you might need to set "Always Trus message.setfile.dmg=Setting custom icon on DMG file skipped because 'SetFile' utility was not found. Installing Xcode with Command Line Tools should resolve this issue. message.codesign.failed.reason.app.content="codesign" failed and additional application content was supplied via the "--app-content" parameter. Probably the additional content broke the integrity of the application bundle and caused the failure. Ensure content supplied via the "--app-content" parameter does not break the integrity of the application bundle, or add it in the post-processing step. message.codesign.failed.reason.xcode.tools=Possible reason for "codesign" failure is missing Xcode with command line developer tools. Install Xcode with command line developer tools to see if it resolves the problem. +message.runtime-image-invalid=Provided runtime image at "{0}" is invalid or corrupted. +message.runtime-image-invalid.advice=Runtime image should be valid JDK bundle or JDK image. warning.unsigned.app.image=Warning: Using unsigned app-image to build signed {0}. warning.per.user.app.image.signed=Warning: Support for per-user configuration of the installed application will not be supported due to missing "{0}" in predefined signed application image. diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/resources/RuntimeBundle-Info.plist.template b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/resources/RuntimeBundle-Info.plist.template new file mode 100644 index 0000000000000..5a1492e2eab46 --- /dev/null +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/resources/RuntimeBundle-Info.plist.template @@ -0,0 +1,39 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + libjli.dylib + CFBundleIdentifier + CF_BUNDLE_IDENTIFIER + CFBundleInfoDictionaryVersion + 7.0 + CFBundleName + CF_BUNDLE_NAME + CFBundlePackageType + BNDL + CFBundleShortVersionString + CF_BUNDLE_SHORT_VERSION_STRING + CFBundleSignature + ???? + CFBundleVersion + CF_BUNDLE_VERSION + NSMicrophoneUsageDescription + The application is requesting access to the microphone. + JavaVM + + JVMCapabilities + + CommandLine + + JVMPlatformVersion + CF_BUNDLE_VERSION + JVMVendor + CF_BUNDLE_VENDOR + JVMVersion + CF_BUNDLE_VERSION + + + diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ApplicationBuilder.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ApplicationBuilder.java index 09bf04372ceda..502d80c26977e 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ApplicationBuilder.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ApplicationBuilder.java @@ -75,6 +75,7 @@ Application create() throws ConfigException { Optional.ofNullable(vendor).orElseGet(DEFAULTS::vendor), Optional.ofNullable(copyright).orElseGet(DEFAULTS::copyright), Optional.ofNullable(srcDir), + Optional.ofNullable(runtimeImage), Optional.ofNullable(contentDirs).orElseGet(List::of), appImageLayout, Optional.ofNullable(runtimeBuilder), launchersAsList, Map.of()); } @@ -147,6 +148,11 @@ ApplicationBuilder srcDir(Path v) { return this; } + ApplicationBuilder runtimeImage(Path v) { + runtimeImage = v; + return this; + } + ApplicationBuilder contentDirs(List v) { contentDirs = v; return this; @@ -187,6 +193,7 @@ String copyright() { private String vendor; private String copyright; private Path srcDir; + private Path runtimeImage; private List contentDirs; private AppImageLayout appImageLayout; private RuntimeBuilder runtimeBuilder; diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/FromParams.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/FromParams.java index 92059b875905e..1fded96a3ba13 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/FromParams.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/FromParams.java @@ -87,6 +87,7 @@ static ApplicationBuilder createApplicationBuilder(Map p VENDOR.copyInto(params, appBuilder::vendor); COPYRIGHT.copyInto(params, appBuilder::copyright); SOURCE_DIR.copyInto(params, appBuilder::srcDir); + PREDEFINED_RUNTIME_IMAGE.copyInto(params, appBuilder::runtimeImage); APP_CONTENT.copyInto(params, appBuilder::contentDirs); final var isRuntimeInstaller = isRuntimeInstaller(params); diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/Application.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/Application.java index f164daf3eb064..e80f8aec303fb 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/Application.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/Application.java @@ -88,6 +88,17 @@ public interface Application extends BundleSpec { */ Optional srcDir(); + /** + * Gets the source directory of this application if available or an empty + * {@link Optional} instance. + *

+ * Source directory is a directory with the applications's classes and other + * resources. + * + * @return the source directory of this application + */ + Optional runtimeImage(); + /** * Gets the input content directories of this application. *

@@ -245,7 +256,7 @@ default Stream fileAssociations() { * Default implementation of {@link Application} interface. */ record Stub(String name, String description, String version, String vendor, String copyright, Optional srcDir, - List contentDirs, AppImageLayout imageLayout, Optional runtimeBuilder, + Optional runtimeImage, List contentDirs, AppImageLayout imageLayout, Optional runtimeBuilder, List launchers, Map extraAppImageFileData) implements Application { } } diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java index 7f9feb986b482..4fe6a516d1068 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java @@ -967,7 +967,12 @@ public static enum ReadOnlyPathAssert{ APP_CONTENT(new Builder("--app-content").multiple().create()), RESOURCE_DIR(new Builder("--resource-dir").create()), MAC_DMG_CONTENT(new Builder("--mac-dmg-content").multiple().create()), - RUNTIME_IMAGE(new Builder("--runtime-image").create()); + RUNTIME_IMAGE(new Builder("--runtime-image").enable(cmd -> { + // External runtime image should be R/O unless it is runtime installer + // on macOS. On macOS runtime image will be signed ad-hoc or with + // real certificate when creating runtime installers. + return !(cmd.isRuntime() && TKit.isOSX()); + }).create()); ReadOnlyPathAssert(Function> getPaths) { this.getPaths = getPaths; @@ -1082,11 +1087,7 @@ public static enum AppLayoutAssert { 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 -> { diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacHelper.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacHelper.java index 7b676737ed31c..d625362d3b5e7 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacHelper.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacHelper.java @@ -334,7 +334,7 @@ static Path getInstallationDirectory(JPackageCommand cmd) { 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) { @@ -416,6 +416,9 @@ private static final class Inner { ).map(Path::of).collect(toSet()); private static final Set RUNTIME_BUNDLE_CONTENTS = Stream.of( - "Home" + "Home", + "MacOS", + "Info.plist", + "_CodeSignature" ).map(Path::of).collect(toSet()); } diff --git a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/AppImageFileTest.java b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/AppImageFileTest.java index 2bbb16c71d39a..e53108e534c98 100644 --- a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/AppImageFileTest.java +++ b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/AppImageFileTest.java @@ -117,7 +117,7 @@ AppImageFile create() { List.of(), false, null, Optional.empty(), null, Map.of()); final var app = new Application.Stub(null, null, version, null, null, - Optional.empty(), List.of(), null, Optional.empty(), + Optional.empty(), Optional.empty(), List.of(), null, Optional.empty(), new ApplicationLaunchers(mainLauncher, additionalLaunchers).asList(), extra); return new AppImageFile(app); diff --git a/test/jdk/tools/jpackage/macosx/SigningRuntimeImagePackageTest.java b/test/jdk/tools/jpackage/macosx/SigningRuntimeImagePackageTest.java new file mode 100644 index 0000000000000..124632bc58239 --- /dev/null +++ b/test/jdk/tools/jpackage/macosx/SigningRuntimeImagePackageTest.java @@ -0,0 +1,263 @@ +/* + * 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.nio.file.Path; +import java.io.IOException; + +import jdk.jpackage.test.ApplicationLayout; +import jdk.jpackage.test.JPackageCommand; +import jdk.jpackage.test.PackageTest; +import jdk.jpackage.test.PackageType; +import jdk.jpackage.test.MacHelper; +import jdk.jpackage.test.Annotations.Test; +import jdk.jpackage.test.Annotations.Parameter; +import jdk.jpackage.test.TKit; +import jdk.jpackage.test.JavaTool; +import jdk.jpackage.test.Executor; + +/** + * 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 systerm 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 void verifyPKG(JPackageCommand cmd) { + Path outputBundle = cmd.outputBundle(); + SigningBase.verifyPkgutil(outputBundle, isPKGSigned(cmd), getCertIndex(cmd)); + if (isPKGSigned(cmd)) { + SigningBase.verifySpctl(outputBundle, "install", getCertIndex(cmd)); + } + } + + private static void verifyDMG(JPackageCommand cmd) { + Path outputBundle = cmd.outputBundle(); + SigningBase.verifyDMG(outputBundle); + } + + private static void verifyRuntimeImageInDMG(JPackageCommand cmd, + boolean isRuntimeImageSigned, + int JDKBundleCertIndex) { + MacHelper.withExplodedDmg(cmd, dmgImage -> { + Path launcherPath = ApplicationLayout.platformAppImage() + .resolveAt(dmgImage).launchersDirectory().resolve("libjli.dylib"); + // We will be called with all folders in DMG since JDK-8263155, but + // we only need to verify JDK bundle. + if (dmgImage.endsWith(cmd.name() + ".jdk")) { + SigningBase.verifyCodesign(launcherPath, isRuntimeImageSigned, + JDKBundleCertIndex); + SigningBase.verifyCodesign(dmgImage, isRuntimeImageSigned, + JDKBundleCertIndex); + if (isRuntimeImageSigned) { + SigningBase.verifySpctl(dmgImage, "exec", JDKBundleCertIndex); + } + } + }); + } + + private static boolean isPKGSigned(JPackageCommand cmd) { + return cmd.hasArgument("--mac-signing-key-user-name") || + cmd.hasArgument("--mac-installer-sign-identity"); + } + + private static int getCertIndex(JPackageCommand cmd) { + if (cmd.hasArgument("--mac-signing-key-user-name")) { + String devName = cmd.getArgumentValue("--mac-signing-key-user-name"); + return SigningBase.getDevNameIndex(devName); + } else { + return SigningBase.CertIndex.INVALID_INDEX.value(); + } + } + + private static Path getRuntimeImagePath(boolean useJDKBundle, + boolean isRuntimeImageSigned, + int JDKBundleCertIndex) throws IOException { + final Path runtimeBundleDir = + TKit.createTempDirectory("runtimebundle"); + final Path runtimeImageImage = + runtimeBundleDir.resolve("image"); + + new Executor() + .setToolProvider(JavaTool.JLINK) + .dumpOutput() + .addArguments( + "--output", runtimeImageImage.toString(), + "--add-modules", "java.desktop", + "--strip-debug", + "--no-header-files", + "--no-man-pages") + .execute(); + + if (useJDKBundle) { + // We will use jpackage to create JDK bundle from image signed or + // unsigned. + + final Path runtimeBundleDMG = + runtimeBundleDir.resolve("dmg"); + final Path runtimeBundleBundle = + runtimeBundleDir.resolve("bundle"); + + Executor ex = new Executor(); + ex.setToolProvider(JavaTool.JPACKAGE) + .dumpOutput() + .addArguments( + "--type", "dmg", + "--name", "foo", + "--runtime-image", runtimeImageImage.toAbsolutePath().toString(), + "--dest", runtimeBundleDMG.toAbsolutePath().toString()); + + if (isRuntimeImageSigned) { + ex.addArguments( + "--mac-sign", + "--mac-signing-keychain", SigningBase.getKeyChain(), + "--mac-signing-key-user-name", SigningBase.getDevName(JDKBundleCertIndex)); + } + + ex.execute(); + + JPackageCommand dummyCMD = new JPackageCommand(); + dummyCMD.addArguments( + "--type", "dmg", + "--name", "foo", + "--dest", runtimeBundleDMG.toAbsolutePath().toString() + ); + + MacHelper.withExplodedDmg(dummyCMD, dmgImage -> { + if (dmgImage.endsWith(dummyCMD.name() + ".jdk")) { + Executor.of("cp", "-R") + .addArgument(dmgImage) + .addArgument(runtimeBundleBundle.toAbsolutePath().toString()) + .execute(0); + } + }); + + return runtimeBundleBundle.toAbsolutePath(); + } else { + return runtimeImageImage.toAbsolutePath(); + } + } + + @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 int JDKBundleCertIndex = JDKBundleCert.value(); + final int signCertIndex = signCert.value(); + + final boolean isRuntimeImageSigned = + (JDKBundleCertIndex != SigningBase.CertIndex.INVALID_INDEX.value()); + final boolean isSigned = + (signCertIndex != SigningBase.CertIndex.INVALID_INDEX.value()); + + new PackageTest() + .forTypes(PackageType.MAC) + .addInitializer(cmd -> { + cmd.addArguments("--runtime-image", + getRuntimeImagePath(useJDKBundle, + isRuntimeImageSigned, JDKBundleCertIndex)); + // Remove --input parameter from jpackage command line as we don't + // create input directory in the test and jpackage fails + // if --input references non existant directory. + cmd.removeArgumentWithValue("--input"); + + if (isSigned) { + cmd.addArguments("--mac-sign", + "--mac-signing-keychain", SigningBase.getKeyChain()); + cmd.addArguments("--mac-signing-key-user-name", + SigningBase.getDevName(signCertIndex)); + } + }) + .forTypes(PackageType.MAC_PKG) + .addBundleVerifier(SigningRuntimeImagePackageTest::verifyPKG) + .forTypes(PackageType.MAC_DMG) + .addBundleVerifier(SigningRuntimeImagePackageTest::verifyDMG) + .addBundleVerifier(cmd -> { + int certIndex = SigningBase.CertIndex.INVALID_INDEX.value(); + if (isSigned) + certIndex = signCertIndex; + else if (isRuntimeImageSigned) + certIndex = JDKBundleCertIndex; + verifyRuntimeImageInDMG(cmd, isRuntimeImageSigned || isSigned, + certIndex); + }) + .run(); + } +} diff --git a/test/jdk/tools/jpackage/share/ErrorTest.java b/test/jdk/tools/jpackage/share/ErrorTest.java index c352decc0f333..677d38bc4db69 100644 --- a/test/jdk/tools/jpackage/share/ErrorTest.java +++ b/test/jdk/tools/jpackage/share/ErrorTest.java @@ -28,6 +28,7 @@ import static jdk.internal.util.OperatingSystem.WINDOWS; import static jdk.jpackage.test.CannedFormattedString.cannedAbsolutePath; +import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; @@ -40,6 +41,9 @@ import java.util.function.Supplier; import java.util.regex.Pattern; import java.util.stream.Stream; +import java.io.IOException; +import java.io.UncheckedIOException; + import jdk.jpackage.internal.util.TokenReplace; import jdk.jpackage.test.Annotations.Parameter; import jdk.jpackage.test.Annotations.ParameterSupplier; @@ -88,6 +92,29 @@ enum Token { return appImageCmd.outputBundle().toString(); }), + INVALID_MAC_JDK_BUNDLE(cmd -> { + // Missing "Contents/MacOS/libjli.dylib" + try { + final Path runtimePath = TKit.createTempDirectory("invalidJDKBundle"); + Files.createDirectories(runtimePath.resolve("Contents/Home")); + Files.createFile(runtimePath.resolve("Contents/Info.plist")); + return runtimePath.toAbsolutePath().toString(); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + }), + INVALID_JDK_IMAGE(cmd -> { + // Missing ""lib/*/libjli.dylib"" + try { + final Path runtimePath = TKit.createTempDirectory("invalidJDKImage"); + Files.createDirectories(runtimePath.resolve("jmods")); + Files.createDirectories(runtimePath.resolve("lib")); + Files.createFile(runtimePath.resolve("lib/src.zip")); + return runtimePath.toAbsolutePath().toString(); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + }), ADD_LAUNCHER_PROPERTY_FILE; private Token() { @@ -603,7 +630,19 @@ public static Collection testMac() { testSpec().nativeType().addArgs("--mac-app-store", "--runtime-image", Token.JAVA_HOME.token()) .error("ERR_MacAppStoreRuntimeBinExists", JPackageCommand.cannedArgument(cmd -> { return Path.of(cmd.getArgumentValue("--runtime-image")).toAbsolutePath(); - }, Token.JAVA_HOME.token())) + }, Token.JAVA_HOME.token())), + testSpec().noAppDesc().nativeType() + .addArgs("--runtime-image", Token.INVALID_MAC_JDK_BUNDLE.token()) + .error("message.runtime-image-invalid", JPackageCommand.cannedArgument(cmd -> { + return Path.of(cmd.getArgumentValue("--runtime-image")).toAbsolutePath(); + }, Token.INVALID_MAC_JDK_BUNDLE.token())) + .error("message.runtime-image-invalid.advice"), + testSpec().noAppDesc().nativeType() + .addArgs("--runtime-image", Token.INVALID_JDK_IMAGE.token()) + .error("message.runtime-image-invalid", JPackageCommand.cannedArgument(cmd -> { + return Path.of(cmd.getArgumentValue("--runtime-image")).toAbsolutePath(); + }, Token.INVALID_JDK_IMAGE.token())) + .error("message.runtime-image-invalid.advice") ).map(TestSpec.Builder::create).toList()); // Test a few app-image options that should not be used when signing external app image