8371924: --mac-app-store should be accepted option for app image signing

Reviewed-by: almatvee
This commit is contained in:
Alexey Semenyuk 2026-03-24 01:17:38 +00:00
parent d85fbd38cd
commit e0fe86b53b
11 changed files with 138 additions and 32 deletions

View File

@ -27,8 +27,6 @@ package jdk.jpackage.internal;
import static jdk.jpackage.internal.FromOptions.buildApplicationBuilder;
import static jdk.jpackage.internal.FromOptions.createPackageBuilder;
import static jdk.jpackage.internal.MacPackagingPipeline.APPLICATION_LAYOUT;
import static jdk.jpackage.internal.MacRuntimeValidator.validateRuntimeHasJliLib;
import static jdk.jpackage.internal.MacRuntimeValidator.validateRuntimeHasNoBinDir;
import static jdk.jpackage.internal.OptionUtils.isBundlingOperation;
import static jdk.jpackage.internal.cli.StandardBundlingOperation.CREATE_MAC_PKG;
import static jdk.jpackage.internal.cli.StandardOption.APPCLASS;
@ -203,12 +201,14 @@ final class MacFromOptions {
final var predefinedRuntimeLayout = PREDEFINED_RUNTIME_IMAGE.findIn(options)
.map(MacPackage::guessRuntimeLayout);
predefinedRuntimeLayout.ifPresent(layout -> {
validateRuntimeHasJliLib(layout);
if (MAC_APP_STORE.containsIn(options)) {
validateRuntimeHasNoBinDir(layout);
}
});
predefinedRuntimeLayout.ifPresent(MacRuntimeValidator::validateRuntimeHasJliLib);
if (MAC_APP_STORE.containsIn(options)) {
PREDEFINED_APP_IMAGE.findIn(options)
.map(APPLICATION_LAYOUT::resolveAt)
.ifPresent(MacRuntimeValidator::validateRuntimeHasNoBinDir);
predefinedRuntimeLayout.ifPresent(MacRuntimeValidator::validateRuntimeHasNoBinDir);
}
final var launcherFromOptions = new LauncherFromOptions().faMapper(MacFromOptions::createMacFa);
@ -269,11 +269,13 @@ final class MacFromOptions {
final boolean sign = MAC_SIGN.getFrom(options);
final boolean appStore;
if (PREDEFINED_APP_IMAGE.containsIn(options)) {
if (MAC_APP_STORE.containsIn(options)) {
appStore = MAC_APP_STORE.getFrom(options);
} else if (PREDEFINED_APP_IMAGE.containsIn(options)) {
final var appImageFileOptions = appBuilder.externalApplication().orElseThrow().extra();
appStore = MAC_APP_STORE.getFrom(appImageFileOptions);
} else {
appStore = MAC_APP_STORE.getFrom(options);
appStore = false;
}
appBuilder.appStore(appStore);

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2025, 2026, 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
@ -30,6 +30,10 @@ import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.util.function.Predicate;
import jdk.jpackage.internal.model.AppImageLayout;
import jdk.jpackage.internal.model.ApplicationLayout;
import jdk.jpackage.internal.model.ConfigException;
import jdk.jpackage.internal.model.JPackageException;
import jdk.jpackage.internal.model.RuntimeLayout;
final class MacRuntimeValidator {
@ -45,17 +49,29 @@ final class MacRuntimeValidator {
throw new UncheckedIOException(ex);
}
throw I18N.buildConfigException("error.invalid-runtime-image-missing-file",
throw new JPackageException(I18N.format("error.invalid-runtime-image-missing-file",
runtimeLayout.rootDirectory(),
runtimeLayout.unresolve().runtimeDirectory().resolve("lib/**").resolve(jliName)).create();
runtimeLayout.unresolve().runtimeDirectory().resolve("lib/**").resolve(jliName)));
}
static void validateRuntimeHasNoBinDir(RuntimeLayout runtimeLayout) {
if (Files.isDirectory(runtimeLayout.runtimeDirectory().resolve("bin"))) {
throw I18N.buildConfigException()
.message("error.invalid-runtime-image-bin-dir", runtimeLayout.rootDirectory())
.advice("error.invalid-runtime-image-bin-dir.advice", "--mac-app-store")
.create();
static void validateRuntimeHasNoBinDir(AppImageLayout appImageLayout) {
if (Files.isDirectory(appImageLayout.runtimeDirectory().resolve("bin"))) {
switch (appImageLayout) {
case RuntimeLayout runtimeLayout -> {
throw new ConfigException(
I18N.format("error.invalid-runtime-image-bin-dir", runtimeLayout.rootDirectory()),
I18N.format("error.invalid-runtime-image-bin-dir.advice", "--mac-app-store"));
}
case ApplicationLayout appLayout -> {
throw new JPackageException(I18N.format("error.invalid-app-image-runtime-image-bin-dir",
appLayout.rootDirectory().relativize(appLayout.runtimeDirectory()),
appLayout.rootDirectory()));
}
default -> {
throw new IllegalArgumentException();
}
}
}
}
}

View File

@ -29,6 +29,7 @@ error.cert.not.found=No certificate found matching [{0}] using keychain [{1}]
error.multiple.certs.found=Multiple certificates matching name [{0}] found in keychain [{1}]
error.app-image.mac-sign.required=--mac-sign option is required with predefined application image and with type [app-image]
error.invalid-runtime-image-missing-file=Runtime image "{0}" is missing "{1}" file
error.invalid-app-image-runtime-image-bin-dir=Runtime directory {0} in the predefined application image [{1}] should not contain "bin" folder
error.invalid-runtime-image-bin-dir=Runtime image "{0}" should not contain "bin" folder
error.invalid-runtime-image-bin-dir.advice=Use --strip-native-commands jlink option when generating runtime image used with {0} option
error.invalid-app-image-plist-file=Invalid "{0}" file in the predefined application image

View File

@ -342,7 +342,7 @@ public final class StandardOption {
public static final OptionValue<Boolean> MAC_SIGN = booleanOption("mac-sign").scope(MAC_SIGNING).addAliases("s").create();
public static final OptionValue<Boolean> MAC_APP_STORE = booleanOption("mac-app-store").create();
public static final OptionValue<Boolean> MAC_APP_STORE = booleanOption("mac-app-store").scope(MAC_SIGNING).create();
public static final OptionValue<String> MAC_APP_CATEGORY = stringOption("mac-app-category").create();

View File

@ -392,7 +392,6 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
}
public JPackageCommand setFakeRuntime() {
verifyMutable();
addPrerequisiteAction(cmd -> {
cmd.setArgumentValue("--runtime-image", createInputRuntimeImage(RuntimeImageType.RUNTIME_TYPE_FAKE));
});
@ -400,12 +399,22 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
return this;
}
public JPackageCommand usePredefinedAppImage(JPackageCommand appImageCmd) {
appImageCmd.verifyIsOfType(PackageType.IMAGE);
verifyIsOfType(PackageType.IMAGE);
appImageCmd.getVerifyActionsWithRole(ActionRole.LAUNCHER_VERIFIER).forEach(verifier -> {
addVerifyAction(verifier, ActionRole.LAUNCHER_VERIFIER);
});
return usePredefinedAppImage(appImageCmd.outputBundle());
}
public JPackageCommand usePredefinedAppImage(Path predefinedAppImagePath) {
return setArgumentValue("--app-image", Objects.requireNonNull(predefinedAppImagePath))
.removeArgumentWithValue("--input");
}
JPackageCommand addPrerequisiteAction(ThrowingConsumer<JPackageCommand, ? extends Exception> action) {
verifyMutable();
prerequisiteActions.add(action);
return this;
}
@ -421,6 +430,7 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
}
JPackageCommand addVerifyAction(ThrowingConsumer<JPackageCommand, ? extends Exception> action, ActionRole actionRole) {
verifyMutable();
verifyActions.add(action, actionRole);
return this;
}
@ -2033,7 +2043,7 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
// `--runtime-image` parameter set.
public static final Path DEFAULT_RUNTIME_IMAGE = Optional.ofNullable(TKit.getConfigProperty("runtime-image")).map(Path::of).orElse(null);
public final static String DEFAULT_VERSION = "1.0";
public static final String DEFAULT_VERSION = "1.0";
// [HH:mm:ss.SSS]
private static final Pattern TIMESTAMP_REGEXP = Pattern.compile(

View File

@ -372,14 +372,21 @@ public final class LauncherVerifier {
TKit.assertTrue(entitlements.isPresent(), String.format("Check [%s] launcher is signed with entitlements", name));
String expectedEntitlementsOrigin;
var customFile = Optional.ofNullable(cmd.getArgumentValue("--mac-entitlements")).map(Path::of);
if (customFile.isEmpty()) {
if (customFile.isPresent()) {
expectedEntitlementsOrigin = String.format("custom entitlements from [%s] file", customFile.get());
} else {
// Try from the resource dir.
var resourceDirFile = Optional.ofNullable(cmd.getArgumentValue("--resource-dir")).map(Path::of).map(resourceDir -> {
return resourceDir.resolve(cmd.name() + ".entitlements");
}).filter(Files::exists);
if (resourceDirFile.isPresent()) {
customFile = resourceDirFile;
expectedEntitlementsOrigin = "custom entitlements from the resource directory";
} else {
expectedEntitlementsOrigin = null;
}
}
@ -388,11 +395,14 @@ public final class LauncherVerifier {
expected = new PListReader(Files.readAllBytes(customFile.orElseThrow())).toMap(true);
} else if (cmd.hasArgument("--mac-app-store")) {
expected = DefaultEntitlements.APP_STORE;
expectedEntitlementsOrigin = "App Store entitlements";
} else {
expectedEntitlementsOrigin = "default entitlements";
expected = DefaultEntitlements.STANDARD;
}
TKit.assertEquals(expected, entitlements.orElseThrow().toMap(true), String.format("Check [%s] launcher is signed with expected entitlements", name));
TKit.assertEquals(expected, entitlements.orElseThrow().toMap(true),
String.format("Check [%s] launcher is signed with %s", name, expectedEntitlementsOrigin));
}
private void executeLauncher(JPackageCommand cmd) throws IOException {

View File

@ -40,6 +40,7 @@ ErrorTest.test(WIN_MSI; app-desc=Hello; args-add=[--app-version, 1234]; errors=[
ErrorTest.test(WIN_MSI; app-desc=Hello; args-add=[--app-version, 256.1]; errors=[message.error-header+[error.msi-product-version-major-out-of-range], message.advice-header+[error.version-string-wrong-format.advice]])
ErrorTest.test(WIN_MSI; app-desc=Hello; args-add=[--launcher-as-service]; errors=[message.error-header+[error.missing-service-installer], message.advice-header+[error.missing-service-installer.advice]])
ErrorTest.test(args-add=[@foo]; errors=[message.error-header+[ERR_CannotParseOptions, foo]])
ErrorTest.testMacSignAppStoreInvalidRuntime
ErrorTest.testMacSignWithoutIdentity(IMAGE; app-desc=Hello; args-add=[--mac-sign, --mac-signing-keychain, @@EMPTY_KEYCHAIN@@]; errors=[message.error-header+[error.cert.not.found, CODE_SIGN, EMPTY_KEYCHAIN]])
ErrorTest.testMacSignWithoutIdentity(IMAGE; args-add=[--app-image, @@APP_IMAGE_WITH_SHORT_NAME@@, --mac-sign, --mac-signing-keychain, @@EMPTY_KEYCHAIN@@]; errors=[message.error-header+[error.cert.not.found, CODE_SIGN, EMPTY_KEYCHAIN]])
ErrorTest.testMacSignWithoutIdentity(MAC_DMG; app-desc=Hello; args-add=[--mac-sign, --mac-signing-keychain, @@EMPTY_KEYCHAIN@@]; errors=[message.error-header+[error.cert.not.found, CODE_SIGN, EMPTY_KEYCHAIN]])

View File

@ -29,7 +29,7 @@
| --linux-shortcut | linux-deb, linux-rpm | x | x | x | USE_LAST |
| --mac-app-category | mac-bundle | x | x | | USE_LAST |
| --mac-app-image-sign-identity | mac | x | x | | USE_LAST |
| --mac-app-store | mac-bundle | x | x | | USE_LAST |
| --mac-app-store | mac | x | x | | USE_LAST |
| --mac-dmg-content | mac-dmg | x | x | | CONCATENATE |
| --mac-entitlements | mac | x | x | | USE_LAST |
| --mac-installer-sign-identity | mac-pkg | x | x | | USE_LAST |

View File

@ -64,7 +64,6 @@ public class SigningAppImageTest {
var testAL = new AdditionalLauncher("testAL");
testAL.applyTo(cmd);
cmd.executeAndAssertHelloAppImageCreated();
MacSign.withKeychain(keychain -> {
sign.addTo(cmd);

View File

@ -26,6 +26,7 @@ import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import jdk.jpackage.test.AdditionalLauncher;
@ -68,6 +69,22 @@ public class SigningAppImageTwoStepsTest {
spec.test();
}
@Test
public static void testAppStore() {
var sign = new SignKeyOptionWithKeychain(
SignKeyOption.Type.SIGN_KEY_USER_SHORT_NAME,
SigningBase.StandardCertificateRequest.CODESIGN,
SigningBase.StandardKeychain.MAIN.keychain());
var spec = new TestSpec(Optional.empty(), sign);
spec.signAppImage(spec.createAppImage(), Optional.of(cmd -> {
cmd.addArgument("--mac-app-store");
}));
}
public record TestSpec(Optional<SignKeyOptionWithKeychain> signAppImage, SignKeyOptionWithKeychain sign) {
public TestSpec {
@ -133,7 +150,7 @@ public class SigningAppImageTwoStepsTest {
private SignKeyOptionWithKeychain sign;
}
void test() {
JPackageCommand createAppImage() {
var appImageCmd = JPackageCommand.helloAppImage()
.setFakeRuntime()
.setArgumentValue("--dest", TKit.createTempDirectory("appimage"));
@ -150,16 +167,29 @@ public class SigningAppImageTwoStepsTest {
}, signOption.keychain());
}, appImageCmd::execute);
return appImageCmd;
}
void signAppImage(JPackageCommand appImageCmd, Optional<Consumer<JPackageCommand>> mutator) {
Objects.requireNonNull(appImageCmd);
Objects.requireNonNull(mutator);
MacSign.withKeychain(keychain -> {
var cmd = new JPackageCommand()
.setPackageType(PackageType.IMAGE)
.addArguments("--app-image", appImageCmd.outputBundle())
.usePredefinedAppImage(appImageCmd)
.mutate(sign::addTo);
mutator.ifPresent(cmd::mutate);
cmd.executeAndAssertHelloAppImageCreated();
MacSignVerify.verifyAppImageSigned(cmd, sign.certRequest());
}, sign.keychain());
}
void test() {
signAppImage(createAppImage(), Optional.empty());
}
}
public static Collection<Object[]> test() {

View File

@ -26,7 +26,6 @@ import static java.util.stream.Collectors.toMap;
import static jdk.internal.util.OperatingSystem.LINUX;
import static jdk.internal.util.OperatingSystem.MACOS;
import static jdk.internal.util.OperatingSystem.WINDOWS;
import static jdk.jpackage.internal.util.PListWriter.writeDict;
import static jdk.jpackage.internal.util.PListWriter.writePList;
import static jdk.jpackage.internal.util.XmlUtils.createXml;
import static jdk.jpackage.internal.util.XmlUtils.toXmlConsumer;
@ -35,6 +34,7 @@ import static jdk.jpackage.internal.util.function.ThrowingSupplier.toSupplier;
import static jdk.jpackage.test.JPackageCommand.makeAdvice;
import static jdk.jpackage.test.JPackageCommand.makeError;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
@ -56,6 +56,7 @@ import jdk.jpackage.internal.util.TokenReplace;
import jdk.jpackage.test.Annotations.Parameter;
import jdk.jpackage.test.Annotations.ParameterSupplier;
import jdk.jpackage.test.Annotations.Test;
import jdk.jpackage.test.ApplicationLayout;
import jdk.jpackage.test.CannedArgument;
import jdk.jpackage.test.CannedFormattedString;
import jdk.jpackage.test.JPackageCommand;
@ -699,11 +700,48 @@ public final class ErrorTest {
));
}
@Test(ifOS = MACOS)
public static void testMacSignAppStoreInvalidRuntime() throws IOException {
// Create app image with the runtime directory content that will fail the subsequent signing jpackage command.
var appImageCmd = JPackageCommand.helloAppImage().setFakeRuntime();
appImageCmd.executeAndAssertImageCreated();
Files.createDirectory(appImageCmd.appLayout().runtimeHomeDirectory().resolve("bin"));
final var keychain = SignEnvMock.SingleCertificateKeychain.FOO.keychain();
var spec = testSpec()
.noAppDesc()
.addArgs("--mac-app-store", "--mac-sign", "--app-image", appImageCmd.outputBundle().toString())
.error("error.invalid-app-image-runtime-image-bin-dir",
ApplicationLayout.macAppImage().runtimeHomeDirectory(), appImageCmd.outputBundle())
.create();
TKit.withNewState(() -> {
var script = Script.build()
// Disable the mutation making mocks "run once".
.commandMockBuilderMutator(null)
// Replace "/usr/bin/security" with the mock bound to the keychain mock.
.map(MacSignMockUtils.securityMock(SignEnvMock.VALUE))
// Don't mock other external commands.
.use(VerbatimCommandMock.INSTANCE)
.createLoop();
// Create jpackage tool provider using the /usr/bin/security mock.
var jpackage = JPackageMockUtils.createJPackageToolProvider(OperatingSystem.MACOS, script);
// Override the default jpackage tool provider with the one using the /usr/bin/security mock.
JPackageCommand.useToolProviderByDefault(jpackage);
spec.test();
});
}
@Test(ifOS = MACOS)
@ParameterSupplier
@ParameterSupplier("testMacPkgSignWithoutIdentity")
public static void testMacSignWithoutIdentity(TestSpec spec) {
// The test called JPackage Command.useToolProviderBy Default(),
// The test calls JPackageCommand.useToolProviderByDefault(),
// which alters global variables in the test library,
// so run the test case with a new global state to isolate the alteration of the globals.
TKit.withNewState(() -> {
@ -998,8 +1036,7 @@ public final class ErrorTest {
// Test a few app-image options that should not be used when signing external app image
testCases.addAll(Stream.of(
new ArgumentGroup("--app-version", "2.0"),
new ArgumentGroup("--name", "foo"),
new ArgumentGroup("--mac-app-store")
new ArgumentGroup("--name", "foo")
).flatMap(argGroup -> {
var withoutSign = testSpec()
.noAppDesc()