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.MacApplication;
import jdk.jpackage.internal.model.RuntimeLayout;
import jdk.jpackage.internal.util.MacBundle;
import jdk.jpackage.internal.util.PathUtils;
import jdk.jpackage.internal.util.Result;
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.
*
* 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.validateRuntimeHasNoBinDir;
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.ICON;
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_STORE;
@ -52,11 +52,13 @@ import static jdk.jpackage.internal.model.StandardPackageType.MAC_PKG;
import static jdk.jpackage.internal.util.function.ExceptionBox.toUnchecked;
import java.nio.file.Path;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import jdk.jpackage.internal.ApplicationBuilder.MainLauncherStartupInfo;
import jdk.jpackage.internal.SigningIdentityBuilder.ExpiredCertificateException;
import jdk.jpackage.internal.SigningIdentityBuilder.StandardCertificateSelector;
import jdk.jpackage.internal.cli.OptionValue;
import jdk.jpackage.internal.cli.Options;
import jdk.jpackage.internal.cli.StandardFaOption;
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.PackageType;
import jdk.jpackage.internal.model.RuntimeLayout;
import jdk.jpackage.internal.util.MacBundle;
import jdk.jpackage.internal.util.Result;
import jdk.jpackage.internal.util.function.ExceptionBox;
@ -276,16 +279,12 @@ final class MacFromOptions {
final var builder = new MacPackageBuilder(createPackageBuilder(options, app.app(), type));
app.externalApp()
.map(ExternalApplication::extra)
.flatMap(MAC_SIGN::findIn)
.ifPresent(builder::predefinedAppImageSigned);
PREDEFINED_RUNTIME_IMAGE.findIn(options)
.map(MacBundle::new)
.filter(MacBundle::isValid)
.map(MacBundle::isSigned)
.ifPresent(builder::predefinedAppImageSigned);
for (OptionValue<Path> ov : List.of(PREDEFINED_APP_IMAGE, PREDEFINED_RUNTIME_IMAGE)) {
ov.findIn(options)
.flatMap(MacBundle::fromPath)
.map(MacPackagingPipeline::isSigned)
.ifPresent(builder::predefinedAppImageSigned);
}
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.
*
* 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.PackageType;
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.function.ThrowingConsumer;
@ -178,13 +180,10 @@ final class MacPackagingPipeline {
builder.task(MacCopyAppImageTaskID.COPY_RUNTIME_JLILIB)
.appImageAction(MacPackagingPipeline::copyJliLib).add();
final var predefinedRuntimeBundle = Optional.of(
new MacBundle(p.predefinedAppImage().orElseThrow())).filter(MacBundle::isValid);
// Don't create ".package" file.
disabledTasks.add(MacCopyAppImageTaskID.COPY_PACKAGE_FILE);
if (predefinedRuntimeBundle.isPresent()) {
if (MacBundle.fromPath(p.predefinedAppImage().orElseThrow()).isPresent()) {
// The input runtime image is a macOS bundle.
// Disable all alterations of the input bundle, but keep the signing enabled.
disabledTasks.addAll(List.of(MacCopyAppImageTaskID.values()));
@ -195,7 +194,7 @@ final class MacPackagingPipeline {
.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.
// Disable the signing, i.e. don't re-sign the input bundle.
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,
AppImageLayout dstAppImage) throws IOException {
@ -286,7 +309,7 @@ final class MacPackagingPipeline {
final Optional<MacBundle> srcMacBundle;
if (pkg.isRuntimeInstaller()) {
srcMacBundle = MacBundle.fromAppImageLayout(srcAppImage);
srcMacBundle = macBundleFromAppImageLayout(srcAppImage);
} else {
srcMacBundle = Optional.empty();
}
@ -297,7 +320,7 @@ final class MacPackagingPipeline {
try {
FileUtils.copyRecursive(
inputBundle.root(),
MacBundle.fromAppImageLayout(dstAppImage).orElseThrow().root(),
macBundleFromAppImageLayout(dstAppImage).orElseThrow().root(),
LinkOption.NOFOLLOW_LINKS);
} catch (IOException ex) {
throw new UncheckedIOException(ex);
@ -415,7 +438,7 @@ final class MacPackagingPipeline {
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)));
@ -468,7 +491,7 @@ final class MacPackagingPipeline {
}
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 -> {
@ -550,7 +573,7 @@ final class MacPackagingPipeline {
private static MacBundle runtimeBundle(AppImageBuildEnv<MacApplication, AppImageLayout> env) {
if (env.app().isRuntime()) {
return MacBundle.fromAppImageLayout(env.resolvedLayout()).orElseThrow();
return macBundleFromAppImageLayout(env.resolvedLayout()).orElseThrow();
} else {
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 {

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

View File

@ -233,6 +233,12 @@ public final class StandardOption {
.mutate(createOptionSpecBuilderMutator((b, context) -> {
if (context.os() == OperatingSystem.MACOS) {
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();

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
@ -38,6 +38,7 @@ import java.util.function.Consumer;
import java.util.function.Predicate;
import jdk.jpackage.internal.cli.Validator.ValidatingConsumerException;
import jdk.jpackage.internal.util.FileUtils;
import jdk.jpackage.internal.util.MacBundle;
final public class StandardValidator {
@ -138,6 +139,10 @@ final public class StandardValidator {
return true;
};
public static Predicate<Path> IS_VALID_MAC_BUNDLE = path -> {
return MacBundle.fromPath(path).isPresent();
};
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.
*
* This code is free software; you can redistribute it and/or modify it
@ -24,20 +24,55 @@
*/
package jdk.jpackage.internal.cli;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Stream;
@FunctionalInterface
interface Validator<T, U extends Exception> {
List<U> validate(OptionName optionName, ParsedValue<T> optionValue);
default Validator<T, ? extends Exception> andThen(Validator<T, ? extends Exception> after) {
return reduce(this, after);
default Validator<T, ? extends Exception> and(Validator<T, ? extends Exception> 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-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-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.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

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
@ -23,54 +23,49 @@
* questions.
*/
package jdk.jpackage.internal;
package jdk.jpackage.internal.util;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Objects;
import java.util.Optional;
import jdk.jpackage.internal.model.AppImageLayout;
/**
* 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>
*/
record MacBundle(Path root) {
public record MacBundle(Path root) {
MacBundle {
public MacBundle {
Objects.requireNonNull(root);
}
boolean isValid() {
public boolean isValid() {
return Files.isDirectory(contentsDir()) && Files.isDirectory(macOsDir()) && Files.isRegularFile(infoPlistFile());
}
boolean isSigned() {
return Files.isDirectory(contentsDir().resolve("_CodeSignature"));
}
Path contentsDir() {
public Path contentsDir() {
return root.resolve("Contents");
}
Path homeDir() {
public Path homeDir() {
return contentsDir().resolve("Home");
}
Path macOsDir() {
public Path macOsDir() {
return contentsDir().resolve("MacOS");
}
Path resourcesDir() {
public Path resourcesDir() {
return contentsDir().resolve("Resources");
}
Path infoPlistFile() {
public Path infoPlistFile() {
return contentsDir().resolve("Info.plist");
}
static Optional<MacBundle> fromPath(Path path) {
public static Optional<MacBundle> fromPath(Path path) {
var bundle = new MacBundle(path);
if (bundle.isValid()) {
return Optional.of(bundle);
@ -78,20 +73,4 @@ record MacBundle(Path root) {
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;
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) {
return ApplicationLayout.platformAppImage()
@ -66,7 +66,7 @@ public record AppImageFile(String mainLauncherName, Optional<String> mainLaunche
}
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) {
@ -103,10 +103,6 @@ public record AppImageFile(String mainLauncherName, Optional<String> mainLaunche
xml.writeEndElement();
}));
xml.writeStartElement("signed");
xml.writeCharacters(Boolean.toString(macSigned));
xml.writeEndElement();
xml.writeStartElement("app-store");
xml.writeCharacters(Boolean.toString(macAppStore));
xml.writeEndElement();
@ -140,10 +136,6 @@ public record AppImageFile(String mainLauncherName, Optional<String> mainLaunche
var mainLauncherClassName = Optional.ofNullable(xPath.evaluate(
"/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(
"/jpackage-state/app-store/text()", doc)).map(
Boolean::parseBoolean).orElse(false);
@ -171,7 +163,6 @@ public record AppImageFile(String mainLauncherName, Optional<String> mainLaunche
mainLauncherName,
mainLauncherClassName,
version,
macSigned,
macAppStore,
Collections.unmodifiableMap(launchers));

View File

@ -1385,7 +1385,7 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
if (!isImagePackageType() && hasArgument("--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.
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);
if (TKit.isOSX()) {
boolean expectedValue = MacHelper.appImageSigned(this);
boolean actualValue = aif.macSigned();
TKit.assertEquals(expectedValue, actualValue,
"Check for unexpected value of <signed> property in app image file");
expectedValue = hasArgument("--mac-app-store");
actualValue = aif.macAppStore();
var expectedValue = hasArgument("--mac-app-store");
var actualValue = aif.macAppStore();
TKit.assertEquals(expectedValue, actualValue,
"Check for unexpected value of <app-store> property in app image file");
}
@ -1437,7 +1432,7 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
} else {
if (TKit.isOSX() && hasArgument("--app-image")) {
String appImage = getArgumentValue("--app-image");
if (AppImageFile.load(Path.of(appImage)).macSigned()) {
if (MacHelper.isBundleSigned(Path.of(appImage))) {
assertFileNotInAppImage(lookupPath);
} else {
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.XmlUtils.initDocumentBuilder;
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 java.io.ByteArrayInputStream;
@ -45,6 +46,7 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@ -59,14 +61,13 @@ import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.UnaryOperator;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathFactory;
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.RetryExecutor;
@ -89,8 +90,8 @@ public final class MacHelper {
// See JDK-8373105. "hdiutil" does not handle such cases very good.
final var mountRoot = TKit.createTempDirectory("mountRoot");
// Explode DMG assuming this can require interaction, thus use `yes`.
final var attachStdout = Executor.of("sh", "-c", String.join(" ",
// Explode the DMG assuming this can require interaction if the DMG has a license, thus use `yes`.
final var attachExec = Executor.of("sh", "-c", String.join(" ",
"yes",
"|",
"/usr/bin/hdiutil",
@ -99,14 +100,34 @@ public final class MacHelper {
"-mountroot", PathUtils.normalizedAbsolutePathString(mountRoot),
"-nobrowse",
"-plist"
)).saveOutput().storeOutputInFiles().executeAndRepeatUntilExitCode(0, 10, 6).stdout();
)).saveOutput().storeOutputInFiles().binaryOutput();
final var attachResult = attachExec.executeAndRepeatUntilExitCode(0, 10, 6);
final Path mountPoint;
boolean mountPointInitialized = false;
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.
mountPoint = readPList(attachStdout).queryArrayValue("system-entities", false)
mountPoint = readPList(plistXml).queryArrayValue("system-entities", false)
.map(PListReader.class::cast)
.map(dict -> {
return dict.findValue("mount-point");
@ -117,7 +138,7 @@ public final class MacHelper {
} finally {
if (!mountPointInitialized) {
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");
}
}
@ -168,19 +189,13 @@ public final class MacHelper {
public static PListReader readPList(Path path) {
TKit.assertReadableFileExists(path);
return ThrowingSupplier.toSupplier(() -> readPList(Files.readAllLines(
path))).get();
return readPList(toFunction(Files::readAllBytes).apply(path));
}
public static PListReader readPList(List<String> lines) {
return readPList(lines.stream());
}
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 PListReader readPList(byte[] xml) {
return ThrowingSupplier.toSupplier(() -> {
return new PListReader(xml);
}).get();
}
public static Map<String, String> flatMapPList(PListReader plistReader) {
@ -265,13 +280,13 @@ public final class MacHelper {
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);
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.
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.
return true;
}
@ -301,6 +316,14 @@ public final class MacHelper {
}).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)
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 plistPath = ApplicationLayout.macAppImage().resolveAt(predefinedAppImage).contentDirectory().resolve("Info.plist");
var plistPath = MacBundle.fromPath(predefinedAppImage).orElseThrow().infoPlistFile();
try (var plistStream = Files.newInputStream(plistPath)) {
var plist = new PListReader(initDocumentBuilder().parse(plistStream));

View File

@ -22,7 +22,6 @@
*/
package jdk.jpackage.test;
import static jdk.jpackage.internal.util.function.ThrowingSupplier.toSupplier;
import static jdk.jpackage.test.MacSign.DigestAlgorithm.SHA256;
import java.nio.file.Path;
@ -30,10 +29,8 @@ import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.HexFormat;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Pattern;
import jdk.jpackage.internal.util.PListReader;
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.
// 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);
TKit.assertEquals(certRequest.name(), signOrigin,
@ -92,10 +89,14 @@ public final class MacSignVerify {
}
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();
var xml = result.stdout();
if (xml.isEmpty()) {
var xml = result.byteStdout();
if (xml.length == 0) {
return Optional.empty();
} else {
return Optional.of(MacHelper.readPList(xml));
@ -135,17 +136,33 @@ public final class MacSignVerify {
public static final String ADHOC_SIGN_ORIGIN = "-";
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();
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()));
return toSupplier(() -> {
try {
return Optional.of(new PListReader(String.join("", result.getOutput()).getBytes()).queryValue("assessment:originator"));
} catch (NoSuchElementException ex) {
return Optional.<String>empty();
return findSpctlSignOrigin(type, path, false);
}
public static Optional<String> findSpctlSignOrigin(SpctlType type, Path path, boolean acceptBrokenSignature) {
final var exec = Executor.of(
"/usr/sbin/spctl",
"-vv",
"--raw",
"--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) {

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.
*
* 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.LINUX_LAUNCHER_SHORTCUT;
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_MENU_SHORTCUT;
import static jdk.jpackage.internal.cli.StandardOption.APPCLASS;
@ -514,7 +513,6 @@ public class AppImageFileTest {
"<main-class>Foo</main-class>",
"<y/>",
"<x>property-x</x>",
"<signed>true</signed>",
"<app-store>False</app-store>",
"<add-launcher name='add-launcher'>",
" <description>Quick brown fox</description>",
@ -546,8 +544,7 @@ public class AppImageFileTest {
.addExtra(WIN_LAUNCHER_MENU_SHORTCUT, new LauncherShortcut(LauncherShortcutStartupDirectory.APP_DIR)).commit()).create());
testCases.add(builder.os(OperatingSystem.MACOS).expect(appBuilder.get().commit()
.addExtra(MAC_APP_STORE, false)
.addExtra(MAC_SIGNED, true)).create());
.addExtra(MAC_APP_STORE, false)).create());
return testCases;
}
@ -580,7 +577,6 @@ public class AppImageFileTest {
"<main-class>OverwrittenMain</main-class>",
"<main-class>Main</main-class>",
"<x>property-x</x>",
"<signed>true</signed>",
"<add-launcher name='service-launcher' service='true'>",
" <linux-shortcut><nested>foo</nested></linux-shortcut>",
" <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.
*
* 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.UnaryOperator;
import java.util.stream.StreamSupport;
import jdk.jpackage.internal.cli.Validator.ParsedValue;
import jdk.jpackage.internal.cli.Validator.ValidatorException;
import jdk.jpackage.test.JUnitUtils;
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,
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.
*
* 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.List;
import java.util.Objects;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Stream;
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.ValidatingConsumerException;
import jdk.jpackage.internal.cli.Validator.ValidatorException;
@ -187,46 +188,97 @@ public class ValidatorTest {
}
@Test
public void test_andThen() {
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();
};
public void test_and() {
Function<Validator<String, ? extends Exception>, List<? extends Exception>> validate = validator -> {
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 bar = createFailingValidator.apply("bar");
var buz = createFailingValidator.apply("buz");
var foo = failingValidator("foo");
var bar = failingValidator("bar");
var buz = failingValidator("buz");
assertExceptionListEquals(List.of(
new TestException("foo"),
new TestException("bar"),
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(
new TestException("bar"),
new TestException("buz"),
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(
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
@ -269,6 +321,17 @@ public class ValidatorTest {
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 {

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.
*
* 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.util.function.Predicate;
import jdk.internal.util.OperatingSystem;
import jdk.jpackage.internal.util.MacBundle;
import jdk.jpackage.internal.util.XmlUtils;
import jdk.jpackage.test.Annotations.Parameter;
import jdk.jpackage.test.Annotations.Test;
@ -133,8 +134,7 @@ public class AppImagePackageTest {
*/
@Test
public static void testBadAppImage() throws IOException {
Path appImageDir = TKit.createTempDirectory("appimage");
Files.createFile(appImageDir.resolve("foo"));
Path appImageDir = createInvalidAppImage();
configureBadAppImage(appImageDir).addInitializer(cmd -> {
cmd.removeArgumentWithValue("--name");
}).run(Action.CREATE);
@ -145,8 +145,7 @@ public class AppImagePackageTest {
*/
@Test
public static void testBadAppImage2() throws IOException {
Path appImageDir = TKit.createTempDirectory("appimage");
Files.createFile(appImageDir.resolve("foo"));
Path appImageDir = createInvalidAppImage();
configureBadAppImage(appImageDir).run(Action.CREATE);
}
@ -227,4 +226,19 @@ public class AppImagePackageTest {
+ 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"),
// Bundle for mac app store should not have runtime commands
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());
macInvalidRuntime(testCases::add);