diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacAppImageBuilder.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacAppImageBuilder.java index 02860b61e4ff4..1f78c422fe56d 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacAppImageBuilder.java +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacAppImageBuilder.java @@ -70,6 +70,7 @@ import static jdk.jpackage.internal.StandardBundlerParam.MAIN_CLASS; import static jdk.jpackage.internal.StandardBundlerParam.PREDEFINED_APP_IMAGE; import static jdk.jpackage.internal.StandardBundlerParam.VERSION; +import static jdk.jpackage.internal.StandardBundlerParam.VENDOR; import static jdk.jpackage.internal.StandardBundlerParam.ADD_LAUNCHERS; import static jdk.jpackage.internal.StandardBundlerParam.SIGN_BUNDLE; import static jdk.jpackage.internal.StandardBundlerParam.APP_STORE; @@ -86,6 +87,8 @@ public class MacAppImageBuilder extends AbstractAppImageBuilder { "Info-lite.plist.template"; private static final String TEMPLATE_RUNTIME_INFO_PLIST = "Runtime-Info.plist.template"; + private static final String TEMPLATE_RUNTIMEIMAGE_INFO_PLIST = + "RuntimeImage-Info.plist.template"; private final Path root; private final Path contentsDir; @@ -287,7 +290,7 @@ public void prepareApplicationFiles(Map params) } try { - doSigning(params); + doSigning(params, root, true); } catch (Exception ex) { // Restore original app image file if signing failed if (appImageFile != null) { @@ -366,7 +369,7 @@ public void prepareApplicationFiles(Map params) copyRuntimeFiles(params); - doSigning(params); + doSigning(params, root, true); } private void copyRuntimeFiles(Map params) @@ -392,8 +395,8 @@ private void copyRuntimeFiles(Map params) } } - private void doSigning(Map params) - throws IOException { + public static void doSigning(Map params, Path root, + boolean isAppImage) throws IOException { if (Optional.ofNullable( SIGN_BUNDLE.fetchFrom(params)).orElse(Boolean.TRUE)) { @@ -415,16 +418,26 @@ private void doSigning(Map params) } } if (signingIdentity != null) { - signAppBundle(params, root, signingIdentity, + if (isAppImage) { + signAppBundle(params, root, signingIdentity, BUNDLE_ID_SIGNING_PREFIX.fetchFrom(params), ENTITLEMENTS.fetchFrom(params)); + } else { + signRuntimeOrFrameworkBundle(params, root, signingIdentity, + BUNDLE_ID_SIGNING_PREFIX.fetchFrom(params), + ENTITLEMENTS.fetchFrom(params)); + } } else { // Case when user requested to sign installer only signAppBundle(params, root, "-", null, null); } restoreKeychainList(params); } else { - signAppBundle(params, root, "-", null, null); + if (isAppImage) { + signAppBundle(params, root, "-", null, null); + } else { + signRuntimeOrFrameworkBundle(params, root, "-", null, null); + } } } @@ -432,7 +445,7 @@ private static String getLauncherName(Map params) { return APP_NAME.fetchFrom(params); } - private String getBundleName(Map params) { + private static String getBundleName(Map params) { if (MAC_CF_BUNDLE_NAME.fetchFrom(params) != null) { String bn = MAC_CF_BUNDLE_NAME.fetchFrom(params); if (bn.length() > 16) { @@ -481,6 +494,34 @@ private void writeRuntimeInfoPlist(Path file, .saveToFile(file); } + static void writeRuntimeImageInfoPlist(Path file, + Map params) throws IOException { + Map data = new HashMap<>(); + String identifier = MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params); + data.put("CF_BUNDLE_IDENTIFIER", identifier); + String name = getBundleName(params); + data.put("CF_BUNDLE_NAME", name); + String ver = VERSION.fetchFrom(params); + String sver = ver; + int index = ver.indexOf("."); + if (index > 0 && ((index + 1) < ver.length())) { + index = ver.indexOf(".", index + 1); + if (index > 0 ) { + sver = ver.substring(0, index); + } + } + data.put("CF_BUNDLE_VERSION", ver); + data.put("CF_BUNDLE_SHORT_VERSION_STRING", sver); + String ven = VENDOR.fetchFrom(params); + data.put("CF_BUNDLE_VENDOR", ven); + + createResource(TEMPLATE_RUNTIMEIMAGE_INFO_PLIST, params) + .setPublicName("RuntimeImage-Info.plist") + .setCategory(I18N.getString("resource.runtime-bundle-info-plist")) + .setSubstitutionData(data) + .saveToFile(file); + } + private void writeStringArrayPlist(StringBuilder sb, String key, List values) { if (values != null && !values.isEmpty()) { @@ -779,6 +820,36 @@ private static boolean isXcodeDevToolsInstalled() { return true; } + public static void signRuntimeOrFrameworkBundle( + Map params, Path pathToSign, + String signingIdentity, String identifierPrefix, Path entitlements) + throws IOException { + AtomicReference toThrow = new AtomicReference<>(); + String keyChain = SIGNING_KEYCHAIN.fetchFrom(params); + + Consumer signIdentifiedByPList = path -> { + // noinspection ThrowableResultOfMethodCallIgnored + if (toThrow.get() != null) + return; + + try { + List args = getCodesignArgs(true, path, signingIdentity, + identifierPrefix, entitlements, keyChain); + ProcessBuilder pb = new ProcessBuilder(args); + runCodesign(pb, false, params); + } catch (IOException e) { + toThrow.set(e); + } + }; + + signIdentifiedByPList.accept(pathToSign); + + IOException ioe = toThrow.get(); + if (ioe != null) { + throw ioe; + } + } + static void signAppBundle( Map params, Path appLocation, String signingIdentity, String identifierPrefix, Path entitlements) @@ -864,34 +935,22 @@ static void signAppBundle( return; } - // sign all runtime and frameworks - Consumer signIdentifiedByPList = path -> { - //noinspection ThrowableResultOfMethodCallIgnored - if (toThrow.get() != null) return; - - try { - List args = getCodesignArgs(true, path, signingIdentity, - identifierPrefix, entitlements, keyChain); - ProcessBuilder pb = new ProcessBuilder(args); - runCodesign(pb, false, params); - } catch (IOException e) { - toThrow.set(e); - } - }; - Path javaPath = appLocation.resolve("Contents/runtime"); if (Files.isDirectory(javaPath)) { - signIdentifiedByPList.accept(javaPath); - - ioe = toThrow.get(); - if (ioe != null) { - throw ioe; - } + signRuntimeOrFrameworkBundle(params, javaPath, signingIdentity, + identifierPrefix, entitlements); } Path frameworkPath = appLocation.resolve("Contents/Frameworks"); if (Files.isDirectory(frameworkPath)) { try (var fileList = Files.list(frameworkPath)) { - fileList.forEach(signIdentifiedByPList); + fileList.forEach(pathToSign -> { + try { + signRuntimeOrFrameworkBundle(params, pathToSign, + signingIdentity, identifierPrefix, entitlements); + } catch (IOException ioe2) { + toThrow.set(ioe2); + } + }); } ioe = toThrow.get(); 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 ef94a97e22bf1..5be590e66eb33 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacBaseInstallerBundler.java +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacBaseInstallerBundler.java @@ -30,14 +30,20 @@ import java.nio.file.LinkOption; import java.nio.file.Path; import java.text.MessageFormat; +import java.util.HashMap; import java.util.Map; import java.util.Optional; +import java.util.NoSuchElementException; +import java.util.stream.Stream; + import static jdk.jpackage.internal.StandardBundlerParam.APP_NAME; import static jdk.jpackage.internal.StandardBundlerParam.INSTALLER_NAME; import static jdk.jpackage.internal.StandardBundlerParam.INSTALL_DIR; import static jdk.jpackage.internal.StandardBundlerParam.PREDEFINED_APP_IMAGE; +import static jdk.jpackage.internal.StandardBundlerParam.PREDEFINED_RUNTIME_IMAGE; import static jdk.jpackage.internal.StandardBundlerParam.VERSION; import static jdk.jpackage.internal.StandardBundlerParam.SIGN_BUNDLE; +import static jdk.jpackage.internal.StandardBundlerParam.TEMP_ROOT; import jdk.jpackage.internal.model.ConfigException; import jdk.jpackage.internal.model.PackagerException; @@ -160,6 +166,23 @@ protected void validateAppImageAndBundeler( "warning.unsigned.app.image"), getID())); } } + } else if (StandardBundlerParam.isRuntimeInstaller(params)) { + // Call appImageBundler.validate(params); to validate signing + // requirements. + appImageBundler.validate(params); + + Path runtimeImage = PREDEFINED_RUNTIME_IMAGE.fetchFrom(params); + + // Make sure we have valid runtime image. + if (!isRuntimeImageJDKBundle(runtimeImage) + && !isRuntimeImageJDKImage(runtimeImage)) { + throw new ConfigException( + MessageFormat.format(I18N.getString( + "message.runtime-image-invalid"), + runtimeImage.toString()), + I18N.getString( + "message.runtime-image-invalid.advice")); + } } else { appImageBundler.validate(params); } @@ -171,6 +194,8 @@ protected Path prepareAppBundle(Map params) Path appImageRoot = APP_IMAGE_TEMP_ROOT.fetchFrom(params); Path predefinedImage = StandardBundlerParam.getPredefinedAppImage(params); + Path runtimeImage = + PREDEFINED_RUNTIME_IMAGE.fetchFrom(params); if (predefinedImage != null) { appDir = appImageRoot.resolve(APP_NAME.fetchFrom(params) + ".app"); FileUtils.copyRecursive(predefinedImage, appDir, @@ -188,6 +213,32 @@ protected Path prepareAppBundle(Map params) // need to re-sign it after modification. MacAppImageBuilder.signAppBundle(params, appDir, "-", null, null); } + } else if (StandardBundlerParam.isRuntimeInstaller(params)) { + if (isRuntimeImageJDKBundle(runtimeImage)) { + appDir = runtimeImage; + } else { + // It is a valid JDK image, so convert it to JDK bundle. + Path jdkBundleRoot = Files.createTempDirectory(TEMP_ROOT.fetchFrom(params), + "root-"); + + convertJDKImageToJDKBundle(jdkBundleRoot, runtimeImage, params); + + appDir = jdkBundleRoot; + } + + // Figure out if we need to sign + boolean signingRequested = Optional.ofNullable( + SIGN_BUNDLE.fetchFrom(params)).orElse(Boolean.FALSE); + + // Check if bundle is already signed + Path codeSignature = appDir.resolve("Contents/_CodeSignature"); + boolean bundleIsSigned = IOUtils.exists(codeSignature); + + // We do signing when it is requested or if bundle is not signed to + // create ad-hoc signature + if (signingRequested || !bundleIsSigned) { + MacAppImageBuilder.doSigning(params, appDir, false); + } } else { appDir = appImageBundler.execute(params, appImageRoot); } @@ -195,6 +246,62 @@ protected Path prepareAppBundle(Map params) return appDir; } + public static void convertJDKImageToJDKBundle(Path jdkBundleRoot, + Path runtimeImage, + Map params) throws IOException { + Path path1 = jdkBundleRoot.resolve("Contents/Home"); + Files.createDirectories(path1); + FileUtils.copyRecursive(runtimeImage, path1); + + // Copy libjli.dylib library + Path path2 = Files.createDirectories( + jdkBundleRoot.resolve("Contents/MacOS")); + + final Path jliName = Path.of("libjli.dylib"); + try (Stream walk = Files.walk(runtimeImage.resolve("lib"))) { + final Path jli = walk + .filter(file -> file.getFileName().equals(jliName)) + .findFirst() + .get(); + Files.copy(jli, path2.resolve(jliName)); + } + + MacAppImageBuilder.writeRuntimeImageInfoPlist( + jdkBundleRoot.resolve("Contents/Info.plist"), params); + } + + // JDK bundle: "Contents/Home", "Contents/MacOS/libjli.dylib" + // and "Contents/Info.plist" + private boolean isRuntimeImageJDKBundle(Path runtimeImage) { + Path path1 = runtimeImage.resolve("Contents/Home"); + Path path2 = runtimeImage.resolve("Contents/MacOS/libjli.dylib"); + Path path3 = runtimeImage.resolve("Contents/Info.plist"); + if (IOUtils.exists(path1) + && path1.toFile().list() != null + && path1.toFile().list().length > 0 + && IOUtils.exists(path2) + && IOUtils.exists(path3)) { + return true; + } + + return false; + } + + // JDK image: "lib/*/libjli.dylib" + private boolean isRuntimeImageJDKImage(Path runtimeImage) { + final Path jliName = Path.of("libjli.dylib"); + try (Stream walk = Files.walk(runtimeImage.resolve("lib"))) { + final Path jli = walk + .filter(file -> file.getFileName().equals(jliName)) + .findFirst() + .get(); + return IOUtils.exists(jli); + } catch (IOException | NoSuchElementException ex) { + Log.verbose(ex); + return false; + } + } + @Override public String getBundleType() { return "INSTALLER"; diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacDmgBundler.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacDmgBundler.java index 3e324573bcf94..ca945f54e225e 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacDmgBundler.java +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacDmgBundler.java @@ -127,8 +127,16 @@ private void prepareDMGSetupScript(Path appLocation, data.put("DEPLOY_BG_FILE", bgFile.toString()); data.put("DEPLOY_VOLUME_PATH", volumePath.toString()); data.put("DEPLOY_APPLICATION_NAME", APP_NAME.fetchFrom(params)); - String targetItem = (StandardBundlerParam.isRuntimeInstaller(params)) ? - APP_NAME.fetchFrom(params) : appLocation.getFileName().toString(); + String targetItem = null; + if (StandardBundlerParam.isRuntimeInstaller(params)) { + if (APP_NAME.fetchFrom(params).endsWith(".jdk")) { + targetItem = APP_NAME.fetchFrom(params); + } else { + targetItem = APP_NAME.fetchFrom(params).concat(".jdk"); + } + } else { + targetItem = appLocation.getFileName().toString(); + } data.put("DEPLOY_TARGET", targetItem); data.put("DEPLOY_INSTALL_LOCATION", getInstallDir(params, true)); data.put("DEPLOY_INSTALL_LOCATION_DISPLAY_NAME", @@ -275,17 +283,16 @@ private Path buildDMG( Map params, Path newRoot = Files.createTempDirectory(TEMP_ROOT.fetchFrom(params), "root-"); - // first, is this already a runtime with - // /Contents/Home - if so we need the Home dir - Path home = appLocation.resolve("Contents/Home"); - Path source = (Files.exists(home)) ? home : appLocation; - - // Then we need to put back the /Content/Home - Path root = newRoot.resolve( + // We need to copy entire runtime folder as provided to include + // .plist and signing files. + Path dest = newRoot.resolve( MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params)); - Path dest = root.resolve("Contents/Home"); + // Add .jdk if needed. + if (!dest.getFileName().endsWith(".jdk")) { + dest = dest.resolveSibling(dest.getFileName() + ".jdk"); + } - FileUtils.copyRecursive(source, dest); + FileUtils.copyRecursive(appLocation, dest); srcFolder = newRoot; } diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPkgBundler.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPkgBundler.java index ecd9a94e6c0ea..caa9d19d96bd9 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPkgBundler.java +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPkgBundler.java @@ -435,15 +435,16 @@ private String getRoot(Map params, Path source, dest; if (StandardBundlerParam.isRuntimeInstaller(params)) { - // firs, is this already a runtime with - // /Contents/Home - if so we need the Home dir - Path original = appLocation; - Path home = original.resolve("Contents/Home"); - source = (Files.exists(home)) ? home : original; + source = appLocation; - // Then we need to put back the /Content/Home + // We need to copy entire runtime folder as provided to include + // .plist and signing files. dest = newRoot.resolve( - MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params) + "/Contents/Home"); + MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params)); + // Add .jdk if needed. + if (!dest.getFileName().endsWith(".jdk")) { + dest = dest.resolveSibling(dest.getFileName() + ".jdk"); + } } else { source = appLocation; dest = newRoot.resolve(appLocation.getFileName()); 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 e7e4b2adb6170..6581593d5422c 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 @@ -83,5 +84,7 @@ message.setfile.dmg=Setting custom icon on DMG file skipped because 'SetFile' ut message.install-dir-ignored=Warning: "--install-dir" option is ignored for DMG packaging. The installation directory will default to {0}. 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/RuntimeImage-Info.plist.template b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/resources/RuntimeImage-Info.plist.template new file mode 100644 index 0000000000000..5a1492e2eab46 --- /dev/null +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/resources/RuntimeImage-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/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java index 476b6a3ef746d..de228ef632acc 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java @@ -955,7 +955,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; @@ -1070,11 +1075,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/macosx/SigningRuntimeImagePackageTest.java b/test/jdk/tools/jpackage/macosx/SigningRuntimeImagePackageTest.java new file mode 100644 index 0000000000000..bb1e14583f5f7 --- /dev/null +++ b/test/jdk/tools/jpackage/macosx/SigningRuntimeImagePackageTest.java @@ -0,0 +1,246 @@ +/* + * 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.util.LinkedHashMap; +import java.util.Map; +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; +import jdk.jpackage.internal.MacBaseInstallerBundler; +import jdk.jpackage.internal.MacAppImageBuilder; + +/** + * 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 app. + 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 runtimeImageDir = + TKit.createTempDirectory("runtimeimage").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(); + + if (useJDKBundle) { + Map params = new LinkedHashMap<>(); + params.put("name", "Foo"); + params.put("runtime-image", runtimeImageDir); + + final Path runtimeBundleDir = + TKit.createTempDirectory("runtimebundle").resolve("data"); + + MacBaseInstallerBundler.convertJDKImageToJDKBundle(runtimeBundleDir, + runtimeImageDir, params); + + if (isRuntimeImageSigned) { + params = new LinkedHashMap<>(); + params.put("name", "Foo"); + params.put("runtime-image", runtimeBundleDir); + params.put("mac-sign", Boolean.TRUE); + params.put("mac-signing-keychain", + SigningBase.getKeyChain()); + params.put("mac-signing-key-user-name", + SigningBase.getDevName(JDKBundleCertIndex)); + + MacAppImageBuilder.doSigning(params, runtimeBundleDir, false); + } + + return runtimeBundleDir; + } else { + return runtimeImageDir; + } + } + + @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..71380ba68ae39 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,7 @@ import java.util.function.Supplier; import java.util.regex.Pattern; import java.util.stream.Stream; +import java.io.IOException; import jdk.jpackage.internal.util.TokenReplace; import jdk.jpackage.test.Annotations.Parameter; import jdk.jpackage.test.Annotations.ParameterSupplier; @@ -88,6 +90,31 @@ enum Token { return appImageCmd.outputBundle().toString(); }), + INVALID_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) { + TKit.error(ex.getMessage()); + return null; + } + }), + 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) { + TKit.error(ex.getMessage()); + return null; + } + }), 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_JDK_BUNDLE.token()) + .error("message.runtime-image-invalid", JPackageCommand.cannedArgument(cmd -> { + return Path.of(cmd.getArgumentValue("--runtime-image")).toAbsolutePath(); + }, Token.INVALID_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