8375054: Removed "signed" property from jpackage app image file

Reviewed-by: almatvee
This commit is contained in:
Alexey Semenyuk 2026-01-13 13:33:41 +00:00
parent a90c7eee6f
commit f7be1dcf29
20 changed files with 380 additions and 191 deletions

View File

@ -47,6 +47,7 @@ import jdk.jpackage.internal.model.ApplicationLayout;
import jdk.jpackage.internal.model.Launcher; import jdk.jpackage.internal.model.Launcher;
import jdk.jpackage.internal.model.MacApplication; import jdk.jpackage.internal.model.MacApplication;
import jdk.jpackage.internal.model.RuntimeLayout; import jdk.jpackage.internal.model.RuntimeLayout;
import jdk.jpackage.internal.util.MacBundle;
import jdk.jpackage.internal.util.PathUtils; import jdk.jpackage.internal.util.PathUtils;
import jdk.jpackage.internal.util.Result; import jdk.jpackage.internal.util.Result;
import jdk.jpackage.internal.util.function.ExceptionBox; import jdk.jpackage.internal.util.function.ExceptionBox;

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. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* *
* This code is free software; you can redistribute it and/or modify it * This code is free software; you can redistribute it and/or modify it
@ -30,8 +30,8 @@ import static jdk.jpackage.internal.MacPackagingPipeline.APPLICATION_LAYOUT;
import static jdk.jpackage.internal.MacRuntimeValidator.validateRuntimeHasJliLib; import static jdk.jpackage.internal.MacRuntimeValidator.validateRuntimeHasJliLib;
import static jdk.jpackage.internal.MacRuntimeValidator.validateRuntimeHasNoBinDir; import static jdk.jpackage.internal.MacRuntimeValidator.validateRuntimeHasNoBinDir;
import static jdk.jpackage.internal.cli.StandardBundlingOperation.SIGN_MAC_APP_IMAGE; import static jdk.jpackage.internal.cli.StandardBundlingOperation.SIGN_MAC_APP_IMAGE;
import static jdk.jpackage.internal.cli.StandardOption.ICON;
import static jdk.jpackage.internal.cli.StandardOption.APPCLASS; import static jdk.jpackage.internal.cli.StandardOption.APPCLASS;
import static jdk.jpackage.internal.cli.StandardOption.ICON;
import static jdk.jpackage.internal.cli.StandardOption.MAC_APP_CATEGORY; import static jdk.jpackage.internal.cli.StandardOption.MAC_APP_CATEGORY;
import static jdk.jpackage.internal.cli.StandardOption.MAC_APP_IMAGE_SIGN_IDENTITY; import static jdk.jpackage.internal.cli.StandardOption.MAC_APP_IMAGE_SIGN_IDENTITY;
import static jdk.jpackage.internal.cli.StandardOption.MAC_APP_STORE; import static jdk.jpackage.internal.cli.StandardOption.MAC_APP_STORE;
@ -52,11 +52,13 @@ import static jdk.jpackage.internal.model.StandardPackageType.MAC_PKG;
import static jdk.jpackage.internal.util.function.ExceptionBox.toUnchecked; import static jdk.jpackage.internal.util.function.ExceptionBox.toUnchecked;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import jdk.jpackage.internal.ApplicationBuilder.MainLauncherStartupInfo; import jdk.jpackage.internal.ApplicationBuilder.MainLauncherStartupInfo;
import jdk.jpackage.internal.SigningIdentityBuilder.ExpiredCertificateException; import jdk.jpackage.internal.SigningIdentityBuilder.ExpiredCertificateException;
import jdk.jpackage.internal.SigningIdentityBuilder.StandardCertificateSelector; import jdk.jpackage.internal.SigningIdentityBuilder.StandardCertificateSelector;
import jdk.jpackage.internal.cli.OptionValue;
import jdk.jpackage.internal.cli.Options; import jdk.jpackage.internal.cli.Options;
import jdk.jpackage.internal.cli.StandardFaOption; import jdk.jpackage.internal.cli.StandardFaOption;
import jdk.jpackage.internal.model.ApplicationLaunchers; import jdk.jpackage.internal.model.ApplicationLaunchers;
@ -71,6 +73,7 @@ import jdk.jpackage.internal.model.MacPackage;
import jdk.jpackage.internal.model.MacPkgPackage; import jdk.jpackage.internal.model.MacPkgPackage;
import jdk.jpackage.internal.model.PackageType; import jdk.jpackage.internal.model.PackageType;
import jdk.jpackage.internal.model.RuntimeLayout; import jdk.jpackage.internal.model.RuntimeLayout;
import jdk.jpackage.internal.util.MacBundle;
import jdk.jpackage.internal.util.Result; import jdk.jpackage.internal.util.Result;
import jdk.jpackage.internal.util.function.ExceptionBox; import jdk.jpackage.internal.util.function.ExceptionBox;
@ -276,16 +279,12 @@ final class MacFromOptions {
final var builder = new MacPackageBuilder(createPackageBuilder(options, app.app(), type)); final var builder = new MacPackageBuilder(createPackageBuilder(options, app.app(), type));
app.externalApp() for (OptionValue<Path> ov : List.of(PREDEFINED_APP_IMAGE, PREDEFINED_RUNTIME_IMAGE)) {
.map(ExternalApplication::extra) ov.findIn(options)
.flatMap(MAC_SIGN::findIn) .flatMap(MacBundle::fromPath)
.ifPresent(builder::predefinedAppImageSigned); .map(MacPackagingPipeline::isSigned)
.ifPresent(builder::predefinedAppImageSigned);
PREDEFINED_RUNTIME_IMAGE.findIn(options) }
.map(MacBundle::new)
.filter(MacBundle::isValid)
.map(MacBundle::isSigned)
.ifPresent(builder::predefinedAppImageSigned);
return builder; return builder;
} }

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. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* *
* This code is free software; you can redistribute it and/or modify it * This code is free software; you can redistribute it and/or modify it
@ -76,6 +76,8 @@ import jdk.jpackage.internal.model.MacPackage;
import jdk.jpackage.internal.model.Package; import jdk.jpackage.internal.model.Package;
import jdk.jpackage.internal.model.PackageType; import jdk.jpackage.internal.model.PackageType;
import jdk.jpackage.internal.util.FileUtils; import jdk.jpackage.internal.util.FileUtils;
import jdk.jpackage.internal.util.MacBundle;
import jdk.jpackage.internal.util.PListReader;
import jdk.jpackage.internal.util.PathUtils; import jdk.jpackage.internal.util.PathUtils;
import jdk.jpackage.internal.util.function.ThrowingConsumer; import jdk.jpackage.internal.util.function.ThrowingConsumer;
@ -178,13 +180,10 @@ final class MacPackagingPipeline {
builder.task(MacCopyAppImageTaskID.COPY_RUNTIME_JLILIB) builder.task(MacCopyAppImageTaskID.COPY_RUNTIME_JLILIB)
.appImageAction(MacPackagingPipeline::copyJliLib).add(); .appImageAction(MacPackagingPipeline::copyJliLib).add();
final var predefinedRuntimeBundle = Optional.of(
new MacBundle(p.predefinedAppImage().orElseThrow())).filter(MacBundle::isValid);
// Don't create ".package" file. // Don't create ".package" file.
disabledTasks.add(MacCopyAppImageTaskID.COPY_PACKAGE_FILE); disabledTasks.add(MacCopyAppImageTaskID.COPY_PACKAGE_FILE);
if (predefinedRuntimeBundle.isPresent()) { if (MacBundle.fromPath(p.predefinedAppImage().orElseThrow()).isPresent()) {
// The input runtime image is a macOS bundle. // The input runtime image is a macOS bundle.
// Disable all alterations of the input bundle, but keep the signing enabled. // Disable all alterations of the input bundle, but keep the signing enabled.
disabledTasks.addAll(List.of(MacCopyAppImageTaskID.values())); disabledTasks.addAll(List.of(MacCopyAppImageTaskID.values()));
@ -195,7 +194,7 @@ final class MacPackagingPipeline {
.appImageAction(MacPackagingPipeline::writeRuntimeInfoPlist).add(); .appImageAction(MacPackagingPipeline::writeRuntimeInfoPlist).add();
} }
if (predefinedRuntimeBundle.map(MacBundle::isSigned).orElse(false) && !((MacPackage)p).app().sign()) { if (((MacPackage)p).predefinedAppImageSigned().orElse(false) && !((MacPackage)p).app().sign()) {
// The input runtime is a signed bundle; explicit signing is not requested for the package. // The input runtime is a signed bundle; explicit signing is not requested for the package.
// Disable the signing, i.e. don't re-sign the input bundle. // Disable the signing, i.e. don't re-sign the input bundle.
disabledTasks.add(MacCopyAppImageTaskID.COPY_SIGN); disabledTasks.add(MacCopyAppImageTaskID.COPY_SIGN);
@ -279,6 +278,30 @@ final class MacPackagingPipeline {
} }
} }
static boolean isSigned(MacBundle bundle) {
var result = toSupplier(Executor.of(
"/usr/sbin/spctl",
"-vv",
"--raw",
"--assess",
"--type", "exec",
bundle.root().toString()).setQuiet(true).saveOutput(true).binaryOutput()::execute).get();
switch (result.getExitCode()) {
case 0, 3 -> {
// These exit codes are accompanied with valid plist xml.
return toSupplier(() -> {
return new PListReader(result.byteStdout()).findValue("assessment:originator").isPresent();
}).get();
}
default -> {
// Likely to be an "a sealed resource is missing or invalid" error.
return false;
}
}
}
private static void copyAppImage(MacPackage pkg, AppImageLayout srcAppImage, private static void copyAppImage(MacPackage pkg, AppImageLayout srcAppImage,
AppImageLayout dstAppImage) throws IOException { AppImageLayout dstAppImage) throws IOException {
@ -286,7 +309,7 @@ final class MacPackagingPipeline {
final Optional<MacBundle> srcMacBundle; final Optional<MacBundle> srcMacBundle;
if (pkg.isRuntimeInstaller()) { if (pkg.isRuntimeInstaller()) {
srcMacBundle = MacBundle.fromAppImageLayout(srcAppImage); srcMacBundle = macBundleFromAppImageLayout(srcAppImage);
} else { } else {
srcMacBundle = Optional.empty(); srcMacBundle = Optional.empty();
} }
@ -297,7 +320,7 @@ final class MacPackagingPipeline {
try { try {
FileUtils.copyRecursive( FileUtils.copyRecursive(
inputBundle.root(), inputBundle.root(),
MacBundle.fromAppImageLayout(dstAppImage).orElseThrow().root(), macBundleFromAppImageLayout(dstAppImage).orElseThrow().root(),
LinkOption.NOFOLLOW_LINKS); LinkOption.NOFOLLOW_LINKS);
} catch (IOException ex) { } catch (IOException ex) {
throw new UncheckedIOException(ex); throw new UncheckedIOException(ex);
@ -415,7 +438,7 @@ final class MacPackagingPipeline {
final var app = env.app(); final var app = env.app();
final var infoPlistFile = MacBundle.fromAppImageLayout(env.resolvedLayout()).orElseThrow().infoPlistFile(); final var infoPlistFile = macBundleFromAppImageLayout(env.resolvedLayout()).orElseThrow().infoPlistFile();
Log.verbose(I18N.format("message.preparing-info-plist", PathUtils.normalizedAbsolutePathString(infoPlistFile))); Log.verbose(I18N.format("message.preparing-info-plist", PathUtils.normalizedAbsolutePathString(infoPlistFile)));
@ -468,7 +491,7 @@ final class MacPackagingPipeline {
} }
final Runnable signAction = () -> { final Runnable signAction = () -> {
AppImageSigner.createSigner(app, codesignConfigBuilder.create()).accept(MacBundle.fromAppImageLayout(env.resolvedLayout()).orElseThrow()); AppImageSigner.createSigner(app, codesignConfigBuilder.create()).accept(macBundleFromAppImageLayout(env.resolvedLayout()).orElseThrow());
}; };
app.signingConfig().flatMap(AppImageSigningConfig::keychain).map(Keychain::new).ifPresentOrElse(keychain -> { app.signingConfig().flatMap(AppImageSigningConfig::keychain).map(Keychain::new).ifPresentOrElse(keychain -> {
@ -550,7 +573,7 @@ final class MacPackagingPipeline {
private static MacBundle runtimeBundle(AppImageBuildEnv<MacApplication, AppImageLayout> env) { private static MacBundle runtimeBundle(AppImageBuildEnv<MacApplication, AppImageLayout> env) {
if (env.app().isRuntime()) { if (env.app().isRuntime()) {
return MacBundle.fromAppImageLayout(env.resolvedLayout()).orElseThrow(); return macBundleFromAppImageLayout(env.resolvedLayout()).orElseThrow();
} else { } else {
return new MacBundle(((MacApplicationLayout)env.resolvedLayout()).runtimeRootDirectory()); return new MacBundle(((MacApplicationLayout)env.resolvedLayout()).runtimeRootDirectory());
} }
@ -595,6 +618,22 @@ final class MacPackagingPipeline {
}; };
} }
private static Optional<MacBundle> macBundleFromAppImageLayout(AppImageLayout layout) {
final var root = layout.rootDirectory();
final var bundleSubdir = root.relativize(layout.runtimeDirectory());
final var contentsDirname = Path.of("Contents");
var bundleRoot = root;
for (int i = 0; i != bundleSubdir.getNameCount(); i++) {
var nameComponent = bundleSubdir.getName(i);
if (contentsDirname.equals(nameComponent)) {
return Optional.of(new MacBundle(bundleRoot));
} else {
bundleRoot = bundleRoot.resolve(nameComponent);
}
}
return Optional.empty();
}
private record TaskContextProxy(TaskContext delegate, boolean forApp, boolean copyAppImage) implements TaskContext { private record TaskContextProxy(TaskContext delegate, boolean forApp, boolean copyAppImage) implements TaskContext {

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. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* *
* This code is free software; you can redistribute it and/or modify it * This code is free software; you can redistribute it and/or modify it
@ -28,7 +28,6 @@ import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toUnmodifiableMap; import static java.util.stream.Collectors.toUnmodifiableMap;
import static jdk.jpackage.internal.cli.StandardAppImageFileOption.MAC_APP_STORE; import static jdk.jpackage.internal.cli.StandardAppImageFileOption.MAC_APP_STORE;
import static jdk.jpackage.internal.cli.StandardAppImageFileOption.MAC_MAIN_CLASS; import static jdk.jpackage.internal.cli.StandardAppImageFileOption.MAC_MAIN_CLASS;
import static jdk.jpackage.internal.cli.StandardAppImageFileOption.MAC_SIGNED;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Map; import java.util.Map;
@ -96,9 +95,6 @@ public interface MacApplication extends Application, MacApplicationMixin {
} }
public enum ExtraAppImageFileField { public enum ExtraAppImageFileField {
SIGNED(MAC_SIGNED, app -> {
return Optional.of(Boolean.toString(app.sign()));
}),
APP_STORE(MAC_APP_STORE, app -> { APP_STORE(MAC_APP_STORE, app -> {
return Optional.of(Boolean.toString(app.appStore())); return Optional.of(Boolean.toString(app.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. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* *
* This code is free software; you can redistribute it and/or modify it * This code is free software; you can redistribute it and/or modify it
@ -73,6 +73,7 @@ final class OptionSpecBuilder<T> {
valuePattern = other.valuePattern; valuePattern = other.valuePattern;
converterBuilder = other.converterBuilder.copy(); converterBuilder = other.converterBuilder.copy();
validatorBuilder = other.validatorBuilder.copy(); validatorBuilder = other.validatorBuilder.copy();
validator = other.validator;
if (other.arrayDefaultValue != null) { if (other.arrayDefaultValue != null) {
arrayDefaultValue = Arrays.copyOf(other.arrayDefaultValue, other.arrayDefaultValue.length); arrayDefaultValue = Arrays.copyOf(other.arrayDefaultValue, other.arrayDefaultValue.length);
@ -135,10 +136,20 @@ final class OptionSpecBuilder<T> {
scope, scope,
OptionSpecBuilder.this.mergePolicy().orElse(MergePolicy.CONCATENATE), OptionSpecBuilder.this.mergePolicy().orElse(MergePolicy.CONCATENATE),
defaultArrayOptionalValue(), defaultArrayOptionalValue(),
Optional.of(arryValuePattern()), Optional.of(arrayValuePattern()),
OptionSpecBuilder.this.description().orElse("")); OptionSpecBuilder.this.description().orElse(""));
} }
Optional<? extends Validator<T, RuntimeException>> createValidator() {
return Optional.ofNullable(validator).or(() -> {
if (validatorBuilder.hasValidatingMethod()) {
return Optional.of(validatorBuilder.create());
} else {
return Optional.empty();
}
});
}
OptionSpecBuilder<T> tokenizer(String splitRegexp) { OptionSpecBuilder<T> tokenizer(String splitRegexp) {
Objects.requireNonNull(splitRegexp); Objects.requireNonNull(splitRegexp);
return tokenizer(str -> { return tokenizer(str -> {
@ -162,11 +173,13 @@ final class OptionSpecBuilder<T> {
OptionSpecBuilder<T> validatorExceptionFormatString(String v) { OptionSpecBuilder<T> validatorExceptionFormatString(String v) {
validatorBuilder.formatString(v); validatorBuilder.formatString(v);
validator = null;
return this; return this;
} }
OptionSpecBuilder<T> validatorExceptionFormatString(UnaryOperator<String> mutator) { OptionSpecBuilder<T> validatorExceptionFormatString(UnaryOperator<String> mutator) {
validatorBuilder.formatString(mutator.apply(validatorBuilder.formatString().orElse(null))); validatorBuilder.formatString(mutator.apply(validatorBuilder.formatString().orElse(null)));
validator = null;
return this; return this;
} }
@ -182,6 +195,7 @@ final class OptionSpecBuilder<T> {
OptionSpecBuilder<T> validatorExceptionFactory(OptionValueExceptionFactory<? extends RuntimeException> v) { OptionSpecBuilder<T> validatorExceptionFactory(OptionValueExceptionFactory<? extends RuntimeException> v) {
validatorBuilder.exceptionFactory(v); validatorBuilder.exceptionFactory(v);
validator = null;
return this; return this;
} }
@ -225,18 +239,27 @@ final class OptionSpecBuilder<T> {
OptionSpecBuilder<T> validator(Predicate<T> v) { OptionSpecBuilder<T> validator(Predicate<T> v) {
validatorBuilder.predicate(v::test); validatorBuilder.predicate(v::test);
validator = null;
return this; return this;
} }
@SuppressWarnings("overloads") @SuppressWarnings("overloads")
OptionSpecBuilder<T> validator(Consumer<T> v) { OptionSpecBuilder<T> validator(Consumer<T> v) {
validatorBuilder.consumer(v::accept); validatorBuilder.consumer(v::accept);
validator = null;
return this; return this;
} }
@SuppressWarnings("overloads") @SuppressWarnings("overloads")
OptionSpecBuilder<T> validator(UnaryOperator<Validator.Builder<T, RuntimeException>> mutator) { OptionSpecBuilder<T> validator(UnaryOperator<Validator.Builder<T, RuntimeException>> mutator) {
validatorBuilder = mutator.apply(validatorBuilder); validatorBuilder = mutator.apply(validatorBuilder);
validator = null;
return this;
}
OptionSpecBuilder<T> validator(Validator<T, RuntimeException> v) {
validatorBuilder.predicate(null).consumer(null);
validator = Objects.requireNonNull(v);
return this; return this;
} }
@ -247,6 +270,7 @@ final class OptionSpecBuilder<T> {
OptionSpecBuilder<T> withoutValidator() { OptionSpecBuilder<T> withoutValidator() {
validatorBuilder.predicate(null).consumer(null); validatorBuilder.predicate(null).consumer(null);
validator = null;
return this; return this;
} }
@ -423,14 +447,6 @@ final class OptionSpecBuilder<T> {
} }
} }
private Optional<Validator<T, ? extends RuntimeException>> createValidator() {
if (validatorBuilder.hasValidatingMethod()) {
return Optional.of(validatorBuilder.create());
} else {
return Optional.empty();
}
}
private OptionValueConverter<T[]> createArrayConverter() { private OptionValueConverter<T[]> createArrayConverter() {
final var newBuilder = converterBuilder.copy(); final var newBuilder = converterBuilder.copy();
newBuilder.tokenizer(Optional.ofNullable(arrayTokenizer).orElse(str -> { newBuilder.tokenizer(Optional.ofNullable(arrayTokenizer).orElse(str -> {
@ -440,7 +456,7 @@ final class OptionSpecBuilder<T> {
return newBuilder.createArray(); return newBuilder.createArray();
} }
private String arryValuePattern() { private String arrayValuePattern() {
final var elementValuePattern = OptionSpecBuilder.this.valuePattern().orElseThrow(); final var elementValuePattern = OptionSpecBuilder.this.valuePattern().orElseThrow();
if (arrayValuePatternSeparator == null) { if (arrayValuePatternSeparator == null) {
return elementValuePattern; return elementValuePattern;
@ -468,6 +484,7 @@ final class OptionSpecBuilder<T> {
private String valuePattern; private String valuePattern;
private OptionValueConverter.Builder<T> converterBuilder = OptionValueConverter.build(); private OptionValueConverter.Builder<T> converterBuilder = OptionValueConverter.build();
private Validator.Builder<T, RuntimeException> validatorBuilder = Validator.build(); private Validator.Builder<T, RuntimeException> validatorBuilder = Validator.build();
private Validator<T, RuntimeException> validator;
private T[] arrayDefaultValue; private T[] arrayDefaultValue;
private String arrayValuePatternSeparator; private String arrayValuePatternSeparator;

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. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* *
* This code is free software; you can redistribute it and/or modify it * This code is free software; you can redistribute it and/or modify it
@ -169,15 +169,6 @@ public final class StandardAppImageFileOption {
.mutate(setPlatformScope(OperatingSystem.MACOS)) .mutate(setPlatformScope(OperatingSystem.MACOS))
.toOptionValueBuilder().id(StandardOption.MAC_APP_STORE.id()).create(); .toOptionValueBuilder().id(StandardOption.MAC_APP_STORE.id()).create();
/**
* Is an application image is signed. macOS-only.
*/
public static final OptionValue<Boolean> MAC_SIGNED = booleanOption("signed")
.inScope(AppImageFileOptionScope.APP)
.mutate(setPlatformScope(OperatingSystem.MACOS))
.toOptionValueBuilder().id(StandardOption.MAC_SIGN.id()).create();
public static final class InvalidOptionValueException extends RuntimeException { public static final class InvalidOptionValueException extends RuntimeException {
InvalidOptionValueException(String str, Throwable t) { InvalidOptionValueException(String str, Throwable t) {

View File

@ -233,6 +233,12 @@ public final class StandardOption {
.mutate(createOptionSpecBuilderMutator((b, context) -> { .mutate(createOptionSpecBuilderMutator((b, context) -> {
if (context.os() == OperatingSystem.MACOS) { if (context.os() == OperatingSystem.MACOS) {
b.description("help.option.app-image" + resourceKeySuffix(context.os())); b.description("help.option.app-image" + resourceKeySuffix(context.os()));
var directoryValidator = b.createValidator().orElseThrow();
var macBundleValidator = b
.validatorExceptionFormatString("error.parameter-not-mac-bundle")
.validator(StandardValidator.IS_VALID_MAC_BUNDLE)
.createValidator().orElseThrow();
b.validator(Validator.and(directoryValidator, macBundleValidator));
} }
})) }))
.create(); .create();

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. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* *
* This code is free software; you can redistribute it and/or modify it * This code is free software; you can redistribute it and/or modify it
@ -38,6 +38,7 @@ import java.util.function.Consumer;
import java.util.function.Predicate; import java.util.function.Predicate;
import jdk.jpackage.internal.cli.Validator.ValidatingConsumerException; import jdk.jpackage.internal.cli.Validator.ValidatingConsumerException;
import jdk.jpackage.internal.util.FileUtils; import jdk.jpackage.internal.util.FileUtils;
import jdk.jpackage.internal.util.MacBundle;
final public class StandardValidator { final public class StandardValidator {
@ -138,6 +139,10 @@ final public class StandardValidator {
return true; return true;
}; };
public static Predicate<Path> IS_VALID_MAC_BUNDLE = path -> {
return MacBundle.fromPath(path).isPresent();
};
public static final class DirectoryListingIOException extends RuntimeException { public static final class DirectoryListingIOException extends RuntimeException {

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. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* *
* This code is free software; you can redistribute it and/or modify it * This code is free software; you can redistribute it and/or modify it
@ -24,20 +24,55 @@
*/ */
package jdk.jpackage.internal.cli; package jdk.jpackage.internal.cli;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.stream.Stream;
@FunctionalInterface @FunctionalInterface
interface Validator<T, U extends Exception> { interface Validator<T, U extends Exception> {
List<U> validate(OptionName optionName, ParsedValue<T> optionValue); List<U> validate(OptionName optionName, ParsedValue<T> optionValue);
default Validator<T, ? extends Exception> andThen(Validator<T, ? extends Exception> after) { default Validator<T, ? extends Exception> and(Validator<T, ? extends Exception> after) {
return reduce(this, after); Objects.requireNonNull(after);
var before = this;
return (optionName, optionValue) -> {
return Stream.concat(
before.validate(optionName, optionValue).stream(),
after.validate(optionName, optionValue).stream()
).toList();
};
}
default Validator<T, ? extends Exception> or(Validator<T, ? extends Exception> after) {
Objects.requireNonNull(after);
var before = this;
return (optionName, optionValue) -> {
var bErrors = before.validate(optionName, optionValue);
if (bErrors.isEmpty()) {
return List.of();
}
var aErrors = after.validate(optionName, optionValue);
if (aErrors.isEmpty()) {
return List.of();
}
return Stream.concat(bErrors.stream(), aErrors.stream()).toList();
};
}
@SuppressWarnings("unchecked")
static <T, U extends Exception> Validator<T, U> and(Validator<T, U> first, Validator<T, U> second) {
return (Validator<T, U>)first.and(second);
}
@SuppressWarnings("unchecked")
static <T, U extends Exception> Validator<T, U> or(Validator<T, U> first, Validator<T, U> second) {
return (Validator<T, U>)first.or(second);
} }
/** /**
@ -251,15 +286,4 @@ interface Validator<T, U extends Exception> {
} }
} }
} }
@SafeVarargs
private static <T> Validator<T, ? extends Exception> reduce(Validator<T, ? extends Exception>... validators) {
@SuppressWarnings("varargs")
var theValidators = List.of(validators);
return (optionName, optionValue) -> {
return theValidators.stream().map(validator -> {
return validator.validate(optionName, optionValue);
}).flatMap(Collection::stream).map(Exception.class::cast).toList();
};
}
} }

View File

@ -75,6 +75,7 @@ error.parameter-not-directory=The value "{0}" provided for parameter {1} is not
error.parameter-not-empty-directory=The value "{0}" provided for parameter {1} is not an empty directory or non existent path error.parameter-not-empty-directory=The value "{0}" provided for parameter {1} is not an empty directory or non existent path
error.parameter-not-url=The value "{0}" provided for parameter {1} is not a valid URL error.parameter-not-url=The value "{0}" provided for parameter {1} is not a valid URL
error.parameter-not-launcher-shortcut-dir=The value "{0}" provided for parameter {1} is not a valid shortcut startup directory error.parameter-not-launcher-shortcut-dir=The value "{0}" provided for parameter {1} is not a valid shortcut startup directory
error.parameter-not-mac-bundle=The value "{0}" provided for parameter {1} is not a valid macOS bundle
error.path-parameter-ioexception=I/O error accessing path value "{0}" of parameter {1} error.path-parameter-ioexception=I/O error accessing path value "{0}" of parameter {1}
error.parameter-add-launcher-malformed=The value "{0}" provided for parameter {1} does not match the pattern <name>=<file path> error.parameter-add-launcher-malformed=The value "{0}" provided for parameter {1} does not match the pattern <name>=<file path>
error.parameter-add-launcher-not-file=The value of path to a property file "{0}" provided for additional launcher "{1}" is not a valid file path error.parameter-add-launcher-not-file=The value of path to a property file "{0}" provided for additional launcher "{1}" is not a valid file path

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. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* *
* This code is free software; you can redistribute it and/or modify it * This code is free software; you can redistribute it and/or modify it
@ -23,54 +23,49 @@
* questions. * questions.
*/ */
package jdk.jpackage.internal; package jdk.jpackage.internal.util;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import jdk.jpackage.internal.model.AppImageLayout;
/** /**
* An abstraction of macOS Application bundle. * An abstraction of macOS Application bundle.
* *
* @see <a href="https://en.wikipedia.org/wiki/Bundle_(macOS)#Application_bundles">https://en.wikipedia.org/wiki/Bundle_(macOS)#Application_bundles</a> * @see <a href="https://en.wikipedia.org/wiki/Bundle_(macOS)#Application_bundles">https://en.wikipedia.org/wiki/Bundle_(macOS)#Application_bundles</a>
*/ */
record MacBundle(Path root) { public record MacBundle(Path root) {
MacBundle { public MacBundle {
Objects.requireNonNull(root); Objects.requireNonNull(root);
} }
boolean isValid() { public boolean isValid() {
return Files.isDirectory(contentsDir()) && Files.isDirectory(macOsDir()) && Files.isRegularFile(infoPlistFile()); return Files.isDirectory(contentsDir()) && Files.isDirectory(macOsDir()) && Files.isRegularFile(infoPlistFile());
} }
boolean isSigned() { public Path contentsDir() {
return Files.isDirectory(contentsDir().resolve("_CodeSignature"));
}
Path contentsDir() {
return root.resolve("Contents"); return root.resolve("Contents");
} }
Path homeDir() { public Path homeDir() {
return contentsDir().resolve("Home"); return contentsDir().resolve("Home");
} }
Path macOsDir() { public Path macOsDir() {
return contentsDir().resolve("MacOS"); return contentsDir().resolve("MacOS");
} }
Path resourcesDir() { public Path resourcesDir() {
return contentsDir().resolve("Resources"); return contentsDir().resolve("Resources");
} }
Path infoPlistFile() { public Path infoPlistFile() {
return contentsDir().resolve("Info.plist"); return contentsDir().resolve("Info.plist");
} }
static Optional<MacBundle> fromPath(Path path) { public static Optional<MacBundle> fromPath(Path path) {
var bundle = new MacBundle(path); var bundle = new MacBundle(path);
if (bundle.isValid()) { if (bundle.isValid()) {
return Optional.of(bundle); return Optional.of(bundle);
@ -78,20 +73,4 @@ record MacBundle(Path root) {
return Optional.empty(); return Optional.empty();
} }
} }
static Optional<MacBundle> fromAppImageLayout(AppImageLayout layout) {
final var root = layout.rootDirectory();
final var bundleSubdir = root.relativize(layout.runtimeDirectory());
final var contentsDirname = Path.of("Contents");
var bundleRoot = root;
for (int i = 0; i != bundleSubdir.getNameCount(); i++) {
var nameComponent = bundleSubdir.getName(i);
if (contentsDirname.equals(nameComponent)) {
return Optional.of(new MacBundle(bundleRoot));
} else {
bundleRoot = bundleRoot.resolve(nameComponent);
}
}
return Optional.empty();
}
} }

View File

@ -47,7 +47,7 @@ import org.w3c.dom.Document;
import org.w3c.dom.Element; import org.w3c.dom.Element;
public record AppImageFile(String mainLauncherName, Optional<String> mainLauncherClassName, public record AppImageFile(String mainLauncherName, Optional<String> mainLauncherClassName,
String version, boolean macSigned, boolean macAppStore, Map<String, Map<String, String>> launchers) { String version, boolean macAppStore, Map<String, Map<String, String>> launchers) {
public static Path getPathInAppImage(Path appImageDir) { public static Path getPathInAppImage(Path appImageDir) {
return ApplicationLayout.platformAppImage() return ApplicationLayout.platformAppImage()
@ -66,7 +66,7 @@ public record AppImageFile(String mainLauncherName, Optional<String> mainLaunche
} }
public AppImageFile(String mainLauncherName, Optional<String> mainLauncherClassName) { public AppImageFile(String mainLauncherName, Optional<String> mainLauncherClassName) {
this(mainLauncherName, mainLauncherClassName, "1.0", false, false, Map.of(mainLauncherName, Map.of())); this(mainLauncherName, mainLauncherClassName, "1.0", false, Map.of(mainLauncherName, Map.of()));
} }
public AppImageFile(String mainLauncherName, String mainLauncherClassName) { public AppImageFile(String mainLauncherName, String mainLauncherClassName) {
@ -103,10 +103,6 @@ public record AppImageFile(String mainLauncherName, Optional<String> mainLaunche
xml.writeEndElement(); xml.writeEndElement();
})); }));
xml.writeStartElement("signed");
xml.writeCharacters(Boolean.toString(macSigned));
xml.writeEndElement();
xml.writeStartElement("app-store"); xml.writeStartElement("app-store");
xml.writeCharacters(Boolean.toString(macAppStore)); xml.writeCharacters(Boolean.toString(macAppStore));
xml.writeEndElement(); xml.writeEndElement();
@ -140,10 +136,6 @@ public record AppImageFile(String mainLauncherName, Optional<String> mainLaunche
var mainLauncherClassName = Optional.ofNullable(xPath.evaluate( var mainLauncherClassName = Optional.ofNullable(xPath.evaluate(
"/jpackage-state/main-class/text()", doc)); "/jpackage-state/main-class/text()", doc));
var macSigned = Optional.ofNullable(xPath.evaluate(
"/jpackage-state/signed/text()", doc)).map(
Boolean::parseBoolean).orElse(false);
var macAppStore = Optional.ofNullable(xPath.evaluate( var macAppStore = Optional.ofNullable(xPath.evaluate(
"/jpackage-state/app-store/text()", doc)).map( "/jpackage-state/app-store/text()", doc)).map(
Boolean::parseBoolean).orElse(false); Boolean::parseBoolean).orElse(false);
@ -171,7 +163,6 @@ public record AppImageFile(String mainLauncherName, Optional<String> mainLaunche
mainLauncherName, mainLauncherName,
mainLauncherClassName, mainLauncherClassName,
version, version,
macSigned,
macAppStore, macAppStore,
Collections.unmodifiableMap(launchers)); Collections.unmodifiableMap(launchers));

View File

@ -1385,7 +1385,7 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
if (!isImagePackageType() && hasArgument("--app-image")) { if (!isImagePackageType() && hasArgument("--app-image")) {
// Build native macOS package from an external app image. // Build native macOS package from an external app image.
// If the external app image is signed, ".jpackage.xml" file should be kept, otherwise removed. // If the external app image is signed, ".jpackage.xml" file should be kept, otherwise removed.
return AppImageFile.load(Path.of(getArgumentValue("--app-image"))).macSigned(); return MacHelper.isBundleSigned(Path.of(getArgumentValue("--app-image")));
} }
} }
@ -1406,13 +1406,8 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
final AppImageFile aif = AppImageFile.load(rootDir); final AppImageFile aif = AppImageFile.load(rootDir);
if (TKit.isOSX()) { if (TKit.isOSX()) {
boolean expectedValue = MacHelper.appImageSigned(this); var expectedValue = hasArgument("--mac-app-store");
boolean actualValue = aif.macSigned(); var actualValue = aif.macAppStore();
TKit.assertEquals(expectedValue, actualValue,
"Check for unexpected value of <signed> property in app image file");
expectedValue = hasArgument("--mac-app-store");
actualValue = aif.macAppStore();
TKit.assertEquals(expectedValue, actualValue, TKit.assertEquals(expectedValue, actualValue,
"Check for unexpected value of <app-store> property in app image file"); "Check for unexpected value of <app-store> property in app image file");
} }
@ -1437,7 +1432,7 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
} else { } else {
if (TKit.isOSX() && hasArgument("--app-image")) { if (TKit.isOSX() && hasArgument("--app-image")) {
String appImage = getArgumentValue("--app-image"); String appImage = getArgumentValue("--app-image");
if (AppImageFile.load(Path.of(appImage)).macSigned()) { if (MacHelper.isBundleSigned(Path.of(appImage))) {
assertFileNotInAppImage(lookupPath); assertFileNotInAppImage(lookupPath);
} else { } else {
assertFileInAppImage(lookupPath); assertFileInAppImage(lookupPath);

View File

@ -33,6 +33,7 @@ import static jdk.jpackage.internal.util.PListWriter.writeStringArray;
import static jdk.jpackage.internal.util.PListWriter.writeStringOptional; import static jdk.jpackage.internal.util.PListWriter.writeStringOptional;
import static jdk.jpackage.internal.util.XmlUtils.initDocumentBuilder; import static jdk.jpackage.internal.util.XmlUtils.initDocumentBuilder;
import static jdk.jpackage.internal.util.XmlUtils.toXmlConsumer; import static jdk.jpackage.internal.util.XmlUtils.toXmlConsumer;
import static jdk.jpackage.internal.util.function.ThrowingFunction.toFunction;
import static jdk.jpackage.internal.util.function.ThrowingRunnable.toRunnable; import static jdk.jpackage.internal.util.function.ThrowingRunnable.toRunnable;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
@ -45,6 +46,7 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.LinkOption; import java.nio.file.LinkOption;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@ -59,14 +61,13 @@ import java.util.function.BiFunction;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.UnaryOperator; import java.util.function.UnaryOperator;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter; import javax.xml.stream.XMLStreamWriter;
import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathFactory; import javax.xml.xpath.XPathFactory;
import jdk.jpackage.internal.util.FileUtils; import jdk.jpackage.internal.util.FileUtils;
import jdk.jpackage.internal.util.MacBundle;
import jdk.jpackage.internal.util.PListReader; import jdk.jpackage.internal.util.PListReader;
import jdk.jpackage.internal.util.PathUtils; import jdk.jpackage.internal.util.PathUtils;
import jdk.jpackage.internal.util.RetryExecutor; import jdk.jpackage.internal.util.RetryExecutor;
@ -89,8 +90,8 @@ public final class MacHelper {
// See JDK-8373105. "hdiutil" does not handle such cases very good. // See JDK-8373105. "hdiutil" does not handle such cases very good.
final var mountRoot = TKit.createTempDirectory("mountRoot"); final var mountRoot = TKit.createTempDirectory("mountRoot");
// Explode DMG assuming this can require interaction, thus use `yes`. // Explode the DMG assuming this can require interaction if the DMG has a license, thus use `yes`.
final var attachStdout = Executor.of("sh", "-c", String.join(" ", final var attachExec = Executor.of("sh", "-c", String.join(" ",
"yes", "yes",
"|", "|",
"/usr/bin/hdiutil", "/usr/bin/hdiutil",
@ -99,14 +100,34 @@ public final class MacHelper {
"-mountroot", PathUtils.normalizedAbsolutePathString(mountRoot), "-mountroot", PathUtils.normalizedAbsolutePathString(mountRoot),
"-nobrowse", "-nobrowse",
"-plist" "-plist"
)).saveOutput().storeOutputInFiles().executeAndRepeatUntilExitCode(0, 10, 6).stdout(); )).saveOutput().storeOutputInFiles().binaryOutput();
final var attachResult = attachExec.executeAndRepeatUntilExitCode(0, 10, 6);
final Path mountPoint; final Path mountPoint;
boolean mountPointInitialized = false; boolean mountPointInitialized = false;
try { try {
byte[] stdout = attachResult.byteStdout();
// If the DMG has a license, it will be printed to the stdout before the plist content.
// All bytes before the XML declaration of the plist must be skipped.
// We need to find the location of the {'<', '?', 'x', 'm', 'l'} byte array
// (the XML declaration) in the captured binary stdout.
// Instead of crafting an ad-hoc function that operates on byte arrays,
// we will convert the byte array into a String instance using
// an 8-bit character set (ISO-8859-1) and use the standard String#indexOf().
var startPlistIndex = new String(stdout, StandardCharsets.ISO_8859_1).indexOf("<?xml");
byte[] plistXml;
if (startPlistIndex > 0) {
plistXml = Arrays.copyOfRange(stdout, startPlistIndex, stdout.length);
} else {
plistXml = stdout;
}
// One of "dict" items of "system-entities" array property should contain "mount-point" string property. // One of "dict" items of "system-entities" array property should contain "mount-point" string property.
mountPoint = readPList(attachStdout).queryArrayValue("system-entities", false) mountPoint = readPList(plistXml).queryArrayValue("system-entities", false)
.map(PListReader.class::cast) .map(PListReader.class::cast)
.map(dict -> { .map(dict -> {
return dict.findValue("mount-point"); return dict.findValue("mount-point");
@ -117,7 +138,7 @@ public final class MacHelper {
} finally { } finally {
if (!mountPointInitialized) { if (!mountPointInitialized) {
TKit.trace("Unexpected plist file missing `system-entities` array:"); TKit.trace("Unexpected plist file missing `system-entities` array:");
attachStdout.forEach(TKit::trace); attachResult.toCharacterResult(attachExec.charset(), false).stdout().forEach(TKit::trace);
TKit.trace("Done"); TKit.trace("Done");
} }
} }
@ -168,19 +189,13 @@ public final class MacHelper {
public static PListReader readPList(Path path) { public static PListReader readPList(Path path) {
TKit.assertReadableFileExists(path); TKit.assertReadableFileExists(path);
return ThrowingSupplier.toSupplier(() -> readPList(Files.readAllLines( return readPList(toFunction(Files::readAllBytes).apply(path));
path))).get();
} }
public static PListReader readPList(List<String> lines) { public static PListReader readPList(byte[] xml) {
return readPList(lines.stream()); return ThrowingSupplier.toSupplier(() -> {
} return new PListReader(xml);
}).get();
public static PListReader readPList(Stream<String> lines) {
return ThrowingSupplier.toSupplier(() -> new PListReader(lines
// Skip leading lines before xml declaration
.dropWhile(Pattern.compile("\\s?<\\?xml\\b.+\\?>").asPredicate().negate())
.collect(Collectors.joining()).getBytes(StandardCharsets.UTF_8))).get();
} }
public static Map<String, String> flatMapPList(PListReader plistReader) { public static Map<String, String> flatMapPList(PListReader plistReader) {
@ -265,13 +280,13 @@ public final class MacHelper {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
var runtimeImage = Optional.ofNullable(cmd.getArgumentValue("--runtime-image")).map(Path::of); var runtimeImageBundle = Optional.ofNullable(cmd.getArgumentValue("--runtime-image")).map(Path::of).flatMap(MacBundle::fromPath);
var appImage = Optional.ofNullable(cmd.getArgumentValue("--app-image")).map(Path::of); var appImage = Optional.ofNullable(cmd.getArgumentValue("--app-image")).map(Path::of);
if (cmd.isRuntime() && Files.isDirectory(runtimeImage.orElseThrow().resolve("Contents/_CodeSignature"))) { if (cmd.isRuntime() && runtimeImageBundle.map(MacHelper::isBundleSigned).orElse(false)) {
// If the predefined runtime is a signed bundle, bundled image should be signed too. // If the predefined runtime is a signed bundle, bundled image should be signed too.
return true; return true;
} else if (appImage.map(AppImageFile::load).map(AppImageFile::macSigned).orElse(false)) { } else if (appImage.map(MacHelper::isBundleSigned).orElse(false)) {
// The external app image is signed, so the app image is signed too. // The external app image is signed, so the app image is signed too.
return true; return true;
} }
@ -301,6 +316,14 @@ public final class MacHelper {
}).run(); }).run();
} }
static boolean isBundleSigned(Path bundleRoot) {
return isBundleSigned(MacBundle.fromPath(bundleRoot).orElseThrow(IllegalArgumentException::new));
}
static boolean isBundleSigned(MacBundle bundle) {
return MacSignVerify.findSpctlSignOrigin(MacSignVerify.SpctlType.EXEC, bundle.root(), true).isPresent();
}
private static void createFaPListFragmentFromFaProperties(JPackageCommand cmd, XMLStreamWriter xml) private static void createFaPListFragmentFromFaProperties(JPackageCommand cmd, XMLStreamWriter xml)
throws XMLStreamException, IOException { throws XMLStreamException, IOException {
@ -383,7 +406,7 @@ public final class MacHelper {
var predefinedAppImage = Path.of(Optional.ofNullable(cmd.getArgumentValue("--app-image")).orElseThrow(IllegalArgumentException::new)); var predefinedAppImage = Path.of(Optional.ofNullable(cmd.getArgumentValue("--app-image")).orElseThrow(IllegalArgumentException::new));
var plistPath = ApplicationLayout.macAppImage().resolveAt(predefinedAppImage).contentDirectory().resolve("Info.plist"); var plistPath = MacBundle.fromPath(predefinedAppImage).orElseThrow().infoPlistFile();
try (var plistStream = Files.newInputStream(plistPath)) { try (var plistStream = Files.newInputStream(plistPath)) {
var plist = new PListReader(initDocumentBuilder().parse(plistStream)); var plist = new PListReader(initDocumentBuilder().parse(plistStream));

View File

@ -22,7 +22,6 @@
*/ */
package jdk.jpackage.test; package jdk.jpackage.test;
import static jdk.jpackage.internal.util.function.ThrowingSupplier.toSupplier;
import static jdk.jpackage.test.MacSign.DigestAlgorithm.SHA256; import static jdk.jpackage.test.MacSign.DigestAlgorithm.SHA256;
import java.nio.file.Path; import java.nio.file.Path;
@ -30,10 +29,8 @@ import java.security.cert.X509Certificate;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HexFormat; import java.util.HexFormat;
import java.util.List; import java.util.List;
import java.util.NoSuchElementException;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import jdk.jpackage.internal.util.PListReader; import jdk.jpackage.internal.util.PListReader;
import jdk.jpackage.test.MacSign.CertificateHash; import jdk.jpackage.test.MacSign.CertificateHash;
@ -66,7 +63,7 @@ public final class MacSignVerify {
}); });
// Set to "null" if the sign origin is not found, instead of bailing out with an exception. // Set to "null" if the sign origin is not found, instead of bailing out with an exception.
// Let is fail in the following TKit.assertEquals() call with a proper log message. // Let it fail in the following TKit.assertEquals() call with a proper log message.
var signOrigin = findSpctlSignOrigin(SpctlType.EXEC, bundleRoot).orElse(null); var signOrigin = findSpctlSignOrigin(SpctlType.EXEC, bundleRoot).orElse(null);
TKit.assertEquals(certRequest.name(), signOrigin, TKit.assertEquals(certRequest.name(), signOrigin,
@ -92,10 +89,14 @@ public final class MacSignVerify {
} }
public static Optional<PListReader> findEntitlements(Path path) { public static Optional<PListReader> findEntitlements(Path path) {
final var exec = Executor.of("/usr/bin/codesign", "-d", "--entitlements", "-", "--xml", path.toString()).saveOutput().dumpOutput(); final var exec = Executor.of(
"/usr/bin/codesign",
"-d",
"--entitlements", "-",
"--xml", path.toString()).saveOutput().dumpOutput().binaryOutput();
final var result = exec.execute(); final var result = exec.execute();
var xml = result.stdout(); var xml = result.byteStdout();
if (xml.isEmpty()) { if (xml.length == 0) {
return Optional.empty(); return Optional.empty();
} else { } else {
return Optional.of(MacHelper.readPList(xml)); return Optional.of(MacHelper.readPList(xml));
@ -135,17 +136,33 @@ public final class MacSignVerify {
public static final String ADHOC_SIGN_ORIGIN = "-"; public static final String ADHOC_SIGN_ORIGIN = "-";
public static Optional<String> findSpctlSignOrigin(SpctlType type, Path path) { public static Optional<String> findSpctlSignOrigin(SpctlType type, Path path) {
final var exec = Executor.of("/usr/sbin/spctl", "-vv", "--raw", "--assess", "--type", type.value(), path.toString()).saveOutput().discardStderr(); return findSpctlSignOrigin(type, path, false);
final var result = exec.executeWithoutExitCodeCheck(); }
TKit.assertTrue(Set.of(0, 3).contains(result.getExitCode()),
String.format("Check exit code of command %s is either 0 or 3", exec.getPrintableCommandLine())); public static Optional<String> findSpctlSignOrigin(SpctlType type, Path path, boolean acceptBrokenSignature) {
return toSupplier(() -> { final var exec = Executor.of(
try { "/usr/sbin/spctl",
return Optional.of(new PListReader(String.join("", result.getOutput()).getBytes()).queryValue("assessment:originator")); "-vv",
} catch (NoSuchElementException ex) { "--raw",
return Optional.<String>empty(); "--assess",
"--type", type.value(),
path.toString()).saveOutput().discardStderr().binaryOutput();
Executor.Result result;
if (acceptBrokenSignature) {
result = exec.executeWithoutExitCodeCheck();
switch (result.getExitCode()) {
case 0, 3 -> {
// NOP
}
default -> {
// No plist XML to process.
return Optional.empty();
}
} }
}).get(); } else {
result = exec.execute(0, 3);
}
return MacHelper.readPList(result.byteStdout()).findValue("assessment:originator");
} }
public static Optional<String> findCodesignSignOrigin(Path path) { public static Optional<String> findCodesignSignOrigin(Path path) {

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2019, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* *
* This code is free software; you can redistribute it and/or modify it * This code is free software; you can redistribute it and/or modify it
@ -29,7 +29,6 @@ import static jdk.jpackage.internal.cli.StandardAppImageFileOption.LAUNCHER_AS_S
import static jdk.jpackage.internal.cli.StandardAppImageFileOption.LAUNCHER_NAME; import static jdk.jpackage.internal.cli.StandardAppImageFileOption.LAUNCHER_NAME;
import static jdk.jpackage.internal.cli.StandardAppImageFileOption.LINUX_LAUNCHER_SHORTCUT; import static jdk.jpackage.internal.cli.StandardAppImageFileOption.LINUX_LAUNCHER_SHORTCUT;
import static jdk.jpackage.internal.cli.StandardAppImageFileOption.MAC_APP_STORE; import static jdk.jpackage.internal.cli.StandardAppImageFileOption.MAC_APP_STORE;
import static jdk.jpackage.internal.cli.StandardAppImageFileOption.MAC_SIGNED;
import static jdk.jpackage.internal.cli.StandardAppImageFileOption.WIN_LAUNCHER_DESKTOP_SHORTCUT; import static jdk.jpackage.internal.cli.StandardAppImageFileOption.WIN_LAUNCHER_DESKTOP_SHORTCUT;
import static jdk.jpackage.internal.cli.StandardAppImageFileOption.WIN_LAUNCHER_MENU_SHORTCUT; import static jdk.jpackage.internal.cli.StandardAppImageFileOption.WIN_LAUNCHER_MENU_SHORTCUT;
import static jdk.jpackage.internal.cli.StandardOption.APPCLASS; import static jdk.jpackage.internal.cli.StandardOption.APPCLASS;
@ -514,7 +513,6 @@ public class AppImageFileTest {
"<main-class>Foo</main-class>", "<main-class>Foo</main-class>",
"<y/>", "<y/>",
"<x>property-x</x>", "<x>property-x</x>",
"<signed>true</signed>",
"<app-store>False</app-store>", "<app-store>False</app-store>",
"<add-launcher name='add-launcher'>", "<add-launcher name='add-launcher'>",
" <description>Quick brown fox</description>", " <description>Quick brown fox</description>",
@ -546,8 +544,7 @@ public class AppImageFileTest {
.addExtra(WIN_LAUNCHER_MENU_SHORTCUT, new LauncherShortcut(LauncherShortcutStartupDirectory.APP_DIR)).commit()).create()); .addExtra(WIN_LAUNCHER_MENU_SHORTCUT, new LauncherShortcut(LauncherShortcutStartupDirectory.APP_DIR)).commit()).create());
testCases.add(builder.os(OperatingSystem.MACOS).expect(appBuilder.get().commit() testCases.add(builder.os(OperatingSystem.MACOS).expect(appBuilder.get().commit()
.addExtra(MAC_APP_STORE, false) .addExtra(MAC_APP_STORE, false)).create());
.addExtra(MAC_SIGNED, true)).create());
return testCases; return testCases;
} }
@ -580,7 +577,6 @@ public class AppImageFileTest {
"<main-class>OverwrittenMain</main-class>", "<main-class>OverwrittenMain</main-class>",
"<main-class>Main</main-class>", "<main-class>Main</main-class>",
"<x>property-x</x>", "<x>property-x</x>",
"<signed>true</signed>",
"<add-launcher name='service-launcher' service='true'>", "<add-launcher name='service-launcher' service='true'>",
" <linux-shortcut><nested>foo</nested></linux-shortcut>", " <linux-shortcut><nested>foo</nested></linux-shortcut>",
" <description>service-launcher description</description>", " <description>service-launcher description</description>",

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. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* *
* This code is free software; you can redistribute it and/or modify it * This code is free software; you can redistribute it and/or modify it
@ -34,6 +34,8 @@ import java.util.function.Consumer;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.UnaryOperator; import java.util.function.UnaryOperator;
import java.util.stream.StreamSupport; import java.util.stream.StreamSupport;
import jdk.jpackage.internal.cli.Validator.ParsedValue;
import jdk.jpackage.internal.cli.Validator.ValidatorException;
import jdk.jpackage.test.JUnitUtils; import jdk.jpackage.test.JUnitUtils;
final class TestUtils { final class TestUtils {
@ -152,6 +154,31 @@ final class TestUtils {
} }
static final class RecordingValidator<T, U extends Exception> implements Validator<T, U> {
RecordingValidator(Validator<T, U> validator) {
this.validator = Objects.requireNonNull(validator);
}
@Override
public List<U> validate(OptionName optionName, ParsedValue<T> optionValue) {
counter++;
return validator.validate(optionName, optionValue);
}
int counter() {
return counter;
}
void resetCounter() {
counter = 0;
}
private final Validator<T, U> validator;
private int counter;
}
private record RecordingExceptionFactory(OptionValueExceptionFactory<? extends RuntimeException> factory, private record RecordingExceptionFactory(OptionValueExceptionFactory<? extends RuntimeException> factory,
Consumer<OptionFailure> sink) implements OptionValueExceptionFactory<RuntimeException> { Consumer<OptionFailure> sink) implements OptionValueExceptionFactory<RuntimeException> {

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. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* *
* This code is free software; you can redistribute it and/or modify it * This code is free software; you can redistribute it and/or modify it
@ -32,10 +32,11 @@ import static org.junit.jupiter.api.Assertions.assertThrowsExactly;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.stream.Stream;
import jdk.jpackage.internal.cli.TestUtils.TestException; import jdk.jpackage.internal.cli.TestUtils.TestException;
import jdk.jpackage.internal.cli.TestUtils.RecordingValidator;
import jdk.jpackage.internal.cli.Validator.ParsedValue; import jdk.jpackage.internal.cli.Validator.ParsedValue;
import jdk.jpackage.internal.cli.Validator.ValidatingConsumerException; import jdk.jpackage.internal.cli.Validator.ValidatingConsumerException;
import jdk.jpackage.internal.cli.Validator.ValidatorException; import jdk.jpackage.internal.cli.Validator.ValidatorException;
@ -187,46 +188,97 @@ public class ValidatorTest {
} }
@Test @Test
public void test_andThen() { public void test_and() {
Function<String, Validator<String, Exception>> createFailingValidator = exceptionMessage -> {
Objects.requireNonNull(exceptionMessage);
var exceptionFactory = OptionValueExceptionFactory.build().ctor(TestException::new).messageFormatter((_, _) -> {
return exceptionMessage;
}).create();
return Validator.<String, Exception>build()
.predicate(_ -> false)
.formatString("")
.exceptionFactory(exceptionFactory).create();
};
Function<Validator<String, ? extends Exception>, List<? extends Exception>> validate = validator -> { Function<Validator<String, ? extends Exception>, List<? extends Exception>> validate = validator -> {
return validator.validate(OptionName.of("a"), ParsedValue.create("str", StringToken.of("str"))); return validator.validate(OptionName.of("a"), ParsedValue.create("str", StringToken.of("str")));
}; };
var pass = Validator.<String, RuntimeException>build().predicate(_ -> true).create(); var pass = new RecordingValidator<>(Validator.<String, RuntimeException>build().predicate(_ -> true).create());
var foo = createFailingValidator.apply("foo"); var foo = failingValidator("foo");
var bar = createFailingValidator.apply("bar"); var bar = failingValidator("bar");
var buz = createFailingValidator.apply("buz"); var buz = failingValidator("buz");
assertExceptionListEquals(List.of( assertExceptionListEquals(List.of(
new TestException("foo"), new TestException("foo"),
new TestException("bar"), new TestException("bar"),
new TestException("buz") new TestException("buz")
), validate.apply(foo.andThen(bar).andThen(pass).andThen(buz))); ), validate.apply(foo.and(bar).and(pass).and(buz)));
assertEquals(1, pass.counter());
pass.resetCounter();
assertExceptionListEquals(List.of( assertExceptionListEquals(List.of(
new TestException("bar"), new TestException("bar"),
new TestException("buz"), new TestException("buz"),
new TestException("foo") new TestException("foo")
), validate.apply(pass.andThen(bar).andThen(buz).andThen(foo))); ), validate.apply(pass.and(bar).and(buz).and(foo)));
assertEquals(1, pass.counter());
assertExceptionListEquals(List.of( assertExceptionListEquals(List.of(
new TestException("foo"), new TestException("foo"),
new TestException("foo") new TestException("foo")
), validate.apply(foo.andThen(foo))); ), validate.apply(foo.and(foo)));
pass.resetCounter();
assertExceptionListEquals(List.of(
), validate.apply(pass.and(pass)));
assertEquals(2, pass.counter());
}
@Test
public void test_or() {
Function<Validator<String, ? extends Exception>, List<? extends Exception>> validate = validator -> {
return validator.validate(OptionName.of("a"), ParsedValue.create("str", StringToken.of("str")));
};
var pass = new RecordingValidator<>(Validator.<String, RuntimeException>build().predicate(_ -> true).create());
var foo = new RecordingValidator<>(failingValidator("foo"));
var bar = new RecordingValidator<>(failingValidator("bar"));
var buz = new RecordingValidator<>(failingValidator("buz"));
Runnable resetCounters = () -> {
Stream.of(pass, foo, bar, buz).forEach(RecordingValidator::resetCounter);
};
assertExceptionListEquals(List.of(
new TestException("foo"),
new TestException("bar"),
new TestException("buz")
), validate.apply(foo.or(bar).or(buz)));
assertEquals(1, foo.counter());
assertEquals(1, bar.counter());
assertEquals(1, buz.counter());
resetCounters.run();
assertExceptionListEquals(List.of(
), validate.apply(foo.or(bar).or(pass).or(buz)));
assertEquals(1, foo.counter());
assertEquals(1, bar.counter());
assertEquals(1, pass.counter());
assertEquals(0, buz.counter());
resetCounters.run();
assertExceptionListEquals(List.of(
), validate.apply(pass.or(bar).or(buz).or(foo)));
assertEquals(1, pass.counter());
assertEquals(0, bar.counter());
assertEquals(0, buz.counter());
assertEquals(0, foo.counter());
resetCounters.run();
assertExceptionListEquals(List.of(
new TestException("foo"),
new TestException("foo")
), validate.apply(foo.or(foo)));
assertEquals(2, foo.counter());
resetCounters.run();
assertExceptionListEquals(List.of(
), validate.apply(pass.or(pass)));
assertEquals(1, pass.counter());
} }
@ParameterizedTest @ParameterizedTest
@ -269,6 +321,17 @@ public class ValidatorTest {
return data; return data;
} }
private static Validator<String, Exception> failingValidator(String exceptionMessage) {
var exceptionFactory = OptionValueExceptionFactory.build().ctor(TestException::new).messageFormatter((_, _) -> {
return exceptionMessage;
}).create();
return Validator.<String, Exception>build()
.predicate(_ -> false)
.formatString("")
.exceptionFactory(exceptionFactory).create();
}
static final class FooException extends Exception { static final class FooException extends Exception {

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2018, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* *
* This code is free software; you can redistribute it and/or modify it * This code is free software; you can redistribute it and/or modify it
@ -26,6 +26,7 @@ import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.function.Predicate; import java.util.function.Predicate;
import jdk.internal.util.OperatingSystem; import jdk.internal.util.OperatingSystem;
import jdk.jpackage.internal.util.MacBundle;
import jdk.jpackage.internal.util.XmlUtils; import jdk.jpackage.internal.util.XmlUtils;
import jdk.jpackage.test.Annotations.Parameter; import jdk.jpackage.test.Annotations.Parameter;
import jdk.jpackage.test.Annotations.Test; import jdk.jpackage.test.Annotations.Test;
@ -133,8 +134,7 @@ public class AppImagePackageTest {
*/ */
@Test @Test
public static void testBadAppImage() throws IOException { public static void testBadAppImage() throws IOException {
Path appImageDir = TKit.createTempDirectory("appimage"); Path appImageDir = createInvalidAppImage();
Files.createFile(appImageDir.resolve("foo"));
configureBadAppImage(appImageDir).addInitializer(cmd -> { configureBadAppImage(appImageDir).addInitializer(cmd -> {
cmd.removeArgumentWithValue("--name"); cmd.removeArgumentWithValue("--name");
}).run(Action.CREATE); }).run(Action.CREATE);
@ -145,8 +145,7 @@ public class AppImagePackageTest {
*/ */
@Test @Test
public static void testBadAppImage2() throws IOException { public static void testBadAppImage2() throws IOException {
Path appImageDir = TKit.createTempDirectory("appimage"); Path appImageDir = createInvalidAppImage();
Files.createFile(appImageDir.resolve("foo"));
configureBadAppImage(appImageDir).run(Action.CREATE); configureBadAppImage(appImageDir).run(Action.CREATE);
} }
@ -227,4 +226,19 @@ public class AppImagePackageTest {
+ TKit.ICON_SUFFIX)); + TKit.ICON_SUFFIX));
} }
private static Path createInvalidAppImage() throws IOException {
Path appImageDir = TKit.createTempDirectory("appimage");
if (TKit.isOSX()) {
// Create minimal macOS bundle to prevent jpackage bail out early
// with "error.parameter-not-mac-bundle" error.
var bundle = new MacBundle(appImageDir);
Files.createDirectories(bundle.macOsDir());
Files.createFile(bundle.infoPlistFile());
} else {
Files.createFile(appImageDir.resolve("foo"));
}
return appImageDir;
}
} }

View File

@ -643,7 +643,12 @@ public final class ErrorTest {
.error("message.invalid-identifier", "#1"), .error("message.invalid-identifier", "#1"),
// Bundle for mac app store should not have runtime commands // Bundle for mac app store should not have runtime commands
testSpec().nativeType().addArgs("--mac-app-store", "--jlink-options", "--bind-services") testSpec().nativeType().addArgs("--mac-app-store", "--jlink-options", "--bind-services")
.error("ERR_MissingJLinkOptMacAppStore", "--strip-native-commands") .error("ERR_MissingJLinkOptMacAppStore", "--strip-native-commands"),
// Predefined app image must be a valid macOS bundle.
testSpec().noAppDesc().nativeType().addArgs("--app-image", Token.EMPTY_DIR.token())
.error("error.parameter-not-mac-bundle", JPackageCommand.cannedArgument(cmd -> {
return Path.of(cmd.getArgumentValue("--app-image"));
}, Token.EMPTY_DIR.token()), "--app-image")
).map(TestSpec.Builder::create).toList()); ).map(TestSpec.Builder::create).toList());
macInvalidRuntime(testCases::add); macInvalidRuntime(testCases::add);