From f7be1dcf296d28f8e004d180038ab715153a6c15 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Tue, 13 Jan 2026 13:33:41 +0000 Subject: [PATCH] 8375054: Removed "signed" property from jpackage app image file Reviewed-by: almatvee --- .../jdk/jpackage/internal/AppImageSigner.java | 1 + .../jdk/jpackage/internal/MacFromOptions.java | 23 ++-- .../internal/MacPackagingPipeline.java | 61 ++++++++-- .../internal/model/MacApplication.java | 6 +- .../internal/cli/OptionSpecBuilder.java | 39 +++++-- .../cli/StandardAppImageFileOption.java | 11 +- .../jpackage/internal/cli/StandardOption.java | 6 + .../internal/cli/StandardValidator.java | 7 +- .../jdk/jpackage/internal/cli/Validator.java | 54 ++++++--- .../resources/MainResources.properties | 1 + .../jpackage/internal/util}/MacBundle.java | 43 ++----- .../jdk/jpackage/test/AppImageFile.java | 13 +-- .../jdk/jpackage/test/JPackageCommand.java | 13 +-- .../helpers/jdk/jpackage/test/MacHelper.java | 67 +++++++---- .../jdk/jpackage/test/MacSignVerify.java | 51 ++++++--- .../jpackage/internal/AppImageFileTest.java | 8 +- .../jdk/jpackage/internal/cli/TestUtils.java | 29 ++++- .../jpackage/internal/cli/ValidatorTest.java | 107 ++++++++++++++---- .../jpackage/share/AppImagePackageTest.java | 24 +++- test/jdk/tools/jpackage/share/ErrorTest.java | 7 +- 20 files changed, 380 insertions(+), 191 deletions(-) rename src/jdk.jpackage/{macosx/classes/jdk/jpackage/internal => share/classes/jdk/jpackage/internal/util}/MacBundle.java (63%) diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/AppImageSigner.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/AppImageSigner.java index 81e04ad7ed1..4c5edf43627 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/AppImageSigner.java +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/AppImageSigner.java @@ -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; diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacFromOptions.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacFromOptions.java index b1094331740..cdf33d6dcba 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacFromOptions.java +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacFromOptions.java @@ -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 ov : List.of(PREDEFINED_APP_IMAGE, PREDEFINED_RUNTIME_IMAGE)) { + ov.findIn(options) + .flatMap(MacBundle::fromPath) + .map(MacPackagingPipeline::isSigned) + .ifPresent(builder::predefinedAppImageSigned); + } return builder; } diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPackagingPipeline.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPackagingPipeline.java index 53f297282ba..a53df7f83c2 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPackagingPipeline.java +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPackagingPipeline.java @@ -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 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 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 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 { diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/model/MacApplication.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/model/MacApplication.java index cfe10e8a012..e2b3d30b7ae 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/model/MacApplication.java +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/model/MacApplication.java @@ -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())); }), diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/OptionSpecBuilder.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/OptionSpecBuilder.java index e27d6472369..6cd1c05b57e 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/OptionSpecBuilder.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/OptionSpecBuilder.java @@ -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 { 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 { scope, OptionSpecBuilder.this.mergePolicy().orElse(MergePolicy.CONCATENATE), defaultArrayOptionalValue(), - Optional.of(arryValuePattern()), + Optional.of(arrayValuePattern()), OptionSpecBuilder.this.description().orElse("")); } + Optional> createValidator() { + return Optional.ofNullable(validator).or(() -> { + if (validatorBuilder.hasValidatingMethod()) { + return Optional.of(validatorBuilder.create()); + } else { + return Optional.empty(); + } + }); + } + OptionSpecBuilder tokenizer(String splitRegexp) { Objects.requireNonNull(splitRegexp); return tokenizer(str -> { @@ -162,11 +173,13 @@ final class OptionSpecBuilder { OptionSpecBuilder validatorExceptionFormatString(String v) { validatorBuilder.formatString(v); + validator = null; return this; } OptionSpecBuilder validatorExceptionFormatString(UnaryOperator mutator) { validatorBuilder.formatString(mutator.apply(validatorBuilder.formatString().orElse(null))); + validator = null; return this; } @@ -182,6 +195,7 @@ final class OptionSpecBuilder { OptionSpecBuilder validatorExceptionFactory(OptionValueExceptionFactory v) { validatorBuilder.exceptionFactory(v); + validator = null; return this; } @@ -225,18 +239,27 @@ final class OptionSpecBuilder { OptionSpecBuilder validator(Predicate v) { validatorBuilder.predicate(v::test); + validator = null; return this; } @SuppressWarnings("overloads") OptionSpecBuilder validator(Consumer v) { validatorBuilder.consumer(v::accept); + validator = null; return this; } @SuppressWarnings("overloads") OptionSpecBuilder validator(UnaryOperator> mutator) { validatorBuilder = mutator.apply(validatorBuilder); + validator = null; + return this; + } + + OptionSpecBuilder validator(Validator v) { + validatorBuilder.predicate(null).consumer(null); + validator = Objects.requireNonNull(v); return this; } @@ -247,6 +270,7 @@ final class OptionSpecBuilder { OptionSpecBuilder withoutValidator() { validatorBuilder.predicate(null).consumer(null); + validator = null; return this; } @@ -423,14 +447,6 @@ final class OptionSpecBuilder { } } - private Optional> createValidator() { - if (validatorBuilder.hasValidatingMethod()) { - return Optional.of(validatorBuilder.create()); - } else { - return Optional.empty(); - } - } - private OptionValueConverter createArrayConverter() { final var newBuilder = converterBuilder.copy(); newBuilder.tokenizer(Optional.ofNullable(arrayTokenizer).orElse(str -> { @@ -440,7 +456,7 @@ final class OptionSpecBuilder { 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 { private String valuePattern; private OptionValueConverter.Builder converterBuilder = OptionValueConverter.build(); private Validator.Builder validatorBuilder = Validator.build(); + private Validator validator; private T[] arrayDefaultValue; private String arrayValuePatternSeparator; diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/StandardAppImageFileOption.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/StandardAppImageFileOption.java index 330a650e513..42f90536753 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/StandardAppImageFileOption.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/StandardAppImageFileOption.java @@ -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 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) { diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/StandardOption.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/StandardOption.java index 0fa0af296dc..dddaef8399b 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/StandardOption.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/StandardOption.java @@ -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(); diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/StandardValidator.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/StandardValidator.java index 0038740f9df..cfa97439592 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/StandardValidator.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/StandardValidator.java @@ -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 IS_VALID_MAC_BUNDLE = path -> { + return MacBundle.fromPath(path).isPresent(); + }; + public static final class DirectoryListingIOException extends RuntimeException { diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/Validator.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/Validator.java index 0ddf0e1984f..91d9d03bd9f 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/Validator.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/Validator.java @@ -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 { List validate(OptionName optionName, ParsedValue optionValue); - default Validator andThen(Validator after) { - return reduce(this, after); + default Validator and(Validator 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 or(Validator 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 Validator and(Validator first, Validator second) { + return (Validator)first.and(second); + } + + @SuppressWarnings("unchecked") + static Validator or(Validator first, Validator second) { + return (Validator)first.or(second); } /** @@ -251,15 +286,4 @@ interface Validator { } } } - - @SafeVarargs - private static Validator reduce(Validator... 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(); - }; - } } diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/resources/MainResources.properties b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/resources/MainResources.properties index e97bee79e6e..245d3b892da 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/resources/MainResources.properties +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/resources/MainResources.properties @@ -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 = 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 diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacBundle.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/MacBundle.java similarity index 63% rename from src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacBundle.java rename to src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/MacBundle.java index 723614f9bd6..95629e7d4b5 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacBundle.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/MacBundle.java @@ -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 https://en.wikipedia.org/wiki/Bundle_(macOS)#Application_bundles */ -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 fromPath(Path path) { + public static Optional 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 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(); - } } diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/AppImageFile.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/AppImageFile.java index 1c6c0ce4447..55b13a06620 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/AppImageFile.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/AppImageFile.java @@ -47,7 +47,7 @@ import org.w3c.dom.Document; import org.w3c.dom.Element; public record AppImageFile(String mainLauncherName, Optional mainLauncherClassName, - String version, boolean macSigned, boolean macAppStore, Map> launchers) { + String version, boolean macAppStore, Map> launchers) { public static Path getPathInAppImage(Path appImageDir) { return ApplicationLayout.platformAppImage() @@ -66,7 +66,7 @@ public record AppImageFile(String mainLauncherName, Optional mainLaunche } public AppImageFile(String mainLauncherName, Optional 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 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 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 mainLaunche mainLauncherName, mainLauncherClassName, version, - macSigned, macAppStore, Collections.unmodifiableMap(launchers)); diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java index c5c4f87b097..9cfb75bcdb3 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java @@ -1385,7 +1385,7 @@ public class JPackageCommand extends CommandArguments { 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 { 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 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 property in app image file"); } @@ -1437,7 +1432,7 @@ public class JPackageCommand extends CommandArguments { } 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); diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacHelper.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacHelper.java index 6a5be77457a..1cb5532d46a 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacHelper.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacHelper.java @@ -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(" 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 lines) { - return readPList(lines.stream()); - } - - public static PListReader readPList(Stream 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 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)); diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacSignVerify.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacSignVerify.java index 0ecfd4c3432..9c469c9362e 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacSignVerify.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacSignVerify.java @@ -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 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 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.empty(); + return findSpctlSignOrigin(type, path, false); + } + + public static Optional 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 findCodesignSignOrigin(Path path) { diff --git a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/AppImageFileTest.java b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/AppImageFileTest.java index 375c6aa637a..53f4b9b95aa 100644 --- a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/AppImageFileTest.java +++ b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/AppImageFileTest.java @@ -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 { "Foo", "", "property-x", - "true", "False", "", " Quick brown fox", @@ -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 { "OverwrittenMain", "Main", "property-x", - "true", "", " foo", " service-launcher description", diff --git a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/TestUtils.java b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/TestUtils.java index d3e9ecb09e9..2acf45b663d 100644 --- a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/TestUtils.java +++ b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/TestUtils.java @@ -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 implements Validator { + + RecordingValidator(Validator validator) { + this.validator = Objects.requireNonNull(validator); + } + + @Override + public List validate(OptionName optionName, ParsedValue optionValue) { + counter++; + return validator.validate(optionName, optionValue); + } + + int counter() { + return counter; + } + + void resetCounter() { + counter = 0; + } + + private final Validator validator; + private int counter; + } + + private record RecordingExceptionFactory(OptionValueExceptionFactory factory, Consumer sink) implements OptionValueExceptionFactory { diff --git a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/ValidatorTest.java b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/ValidatorTest.java index d4c48df6a0c..d8d69027f77 100644 --- a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/ValidatorTest.java +++ b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/ValidatorTest.java @@ -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> createFailingValidator = exceptionMessage -> { - Objects.requireNonNull(exceptionMessage); - var exceptionFactory = OptionValueExceptionFactory.build().ctor(TestException::new).messageFormatter((_, _) -> { - return exceptionMessage; - }).create(); - - return Validator.build() - .predicate(_ -> false) - .formatString("") - .exceptionFactory(exceptionFactory).create(); - }; + public void test_and() { Function, List> validate = validator -> { return validator.validate(OptionName.of("a"), ParsedValue.create("str", StringToken.of("str"))); }; - var pass = Validator.build().predicate(_ -> true).create(); + var pass = new RecordingValidator<>(Validator.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, List> validate = validator -> { + return validator.validate(OptionName.of("a"), ParsedValue.create("str", StringToken.of("str"))); + }; + + var pass = new RecordingValidator<>(Validator.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 failingValidator(String exceptionMessage) { + var exceptionFactory = OptionValueExceptionFactory.build().ctor(TestException::new).messageFormatter((_, _) -> { + return exceptionMessage; + }).create(); + + return Validator.build() + .predicate(_ -> false) + .formatString("") + .exceptionFactory(exceptionFactory).create(); + } + static final class FooException extends Exception { diff --git a/test/jdk/tools/jpackage/share/AppImagePackageTest.java b/test/jdk/tools/jpackage/share/AppImagePackageTest.java index bfd731b67d5..ce2300c92d1 100644 --- a/test/jdk/tools/jpackage/share/AppImagePackageTest.java +++ b/test/jdk/tools/jpackage/share/AppImagePackageTest.java @@ -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; + } + } diff --git a/test/jdk/tools/jpackage/share/ErrorTest.java b/test/jdk/tools/jpackage/share/ErrorTest.java index ca1189a191e..f31ac42dea0 100644 --- a/test/jdk/tools/jpackage/share/ErrorTest.java +++ b/test/jdk/tools/jpackage/share/ErrorTest.java @@ -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);