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 19ff78f174e..c908ec7447c 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/AppImageSigner.java +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/AppImageSigner.java @@ -24,7 +24,6 @@ */ package jdk.jpackage.internal; -import static java.util.stream.Collectors.joining; import static jdk.jpackage.internal.MacPackagingPipeline.APPLICATION_LAYOUT; import static jdk.jpackage.internal.model.MacPackage.RUNTIME_BUNDLE_LAYOUT; import static jdk.jpackage.internal.util.function.ThrowingConsumer.toConsumer; @@ -40,7 +39,6 @@ import java.util.Optional; import java.util.Set; import java.util.function.Consumer; import java.util.function.Predicate; -import java.util.stream.Stream; import jdk.jpackage.internal.Codesign.CodesignException; import jdk.jpackage.internal.model.Application; import jdk.jpackage.internal.model.ApplicationLayout; @@ -63,9 +61,10 @@ final class AppImageSigner { throw handleCodesignException(app, ex); } catch (ExceptionBox ex) { if (ex.getCause() instanceof CodesignException codesignEx) { - handleCodesignException(app, codesignEx); + throw handleCodesignException(app, codesignEx); + } else { + throw ex; } - throw ex; } }); } @@ -165,13 +164,9 @@ final class AppImageSigner { } } - private static CodesignException handleCodesignException(MacApplication app, CodesignException ex) { - // Log output of "codesign" in case of error. It should help - // user to diagnose issues when using --mac-app-image-sign-identity. - // In addition add possible reason for failure. For example - // "--app-content" can fail "codesign". - + private static IOException handleCodesignException(MacApplication app, CodesignException ex) { if (!app.contentDirSources().isEmpty()) { + // Additional content may cause signing error. Log.info(I18N.getString("message.codesign.failed.reason.app.content")); } @@ -182,11 +177,7 @@ final class AppImageSigner { Log.info(I18N.getString("message.codesign.failed.reason.xcode.tools")); } - // Log "codesign" output - Log.info(I18N.format("error.tool.failed.with.output", "codesign")); - Log.info(Stream.of(ex.getOutput()).collect(joining("\n")).strip()); - - return ex; + return ex.getCause(); } private static boolean isXcodeDevToolsInstalled() { diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/Codesign.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/Codesign.java index a7cd17b06b9..984202bbfaf 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/Codesign.java +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/Codesign.java @@ -34,22 +34,21 @@ import java.util.Objects; import java.util.Optional; import java.util.function.Consumer; import java.util.function.Supplier; - +import jdk.jpackage.internal.util.CommandOutputControl.UnexpectedExitCodeException; public final class Codesign { public static final class CodesignException extends Exception { - CodesignException(String[] output) { - this.output = output; + CodesignException(UnexpectedExitCodeException cause) { + super(Objects.requireNonNull(cause)); } - String[] getOutput() { - return output; + @Override + public UnexpectedExitCodeException getCause() { + return (UnexpectedExitCodeException)super.getCause(); } - private final String[] output; - private static final long serialVersionUID = 1L; } @@ -96,9 +95,10 @@ public final class Codesign { var exec = Executor.of(cmdline).args(path.toString()).saveOutput(true); configureExecutor.ifPresent(configure -> configure.accept(exec)); - var result = exec.execute(); - if (result.getExitCode() != 0) { - throw new CodesignException(result.getOutput().toArray(String[]::new)); + try { + exec.execute().expectExitCode(0); + } catch (UnexpectedExitCodeException ex) { + throw new CodesignException(ex); } } diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/resources/MacResources.properties b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/resources/MacResources.properties index 0237d49f399..e1b154c5933 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/resources/MacResources.properties +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/resources/MacResources.properties @@ -28,7 +28,6 @@ error.certificate.expired=Certificate expired {0} error.cert.not.found=No certificate found matching [{0}] using keychain [{1}] error.multiple.certs.found=Multiple certificates matching name [{0}] found in keychain [{1}] error.app-image.mac-sign.required=--mac-sign option is required with predefined application image and with type [app-image] -error.tool.failed.with.output="{0}" failed with following output: error.invalid-runtime-image-missing-file=Runtime image "{0}" is missing "{1}" file error.invalid-runtime-image-bin-dir=Runtime image "{0}" should not contain "bin" folder error.invalid-runtime-image-bin-dir.advice=Use --strip-native-commands jlink option when generating runtime image used with {0} option diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/Main.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/Main.java index 562a0d2d3c1..73b4850344b 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/Main.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/Main.java @@ -271,11 +271,9 @@ public final class Main { } messagePrinter.accept(I18N.format("message.error-header", msg)); - if (!verbose) { - messagePrinter.accept(I18N.format("message.failed-command-output-header")); - try (var lines = new BufferedReader(new StringReader(commandOutput)).lines()) { - lines.forEach(messagePrinter); - } + messagePrinter.accept(I18N.format("message.failed-command-output-header")); + try (var lines = new BufferedReader(new StringReader(commandOutput)).lines()) { + lines.forEach(messagePrinter); } } diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/FailedCommandErrorValidator.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/FailedCommandErrorValidator.java new file mode 100644 index 00000000000..ab644c36a5c --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/FailedCommandErrorValidator.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) 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 + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + + +package jdk.jpackage.test; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.regex.Pattern; + +/** + * Validates failed command error in jpackage's output. + */ +public final class FailedCommandErrorValidator { + + public FailedCommandErrorValidator(Pattern cmdlinePattern) { + this.cmdlinePattern = Objects.requireNonNull(cmdlinePattern); + } + + public TKit.TextStreamVerifier.Group createGroup() { + var asPredicate = cmdlinePattern.asPredicate(); + + var errorMessage = exitCode().map(v -> { + return JPackageStringBundle.MAIN.cannedFormattedString("error.command-failed-unexpected-exit-code", v, ""); + }).orElseGet(() -> { + return JPackageStringBundle.MAIN.cannedFormattedString("error.command-failed-unexpected-output", ""); + }); + + var errorMessageWithPrefix = JPackageCommand.makeError(errorMessage).getValue(); + + var group = TKit.TextStreamVerifier.group(); + + group.add(TKit.assertTextStream(cmdlinePattern.pattern()).predicate(line -> { + if (line.startsWith(errorMessageWithPrefix)) { + line = line.substring(errorMessageWithPrefix.length()); + return asPredicate.test(line); + } else { + return false; + } + })); + + group.add(TKit.assertTextStream( + JPackageStringBundle.MAIN.cannedFormattedString("message.failed-command-output-header").getValue() + ).predicate(String::equals)); + + outputVerifier().ifPresent(group::add); + + return group; + } + + public void applyTo(JPackageCommand cmd) { + cmd.validateOutput(createGroup().create()); + } + + public FailedCommandErrorValidator validator(TKit.TextStreamVerifier.Group v) { + outputValidator = v; + return this; + } + + public FailedCommandErrorValidator validator(List validators) { + var group = TKit.TextStreamVerifier.group(); + validators.forEach(group::add); + return validator(group); + } + + public FailedCommandErrorValidator validators(TKit.TextStreamVerifier... validators) { + return validator(List.of(validators)); + } + + public FailedCommandErrorValidator output(List v) { + return validator(v.stream().map(TKit::assertTextStream).toList()); + } + + public FailedCommandErrorValidator output(String... output) { + return output(List.of(output)); + } + + public FailedCommandErrorValidator exitCode(int v) { + exitCode = v; + return this; + } + + private Optional exitCode() { + return Optional.ofNullable(exitCode); + } + + private Optional outputVerifier() { + return Optional.ofNullable(outputValidator); + } + + private final Pattern cmdlinePattern; + private TKit.TextStreamVerifier.Group outputValidator; + private Integer exitCode; +} 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 f81c35cea0b..8c7526be9f9 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java @@ -906,6 +906,11 @@ public class JPackageCommand extends CommandArguments { return this; } + public JPackageCommand validateOutput(TKit.TextStreamVerifier.Group group) { + group.tryCreate().ifPresent(this::validateOutput); + return this; + } + @FunctionalInterface public interface CannedArgument { public String value(JPackageCommand cmd); @@ -947,11 +952,11 @@ public class JPackageCommand extends CommandArguments { public JPackageCommand validateOutput(CannedFormattedString... str) { // Will look up the given errors in the order they are specified. - Stream.of(str).map(this::getValue) + validateOutput(Stream.of(str).map(this::getValue) .map(TKit::assertTextStream) .reduce(TKit.TextStreamVerifier.group(), TKit.TextStreamVerifier.Group::add, - TKit.TextStreamVerifier.Group::add).tryCreate().ifPresent(this::validateOutput); + TKit.TextStreamVerifier.Group::add)); return this; } 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 dc1a7b3512b..1191cd02221 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacHelper.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacHelper.java @@ -782,6 +782,10 @@ public final class MacHelper { return sign(cmd); } + public Optional optionName() { + return type.mapOptionName(certRequest.type()); + } + public List asCmdlineArgs() { String[] args = new String[2]; applyTo((optionName, optionValue) -> { @@ -791,6 +795,10 @@ public final class MacHelper { return List.of(args); } + public Optional passThrough() { + return optionName().map(Name::passThrough); + } + private void applyTo(BiConsumer sink) { type.mapOptionName(certRequest.type()).ifPresent(optionName -> { sink.accept(optionName.optionName(), optionValue()); @@ -886,6 +894,14 @@ public final class MacHelper { return signKeyOption.certRequest(); } + public Optional optionName() { + return signKeyOption.optionName(); + } + + public Optional passThrough() { + return signKeyOption.passThrough(); + } + public JPackageCommand addTo(JPackageCommand cmd) { Optional.ofNullable(cmd.getArgumentValue("--mac-signing-keychain")).ifPresentOrElse(configuredKeychain -> { if (!configuredKeychain.equals(keychain.name())) { diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TKit.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TKit.java index 1639beadb28..7666d1e5167 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TKit.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TKit.java @@ -65,6 +65,7 @@ import java.util.function.BiPredicate; import java.util.function.Consumer; import java.util.function.Predicate; import java.util.function.Supplier; +import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; import jdk.internal.util.OperatingSystem; @@ -1082,6 +1083,11 @@ public final class TKit { predicate(String::contains); } + TextStreamVerifier(Pattern value) { + this(Objects.requireNonNull(value).pattern()); + predicate(value.asPredicate()); + } + TextStreamVerifier(TextStreamVerifier other) { predicate = other.predicate; label = other.label; @@ -1091,6 +1097,10 @@ public final class TKit { value = other.value; } + public TextStreamVerifier copy() { + return new TextStreamVerifier(this); + } + public TextStreamVerifier label(String v) { label = v; return this; @@ -1101,6 +1111,13 @@ public final class TKit { return this; } + public TextStreamVerifier predicate(Predicate v) { + Objects.requireNonNull(v); + return predicate((str, _) -> { + return v.test(str); + }); + } + public TextStreamVerifier negate() { negate = true; return this; @@ -1116,7 +1133,12 @@ public final class TKit { return this; } - private String findMatch(Iterator lineIt) { + public TextStreamVerifier mutate(Consumer mutator) { + mutator.accept(this); + return this; + } + + private String find(Iterator lineIt) { while (lineIt.hasNext()) { final var line = lineIt.next(); if (predicate.test(line, value)) { @@ -1131,7 +1153,7 @@ public final class TKit { } public void apply(Iterator lineIt) { - final String matchedStr = findMatch(lineIt); + final String matchedStr = find(lineIt); final String labelStr = Optional.ofNullable(label).orElse("output"); if (negate) { String msg = String.format( @@ -1180,6 +1202,11 @@ public final class TKit { return this; } + public Group mutate(Consumer mutator) { + mutator.accept(this); + return this; + } + public boolean isEmpty() { return verifiers.isEmpty(); } @@ -1226,6 +1253,18 @@ public final class TKit { return new TextStreamVerifier(what); } + public static TextStreamVerifier assertTextStream(Pattern what) { + return new TextStreamVerifier(what); + } + + public static Consumer> assertEndOfTextStream() { + return it -> { + var tail = new ArrayList(); + it.forEachRemaining(tail::add); + assertStringListEquals(List.of(), tail, "Check the end of the output"); + }; + } + public record PathSnapshot(List contentHashes) { public PathSnapshot { contentHashes.forEach(Objects::requireNonNull); diff --git a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/MainTest.java b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/MainTest.java index 79648260274..46de970a829 100644 --- a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/MainTest.java +++ b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/MainTest.java @@ -279,9 +279,7 @@ public class MainTest extends JUnitAdapter { expectedOutput.add(ExceptionFormatter.STACK_TRACE); } expectedOutput.add(expect.getValue()); - if (!verbose) { - expectedOutput.add(ExceptionFormatter.FAILED_COMMAND_OUTPUT); - } + expectedOutput.add(ExceptionFormatter.FAILED_COMMAND_OUTPUT); data.add(new ErrorReporterTestSpec(cause, expect.getKey(), verbose, expectedOutput)); } } diff --git a/test/jdk/tools/jpackage/macosx/MacSignTest.java b/test/jdk/tools/jpackage/macosx/MacSignTest.java index a824fdb0925..0be494ea469 100644 --- a/test/jdk/tools/jpackage/macosx/MacSignTest.java +++ b/test/jdk/tools/jpackage/macosx/MacSignTest.java @@ -22,22 +22,23 @@ */ import static jdk.jpackage.test.MacHelper.SignKeyOption.Type.SIGN_KEY_IDENTITY; +import static jdk.jpackage.test.MacHelper.SignKeyOption.Type.SIGN_KEY_IDENTITY_APP_IMAGE; import static jdk.jpackage.test.MacHelper.SignKeyOption.Type.SIGN_KEY_USER_FULL_NAME; import static jdk.jpackage.test.MacHelper.SignKeyOption.Type.SIGN_KEY_USER_SHORT_NAME; -import static jdk.jpackage.test.MacHelper.SignKeyOption.Type.SIGN_KEY_IDENTITY_APP_IMAGE; import java.io.IOException; import java.nio.file.Files; -import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.function.Function; import java.util.function.Predicate; +import java.util.regex.Pattern; import java.util.stream.Stream; import jdk.jpackage.test.Annotations.Parameter; import jdk.jpackage.test.Annotations.ParameterSupplier; import jdk.jpackage.test.Annotations.Test; import jdk.jpackage.test.CannedFormattedString; +import jdk.jpackage.test.FailedCommandErrorValidator; import jdk.jpackage.test.JPackageCommand; import jdk.jpackage.test.JPackageStringBundle; import jdk.jpackage.test.MacHelper; @@ -74,22 +75,32 @@ public class MacSignTest { Files.createDirectory(appContent); Files.createFile(appContent.resolve("file")); - final List expectedStrings = new ArrayList<>(); - expectedStrings.add(JPackageStringBundle.MAIN.cannedFormattedString("message.codesign.failed.reason.app.content")); + final var group = TKit.TextStreamVerifier.group(); - expectedStrings.add(JPackageStringBundle.MAIN.cannedFormattedString("error.tool.failed.with.output", "codesign")); + group.add(TKit.assertTextStream(JPackageStringBundle.MAIN.cannedFormattedString( + "message.codesign.failed.reason.app.content").getValue()).predicate(String::equals)); + + final var xcodeWarning = TKit.assertTextStream(JPackageStringBundle.MAIN.cannedFormattedString( + "message.codesign.failed.reason.xcode.tools").getValue()).predicate(String::equals); - final var xcodeWarning = JPackageStringBundle.MAIN.cannedFormattedString("message.codesign.failed.reason.xcode.tools"); if (!MacHelper.isXcodeDevToolsInstalled()) { - expectedStrings.add(xcodeWarning); + group.add(xcodeWarning); } - MacSign.withKeychain(keychain -> { + var keychain = SigningBase.StandardKeychain.MAIN.keychain(); - var signingKeyOption = new SignKeyOptionWithKeychain( - SIGN_KEY_IDENTITY, - SigningBase.StandardCertificateRequest.CODESIGN, - keychain); + var signingKeyOption = new SignKeyOptionWithKeychain( + SIGN_KEY_IDENTITY, + SigningBase.StandardCertificateRequest.CODESIGN, + keychain); + + new FailedCommandErrorValidator(Pattern.compile(String.format( + "/usr/bin/codesign -s %s -vvvv --timestamp --options runtime --prefix \\S+ --keychain %s --entitlements \\S+ \\S+", + Pattern.quote(String.format("'%s'", signingKeyOption.certRequest().name())), + Pattern.quote(keychain.name()) + ))).exitCode(1).createGroup().mutate(group::add); + + MacSign.withKeychain(_ -> { // --app-content and --type app-image // Expect `message.codesign.failed.reason.app.content` message in the log. @@ -97,51 +108,49 @@ public class MacSignTest { // To make jpackage fail, specify bad additional content. JPackageCommand.helloAppImage() .ignoreDefaultVerbose(true) - .validateOutput(expectedStrings.toArray(CannedFormattedString[]::new)) + .validateOutput(group.create()) .addArguments("--app-content", appContent) .mutate(signingKeyOption::addTo) .mutate(cmd -> { if (MacHelper.isXcodeDevToolsInstalled()) { // Check there is no warning about missing xcode command line developer tools. - cmd.validateOutput(TKit.assertTextStream(xcodeWarning.getValue()).negate()); + cmd.validateOutput(xcodeWarning.copy().negate()); } }).execute(1); - }, MacSign.Keychain.UsageBuilder::addToSearchList, SigningBase.StandardKeychain.MAIN.keychain()); + }, MacSign.Keychain.UsageBuilder::addToSearchList, keychain); } @Test - public static void testCodesignUnspecifiedFailure() throws IOException { - - var appImageCmd = JPackageCommand.helloAppImage().setFakeRuntime(); - - appImageCmd.executeIgnoreExitCode().assertExitCodeIsZero(); + public static void testCodesignUnspecificFailure() throws IOException { // This test expects jpackage to respond in a specific way on a codesign failure. - // The simplest option to trigger codesign failure is to request the signing of an invalid bundle. - // Create app content directory with the name known to fail signing. - final var appContent = appImageCmd.appLayout().contentDirectory().resolve("foo.1"); - Files.createDirectory(appContent); - Files.createFile(appContent.resolve("file")); + // There are a few ways to make jpackage fail signing. One is using an erroneous + // combination of a signing key and a keychain. - final List expectedStrings = new ArrayList<>(); - expectedStrings.add(JPackageStringBundle.MAIN.cannedFormattedString("error.tool.failed.with.output", "codesign")); + var signingKeyOption = new SignKeyOption( + SIGN_KEY_IDENTITY, + SigningBase.StandardCertificateRequest.CODESIGN_ACME_TECH_LTD.certRequest( + SigningBase.StandardKeychain.MAIN.keychain())); MacSign.withKeychain(keychain -> { - var signingKeyOption = new SignKeyOptionWithKeychain( - SIGN_KEY_IDENTITY, - SigningBase.StandardCertificateRequest.CODESIGN, - keychain); + // Build a matcher for jpackage's failed command output. + var errorValidator = new FailedCommandErrorValidator(Pattern.compile(String.format( + "/usr/bin/codesign -s %s -vvvv --timestamp --options runtime --prefix \\S+ --keychain %s", + Pattern.quote(String.format("'%s'", signingKeyOption.certRequest().name())), + Pattern.quote(keychain.name()) + ))).exitCode(1).output(String.format("%s: no identity found", signingKeyOption.certRequest().name())).createGroup(); - new JPackageCommand().setPackageType(PackageType.IMAGE) + JPackageCommand.helloAppImage() + .setFakeRuntime() .ignoreDefaultVerbose(true) - .validateOutput(expectedStrings.toArray(CannedFormattedString[]::new)) - .addArguments("--app-image", appImageCmd.outputBundle()) + .validateOutput(errorValidator.create()) .mutate(signingKeyOption::addTo) + .mutate(MacHelper.useKeychain(keychain)) .execute(1); - }, MacSign.Keychain.UsageBuilder::addToSearchList, SigningBase.StandardKeychain.MAIN.keychain()); + }, MacSign.Keychain.UsageBuilder::addToSearchList, SigningBase.StandardKeychain.DUPLICATE.keychain()); } @Test