8377331: jpackage: improve sign errors reporting

Reviewed-by: almatvee
This commit is contained in:
Alexey Semenyuk 2026-02-07 03:27:23 +00:00
parent 40bf0870f7
commit 5152fdcd49
10 changed files with 243 additions and 73 deletions

View File

@ -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() {

View File

@ -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);
}
}

View File

@ -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

View File

@ -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);
}
}

View File

@ -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<TKit.TextStreamVerifier> 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<String> 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<Integer> exitCode() {
return Optional.ofNullable(exitCode);
}
private Optional<TKit.TextStreamVerifier.Group> outputVerifier() {
return Optional.ofNullable(outputValidator);
}
private final Pattern cmdlinePattern;
private TKit.TextStreamVerifier.Group outputValidator;
private Integer exitCode;
}

View File

@ -906,6 +906,11 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
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<JPackageCommand> {
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;
}

View File

@ -782,6 +782,10 @@ public final class MacHelper {
return sign(cmd);
}
public Optional<Name> optionName() {
return type.mapOptionName(certRequest.type());
}
public List<String> asCmdlineArgs() {
String[] args = new String[2];
applyTo((optionName, optionValue) -> {
@ -791,6 +795,10 @@ public final class MacHelper {
return List.of(args);
}
public Optional<Boolean> passThrough() {
return optionName().map(Name::passThrough);
}
private void applyTo(BiConsumer<String, String> 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<SignKeyOption.Name> optionName() {
return signKeyOption.optionName();
}
public Optional<Boolean> passThrough() {
return signKeyOption.passThrough();
}
public JPackageCommand addTo(JPackageCommand cmd) {
Optional.ofNullable(cmd.getArgumentValue("--mac-signing-keychain")).ifPresentOrElse(configuredKeychain -> {
if (!configuredKeychain.equals(keychain.name())) {

View File

@ -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<String> 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<String> lineIt) {
public TextStreamVerifier mutate(Consumer<TextStreamVerifier> mutator) {
mutator.accept(this);
return this;
}
private String find(Iterator<String> 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<String> 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<Group> 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<Iterator<String>> assertEndOfTextStream() {
return it -> {
var tail = new ArrayList<String>();
it.forEachRemaining(tail::add);
assertStringListEquals(List.of(), tail, "Check the end of the output");
};
}
public record PathSnapshot(List<String> contentHashes) {
public PathSnapshot {
contentHashes.forEach(Objects::requireNonNull);

View File

@ -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));
}
}

View File

@ -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<CannedFormattedString> 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<CannedFormattedString> 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