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 b59e6c8ad00..7280f49562c 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/MacAppBundler.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacAppBundler.java index 30c83bcbeab..28d91156059 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacAppBundler.java +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacAppBundler.java @@ -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( diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacBundle.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacBundle.java new file mode 100644 index 00000000000..af07a1145dc --- /dev/null +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacBundle.java @@ -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 https://en.wikipedia.org/wiki/Bundle_(macOS)#Application_bundles + */ +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()); + } +} 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 c13b9d939df..754d09a7156 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacFromParams.java +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacFromParams.java @@ -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; } 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 eea69825a49..556efdd0fd3 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPackagingPipeline.java +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPackagingPipeline.java @@ -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 env) throws IOException { + AppImageBuildEnv 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 env) throws IOException { - writeRuntimeInfoPlist(env.pkg().app(), env.env(), env.resolvedLayout().rootDirectory()); - } + private static void writeRuntimeInfoPlist( + AppImageBuildEnv env) throws IOException { - private static void writeApplicationRuntimeInfoPlist( - AppImageBuildEnv 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 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 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 env) throws IOException { + private static void sign(AppImageBuildEnv env) throws IOException { final var app = env.app(); @@ -410,6 +465,14 @@ final class MacPackagingPipeline { })); } + private static MacBundle runtimeBundle(AppImageBuildEnv 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 { static Path getPath(Application app, ApplicationLayout appLayout) { return appLayout.desktopIntegrationDirectory().resolve(app.name() + ".icns"); 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 a38ac65bbaf..04ab7042ac5 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 @@ -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); } /** diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/resources/ApplicationRuntime-Info.plist.template b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/resources/ApplicationRuntime-Info.plist.template new file mode 100644 index 00000000000..e24cc94fa8e --- /dev/null +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/resources/ApplicationRuntime-Info.plist.template @@ -0,0 +1,24 @@ + + + + + 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 + + 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 1325e3be4f4..7fada9e4305 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 @@ -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 diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/resources/Runtime-Info.plist.template b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/resources/Runtime-Info.plist.template index e24cc94fa8e..5a1492e2eab 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/resources/Runtime-Info.plist.template +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/resources/Runtime-Info.plist.template @@ -20,5 +20,20 @@ ???? 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/FromParams.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/FromParams.java index 92059b87590..5e940aba18b 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/FromParams.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/FromParams.java @@ -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; diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/PackagingPipeline.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/PackagingPipeline.java index d36aad23886..10590a7aa8b 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/PackagingPipeline.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/PackagingPipeline.java @@ -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 diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/Package.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/Package.java index 35db967400f..6d2fdaf0bbb 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/Package.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/Package.java @@ -148,8 +148,16 @@ public interface Package extends BundleSpec { Optional 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. + *

+ * 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. + *

+ * 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 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 7f9feb986b4..169457d6f58 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java @@ -1082,11 +1082,7 @@ public class JPackageCommand extends CommandArguments { 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 7b676737ed3..f4feb3e2fde 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 @@ 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 createBundleContents(String... customItems) { + return Stream.concat(Stream.of(customItems), Stream.of( + "MacOS", + "Info.plist", + "_CodeSignature" + )).map(Path::of).collect(toSet()); + } + static final Set CRITICAL_RUNTIME_FILES = Set.of(Path.of( "Contents/Home/lib/server/libjvm.dylib")); private static final Method getServicePListFileName = initGetServicePListFileName(); - private static final Set APP_BUNDLE_CONTENTS = Stream.of( - "Info.plist", - "MacOS", + private static final Set APP_BUNDLE_CONTENTS = createBundleContents( "app", "runtime", "Resources", - "PkgInfo", - "_CodeSignature" - ).map(Path::of).collect(toSet()); + "PkgInfo" + ); - private static final Set RUNTIME_BUNDLE_CONTENTS = Stream.of( + private static final Set RUNTIME_BUNDLE_CONTENTS = createBundleContents( "Home" - ).map(Path::of).collect(toSet()); + ); } diff --git a/test/jdk/tools/jpackage/macosx/SigningPackageFromTwoStepAppImageTest.java b/test/jdk/tools/jpackage/macosx/SigningPackageFromTwoStepAppImageTest.java index a612c36ca62..d25d9a7fa81 100644 --- a/test/jdk/tools/jpackage/macosx/SigningPackageFromTwoStepAppImageTest.java +++ b/test/jdk/tools/jpackage/macosx/SigningPackageFromTwoStepAppImageTest.java @@ -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" diff --git a/test/jdk/tools/jpackage/macosx/SigningPackageTest.java b/test/jdk/tools/jpackage/macosx/SigningPackageTest.java index e41b0d60397..f0aae601877 100644 --- a/test/jdk/tools/jpackage/macosx/SigningPackageTest.java +++ b/test/jdk/tools/jpackage/macosx/SigningPackageTest.java @@ -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" diff --git a/test/jdk/tools/jpackage/macosx/SigningPackageTwoStepTest.java b/test/jdk/tools/jpackage/macosx/SigningPackageTwoStepTest.java index ccb78fee9f8..3522d8d43e5 100644 --- a/test/jdk/tools/jpackage/macosx/SigningPackageTwoStepTest.java +++ b/test/jdk/tools/jpackage/macosx/SigningPackageTwoStepTest.java @@ -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" diff --git a/test/jdk/tools/jpackage/macosx/SigningRuntimeImagePackageTest.java b/test/jdk/tools/jpackage/macosx/SigningRuntimeImagePackageTest.java new file mode 100644 index 00000000000..8032a4532e9 --- /dev/null +++ b/test/jdk/tools/jpackage/macosx/SigningRuntimeImagePackageTest.java @@ -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(); + } +}