8374839: Improve jpackage information messages

8356116: [macos] Add logging of sign commands in jpackage

Reviewed-by: almatvee
This commit is contained in:
Alexey Semenyuk 2026-05-04 21:00:35 +00:00
parent b485729859
commit 06e6539ceb
119 changed files with 6087 additions and 567 deletions

View File

@ -430,7 +430,7 @@ final class DesktopIntegration extends ShellCustomAction {
BufferedImage bi = ImageIO.read(path.toFile());
return Math.max(bi.getWidth(), bi.getHeight());
} catch (IOException e) {
Log.verbose(e);
Log.trace(e, "Failed to get dimensions of an image at [%s]", path);
}
return 0;
}

View File

@ -66,16 +66,17 @@ public final class LibProvidersLookup {
// Get the list of unique package names.
List<String> neededPackages = neededLibs.stream().map(libPath -> {
try {
List<String> packageNames = packageLookup.apply(libPath).filter(
Objects::nonNull).filter(Predicate.not(String::isBlank)).distinct().collect(
Collectors.toList());
Log.verbose(String.format("%s is provided by %s", libPath, packageNames));
List<String> packageNames = packageLookup.apply(libPath)
.filter(Objects::nonNull)
.filter(Predicate.not(String::isBlank))
.distinct()
.toList();
Log.trace("%s is provided by %s", libPath, packageNames);
return packageNames;
} catch (IOException ex) {
// Ignore and keep going
Log.verbose(ex);
List<String> packageNames = Collections.emptyList();
return packageNames;
Log.trace(ex, "Failed to get required packages for [%s]", libPath);
return List.<String>of();
}
}).flatMap(List::stream).sorted().distinct().toList();
@ -83,10 +84,10 @@ public final class LibProvidersLookup {
}
private static List<Path> getNeededLibsForFile(Path path) throws IOException {
final var result = Executor.of(TOOL_LDD, path.toString()).saveOutput().execute();
final var result = Executor.of(TOOL_LDD, path.toString()).quiet().saveOutput().execute();
if (result.getExitCode() != 0) {
// objdump failed. This is OK if the tool was applied to not a binary file
// ldd failed. This is OK if the tool was applied to not a binary file
return Collections.emptyList();
}
@ -107,7 +108,7 @@ public final class LibProvidersLookup {
try {
libs = getNeededLibsForFile(path);
} catch (IOException ex) {
Log.verbose(ex);
Log.trace(ex, "Failed to get required libraries for [%s]", path);
libs = Collections.emptyList();
}
return libs;

View File

@ -43,6 +43,8 @@ import jdk.jpackage.internal.model.BundlingOperationDescriptor;
import jdk.jpackage.internal.model.LinuxPackage;
import jdk.jpackage.internal.model.PackageType;
import jdk.jpackage.internal.model.StandardPackageType;
import jdk.jpackage.internal.summary.SummaryAccumulator;
import jdk.jpackage.internal.summary.StandardProperty;
import jdk.jpackage.internal.util.Result;
public class LinuxBundlingEnvironment extends DefaultBundlingEnvironment {
@ -78,22 +80,26 @@ public class LinuxBundlingEnvironment extends DefaultBundlingEnvironment {
private static void createDebPackage(Options options, LinuxDebSystemEnvironment sysEnv) {
var pkg = LinuxFromOptions.createLinuxDebPackage(options, sysEnv);
createNativePackage(options,
LinuxFromOptions.createLinuxDebPackage(options, sysEnv),
updateSummary(pkg, OptionUtils.summary(options), sysEnv),
buildEnv()::create,
LinuxBundlingEnvironment::buildPipeline,
(env, pkg, outputDir) -> {
(env, _, outputDir) -> {
return new LinuxDebPackager(env, pkg, outputDir, sysEnv);
});
}
private static void createRpmPackage(Options options, LinuxRpmSystemEnvironment sysEnv) {
var pkg = LinuxFromOptions.createLinuxRpmPackage(options, sysEnv);
createNativePackage(options,
LinuxFromOptions.createLinuxRpmPackage(options, sysEnv),
updateSummary(pkg, OptionUtils.summary(options), sysEnv),
buildEnv()::create,
LinuxBundlingEnvironment::buildPipeline,
(env, pkg, outputDir) -> {
(env, _, outputDir) -> {
return new LinuxRpmPackager(env, pkg, outputDir, sysEnv);
});
}
@ -113,6 +119,14 @@ public class LinuxBundlingEnvironment extends DefaultBundlingEnvironment {
return new BuildEnvFromOptions().predefinedAppImageLayout(APPLICATION_LAYOUT);
}
private static <T extends LinuxPackage> T updateSummary(
T pkg, SummaryAccumulator summary, LinuxSystemEnvironment sysEnv) {
if (!LinuxSystemEnvironment.isWithRequiredPackagesSearch(sysEnv, pkg)) {
summary.put(StandardProperty.LINUX_DISABLE_REQUIRED_PACKAGES_SEARCH);
}
return pkg;
}
private static Result<LinuxSystemEnvironment> adjustPackageArch(LinuxSystemEnvironment sysEnv, StandardPackageType type) {
Objects.requireNonNull(sysEnv);
Objects.requireNonNull(type);

View File

@ -108,8 +108,9 @@ final class LinuxDebPackager extends LinuxPackager<LinuxDebPackage> {
Map<String, String> actualValues = Executor.of(cmdline)
.saveOutput(true)
.quiet()
.executeExpectSuccess()
.getOutput().stream()
.stdout().stream()
.map(line -> line.split(":\\s+", 2))
.collect(Collectors.toMap(
components -> components[0],
@ -149,9 +150,7 @@ final class LinuxDebPackager extends LinuxPackager<LinuxDebPackage> {
List<String> cmdline = new ArrayList<>();
Stream.of(sysEnv.fakeroot(), sysEnv.dpkgdeb()).map(Path::toString).forEach(cmdline::add);
if (Log.isVerbose()) {
cmdline.add("--verbose");
}
cmdline.add("--verbose");
cmdline.addAll(List.of("-b", env.appImageDir().toString(), debFile.toAbsolutePath().toString()));
// run dpkg
@ -276,8 +275,9 @@ final class LinuxDebPackager extends LinuxPackager<LinuxDebPackage> {
var debArch = sysEnv.packageArch().value();
Executor.of(sysEnv.dpkg().toString(), "-S", file.toString())
.quiet()
.saveOutput(true).executeExpectSuccess()
.getOutput().forEach(line -> {
.stdout().forEach(line -> {
Matcher matcher = PACKAGE_NAME_REGEX.matcher(line);
if (matcher.find()) {
String name = matcher.group(1);

View File

@ -46,8 +46,11 @@ import jdk.jpackage.internal.model.LinuxApplication;
import jdk.jpackage.internal.model.LinuxDebPackage;
import jdk.jpackage.internal.model.LinuxLauncher;
import jdk.jpackage.internal.model.LinuxLauncherMixin;
import jdk.jpackage.internal.model.LinuxPackage;
import jdk.jpackage.internal.model.LinuxRpmPackage;
import jdk.jpackage.internal.model.StandardPackageType;
import jdk.jpackage.internal.summary.StandardProperty;
import jdk.jpackage.internal.summary.StandardWarning;
final class LinuxFromOptions {
@ -84,7 +87,11 @@ final class LinuxFromOptions {
LINUX_RPM_LICENSE_TYPE.ifPresentIn(options, pkgBuilder::licenseType);
return pkgBuilder.create();
final var pkg = pkgBuilder.create();
updateSummary(options, pkg);
return pkg;
}
static LinuxDebPackage createLinuxDebPackage(Options options, LinuxDebSystemEnvironment sysEnv) {
@ -99,9 +106,11 @@ final class LinuxFromOptions {
// Show warning if license file is missing
if (pkg.licenseFile().isEmpty()) {
Log.verbose(I18N.getString("message.debs-like-licenses"));
OptionUtils.summary(options).put(StandardWarning.LINUX_DEB_MISSING_LICENSE_FILE);
}
updateSummary(options, pkg);
return pkg;
}
@ -135,4 +144,9 @@ final class LinuxFromOptions {
return version;
}
private static void updateSummary(Options options, LinuxPackage pkg) {
OptionUtils.summary(options).put(StandardProperty.VERSION, pkg.versionWithRelease());
OptionUtils.summary(options).put(StandardProperty.LINUX_PACKAGE_NAME, pkg.packageName());
}
}

View File

@ -47,7 +47,7 @@ record LinuxPackageArch(String value) {
}
private static Result<String> deb() {
var exec = Executor.of("dpkg", "--print-architecture").saveOutput(true);
var exec = Executor.of("dpkg", "--print-architecture").quiet().saveOutput(true);
return Result.of(exec::executeExpectSuccess, IOException.class)
.flatMap(LinuxPackageArch::getStdoutFirstLine);
}

View File

@ -24,9 +24,10 @@
*/
package jdk.jpackage.internal;
import static jdk.jpackage.internal.LinuxSystemEnvironment.isWithRequiredPackagesSearch;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@ -47,7 +48,7 @@ abstract class LinuxPackager<T extends LinuxPackage> implements Consumer<Packagi
this.env = Objects.requireNonNull(env);
this.pkg = Objects.requireNonNull(pkg);
this.outputDir = Objects.requireNonNull(outputDir);
this.withRequiredPackagesLookup = sysEnv.soLookupAvailable() && sysEnv.nativePackageType().equals(pkg.type());
this.withRequiredPackagesLookup = isWithRequiredPackagesSearch(sysEnv, pkg);
customActions = List.of(
DesktopIntegration.create(env, pkg),
@ -135,19 +136,21 @@ abstract class LinuxPackager<T extends LinuxPackage> implements Consumer<Packagi
final List<String> neededLibPackages;
if (withRequiredPackagesLookup) {
neededLibPackages = findRequiredPackages();
Log.trace("Runtime requires: %s", neededLibPackages);
} else {
neededLibPackages = Collections.emptyList();
Log.info(I18N.getString("warning.foreign-app-image"));
}
Log.trace("Features of the package require: %s", caPackages);
// Merge all package lists together.
// Filter out empty names, sort and remove duplicates.
Stream.of(caPackages, neededLibPackages)
requiredPackages = Stream.of(caPackages, neededLibPackages)
.flatMap(List::stream)
.filter(Predicate.not(String::isEmpty))
.sorted().distinct().forEach(requiredPackages::add);
.sorted().distinct().toList();
Log.verbose(String.format("Required packages: %s", requiredPackages));
Log.trace("Required packages: %s", requiredPackages);
}
private List<String> findRequiredPackages() throws IOException {
@ -160,16 +163,16 @@ abstract class LinuxPackager<T extends LinuxPackage> implements Consumer<Packagi
final List<? extends Exception> errors;
try {
errors = findErrorsInOutputPackage();
} catch (Exception ex) {
} catch (IOException ex) {
// Ignore error as it is not critical. Just report it.
Log.verbose(ex);
Log.trace(ex);
return;
}
for (var ex : errors) {
Log.verbose(ex.getLocalizedMessage());
Log.progressWarning(ex);
if (ex instanceof ConfigException cfgEx) {
Log.verbose(cfgEx.getAdvice());
Log.progress(cfgEx.getAdvice());
}
}
}
@ -178,6 +181,6 @@ abstract class LinuxPackager<T extends LinuxPackage> implements Consumer<Packagi
protected final T pkg;
protected final Path outputDir;
private final boolean withRequiredPackagesLookup;
private final List<String> requiredPackages = new ArrayList<>();
private List<String> requiredPackages;
private final List<ShellCustomAction> customActions;
}

View File

@ -95,7 +95,7 @@ final class LinuxRpmPackager extends LinuxPackager<LinuxRpmPackage> {
return Executor.of(sysEnv.rpm().toString(),
"-q", "--queryformat", "%{name}\\n",
"-q", "--whatprovides", file.toString()
).saveOutput(true).executeExpectSuccess().getOutput().stream();
).saveOutput(true).quiet().executeExpectSuccess().stdout().stream();
});
}
@ -119,7 +119,7 @@ final class LinuxRpmPackager extends LinuxPackager<LinuxRpmPackage> {
"-qp",
"--queryformat", properties.stream().map(e -> String.format("%%{%s}", e.name())).collect(joining("\\n")),
outputPackageFile().toString()
).saveOutput(true).executeExpectSuccess().getOutput();
).saveOutput(true).quiet().executeExpectSuccess().stdout();
for (int i = 0; i != properties.size(); i++) {
Optional.ofNullable(properties.get(i).verifyValue(actualValues.get(i))).ifPresent(errors::add);

View File

@ -27,8 +27,10 @@ package jdk.jpackage.internal;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Supplier;
import jdk.jpackage.internal.model.LinuxPackage;
import jdk.jpackage.internal.model.PackageType;
import jdk.jpackage.internal.model.StandardPackageType;
import jdk.jpackage.internal.util.CompositeProxy;
@ -45,6 +47,11 @@ interface LinuxSystemEnvironment extends SystemEnvironment {
});
}
static boolean isWithRequiredPackagesSearch(LinuxSystemEnvironment sysEnv, LinuxPackage pkg) {
Objects.requireNonNull(pkg);
return sysEnv.soLookupAvailable() && sysEnv.nativePackageType().equals(pkg.type());
}
static Optional<StandardPackageType> detectNativePackageType() {
if (Internal.isDebian()) {
return Optional.of(StandardPackageType.LINUX_DEB);
@ -89,7 +96,7 @@ interface LinuxSystemEnvironment extends SystemEnvironment {
// we are just going to run "dpkg -s coreutils" and assume Debian
// or derivative if no error is returned.
try {
Executor.of("dpkg", "-s", "coreutils").executeExpectSuccess();
Executor.of("dpkg", "-s", "coreutils").quiet().executeExpectSuccess();
return true;
} catch (IOException e) {
// just fall thru
@ -101,7 +108,7 @@ interface LinuxSystemEnvironment extends SystemEnvironment {
// we are just going to run "rpm -q rpm" and assume RPM
// or derivative if no error is returned.
try {
Executor.of("rpm", "-q", "rpm").executeExpectSuccess();
Executor.of("rpm", "-q", "rpm").quiet().executeExpectSuccess();
return true;
} catch (IOException e) {
// just fall thru

View File

@ -36,6 +36,9 @@ resource.menu-icon=menu icon
resource.rpm-spec-file=RPM spec file
resource.systemd-unit-file=systemd unit file
summary.property.linux-package-name=Package name
summary.property.linux-required-packages-search=Required packages search
error.tool-not-found.advice=Please install required packages
error.tool-old-version.advice=Please install required packages
@ -55,9 +58,6 @@ message.ldd-not-available=ldd command not found. Package dependencies will not b
message.deb-ldd-not-available.advice=Install "libc-bin" DEB package to get ldd.
message.rpm-ldd-not-available.advice=Install "glibc-common" RPM package to get ldd.
warning.foreign-app-image=Warning: app-image dir not generated by jpackage.
message.not-default-bundler-no-dependencies-lookup={0} is not the default package type. Package dependencies will not be generated.
error.unexpected-package-property=Expected value of "{0}" property is [{1}]. Actual value in output package is [{2}]. Looks like the value of "{0}" property is hardcoded in "{3}" file in the resource directory
error.unexpected-package-property.advice=Use [{0}] pattern string instead of hard coded value [{1}] of {2} property in custom "{3}" file
error.unexpected-default-package-property.advice=Don''t explicitly set value of "{0}" property in custom "{1}" file

View File

@ -84,6 +84,14 @@ final class AppImageSigner {
@Override
public boolean test(Path path) {
var accepted = testInternal(path);
if (!accepted) {
Log.trace("Skip signing [%s]", path);
}
return accepted;
}
private boolean testInternal(Path path) {
if (!Files.isRegularFile(path) || otherExcludePaths.contains(path)) {
return false;
}
@ -139,7 +147,7 @@ final class AppImageSigner {
if (Files.isDirectory(frameworkPath)) {
try (var content = Files.list(frameworkPath)) {
content.forEach(toConsumer(path -> {
codesigners.codesignDir().accept(path);
codesigners.codesignMacBundle().accept(path);
}));
}
}
@ -167,14 +175,14 @@ final class AppImageSigner {
private static IOException handleCodesignException(MacApplication app, CodesignException ex) {
if (!app.contentDirSources().isEmpty()) {
// Additional content may cause signing error.
Log.fatalError(I18N.getString("message.codesign.failed.reason.app.content"));
Log.progressWarning(I18N.getString("message.codesign.failed.reason.app.content"));
}
// Signing might not work without Xcode with command line
// developer tools. Show user if Xcode is missing as possible
// reason.
if (!isXcodeDevToolsInstalled()) {
Log.fatalError(I18N.getString("message.codesign.failed.reason.xcode.tools"));
Log.progressWarning(I18N.getString("message.codesign.failed.reason.xcode.tools"));
}
return ex.getCause();
@ -182,14 +190,14 @@ final class AppImageSigner {
private static boolean isXcodeDevToolsInstalled() {
return Result.of(
Executor.of("/usr/bin/xcrun", "--help").setQuiet(true)::executeExpectSuccess,
Executor.of("/usr/bin/xcrun", "--help").quiet()::executeExpectSuccess,
IOException.class).hasValue();
}
private static void unsign(Path path) throws IOException {
// run quietly
Executor.of("/usr/bin/codesign", "--remove-signature", path.toString())
.setQuiet(true)
.quiet()
.executeExpectSuccess();
}
@ -201,23 +209,27 @@ final class AppImageSigner {
this.codesigners = Objects.requireNonNull(codesigners);
}
private record Codesigners(Consumer<Path> codesignFile, Consumer<Path> codesignExecutableFile, Consumer<Path> codesignDir) implements Consumer<Path> {
private record Codesigners(
Consumer<Path> codesignFile,
Consumer<Path> codesignExecutableFile,
Consumer<Path> codesignMacBundle) implements Consumer<Path> {
Codesigners {
Objects.requireNonNull(codesignFile);
Objects.requireNonNull(codesignExecutableFile);
Objects.requireNonNull(codesignDir);
Objects.requireNonNull(codesignMacBundle);
}
@Override
public void accept(Path path) {
findCodesigner(path).orElseThrow(() -> {
return new IllegalArgumentException(String.format("No codesigner for %s path", PathUtils.normalizedAbsolutePathString(path)));
return new IllegalArgumentException(String.format("No codesigner for [%s]", PathUtils.normalizedAbsolutePathString(path)));
}).accept(path);
}
private Optional<Consumer<Path>> findCodesigner(Path path) {
if (Files.isDirectory(path)) {
return Optional.of(codesignDir);
if (MacBundle.fromPath(path).isPresent()) {
return Optional.of(codesignMacBundle);
} else if (Files.isRegularFile(path)) {
if (Files.isExecutable(path)) {
return Optional.of(codesignExecutableFile);
@ -233,9 +245,9 @@ final class AppImageSigner {
final var codesignExecutableFile = Codesign.build(signingCfg::toCodesignArgs).quiet(true).create().asConsumer();
final var codesignFile = Codesign.build(signingCfgWithoutEntitlements::toCodesignArgs).quiet(true).create().asConsumer();
final var codesignDir = Codesign.build(signingCfg::toCodesignArgs).force(true).create().asConsumer();
final var codesignMacBundle = Codesign.build(signingCfg::toCodesignArgs).force(true).create().asConsumer();
return new Codesigners(codesignFile, codesignExecutableFile, codesignDir);
return new Codesigners(codesignFile, codesignExecutableFile, codesignMacBundle);
}
}

View File

@ -67,7 +67,7 @@ public final class Codesign {
}
return new Codesign(cmdline, quiet ? exec -> {
exec.setQuiet(true);
exec.quiet();
} : null);
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2025, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -72,7 +72,7 @@ record Keychain(String name) {
// Get the current keychain list
final List<String> cmdOutput;
try {
cmdOutput = Executor.of("/usr/bin/security", "list-keychains").saveOutput(true).executeExpectSuccess().getOutput();
cmdOutput = Executor.of("/usr/bin/security", "list-keychains").quiet().saveOutput(true).executeExpectSuccess().stdout();
} catch (IOException ex) {
throw I18N.buildException().message("message.keychain.error").cause(ex).create(KeychainException::new);
}

View File

@ -25,11 +25,17 @@
package jdk.jpackage.internal;
import static jdk.jpackage.internal.cli.StandardValidator.IS_VALID_MAC_BUNDLE_IDENTIFIER;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.mapping;
import static java.util.stream.Collectors.toList;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Comparator;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
@ -45,6 +51,9 @@ import jdk.jpackage.internal.model.JPackageException;
import jdk.jpackage.internal.model.Launcher;
import jdk.jpackage.internal.model.MacApplication;
import jdk.jpackage.internal.model.MacApplicationMixin;
import jdk.jpackage.internal.summary.StandardWarning;
import jdk.jpackage.internal.summary.StandardProperty;
import jdk.jpackage.internal.summary.SummaryAccumulator;
import jdk.jpackage.internal.util.PListReader;
import jdk.jpackage.internal.util.Result;
import jdk.jpackage.internal.util.RootedPath;
@ -64,6 +73,7 @@ final class MacApplicationBuilder {
appStore = other.appStore;
externalInfoPlistFile = other.externalInfoPlistFile;
signingBuilder = other.signingBuilder;
summary = other.summary;
}
MacApplicationBuilder icon(Path v) {
@ -101,6 +111,11 @@ final class MacApplicationBuilder {
return this;
}
MacApplicationBuilder summary(SummaryAccumulator v) {
summary = v;
return this;
}
Optional<ExternalApplication> externalApplication() {
return superBuilder.externalApplication();
}
@ -117,7 +132,9 @@ final class MacApplicationBuilder {
var app = superBuilder.create();
validateAppVersion(app);
validateAppContentDirs(app);
summary().ifPresent(s -> {
validateAppContentDirs(s, app);
});
final var mixin = new MacApplicationMixin.Stub(
validatedIcon(),
@ -127,7 +144,14 @@ final class MacApplicationBuilder {
appStore,
createSigningConfig());
return MacApplication.create(app, mixin);
var macApp = MacApplication.create(app, mixin);
summary().ifPresent(s -> {
s.put(StandardProperty.MAC_BUNDLE_IDENTIFIER, macApp.bundleIdentifier());
s.put(StandardProperty.MAC_BUNDLE_NAME, macApp.bundleName());
});
return macApp;
}
static MacApplication overrideAppImageLayout(MacApplication app, AppImageLayout appImageLayout) {
@ -149,18 +173,59 @@ final class MacApplicationBuilder {
}
}
private static void validateAppContentDirs(Application app) {
app.contentDirSources().stream().filter(rootedPath -> {
private static Stream<Path> appContentTopPaths(Application app) {
return app.contentDirSources().stream().filter(rootedPath -> {
return rootedPath.branch().getNameCount() == 1;
}).map(RootedPath::fullPath).forEach(contentDir -> {
}).map(RootedPath::fullPath);
}
private static void validateAppContentDirs(SummaryAccumulator summary, Application app) {
var warnings = appContentTopPaths(app)
.map(NonStandardAppContentWarning::createMapEntry)
.flatMap(Optional::stream)
.collect(groupingBy(
Map.Entry::getKey,
mapping(Map.Entry::getValue, toList())
)).entrySet().stream()
.sorted(Comparator.comparing(
Map.Entry::getKey,
Comparator.comparing(Enum::ordinal)
))
.map(Map.Entry::getValue)
.flatMap(Collection::stream)
.toList();
if (!warnings.isEmpty()) {
summary.putMultiValue(StandardWarning.MAC_NON_STANDARD_APP_CONTENT, warnings);
}
}
private enum NonStandardAppContentWarning {
NOT_DIRECTORY("warning.non-standard-app-content.not-dir"),
NON_STANDARD_DIRECTOTY_NAME("warning.non-standard-app-content.non-standard-dir-name"),
;
NonStandardAppContentWarning(String formatKey) {
this.formatKey = Objects.requireNonNull(formatKey);
}
static Optional<Map.Entry<NonStandardAppContentWarning, String>> createMapEntry(Path contentDir) {
if (!Files.isDirectory(contentDir)) {
Log.info(I18N.format("warning.app.content.is.not.dir",
contentDir));
return Optional.of(Map.entry(NOT_DIRECTORY, NOT_DIRECTORY.format(contentDir)));
} else if (!CONTENTS_SUB_DIRS.contains(contentDir.getFileName())) {
Log.info(I18N.format("warning.non.standard.contents.sub.dir",
contentDir));
return Optional.of(Map.entry(
NON_STANDARD_DIRECTOTY_NAME,
NON_STANDARD_DIRECTOTY_NAME.format(contentDir.getFileName(), contentDir)
));
} else {
return Optional.empty();
}
});
}
private String format(Object... formatArgs) {
return I18N.format(formatKey, formatArgs);
}
private final String formatKey;
}
private MacApplicationBuilder createCopyForExternalInfoPlistFile() {
@ -186,7 +251,10 @@ final class MacApplicationBuilder {
}
if (builder.superBuilder.version().isEmpty()) {
plist.findValue("CFBundleVersion").ifPresent(builder.superBuilder::version);
plist.findValue("CFBundleVersion").ifPresent(ver -> {
Log.trace("Derive bundle version [%s] from [%s] file", ver, externalInfoPlistFile);
builder.superBuilder.version(ver);
});
}
});
@ -227,9 +295,11 @@ final class MacApplicationBuilder {
return appName;
});
if (value.length() > MAX_BUNDLE_NAME_LENGTH && (bundleName != null)) {
Log.error(I18N.format("message.bundle-name-too-long-warning", "--mac-package-name", value));
}
summary().ifPresent(s -> {
if (value.length() > MAX_BUNDLE_NAME_LENGTH && (bundleName != null)) {
s.put(StandardWarning.MAC_BUNDLE_NAME_TOO_LONG, (Object)value);
}
});
return value;
}
@ -261,7 +331,7 @@ final class MacApplicationBuilder {
}
}
Log.verbose(I18N.format("message.derived-bundle-identifier", derivedValue));
Log.trace("Derived bundle identifier: %s", derivedValue);
return derivedValue;
});
}
@ -274,6 +344,10 @@ final class MacApplicationBuilder {
return Optional.ofNullable(icon).map(LauncherBuilder::validateIcon);
}
private Optional<SummaryAccumulator> summary() {
return Optional.ofNullable(summary);
}
private record Defaults(String category) {
}
@ -284,6 +358,7 @@ final class MacApplicationBuilder {
private boolean appStore;
private Path externalInfoPlistFile;
private AppImageSigningConfigBuilder signingBuilder;
private SummaryAccumulator summary;
private final ApplicationBuilder superBuilder;

View File

@ -35,8 +35,11 @@ import static jdk.jpackage.internal.cli.StandardOption.EXIT_AFTER_CONFIGURATION_
import java.util.Optional;
import jdk.jpackage.internal.cli.Options;
import jdk.jpackage.internal.model.AppImageBundleType;
import jdk.jpackage.internal.model.MacPackage;
import jdk.jpackage.internal.model.Package;
import jdk.jpackage.internal.summary.StandardProperty;
import jdk.jpackage.internal.util.PathUtils;
public class MacBundlingEnvironment extends DefaultBundlingEnvironment {
@ -75,6 +78,12 @@ public class MacBundlingEnvironment extends DefaultBundlingEnvironment {
final var pkg = createSignAppImagePackage(app, env);
OptionUtils.summary(options).put(StandardProperty.MAC_SIGN_APP_IMAGE_OPERATION,
AppImageBundleType.MAC_APP_IMAGE.label(),
PathUtils.normalizedAbsolutePath(env.appImageDir()));
OptionUtils.finalizeAndPrintSummary(options, pkg);
if (EXIT_AFTER_CONFIGURATION_PHASE.getFrom(options)) {
return;
}

View File

@ -54,7 +54,7 @@ public final class MacCertificateUtils {
return toSupplier(() -> {
final var output = Executor.of(args)
.setQuiet(true).saveOutput(true).executeExpectSuccess()
.quiet().saveOutput(true).executeExpectSuccess()
.getOutput();
final byte[] pemCertificatesBuffer = output.stream()

View File

@ -32,7 +32,6 @@ import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@ -139,9 +138,7 @@ record MacDmgPackager(BuildEnv env, MacDmgPackage pkg, Path outputDir,
private void prepareDMGSetupScript() throws IOException {
Path dmgSetup = volumeScript();
Log.verbose(MessageFormat.format(
I18N.getString("message.preparing-dmg-setup"),
dmgSetup.toAbsolutePath().toString()));
Log.progress(I18N.format("message.preparing-dmg-setup", dmgSetup.toAbsolutePath().toString()));
// Prepare DMG setup script
Map<String, String> data = new HashMap<>();
@ -202,10 +199,6 @@ record MacDmgPackager(BuildEnv env, MacDmgPackage pkg, Path outputDir,
}
}
private String hdiUtilVerbosityFlag() {
return env.verbose() ? "-verbose" : "-quiet";
}
private void buildDMG() throws IOException {
boolean copyAppImage = false;
@ -217,7 +210,7 @@ record MacDmgPackager(BuildEnv env, MacDmgPackage pkg, Path outputDir,
Files.createDirectories(protoDMG.getParent());
Files.createDirectories(finalDMG.getParent());
final String hdiUtilVerbosityFlag = hdiUtilVerbosityFlag();
final String hdiUtilVerbosityFlag = "-verbose";
// create temp image
try {
@ -229,7 +222,9 @@ record MacDmgPackager(BuildEnv env, MacDmgPackage pkg, Path outputDir,
"-fs", "HFS+",
"-format", "UDRW").executeExpectSuccess();
} catch (IOException ex) {
Log.verbose(ex); // Log exception
Log.trace(ex, "Failed to create a DMG from the entire app image");
Log.trace("Will create an empty DMG and fill it manually");
// Creating DMG from entire app image failed, so lets try to create empty
// DMG and copy files manually. See JDK-8248059.
@ -285,7 +280,7 @@ record MacDmgPackager(BuildEnv env, MacDmgPackage pkg, Path outputDir,
.timeout(3, TimeUnit.MINUTES)
.executeExpectSuccess();
} catch (IOException ex) {
Log.verbose(ex);
Log.trace(ex, "Failed to set background image");
}
// volume icon
@ -317,11 +312,10 @@ record MacDmgPackager(BuildEnv env, MacDmgPackage pkg, Path outputDir,
normalizedAbsolutePathString(mountedVolume)
).executeExpectSuccess();
} catch (IOException ex) {
Log.error(ex.getMessage());
Log.verbose("Cannot enable custom icon using SetFile utility");
Log.trace(ex, "Failed to set custom icon");
}
} else {
Log.verbose(I18N.getString("message.setfile.dmg"));
Log.progress(I18N.format("message.setfile.dmg"));
}
} finally {
@ -345,12 +339,7 @@ record MacDmgPackager(BuildEnv env, MacDmgPackage pkg, Path outputDir,
.execute();
}
try {
//Delete the temporary image
Files.deleteIfExists(protoDMG);
} catch (IOException ex) {
// Don't care if fails
}
IOUtils.deleteIfExistsIgnoreError(protoDMG);
}
private void detachVolume() throws IOException {
@ -369,7 +358,7 @@ record MacDmgPackager(BuildEnv env, MacDmgPackage pkg, Path outputDir,
}
cmdline.addAll(List.of(
hdiUtilVerbosityFlag(),
"-verbose",
normalizedAbsolutePathString(mountedVolume)
));
@ -392,7 +381,7 @@ record MacDmgPackager(BuildEnv env, MacDmgPackage pkg, Path outputDir,
return hdiutil(
"convert",
normalizedAbsolutePathString(srcDmg),
hdiUtilVerbosityFlag(),
"-verbose",
"-format", "UDZO",
"-o", normalizedAbsolutePathString(finalDmg()));
};
@ -404,14 +393,17 @@ record MacDmgPackager(BuildEnv env, MacDmgPackage pkg, Path outputDir,
.setAttemptTimeout(3, TimeUnit.SECONDS)
.execute();
} catch (IOException ex) {
Log.verbose(ex);
Log.trace(ex, "Failed to convert an interim DMG into the output DMG");
Log.trace("Try to convert a copy of an interim DMG into the output DMG");
// Something holds the file, try to convert a copy.
Path copyDmg = protoCopyDmg();
Files.copy(protoDmg(), copyDmg);
try {
convert.apply(copyDmg).executeExpectSuccess();
} finally {
Files.deleteIfExists(copyDmg);
IOUtils.deleteIfExistsIgnoreError(copyDmg);
}
}
}

View File

@ -70,11 +70,11 @@ record MacDmgSystemEnvironment(Path hdiutil, Path osascript, Optional<Path> setF
return SETFILE_KNOWN_PATHS.stream().filter(setFilePath -> {
// Validate SetFile, if Xcode is not installed it will run, but exit with error code
return Result.of(
Executor.of(setFilePath.toString(), "-h").setQuiet(true)::executeExpectSuccess,
Executor.of(setFilePath.toString(), "-h").quiet()::executeExpectSuccess,
IOException.class).hasValue();
}).findFirst().or(() -> {
// generic find attempt
final var executor = Executor.of("/usr/bin/xcrun", "-find", "SetFile").setQuiet(true).saveFirstLineOfOutput();
final var executor = Executor.of("/usr/bin/xcrun", "-find", "SetFile").quiet().saveFirstLineOfOutput();
return Result.of(executor::executeExpectSuccess, IOException.class).flatMap(execResult -> {
return Result.of(() -> {

View File

@ -186,7 +186,7 @@ final class MacFromOptions {
pkgSigningIdentityBuilder.ifPresent(pkgBuilder::signingBuilder);
return pkgBuilder.create();
return pkgBuilder.summary(OptionUtils.summary(options)).create();
}
private record ApplicationWithDetails(MacApplication app, Optional<ExternalApplication> externalApp) {
@ -243,6 +243,8 @@ final class MacFromOptions {
final var appBuilder = new MacApplicationBuilder(createApplicationBuilder(options));
appBuilder.summary(OptionUtils.summary(options));
if (OptionUtils.isRuntimeInstaller(options)) {
// Predefined runtime image, if specified, can be a macOS bundle or regular directory.
// Notify application builder with the path to the plist file in the predefined runtime image only if the file exists.
@ -344,6 +346,8 @@ final class MacFromOptions {
.ifPresent(builder::predefinedAppImageSigned);
}
builder.summary(OptionUtils.summary(options));
return builder;
}

View File

@ -29,9 +29,12 @@ import static jdk.jpackage.internal.MacPackagingPipeline.LayoutUtils.packagerLay
import java.nio.file.Files;
import java.util.Objects;
import java.util.Optional;
import jdk.jpackage.internal.model.MacApplication;
import jdk.jpackage.internal.model.MacPackage;
import jdk.jpackage.internal.model.MacPackageMixin;
import jdk.jpackage.internal.summary.SummaryAccumulator;
import jdk.jpackage.internal.summary.StandardWarning;
final class MacPackageBuilder {
@ -44,6 +47,11 @@ final class MacPackageBuilder {
return this;
}
MacPackageBuilder summary(SummaryAccumulator v) {
summary = v;
return this;
}
PackageBuilder pkgBuilder() {
return pkgBuilder;
}
@ -60,16 +68,22 @@ final class MacPackageBuilder {
pkg = pkgBuilder.create();
var macPkg = MacPackage.create(pkg, new MacPackageMixin.Stub(pkg.predefinedAppImage().map(v -> predefinedAppImageSigned)));
validatePredefinedAppImage(macPkg);
summary().ifPresent(s -> {
validatePredefinedAppImage(s, macPkg);
});
return macPkg;
}
private static void validatePredefinedAppImage(MacPackage pkg) {
private Optional<SummaryAccumulator> summary() {
return Optional.ofNullable(summary);
}
private static void validatePredefinedAppImage(SummaryAccumulator summary, MacPackage pkg) {
if (pkg.predefinedAppImageSigned().orElse(false) && !pkg.isRuntimeInstaller()) {
pkg.predefinedAppImage().ifPresent(predefinedAppImage -> {
var thePackageFile = PackageFile.getPathInAppImage(APPLICATION_LAYOUT);
if (!Files.exists(predefinedAppImage.resolve(thePackageFile))) {
Log.info(I18N.format("warning.per.user.app.image.signed", thePackageFile));
summary.put(StandardWarning.MAC_SIGNED_PREDEFINED_APP_IMAGE_WITHOUT_PACKAGE_FILE, thePackageFile);
}
});
}
@ -77,4 +91,5 @@ final class MacPackageBuilder {
private final PackageBuilder pkgBuilder;
private boolean predefinedAppImageSigned;
private SummaryAccumulator summary;
}

View File

@ -291,7 +291,7 @@ final class MacPackagingPipeline {
"--raw",
"--assess",
"--type", "exec",
bundle.root().toString()).setQuiet(true).saveOutput(true).binaryOutput()::execute).get();
bundle.root().toString()).quiet().saveOutput(true).binaryOutput()::execute).get();
switch (result.getExitCode()) {
case 0, 3 -> {
@ -445,7 +445,7 @@ final class MacPackagingPipeline {
final var infoPlistFile = macBundleFromAppImageLayout(env.resolvedLayout()).orElseThrow().infoPlistFile();
Log.verbose(I18N.format("message.preparing-info-plist", PathUtils.normalizedAbsolutePathString(infoPlistFile)));
Log.progress(I18N.format("message.preparing-info-plist", PathUtils.normalizedAbsolutePathString(infoPlistFile)));
final String faXml = toSupplier(() -> {
var buf = new StringWriter();

View File

@ -29,6 +29,8 @@ import java.util.Optional;
import jdk.jpackage.internal.model.MacPkgPackage;
import jdk.jpackage.internal.model.MacPkgPackageMixin;
import jdk.jpackage.internal.model.PkgSigningConfig;
import jdk.jpackage.internal.summary.StandardWarning;
import jdk.jpackage.internal.summary.SummaryAccumulator;
final class MacPkgPackageBuilder {
@ -41,9 +43,16 @@ final class MacPkgPackageBuilder {
return this;
}
MacPkgPackageBuilder summary(SummaryAccumulator v) {
summary = v;
return this;
}
MacPkgPackage create() {
var pkg = MacPkgPackage.create(pkgBuilder.create(), new MacPkgPackageMixin.Stub(createSigningConfig()));
validatePredefinedAppImage(pkg);
summary().ifPresent(s -> {
validatePredefinedAppImage(s, pkg);
});
return pkg;
}
@ -53,14 +62,19 @@ final class MacPkgPackageBuilder {
});
}
private static void validatePredefinedAppImage(MacPkgPackage pkg) {
private Optional<SummaryAccumulator> summary() {
return Optional.ofNullable(summary);
}
private static void validatePredefinedAppImage(SummaryAccumulator summary, MacPkgPackage pkg) {
if (!pkg.predefinedAppImageSigned().orElse(false) && pkg.sign()) {
pkg.predefinedAppImage().ifPresent(predefinedAppImage -> {
Log.info(I18N.format("warning.unsigned.app.image", "pkg"));
summary.put(StandardWarning.MAC_SIGNED_PKG_WITH_UNSIGNED_PREDEFINED_APP_IMAGE);
});
}
}
private final MacPackageBuilder pkgBuilder;
private SigningIdentityBuilder signingBuilder;
private SummaryAccumulator summary;
}

View File

@ -34,7 +34,6 @@ import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@ -216,6 +215,7 @@ record MacPkgPackager(BuildEnv env, MacPkgPackage pkg, Optional<Services> servic
pipelineBuilder
.task(PkgPackageTaskID.PREPARE_MAIN_SCRIPTS)
.action(this::prepareMainScripts)
.logActionBegin("message.preparing-scripts")
.addDependent(PackageTaskID.RUN_POST_IMAGE_USER_SCRIPT)
.add()
.task(PkgPackageTaskID.LOG_NO_MAIN_SCRIPTS)
@ -223,6 +223,7 @@ record MacPkgPackager(BuildEnv env, MacPkgPackage pkg, Optional<Services> servic
.addDependent(PackageTaskID.RUN_POST_IMAGE_USER_SCRIPT)
.add()
.task(PkgPackageTaskID.CREATE_DISTRIBUTION_XML_FILE)
.logActionBegin("message.preparing-distribution-dist", distributionXmlFile())
.action(this::prepareDistributionXMLFile)
.addDependent(PackageTaskID.RUN_POST_IMAGE_USER_SCRIPT)
.add()
@ -350,7 +351,6 @@ record MacPkgPackager(BuildEnv env, MacPkgPackage pkg, Optional<Services> servic
}
private void prepareMainScripts() throws IOException {
Log.verbose(I18N.getString("message.preparing-scripts"));
final var scriptsRoot = scriptsRoot().orElseThrow();
@ -371,9 +371,6 @@ record MacPkgPackager(BuildEnv env, MacPkgPackage pkg, Optional<Services> servic
private void prepareDistributionXMLFile() throws IOException {
final var f = distributionXmlFile();
Log.verbose(MessageFormat.format(I18N.getString(
"message.preparing-distribution-dist"), f.toAbsolutePath().toString()));
XmlUtils.createXml(f, xml -> {
xml.writeStartElement("installer-gui-script");
xml.writeAttribute("minSpecVersion", "1");

View File

@ -53,11 +53,14 @@ resource.pkg-background-image=pkg background image
resource.pkg-pdf=project definition file
resource.launchd-plist-file=launchd plist file
message.bundle-name-too-long-warning={0} is set to ''{1}'', which is longer than 16 characters. For a better Mac experience consider shortening it.
summary.property.mac-bundle-identifier=CFBundleIdentifier
summary.property.mac-bundle-name=CFBundleName
summary.property.mac-sign-app-image.format=Sign {0} in "{1}" directory
warning.bundle-name-too-long-warning=Bundle name "{0}" is longer than 16 characters. For a better Mac experience consider shortening it.
message.preparing-info-plist=Preparing Info.plist: {0}.
message.icon-not-icns= The specified icon "{0}" is not an ICNS file and will not be used. The default icon will be used in it's place.
message.keychain.error=Unable to get keychain list.
message.derived-bundle-identifier=Derived bundle identifier: {0}
message.preparing-dmg-setup=Preparing dmg setup: {0}.
message.preparing-scripts=Preparing package scripts.
message.preparing-distribution-dist=Preparing distribution.dist: {0}.
@ -69,7 +72,8 @@ message.dmg.license.button.disagree=Disagree
message.dmg.license.button.print=Print
message.dmg.license.button.save=Save...
message.dmg.license.message=If you agree with the terms of this license, press "Agree" to install the software. If you do not agree, press "Disagree".
warning.unsigned.app.image=Warning: Using unsigned app-image to build signed {0}.
warning.per.user.app.image.signed=Warning: Support for per-user configuration of the installed application will not be supported due to missing "{0}" in predefined signed application image.
warning.non.standard.contents.sub.dir=Warning: The file name of the directory "{0}" specified for the --app-content option is not a standard subdirectory name in the "Contents" directory of the application bundle. The result application bundle may fail code signing and/or notarization.
warning.app.content.is.not.dir=Warning: The value "{0}" of the --app-content option is not a directory. The result application bundle may fail code signing and/or notarization.
warning.unsigned.app.image=Unsigned predefined application image with signed output package
warning.per.user.app.image.signed=Per-user configuration of the installed application will not be supported due to missing "{0}" file in the signed predefined application image
warning.non-standard-app-content=The value of --app-content option may result into failure signing and/or notarizing of the result application bundle
warning.non-standard-app-content.not-dir="{0}" is not a directory
warning.non-standard-app-content.non-standard-dir-name=The name "{0}" of directory "{1}" is not a standard subdirectory name in the "Contents" directory of a macOS bundle

View File

@ -192,7 +192,7 @@ final class ApplicationBuilder {
derivedVersion = derivedVersion.map(v -> {
var mappedVersion = derivedVersionNormalizer.apply(v);
if (!mappedVersion.equals(v)) {
Log.verbose(I18N.format("message.version-normalized", mappedVersion, v));
Log.trace("Normalize derived bundle version from [%s] to [%s]", v, mappedVersion);
}
return mappedVersion;
});
@ -205,10 +205,10 @@ final class ApplicationBuilder {
if (appImageLayout instanceof RuntimeLayout && runtimeReleaseFile != null) {
try {
var releaseVersion = new RuntimeReleaseFile(runtimeReleaseFile).getJavaVersion().toString();
Log.verbose(I18N.format("message.release-version", releaseVersion));
Log.trace("Derive bundle version [%s] from [%s] file", releaseVersion, runtimeReleaseFile);
return Optional.of(releaseVersion);
} catch (Exception ex) {
Log.verbose(ex);
Log.trace(ex, "Failed to derive bundle version from [%s] file", runtimeReleaseFile);
return Optional.empty();
}
} else if (launchers != null) {
@ -218,7 +218,7 @@ final class ApplicationBuilder {
.flatMap(modularStartupInfo -> {
var moduleVersion = modularStartupInfo.moduleVersion();
moduleVersion.ifPresent(v -> {
Log.verbose(I18N.format("message.module-version", v, modularStartupInfo.moduleName()));
Log.trace("Derive bundle version [%s] from [%s] module", v, modularStartupInfo.moduleName());
});
return moduleVersion;
});

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2025, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -42,13 +42,6 @@ interface BuildEnv {
*/
Path buildRoot();
/**
* Returns <code>true</code> if the build should be verbose output.
*
* @return <code>true</code> if the build should be verbose output
*/
boolean verbose();
/**
* Returns the path of the resource directory or an empty {@link Optional}
* instance if none is configured with the build.
@ -108,15 +101,25 @@ interface BuildEnv {
return ((Internal.DefaultBuildEnv)env).copyWithAppImageLayout(appImageLayout);
}
static BuildEnv create(Path buildRoot, Optional<Path> resourceDir, boolean verbose,
Class<?> resourceLocator, AppImageLayout appImageLayout) {
return new Internal.DefaultBuildEnv(buildRoot, resourceDir, verbose,
resourceLocator, appImageLayout);
static BuildEnv create(
Path buildRoot,
Optional<Path> resourceDir,
Class<?> resourceLocator,
AppImageLayout appImageLayout) {
return new Internal.DefaultBuildEnv(
buildRoot,
resourceDir,
resourceLocator,
appImageLayout);
}
static final class Internal {
private record DefaultBuildEnv(Path buildRoot, Optional<Path> resourceDir,
boolean verbose, Class<?> resourceLocator,
private record DefaultBuildEnv(
Path buildRoot,
Optional<Path> resourceDir,
Class<?> resourceLocator,
AppImageLayout appImageLayout) implements BuildEnv {
DefaultBuildEnv {
@ -131,7 +134,7 @@ interface BuildEnv {
}
DefaultBuildEnv copyWithAppImageLayout(AppImageLayout v) {
return new DefaultBuildEnv(buildRoot, resourceDir, verbose, resourceLocator, v);
return new DefaultBuildEnv(buildRoot, resourceDir, resourceLocator, v);
}
@Override

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2025, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -48,13 +48,11 @@ final class BuildEnvBuilder {
String.format("Root work directory [%s] should be empty or non existent", root));
}
return BuildEnv.create(root, Optional.ofNullable(resourceDir), verbose,
ResourceLocator.class, resolvedAppImageLayout());
}
BuildEnvBuilder verbose(boolean v) {
verbose = v;
return this;
return BuildEnv.create(
root,
Optional.ofNullable(resourceDir),
ResourceLocator.class,
resolvedAppImageLayout());
}
BuildEnvBuilder resourceDir(Path v) {
@ -96,7 +94,6 @@ final class BuildEnvBuilder {
private Path appImageDir;
private AppImageLayout appImageLayout;
private Path resourceDir;
private boolean verbose;
private final Path root;
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2025, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -28,7 +28,6 @@ import static jdk.jpackage.internal.cli.StandardOption.PREDEFINED_APP_IMAGE;
import static jdk.jpackage.internal.cli.StandardOption.PREDEFINED_RUNTIME_IMAGE;
import static jdk.jpackage.internal.cli.StandardOption.RESOURCE_DIR;
import static jdk.jpackage.internal.cli.StandardOption.TEMP_ROOT;
import static jdk.jpackage.internal.cli.StandardOption.VERBOSE;
import java.nio.file.Path;
import java.util.Objects;
@ -82,7 +81,6 @@ final class BuildEnvFromOptions {
final var builder = new BuildEnvBuilder(TEMP_ROOT.getFrom(options));
RESOURCE_DIR.ifPresentIn(options, builder::resourceDir);
VERBOSE.ifPresentIn(options, builder::verbose);
if (app.isRuntime()) {
var path = PREDEFINED_RUNTIME_IMAGE.getFrom(options);

View File

@ -130,13 +130,15 @@ class DefaultBundlingEnvironment implements CliBundlingEnvironment {
Objects.requireNonNull(app);
Objects.requireNonNull(pipelineBuilder);
OptionUtils.finalizeAndPrintSummary(options, app);
if (EXIT_AFTER_CONFIGURATION_PHASE.getFrom(options)) {
return;
}
final var outputDir = PathUtils.normalizedAbsolutePath(OptionUtils.outputDir(options).resolve(app.appImageDirName()));
Log.verbose(I18N.getString("message.create-app-image"));
Log.progress(I18N.format("message.create-app-image"));
IOUtils.writableOutputDir(outputDir.getParent());
@ -150,7 +152,7 @@ class DefaultBundlingEnvironment implements CliBundlingEnvironment {
pipelineBuilder.create().execute(BuildEnv.withAppImageDir(env, outputDir), app);
Log.verbose(I18N.getString("message.app-image-created"));
Log.progress(I18N.format("message.app-image-created"));
}
static <T extends Package> void createNativePackage(Options options,
@ -175,6 +177,8 @@ class DefaultBundlingEnvironment implements CliBundlingEnvironment {
Objects.requireNonNull(createPipelineBuilder);
Objects.requireNonNull(pipelineBuilderMutatorFactory);
OptionUtils.finalizeAndPrintSummary(options, pkg);
if (EXIT_AFTER_CONFIGURATION_PHASE.getFrom(options)) {
return;
}
@ -203,6 +207,9 @@ class DefaultBundlingEnvironment implements CliBundlingEnvironment {
@Override
public void createBundle(BundlingOperationDescriptor op, Options cmdline) {
final var bundler = getBundlerSupplier(op).get().orElseThrow();
cmdline = OptionUtils.addSummary(cmdline);
Optional<Path> permanentWorkDirectory = Optional.empty();
try (var tempDir = new TempDirectory(cmdline, Globals.instance().objectFactory())) {
if (!tempDir.deleteOnClose()) {
@ -213,7 +220,7 @@ class DefaultBundlingEnvironment implements CliBundlingEnvironment {
throw new UncheckedIOException(ex);
} finally {
permanentWorkDirectory.ifPresent(workDir -> {
Log.verbose(I18N.format("message.debug-working-directory", workDir.toAbsolutePath()));
Log.progress(I18N.format("message.debug-working-directory", workDir.toAbsolutePath()));
});
}
}

View File

@ -24,11 +24,11 @@
*/
package jdk.jpackage.internal;
import java.io.BufferedReader;
import static jdk.jpackage.internal.log.StandardLogger.COMMAND_LOGGER;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.io.StringReader;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
@ -41,6 +41,7 @@ import java.util.concurrent.TimeUnit;
import java.util.function.UnaryOperator;
import java.util.spi.ToolProvider;
import java.util.stream.Stream;
import jdk.jpackage.internal.log.CommandLogger;
import jdk.jpackage.internal.model.ExecutableAttributesWithCapturedOutput;
import jdk.jpackage.internal.util.CommandLineFormat;
import jdk.jpackage.internal.util.CommandOutputControl;
@ -175,11 +176,15 @@ public final class Executor {
return args;
}
public Executor setQuiet(boolean v) {
public Executor quiet(boolean v) {
quietCommand = v;
return this;
}
public Executor quiet() {
return quiet(true);
}
public Executor mapper(UnaryOperator<Executor> v) {
mapper = v;
return this;
@ -212,8 +217,10 @@ public final class Executor {
throw new IllegalStateException("No target to execute");
}
if (dumpOutput()) {
Log.verbose(String.format("Running %s", CommandLineFormat.DEFAULT.apply(List.of(commandLine().getFirst()))));
var logger = logger();
if (logger.enabled()) {
logger.beforeCommandExecuted(quietCommand, CommandLineFormat.DEFAULT.apply(commandLine()));
}
var printableOutputBuilder = new PrintableOutputBuilder(coc);
@ -229,8 +236,21 @@ public final class Executor {
}
var printableOutput = printableOutputBuilder.create();
if (dumpOutput()) {
log(result, printableOutput);
if (logger.enabled()) {
Optional<Long> pid;
if (result.execAttrs() instanceof ProcessAttributes attrs) {
pid = attrs.pid();
} else {
pid = Optional.empty();
}
logger.afterCommandExecuted(
quietCommand,
result.execAttrs().printableCommandLine(),
pid,
result.exitCode(),
printableOutput);
}
return ExecutableAttributesWithCapturedOutput.augmentResultWithOutput(result, printableOutput);
@ -271,6 +291,10 @@ public final class Executor {
}
}
private CommandLogger logger() {
return Globals.instance().logger(COMMAND_LOGGER);
}
private ProcessBuilder copyProcessBuilder() {
if (processBuilder == null) {
throw new IllegalStateException();
@ -285,47 +309,6 @@ public final class Executor {
return copy;
}
private boolean dumpOutput() {
return Log.isVerbose() && !quietCommand;
}
private static void log(Result result, String printableOutput) throws IOException {
Objects.requireNonNull(result);
Objects.requireNonNull(printableOutput);
Optional<Long> pid;
if (result.execAttrs() instanceof ProcessAttributes attrs) {
pid = attrs.pid();
} else {
pid = Optional.empty();
}
var sb = new StringBuilder();
sb.append("Command");
pid.ifPresent(p -> {
sb.append(" [PID: ").append(p).append("]");
});
sb.append(":\n ").append(result.execAttrs().printableCommandLine());
Log.verbose(sb.toString());
if (!printableOutput.isEmpty()) {
sb.delete(0, sb.length());
sb.append("Output:");
try (var lines = new BufferedReader(new StringReader(printableOutput)).lines()) {
lines.forEach(line -> {
sb.append("\n ").append(line);
});
}
Log.verbose(sb.toString());
}
result.exitCode().ifPresentOrElse(exitCode -> {
Log.verbose("Returned: " + exitCode + "\n");
}, () -> {
Log.verbose("Aborted: timed-out" + "\n");
});
}
private static final class PrintableOutputBuilder {
PrintableOutputBuilder(CommandOutputControl coc) {

View File

@ -24,12 +24,14 @@
*/
package jdk.jpackage.internal;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Supplier;
import jdk.jpackage.internal.cli.OptionValue;
import jdk.jpackage.internal.cli.Options;
import jdk.jpackage.internal.log.Logger;
public final class Globals {
@ -73,16 +75,14 @@ public final class Globals {
return setProperty(EnvironmentProvider.class, v);
}
Log.Logger logger() {
return logger;
public Globals logEnv(Options v) {
checkMutable();
logEnv = Objects.requireNonNull(v);
return this;
}
public void loggerOutputStreams(PrintWriter out, PrintWriter err) {
logger.setPrintWriter(out, err);
}
public void loggerVerbose() {
logger.setVerbose();
public <T extends Logger> T logger(OptionValue<T> ov) {
return ov.getFrom(instance().logEnv);
}
public static int main(Supplier<Integer> mainBody) {
@ -104,7 +104,7 @@ public final class Globals {
}
private ObjectFactory objectFactory = ObjectFactory.DEFAULT;
private final Log.Logger logger = new Log.Logger();
private Options logEnv = Options.concat();
private final Map<Object, Object> properties = new HashMap<>();
private static final ScopedValue<Globals> INSTANCE = ScopedValue.newInstance();

View File

@ -60,4 +60,13 @@ final class IOUtils {
throw new JPackageException(I18N.format("error.cannot-write-to-output-dir", outdir.toAbsolutePath()));
}
}
static void deleteIfExistsIgnoreError(Path path) {
try {
Files.deleteIfExists(path);
} catch (IOException ex) {
Log.trace(ex, "Failed to delete [%s]", path);
}
}
}

View File

@ -25,103 +25,62 @@
package jdk.jpackage.internal;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.Date;
import jdk.jpackage.internal.log.ProgressLogger;
import jdk.jpackage.internal.log.ResourceLogger;
import jdk.jpackage.internal.log.StandardLogger;
import jdk.jpackage.internal.log.TraceLogger;
/**
* Log
*
* General purpose logging mechanism.
*/
public class Log {
public static class Logger {
private boolean verbose = false;
private PrintWriter out = null;
private PrintWriter err = null;
final class Log {
// verbose defaults to true unless environment variable JPACKAGE_DEBUG
// is set to true.
// Then it is only set to true by using --verbose jpackage option
public Logger() {
verbose = ("true".equals(System.getenv("JPACKAGE_DEBUG")));
}
public void setVerbose() {
verbose = true;
}
public boolean isVerbose() {
return verbose;
}
public void setPrintWriter(PrintWriter out, PrintWriter err) {
this.out = out;
this.err = err;
}
public void info(String msg) {
if (out != null) {
out.println(msg);
}
}
public void fatalError(String msg) {
if (err != null) {
err.println(msg);
}
}
public void error(String msg) {
msg = addTimestamp(msg);
if (err != null) {
err.println(msg);
}
}
public void verbose(Throwable t) {
if (out != null && verbose) {
out.print(addTimestamp(""));
t.printStackTrace(out);
}
}
public void verbose(String msg) {
msg = addTimestamp(msg);
if (out != null && verbose) {
out.println(msg);
}
}
private String addTimestamp(String msg) {
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss.SSS");
Date time = new Date(System.currentTimeMillis());
return String.format("[%s] %s", sdf.format(time), msg);
}
private Log() {
}
public static void info(String msg) {
Globals.instance().logger().info(msg);
static void trace(String format, Object... args) {
tracer().trace(format, args);
}
public static void fatalError(String msg) {
Globals.instance().logger().fatalError(msg);
static void trace(Throwable t, String format, Object... args) {
tracer().trace(t, format, args);
}
public static void error(String msg) {
Globals.instance().logger().error(msg);
static void trace(Throwable t) {
tracer().trace(t);
}
public static boolean isVerbose() {
return Globals.instance().logger().isVerbose();
static void useResource(String localizedMsg) {
resourceLogger().useResource(localizedMsg);
}
public static void verbose(String msg) {
Globals.instance().logger().verbose(msg);
static void progress(String localizedMsg) {
progressLogger().progress(localizedMsg);
}
public static void verbose(Throwable t) {
Globals.instance().logger().verbose(t);
static void progressWarning(Exception cause) {
progressLogger().progressWarning(cause);
}
static void progressWarning(Exception cause, String localizedMsg) {
progressLogger().progressWarning(cause, localizedMsg);
}
static void progressWarning(String localizedMsg) {
progressLogger().progressWarning(localizedMsg);
}
private static TraceLogger tracer() {
return Globals.instance().logger(StandardLogger.TRACE_LOGGER);
}
private static ProgressLogger progressLogger() {
return Globals.instance().logger(StandardLogger.PROGRESS_LOGGER);
}
private static ResourceLogger resourceLogger() {
return Globals.instance().logger(StandardLogger.RESOURCE_LOGGER);
}
}

View File

@ -56,13 +56,14 @@ record ModuleInfo(String name, Optional<String> version, Optional<String> mainCl
// is linked in the runtime by simply analyzing the data
// of `release` file.
var releaseFilePath = RuntimeReleaseFile.releaseFilePathInRuntime(cookedRuntime);
try {
var cookedRuntimeModules = RuntimeReleaseFile.loadFromRuntime(cookedRuntime).getModules();
var cookedRuntimeModules = new RuntimeReleaseFile(releaseFilePath).getModules();
if (!cookedRuntimeModules.contains(moduleName)) {
return Optional.empty();
}
} catch (Exception ex) {
Log.verbose(ex);
Log.trace(ex, "Failed to read modules from [%s]", releaseFilePath);
return Optional.empty();
}

View File

@ -30,11 +30,15 @@ import static jdk.jpackage.internal.cli.StandardOption.MAIN_JAR;
import static jdk.jpackage.internal.cli.StandardOption.MODULE;
import static jdk.jpackage.internal.cli.StandardOption.PREDEFINED_APP_IMAGE;
import static jdk.jpackage.internal.cli.StandardOption.PREDEFINED_RUNTIME_IMAGE;
import static jdk.jpackage.internal.log.StandardLogger.SUMMARY_LOGGER;
import java.nio.file.Path;
import java.util.Objects;
import jdk.jpackage.internal.cli.OptionValue;
import jdk.jpackage.internal.cli.Options;
import jdk.jpackage.internal.cli.StandardBundlingOperation;
import jdk.jpackage.internal.model.BundleSpec;
import jdk.jpackage.internal.summary.Summary;
final class OptionUtils {
@ -56,4 +60,21 @@ final class OptionUtils {
static boolean isBundlingOperation(Options options, StandardBundlingOperation op) {
return bundlingOperation(options).equals(Objects.requireNonNull(op));
}
static Options addSummary(Options options) {
return options.copyWithDefaultValue(SUMMARY, Summary::new);
}
static Summary summary(Options options) {
return SUMMARY.getFrom(options);
}
static void finalizeAndPrintSummary(Options options, BundleSpec bundle) {
var summary = summary(options);
summary.putStandardPropertiesIfAbsent(bundlingOperation(options), outputDir(options), bundle);
Globals.instance().logger(SUMMARY_LOGGER).summary(summary);
}
private static final OptionValue<Summary> SUMMARY = OptionValue.create();
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2019, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -257,7 +257,7 @@ final class OverridableResource {
private boolean useExternal(ResourceConsumer dest) throws IOException {
boolean used = externalPath != null && Files.exists(externalPath);
if (used && dest != null) {
Log.verbose(I18N.format("message.using-custom-resource-from-file",
Log.useResource(I18N.format("message.using-custom-resource-from-file",
getPrintableCategory(),
externalPath.toAbsolutePath().normalize()));
@ -284,7 +284,7 @@ final class OverridableResource {
final Path logResourceName = Optional.ofNullable(logPublicName).orElse(
resourceName).normalize();
Log.verbose(I18N.format("message.using-custom-resource",
Log.useResource(I18N.format("message.using-custom-resource",
getPrintableCategory(), logResourceName));
try (InputStream in = Files.newInputStream(customResource)) {
@ -304,7 +304,7 @@ final class OverridableResource {
.orElseGet(() -> {
return resourceName(dest);
});
Log.verbose(I18N.format("message.using-default-resource",
Log.useResource(I18N.format("message.using-default-resource",
defaultName, getPrintableCategory(), resourceName));
try (InputStream in = defaultResourceGetter.apply(defaultName)) {
@ -321,7 +321,7 @@ final class OverridableResource {
.orElseGet(() -> {
return resourceName(dest);
});
Log.verbose(I18N.format("message.no-default-resource",
Log.useResource(I18N.format("message.no-default-resource",
getPrintableCategory(), resourceName));
}
}

View File

@ -378,21 +378,21 @@ final class PackagingPipeline {
private <T extends Application, U extends AppImageLayout> TaskBuilder logAppImageAction(ActionRole role, String keyId, Function<AppImageBuildEnv<T, U>, Object[]> formatArgsSupplier) {
Objects.requireNonNull(keyId);
return appImageAction(role, (AppImageBuildEnv<T, U> env) -> {
Log.verbose(I18N.format(keyId, formatArgsSupplier.apply(env)));
Log.progress(I18N.format(keyId, formatArgsSupplier.apply(env)));
});
}
private <T extends Package, U extends AppImageLayout> TaskBuilder logPackageAction(ActionRole role, String keyId, Function<PackageBuildEnv<T, U>, Object[]> formatArgsSupplier) {
Objects.requireNonNull(keyId);
return packageAction(role, (PackageBuildEnv<T, U> env) -> {
Log.verbose(I18N.format(keyId, formatArgsSupplier.apply(env)));
Log.progress(I18N.format(keyId, formatArgsSupplier.apply(env)));
});
}
private TaskBuilder logAction(ActionRole role, String keyId, Supplier<Object[]> formatArgsSupplier) {
Objects.requireNonNull(keyId);
return action(role, () -> {
Log.verbose(I18N.format(keyId, formatArgsSupplier.get()));
Log.progress(I18N.format(keyId, formatArgsSupplier.get()));
});
}

View File

@ -94,14 +94,14 @@ final class TempDirectory implements Closeable {
path, MAX_REPORTED_UNDELETED_FILE_COUNT).paths();
if (remainingFiles.equals(List.of(path))) {
Log.info(I18N.format("warning.tempdir.cleanup-failed", path));
Log.progressWarning(I18N.format("warning.tempdir.cleanup-failed", path));
} else {
remainingFiles.forEach(file -> {
Log.info(I18N.format("warning.tempdir.cleanup-file-failed", file));
Log.progressWarning(I18N.format("warning.tempdir.cleanup-file-failed", file));
});
}
Log.verbose(ex);
Log.progressWarning(ex);
}
}
return null;
@ -138,7 +138,7 @@ final class TempDirectory implements Closeable {
return addPath(dir, FileVisitResult.SKIP_SUBTREE);
}
} catch (IOException ex) {
Log.verbose(ex);
Log.trace(ex);
}
return FileVisitResult.CONTINUE;
}
@ -170,7 +170,7 @@ final class TempDirectory implements Closeable {
});
} catch (IOException ex) {
Log.verbose(ex);
Log.trace(ex);
}
return new DirectoryListing(Collections.unmodifiableList(paths), !stopped.get());
@ -181,5 +181,5 @@ final class TempDirectory implements Closeable {
private final boolean deleteOnClose;
private final RetryExecutorFactory retryExecutorFactory;
private final static int MAX_REPORTED_UNDELETED_FILE_COUNT = 100;
private static final int MAX_REPORTED_UNDELETED_FILE_COUNT = 100;
}

View File

@ -126,7 +126,7 @@ final class ToolValidator {
String version = null;
try {
var result = Executor.of(cmdline).setQuiet(true).saveOutput().execute();
var result = Executor.of(cmdline).quiet().saveOutput().execute();
var lines = result.content();
if (versionParser != null && minimalVersion != null) {
version = versionParser.apply(lines.stream());

View File

@ -0,0 +1,209 @@
/*
* 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. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* 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.internal.cli;
import java.util.BitSet;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import jdk.jpackage.internal.log.LogEnvironment;
import jdk.jpackage.internal.log.LogEnvironment.Builder;
import jdk.jpackage.internal.log.LogEnvironment.LogSink;
import jdk.jpackage.internal.log.LoggerRole;
import jdk.jpackage.internal.util.SetBuilder;
public final class LogConfigParser {
static Builder valueOf(String str) {
return buildFromCategories(tokenize(str));
}
public static Set<MessageCategory> tokenize(String str) {
Objects.requireNonNull(str);
Supplier<IllegalArgumentException> ex = () -> {
return new IllegalArgumentException(String.format("Invalid value: [%s]", str));
};
var groupCategories = new BitSet(MessageCategory.values().length);
var enableCategories = new BitSet(MessageCategory.values().length);
var disableCategories = new BitSet(MessageCategory.values().length);
Stream.of(str.split("(?<=.),")).filter(Predicate.not(String::isEmpty)).forEach(v -> {
if (v.charAt(0) == '-') {
var category = CONSOLE_CATEGORIES.get(v.substring(1));
if (category == null) {
throw ex.get();
} else {
disableCategories.set(category.ordinal());
}
} else {
Optional.ofNullable(GROUPS.get(v)).ifPresentOrElse(categoryGroup -> {
for (var category : categoryGroup) {
groupCategories.set(category.ordinal());
}
}, () -> {
var category = CONSOLE_CATEGORIES.get(v);
if (category == null) {
throw ex.get();
} else {
enableCategories.set(category.ordinal());
}
});
}
});
var categories = new HashSet<MessageCategory>();
for (var category : MessageCategory.values()) {
if (enableCategories.get(category.ordinal()) ||
(groupCategories.get(category.ordinal()) && !disableCategories.get(category.ordinal()))) {
categories.add(category);
}
}
return categories;
}
static Builder defaultVerbose() {
return buildFromCategories(SetBuilder.<MessageCategory>build()
.add(MessageCategory.values())
.remove(MessageCategory.TRACE, MessageCategory.SYSTEM_LOGGER)
.create());
}
static Builder quiet() {
return buildFromCategories(MessageCategory.ERRORS, MessageCategory.WARNINGS);
}
private LogConfigParser() {
}
public enum MessageCategory {
ERRORS {
public void applyTo(Builder builder) {
builder.enable(LoggerRole.ERROR_LOGGER, LogSink.CONSOLE);
builder.printFailedCommandOutputInConsole(true);
}
},
PROGRESS {
public void applyTo(Builder builder) {
builder.enable(LoggerRole.PROGRESS_LOGGER, LogSink.CONSOLE);
builder.printProgressInConsole(true);
}
},
RESOURCES {
public void applyTo(Builder builder) {
builder.enable(LoggerRole.RESOURCE_LOGGER, LogSink.CONSOLE);
}
},
SUMMARY {
public void applyTo(Builder builder) {
builder.enable(LoggerRole.SUMMARY_LOGGER, LogSink.CONSOLE);
builder.printSummaryInConsole(true);
}
},
SYSTEM_LOGGER {
public void applyTo(Builder builder) {
for (var role : LoggerRole.values()) {
builder.enable(role, LogSink.SYSTEM_LOGGER);
}
}
@Override
String asStringValue() {
return "log";
}
@Override
boolean isConsole() {
return false;
}
},
TOOLS {
public void applyTo(Builder builder) {
builder.enable(LoggerRole.COMMAND_LOGGER, LogSink.CONSOLE);
}
},
TRACE {
public void applyTo(Builder builder) {
TOOLS.applyTo(builder);
builder.enable(LoggerRole.TRACE_LOGGER, LogSink.CONSOLE);
builder.printErrorStackTraceInConsole(true);
builder.printCommandOutputInConsole(true);
builder.printQuietCommands(true);
}
},
WARNINGS {
public void applyTo(Builder builder) {
builder.enable(LoggerRole.SUMMARY_LOGGER, LogSink.CONSOLE);
builder.enable(LoggerRole.PROGRESS_LOGGER, LogSink.CONSOLE);
builder.printSummaryWarningsInConsole(true);
builder.printProgressWarningsInConsole(true);
}
},
;
public abstract void applyTo(Builder builder);
String asStringValue() {
return name().toLowerCase();
}
boolean isConsole() {
return true;
}
}
private static Builder buildFromCategories(Set<MessageCategory> categories) {
var builder = LogEnvironment.build();
categories.forEach(c -> {
c.applyTo(builder);
});
return builder;
}
private static Builder buildFromCategories(MessageCategory... categories) {
return buildFromCategories(Set.of(categories));
}
private static final Map<String, MessageCategory> CONSOLE_CATEGORIES = Stream.of(MessageCategory.values())
.collect(Collectors.toUnmodifiableMap(MessageCategory::asStringValue, x -> x));
private static final Map<String, Set<MessageCategory>> GROUPS = Map.ofEntries(
Map.entry("all", Set.of(MessageCategory.values())),
Map.entry("console", Stream.of(MessageCategory.values())
.filter(MessageCategory::isConsole)
.collect(Collectors.toUnmodifiableSet()))
);
}

View File

@ -28,6 +28,7 @@ package jdk.jpackage.internal.cli;
import static jdk.jpackage.internal.cli.StandardOption.HELP;
import static jdk.jpackage.internal.cli.StandardOption.VERBOSE;
import static jdk.jpackage.internal.cli.StandardOption.VERSION;
import static jdk.jpackage.internal.log.StandardLogger.ERROR_LOGGER;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
@ -49,13 +50,15 @@ import java.util.spi.ToolProvider;
import jdk.internal.opt.CommandLine;
import jdk.internal.util.OperatingSystem;
import jdk.jpackage.internal.Globals;
import jdk.jpackage.internal.Log;
import jdk.jpackage.internal.cli.JOptSimpleOptionsBuilder.ConvertedOptionsBuilder;
import jdk.jpackage.internal.log.ErrorLogger;
import jdk.jpackage.internal.model.ConfigException;
import jdk.jpackage.internal.model.ExecutableAttributesWithCapturedOutput;
import jdk.jpackage.internal.model.JPackageException;
import jdk.jpackage.internal.model.SelfContainedException;
import jdk.jpackage.internal.util.CommandOutputControl.UnexpectedExitCodeException;
import jdk.jpackage.internal.util.CommandOutputControl.UnexpectedResultException;
import jdk.jpackage.internal.util.SetBuilder;
import jdk.jpackage.internal.util.Slot;
import jdk.jpackage.internal.util.function.ExceptionBox;
@ -139,12 +142,10 @@ public final class Main {
Objects.requireNonNull(out);
Objects.requireNonNull(err);
Globals.instance().loggerOutputStreams(out, err);
Globals.instance().logEnv(LogConfigParser.quiet().out(out).err(err).create());
final var runner = new Runner(t -> {
new ErrorReporter(_ -> {
t.printStackTrace(err);
}, Log::fatalError, Log.isVerbose()).reportError(t);
Globals.instance().logger(ERROR_LOGGER).reportError(t);
});
try {
@ -170,7 +171,7 @@ public final class Main {
final var parseResult = Utils.buildParser(os, bundlingEnv).create().apply(mappedArgs.get());
return runner.run(() -> {
final var parsedOptionsBuilder = parseResult.orElseThrow();
var parsedOptionsBuilder = parseResult.orElseThrow();
final var options = parsedOptionsBuilder.create();
@ -190,8 +191,30 @@ public final class Main {
return List.of();
}
var skippedOptions = new ArrayList<OptionIdentifier>();
if (VERBOSE.containsIn(options)) {
Globals.instance().loggerVerbose();
// The "--verbose" option is on the command line.
// Pick this option from the command line and parse its string value.
// This will delay parsing of the rest of the command line
// until the configuration of the logging is complete.
parsedOptionsBuilder.copyWithExcludes(
SetBuilder.build(StandardOption.options())
.remove(VERBOSE.getOption())
.create().stream().map(WithOptionIdentifier::id).toList()
).convertedOptions().map(ConvertedOptionsBuilder::create).map(VERBOSE::getFrom).value().ifPresent(logEnvBuilder -> {
skippedOptions.add(VERBOSE.id());
Globals.instance().logEnv(logEnvBuilder.out(out).err(err).create());
});
} else if ("true".equals(System.getenv("JPACKAGE_DEBUG"))) {
// There is no "--verbose" option on the command line,
// but the "JPACKAGE_DEBUG" environment variable is set to "true".
// Enable the default verbose output.
Globals.instance().logEnv(LogConfigParser.defaultVerbose().out(out).err(err).create());
}
if (!skippedOptions.isEmpty()) {
parsedOptionsBuilder = parsedOptionsBuilder.copyWithExcludes(skippedOptions);
}
final var optionsProcessor = new OptionsProcessor(parsedOptionsBuilder, os, bundlingEnv);
@ -234,17 +257,22 @@ public final class Main {
* Always print the messages for exceptions of any type.
*/
record ErrorReporter(Consumer<Throwable> stackTracePrinter, Consumer<String> messagePrinter, boolean verbose) {
ErrorReporter {
public record ErrorReporter(
Consumer<Throwable> stackTracePrinter,
Consumer<String> messagePrinter,
boolean alwaysPrintStackTrace,
boolean printCommandOutput) implements ErrorLogger {
public ErrorReporter {
Objects.requireNonNull(stackTracePrinter);
Objects.requireNonNull(messagePrinter);
}
ErrorReporter(Consumer<Throwable> stackTracePrinter, Consumer<String> messagePrinter) {
this(stackTracePrinter, messagePrinter, true);
public ErrorReporter(Consumer<Throwable> stackTracePrinter, Consumer<String> messagePrinter) {
this(stackTracePrinter, messagePrinter, true, false);
}
void reportError(Throwable t) {
public void reportError(Throwable t) {
var unfoldedExceptions = new ArrayList<Exception>();
ExceptionBox.visitUnboxedExceptionsRecursively(t, unfoldedExceptions::add);
@ -267,7 +295,7 @@ public final class Main {
var commandOutput = ((ExecutableAttributesWithCapturedOutput)result.execAttrs()).printableOutput();
var printableCommandLine = result.execAttrs().printableCommandLine();
if (verbose) {
if (alwaysPrintStackTrace) {
stackTracePrinter.accept(ex);
}
@ -281,16 +309,18 @@ public final class Main {
}
messagePrinter.accept(I18N.format("message.error-header", msg));
messagePrinter.accept(I18N.format("message.failed-command-output-header"));
try (var lines = new BufferedReader(new StringReader(commandOutput)).lines()) {
lines.forEach(messagePrinter);
if (printCommandOutput) {
messagePrinter.accept(I18N.format("message.failed-command-output-header"));
try (var lines = new BufferedReader(new StringReader(commandOutput)).lines()) {
lines.forEach(messagePrinter);
}
}
}
private void printError(Throwable t, Optional<String> advice) {
var isSelfContained = isSelfContained(t);
if (!isSelfContained || verbose) {
if (!isSelfContained || alwaysPrintStackTrace) {
stackTracePrinter.accept(t);
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2025, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -81,7 +81,8 @@ public sealed interface OptionValue<T> extends WithOptionIdentifier {
}
static final class Builder<T> {
OptionValue<T> create() {
public OptionValue<T> create() {
if (conv != null) {
return conv.create(Optional.ofNullable(defaultValue));
} else {
@ -92,7 +93,7 @@ public sealed interface OptionValue<T> extends WithOptionIdentifier {
}
}
Builder<T> defaultValue(T v) {
public Builder<T> defaultValue(T v) {
defaultValue = v;
return this;
}
@ -103,19 +104,19 @@ public sealed interface OptionValue<T> extends WithOptionIdentifier {
return this;
}
Builder<T> id(OptionIdentifier v) {
public Builder<T> id(OptionIdentifier v) {
id = v;
conv = null;
return this;
}
<U> Builder<T> from(OptionValue<U> base, Function<U, T> conv) {
public <U> Builder<T> from(OptionValue<U> base, Function<U, T> conv) {
id(null).spec(null);
this.conv = new Conv<>(base, conv);
return this;
}
<U> Builder<U> to(Function<T, U> conv) {
public <U> Builder<U> to(Function<T, U> conv) {
return OptionValue.<U>build().from(create(), conv);
}

View File

@ -69,6 +69,7 @@ import jdk.jpackage.internal.model.LauncherShortcutStartupDirectory;
import jdk.jpackage.internal.util.RootedPath;
import jdk.jpackage.internal.model.SelfContainedException;
import jdk.jpackage.internal.util.SetBuilder;
import jdk.jpackage.internal.log.LogEnvironment;
/**
* jpackage command line options
@ -109,7 +110,15 @@ public final class StandardOption {
static final OptionValue<Boolean> VERSION = auxilaryOption("version").create();
public static final OptionValue<Boolean> VERBOSE = auxilaryOption("verbose").create();
static final OptionValue<LogEnvironment.Builder> VERBOSE = option("verbose", LogEnvironment.Builder.class)
.scope(StandardBundlingOperation.values())
.inScope(NOT_BUILDING_APP_IMAGE)
.converterExceptionFactory(ERROR_WITH_VALUE_AND_OPTION_NAME)
.converterExceptionFormatString("error.parameter-invalid-value")
.converter(LogConfigParser::valueOf)
.defaultOptionalValue(LogConfigParser.defaultVerbose())
.valuePattern("[<[-]category(,[-]category)*>]")
.create();
static final OptionValue<BundleType> TYPE = option("type", BundleType.class).addAliases("t")
.scope(StandardBundlingOperation.values()).inScope(NOT_BUILDING_APP_IMAGE)
@ -715,9 +724,8 @@ public final class StandardOption {
private static UnaryOperator<Set<OptionScope>> nativeBundling() {
return scope -> {
return new SetBuilder<OptionScope>()
.set(scope)
.remove(new SetBuilder<OptionScope>().set(StandardBundlingOperation.values()).remove(CREATE_NATIVE).create())
return SetBuilder.build(scope)
.remove(SetBuilder.<OptionScope>build(StandardBundlingOperation.values()).remove(CREATE_NATIVE).create())
.create();
};
}

View File

@ -0,0 +1,130 @@
/*
* 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. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* 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.internal.log;
import java.io.BufferedReader;
import java.io.StringReader;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
public interface CommandLogger extends Logger {
void beforeCommandExecuted(boolean quiet, String cmdline);
void afterCommandExecuted(boolean quiet,
String cmdline, Optional<Long> pid, Optional<Integer> exitCode, String printableOutput);
enum CommandLoggerTrait implements LoggerTrait {
PRINT_COMMAND_RESULT,
PRINT_QUIET_COMMANDS,
}
static CommandLogger create(ConsoleLogger sink, boolean printQuietCommands, boolean printResult) {
var theSink = sink.addTimestampsToOut().out();
return new Details.DefaultLogger(
theSink,
printQuietCommands ? theSink : Utils.DISCARDER,
printResult);
}
static CommandLogger create(System.Logger sink) {
var consumer = Utils.toStringConsumer(sink, System.Logger.Level.DEBUG);
return new Details.DefaultLogger(consumer, consumer, true);
}
static final class Details {
private Details() {
}
private record DefaultLogger(
Consumer<String> sink,
Consumer<String> quietCommandSink,
boolean printResult) implements CommandLogger {
DefaultLogger {
Objects.requireNonNull(sink);
Objects.requireNonNull(quietCommandSink);
}
@Override
public void beforeCommandExecuted(boolean quiet, String cmdline) {
Objects.requireNonNull(cmdline);
sink(quiet).accept(String.format("Running %s", cmdline));
}
@Override
public void afterCommandExecuted(boolean quiet,
String cmdline, Optional<Long> pid, Optional<Integer> exitCode, String printableOutput) {
Objects.requireNonNull(cmdline);
Objects.requireNonNull(pid);
Objects.requireNonNull(exitCode);
Objects.requireNonNull(printableOutput);
if (!printResult) {
return;
}
var theSink = sink(quiet);
var sb = new StringBuilder();
sb.append("Command");
pid.ifPresent(p -> {
sb.append(" [PID: ").append(p).append("]");
});
sb.append(":").append(System.lineSeparator()).append(" ").append(cmdline);
theSink.accept(sb.toString());
if (!printableOutput.isEmpty()) {
sb.delete(0, sb.length());
sb.append("Output:");
try (var lines = new BufferedReader(new StringReader(printableOutput)).lines()) {
lines.forEach(line -> {
sb.append(System.lineSeparator()).append(" ").append(line);
});
}
theSink.accept(sb.toString());
}
exitCode.ifPresentOrElse(v -> {
theSink.accept("Returned: " + v);
}, () -> {
theSink.accept("Aborted: timed-out");
});
}
private Consumer<String> sink(boolean quietCommand) {
return quietCommand ? quietCommandSink : sink;
}
}
}
static final CommandLogger DISCARDING_LOGGER = Utils.discardingLogger(CommandLogger.class);
}

View File

@ -0,0 +1,113 @@
/*
* 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. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* 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.internal.log;
import java.io.PrintWriter;
import java.time.Clock;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.Objects;
import java.util.function.Consumer;
public record ConsoleLogger(Consumer<String> out, Consumer<String> err, Clock timestampClock) {
public ConsoleLogger {
Objects.requireNonNull(out);
Objects.requireNonNull(err);
Objects.requireNonNull(timestampClock);
}
public ConsoleLogger(Consumer<String> out, Consumer<String> err) {
this(out, err, Clock.systemDefaultZone());
}
ConsoleLogger discardOut() {
return new ConsoleLogger(Utils.DISCARDER, err, timestampClock);
}
ConsoleLogger discardErr() {
return new ConsoleLogger(out, Utils.DISCARDER, timestampClock);
}
ConsoleLogger addTimestampsToOut() {
return new ConsoleLogger(addTimestamps(out, timestampClock), err, timestampClock);
}
ConsoleLogger addTimestampsToErr() {
return new ConsoleLogger(out, addTimestamps(err, timestampClock), timestampClock);
}
record ConsoleLoggerOutSink(PrintWriter sink) implements LoggerTrait {
ConsoleLoggerOutSink {
Objects.requireNonNull(sink);
}
}
record ConsoleLoggerErrSink(PrintWriter sink) implements LoggerTrait {
ConsoleLoggerErrSink {
Objects.requireNonNull(sink);
}
}
record ConsoleLoggerTimestampClock(Clock clock) implements LoggerTrait {
ConsoleLoggerTimestampClock {
Objects.requireNonNull(clock);
}
}
private static Consumer<String> addTimestamps(Consumer<String> sink, Clock clock) {
Objects.requireNonNull(sink);
Objects.requireNonNull(clock);
if (sink == Utils.DISCARDER || sink instanceof AddingTimestampsConsumer) {
return sink;
} else {
return new AddingTimestampsConsumer(sink, clock);
}
}
private record AddingTimestampsConsumer(Consumer<String> sink, Clock clock) implements Consumer<String> {
AddingTimestampsConsumer {
Objects.requireNonNull(sink);
Objects.requireNonNull(clock);
}
@Override
public void accept(String str) {
var sb = new StringBuilder();
var time = LocalTime.now(clock);
sb.append('[').append(time.format(TIMESTAMP_FORMATTER)).append("] ");
sb.append(str);
str = sb.toString();
sink.accept(str);
}
private static final DateTimeFormatter TIMESTAMP_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss.SSS");
}
}

View File

@ -0,0 +1,78 @@
/*
* 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. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* 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.internal.log;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Objects;
import jdk.jpackage.internal.cli.Main.ErrorReporter;
public interface ErrorLogger extends Logger {
void reportError(Throwable t);
enum ErrorLoggerTrait implements LoggerTrait {
PRINT_STACK_TRACE_ALWAYS,
PRINT_FAILED_COMMAND_OUTPUT,
}
static ErrorLogger create(ConsoleLogger sink, boolean alwaysPrintStackTrace, boolean printCommandOutput) {
Objects.requireNonNull(sink);
return new ErrorReporter(
t -> {
var buf = new StringWriter();
t.printStackTrace(new PrintWriter(buf));
Utils.writeWithoutTrailingLineSeparator(buf, sink.err());
},
sink.err(),
alwaysPrintStackTrace,
printCommandOutput);
}
static ErrorLogger create(System.Logger sink) {
Objects.requireNonNull(sink);
return new ErrorLogger() {
@Override
public void reportError(Throwable t) {
var buf = new StringWriter();
new ErrorReporter(t2 -> {
t2.printStackTrace(new PrintWriter(buf));
}, str -> {
buf.write(str);
buf.write(System.lineSeparator());
}, true, true).reportError(t);
Utils.writeWithoutTrailingLineSeparator(buf, str -> {
sink.log(System.Logger.Level.ERROR, str);
});
}
};
}
static final ErrorLogger DISCARDING_LOGGER = Utils.discardingLogger(ErrorLogger.class);
}

View File

@ -0,0 +1,59 @@
/*
* 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. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* 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.internal.log;
import java.util.List;
import java.util.Map;
import jdk.internal.util.OperatingSystem;
import jdk.jpackage.internal.util.MultiResourceBundle;
import jdk.jpackage.internal.util.StringBundle;
final class I18N {
private I18N() {
}
static String getString(String key) {
return BUNDLE.getString(key);
}
static String format(String key, Object ... args) {
return BUNDLE.format(key, args);
}
private static final StringBundle BUNDLE;
static {
var prefix = "jdk.jpackage.internal.resources.";
BUNDLE = StringBundle.fromResourceBundle(MultiResourceBundle.create(
prefix + "MainResources",
Map.of(
OperatingSystem.LINUX, List.of(prefix + "LinuxResources"),
OperatingSystem.MACOS, List.of(prefix + "MacResources"),
OperatingSystem.WINDOWS, List.of(prefix + "WinResources")
)
));
}
}

View File

@ -0,0 +1,309 @@
/*
* 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. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* 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.internal.log;
import static java.util.stream.Collectors.toMap;
import java.io.PrintWriter;
import java.time.Clock;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Stream;
import jdk.jpackage.internal.cli.Options;
import jdk.jpackage.internal.log.CommandLogger.CommandLoggerTrait;
import jdk.jpackage.internal.log.ConsoleLogger.ConsoleLoggerErrSink;
import jdk.jpackage.internal.log.ConsoleLogger.ConsoleLoggerOutSink;
import jdk.jpackage.internal.log.ConsoleLogger.ConsoleLoggerTimestampClock;
import jdk.jpackage.internal.log.ErrorLogger.ErrorLoggerTrait;
import jdk.jpackage.internal.log.ProgressLogger.ProgressLoggerTrait;
import jdk.jpackage.internal.log.SummaryLogger.SummaryLoggerTrait;
public final class LogEnvironment {
public enum LogSink {
CONSOLE,
SYSTEM_LOGGER
}
public static Builder build() {
return new Builder();
}
public static final class Builder {
public Options create() {
var consoleTraits = new HashSet<LoggerTrait>();
Optional.ofNullable(out).map(ConsoleLoggerOutSink::new).ifPresent(consoleTraits::add);
Optional.ofNullable(err).map(ConsoleLoggerErrSink::new).ifPresent(consoleTraits::add);
Optional.ofNullable(consoleTimestampClock).map(ConsoleLoggerTimestampClock::new).ifPresent(consoleTraits::add);
var loggerTraits = enabledLoggers.entrySet().stream().collect(toMap(Map.Entry::getKey, e -> {
var enabledSinks = e.getValue();
var traits = new ArrayList<LoggerTrait>(booleanTraits);
if (enabledSinks.contains(LogSink.SYSTEM_LOGGER)) {
traits.add(new SystemLoggerTrait(
Optional.ofNullable(systemLoggerFactory).orElse(System::getLogger).apply("jdk.jpackage")));
}
if (enabledSinks.contains(LogSink.CONSOLE)) {
traits.addAll(consoleTraits);
}
return traits;
}));
return LogEnvironment.create(loggerTraits);
}
public Builder out(PrintWriter v) {
out = v;
return this;
}
public Builder err(PrintWriter v) {
err = v;
return this;
}
public Builder printErrorStackTraceInConsole(boolean v) {
return setBooleanTrait(v, ErrorLoggerTrait.PRINT_STACK_TRACE_ALWAYS);
}
public Builder printCommandOutputInConsole(boolean v) {
return setBooleanTrait(v, CommandLoggerTrait.PRINT_COMMAND_RESULT);
}
public Builder printFailedCommandOutputInConsole(boolean v) {
return setBooleanTrait(v, ErrorLoggerTrait.PRINT_FAILED_COMMAND_OUTPUT);
}
public Builder printQuietCommands(boolean v) {
return setBooleanTrait(v, CommandLoggerTrait.PRINT_QUIET_COMMANDS);
}
public Builder printProgressInConsole(boolean v) {
return setBooleanTrait(v, ProgressLoggerTrait.PRINT_INFO);
}
public Builder printProgressWarningsInConsole(boolean v) {
return setBooleanTrait(v, ProgressLoggerTrait.PRINT_WARNINGS);
}
public Builder printSummaryInConsole(boolean v) {
return setBooleanTrait(v, SummaryLoggerTrait.PRINT_INFO);
}
public Builder printSummaryWarningsInConsole(boolean v) {
return setBooleanTrait(v, SummaryLoggerTrait.PRINT_WARNINGS);
}
public Builder consoleTimestampClock(Clock v) {
consoleTimestampClock = v;
return this;
}
public Builder systemLoggerFactory(Function<String, System.Logger> v) {
systemLoggerFactory = v;
return this;
}
public Builder enable(LoggerRole role, LogSink... sinks) {
Objects.requireNonNull(role);
if (sinks.length == 0) {
enabledLoggers.remove(role);
} else {
enabledLoggers.computeIfAbsent(role, _ -> {
return new HashSet<>();
}).addAll(Set.of(sinks));
}
return this;
}
public Builder mutate(Consumer<Builder> mutator) {
mutator.accept(this);
return this;
}
private Builder setBooleanTrait(boolean set, LoggerTrait trait) {
Objects.requireNonNull(trait);
if (set) {
booleanTraits.add(trait);
} else {
booleanTraits.remove(trait);
}
return this;
}
private PrintWriter out;
private PrintWriter err;
private Clock consoleTimestampClock;
private Function<String, System.Logger> systemLoggerFactory;
private final Set<LoggerTrait> booleanTraits = new HashSet<>();
private final Map<LoggerRole, Set<LogSink>> enabledLoggers = new HashMap<>();
}
static Options create(Map<LoggerRole, ? extends Collection<LoggerTrait>> loggerTraits) {
return Options.of(loggerTraits.entrySet().stream().collect(toMap(e -> {
return e.getKey().logger();
}, e -> {
return e.getKey().createLogger(e.getValue());
})));
}
static CommandLogger createCommandLogger(Collection<LoggerTrait> traits) {
return create(
CommandLogger.class,
traits,
sink -> {
var printQuietCommands = traits.contains(CommandLoggerTrait.PRINT_QUIET_COMMANDS);
var printResult = traits.contains(CommandLoggerTrait.PRINT_COMMAND_RESULT);
return CommandLogger.create(sink, printQuietCommands, printResult);
},
CommandLogger::create).orElse(CommandLogger.DISCARDING_LOGGER);
}
static ProgressLogger createProgressLogger(Collection<LoggerTrait> traits) {
return create(
ProgressLogger.class,
traits,
sink -> {
var printInfo = traits.contains(ProgressLoggerTrait.PRINT_INFO);
var printWarnings = traits.contains(ProgressLoggerTrait.PRINT_WARNINGS);
return ProgressLogger.create(sink, printInfo, printWarnings);
},
ProgressLogger::create).orElse(ProgressLogger.DISCARDING_LOGGER);
}
static ErrorLogger createErrorLogger(Collection<LoggerTrait> traits) {
return create(
ErrorLogger.class,
traits,
sink -> {
var printStackTrace = traits.contains(ErrorLoggerTrait.PRINT_STACK_TRACE_ALWAYS);
var printCommandOutput = traits.contains(ErrorLoggerTrait.PRINT_FAILED_COMMAND_OUTPUT);
return ErrorLogger.create(sink, printStackTrace, printCommandOutput);
},
ErrorLogger::create).orElse(ErrorLogger.DISCARDING_LOGGER);
}
static TraceLogger createTraceLogger(Collection<LoggerTrait> traits) {
return create(
TraceLogger.class,
traits,
TraceLogger::create,
TraceLogger::create).orElse(TraceLogger.DISCARDING_LOGGER);
}
static ResourceLogger createResourceLogger(Collection<LoggerTrait> traits) {
return create(
ResourceLogger.class,
traits,
ResourceLogger::create,
ResourceLogger::create).orElse(ResourceLogger.DISCARDING_LOGGER);
}
static SummaryLogger createSummaryLogger(Collection<LoggerTrait> traits) {
return create(SummaryLogger.class,
traits,
sink -> {
var printInfo = traits.contains(SummaryLoggerTrait.PRINT_INFO);
var printWarnings = traits.contains(SummaryLoggerTrait.PRINT_WARNINGS);
return SummaryLogger.create(sink, printInfo, printWarnings);
},
SummaryLogger::create).orElse(SummaryLogger.DISCARDING_LOGGER);
}
static Optional<ConsoleLogger> createConsoleLoggerSink(Collection<LoggerTrait> traits) {
var out = filterTraitsOfType(ConsoleLoggerOutSink.class, traits.stream())
.map(ConsoleLoggerOutSink::sink).findFirst();
var err = filterTraitsOfType(ConsoleLoggerErrSink.class, traits.stream())
.map(ConsoleLoggerErrSink::sink).findFirst();
var timestampClock = filterTraitsOfType(ConsoleLoggerTimestampClock.class, traits.stream())
.map(ConsoleLoggerTimestampClock::clock)
.findFirst().orElseGet(new ConsoleLogger(Utils.DISCARDER, Utils.DISCARDER)::timestampClock);
if (out.isEmpty() && err.isEmpty()) {
return Optional.empty();
} else {
return Optional.of(new ConsoleLogger(toStringConsumer(out), toStringConsumer(err), timestampClock));
}
}
static Optional<System.Logger> createSystemLoggerSink(Collection<LoggerTrait> traits) {
return filterTraitsOfType(SystemLoggerTrait.class, traits.stream()).map(SystemLoggerTrait::logger).findFirst();
}
private static <T extends Logger> Optional<T> create(
Class<T> loggerType,
Collection<LoggerTrait> traits,
Function<ConsoleLogger, T> fromConsoleSinkCtor,
Function<System.Logger, T> fromSystemLoggerSinkCtor) {
Objects.requireNonNull(traits);
Objects.requireNonNull(fromSystemLoggerSinkCtor);
Objects.requireNonNull(fromConsoleSinkCtor);
var consoleLogger = createConsoleLoggerSink(traits).map(fromConsoleSinkCtor);
var systemLogger = createSystemLoggerSink(traits).map(fromSystemLoggerSinkCtor);
if (consoleLogger.isEmpty() && systemLogger.isEmpty()) {
return Optional.empty();
} else {
return Optional.of(Utils.teeLogger(loggerType, Stream.of(
consoleLogger.stream(),
systemLogger.stream()
).flatMap(x -> x).toList()));
}
}
private static <T extends LoggerTrait> Stream<? extends T> filterTraitsOfType(
Class<? extends T> type, Stream<? extends LoggerTrait> stream) {
Objects.requireNonNull(type);
return stream.filter(type::isInstance).map(type::cast);
}
private static Consumer<String> toStringConsumer(Optional<PrintWriter> pw) {
return pw.<Consumer<String>>map(v -> {
return v::println;
}).orElse(Utils.DISCARDER);
}
private LogEnvironment() {
}
}

View File

@ -0,0 +1,32 @@
/*
* 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. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* 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.internal.log;
public interface Logger {
default boolean enabled() {
return true;
}
}

View File

@ -0,0 +1,63 @@
/*
* 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. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* 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.internal.log;
import java.util.Collection;
import java.util.Objects;
import java.util.function.Function;
import jdk.jpackage.internal.cli.OptionValue;
public enum LoggerRole {
COMMAND_LOGGER(StandardLogger.COMMAND_LOGGER, LogEnvironment::createCommandLogger),
ERROR_LOGGER(StandardLogger.ERROR_LOGGER, LogEnvironment::createErrorLogger),
PROGRESS_LOGGER(StandardLogger.PROGRESS_LOGGER, LogEnvironment::createProgressLogger),
RESOURCE_LOGGER(StandardLogger.RESOURCE_LOGGER, LogEnvironment::createResourceLogger),
SUMMARY_LOGGER(StandardLogger.SUMMARY_LOGGER, LogEnvironment::createSummaryLogger),
TRACE_LOGGER(StandardLogger.TRACE_LOGGER, LogEnvironment::createTraceLogger),
;
LoggerRole(OptionValue<? extends Logger> logger, Function<Collection<LoggerTrait>, ? extends Logger> loggerCtor) {
this.logger = Objects.requireNonNull(logger);
this.loggerCtor = Objects.requireNonNull(loggerCtor);
}
public OptionValue<? extends Logger> logger() {
return logger;
}
Logger createLogger(Collection<LoggerTrait> traits) {
return loggerCtor.apply(traits);
}
private final OptionValue<? extends Logger> logger;
private final Function<Collection<LoggerTrait>, ? extends Logger> loggerCtor;
}

View File

@ -0,0 +1,28 @@
/*
* 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. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* 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.internal.log;
interface LoggerTrait {
}

View File

@ -0,0 +1,126 @@
/*
* 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. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* 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.internal.log;
import java.util.Objects;
public interface ProgressLogger extends Logger {
void progress(String localizedMsg);
void progressWarning(Exception cause);
void progressWarning(Exception cause, String localizedMsg);
void progressWarning(String localizedMsg);
enum ProgressLoggerTrait implements LoggerTrait {
PRINT_INFO,
PRINT_WARNINGS,
}
static ProgressLogger create(ConsoleLogger sink, boolean printInfo, boolean printWarnings) {
if (!printInfo) {
sink = sink.discardOut();
}
if (!printWarnings) {
sink = sink.discardErr();
}
return new Details.Console(sink.addTimestampsToOut());
}
static ProgressLogger create(System.Logger sink) {
return new Details.SystemLogger(sink);
}
static final class Details {
private Details() {
}
private record Console(ConsoleLogger sink) implements ProgressLogger {
Console {
Objects.requireNonNull(sink);
}
@Override
public void progress(String localizedMsg) {
Objects.requireNonNull(localizedMsg);
sink.out().accept(localizedMsg);
}
@Override
public void progressWarning(Exception cause) {
Objects.requireNonNull(cause);
sink.err().accept(I18N.format("progress.warning-header", Utils.toString(cause)));
}
@Override
public void progressWarning(Exception cause, String localizedMsg) {
Objects.requireNonNull(cause);
Objects.requireNonNull(localizedMsg);
sink.err().accept(I18N.format("progress.warning-header2", localizedMsg, Utils.toString(cause)));
}
@Override
public void progressWarning(String localizedMsg) {
Objects.requireNonNull(localizedMsg);
sink.err().accept(I18N.format("progress.warning-header", localizedMsg));
}
}
private record SystemLogger(System.Logger sink) implements ProgressLogger {
SystemLogger {
Objects.requireNonNull(sink);
}
@Override
public void progress(String localizedMsg) {
sink.log(System.Logger.Level.INFO, localizedMsg);
}
@Override
public void progressWarning(Exception cause) {
sink.log(System.Logger.Level.WARNING, "Ignore error", cause);
}
@Override
public void progressWarning(Exception cause, String localizedMsg) {
sink.log(System.Logger.Level.WARNING, localizedMsg, cause);
}
@Override
public void progressWarning(String localizedMsg) {
sink.log(System.Logger.Level.WARNING, localizedMsg);
}
}
}
static final ProgressLogger DISCARDING_LOGGER = Utils.discardingLogger(ProgressLogger.class);
}

View File

@ -0,0 +1,63 @@
/*
* 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. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* 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.internal.log;
import java.util.Objects;
import java.util.function.Consumer;
public interface ResourceLogger extends Logger {
void useResource(String localizedMsg);
static ResourceLogger create(ConsoleLogger sink) {
return new Details.DefaultLogger(sink.addTimestampsToOut().out());
}
static ResourceLogger create(System.Logger sink) {
return new Details.DefaultLogger(Utils.toStringConsumer(sink, System.Logger.Level.INFO));
}
static final class Details {
private Details() {
}
private record DefaultLogger(Consumer<String> sink) implements ResourceLogger {
DefaultLogger {
Objects.requireNonNull(sink);
}
@Override
public void useResource(String localizedMsg) {
Objects.requireNonNull(localizedMsg);
sink.accept(localizedMsg);
}
}
}
static final ResourceLogger DISCARDING_LOGGER = Utils.discardingLogger(ResourceLogger.class);
}

View File

@ -0,0 +1,52 @@
/*
* 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. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* 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.internal.log;
import java.util.Objects;
import jdk.jpackage.internal.cli.OptionValue;
public final class StandardLogger {
private StandardLogger() {
}
public static final OptionValue<CommandLogger> COMMAND_LOGGER = create(CommandLogger.DISCARDING_LOGGER);
public static final OptionValue<ErrorLogger> ERROR_LOGGER = create(ErrorLogger.DISCARDING_LOGGER);
public static final OptionValue<ProgressLogger> PROGRESS_LOGGER = create(ProgressLogger.DISCARDING_LOGGER);
public static final OptionValue<ResourceLogger> RESOURCE_LOGGER = create(ResourceLogger.DISCARDING_LOGGER);
public static final OptionValue<SummaryLogger> SUMMARY_LOGGER = create(SummaryLogger.DISCARDING_LOGGER);
public static final OptionValue<TraceLogger> TRACE_LOGGER = create(TraceLogger.DISCARDING_LOGGER);
private static <T extends Logger> OptionValue<T> create(T defaultValue) {
Objects.requireNonNull(defaultValue);
return OptionValue.<T>build().defaultValue(defaultValue).create();
}
}

View File

@ -0,0 +1,77 @@
/*
* 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. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* 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.internal.log;
import java.util.Objects;
import java.util.function.Consumer;
import jdk.jpackage.internal.summary.Summary;
public interface SummaryLogger extends Logger {
void summary(Summary summary);
enum SummaryLoggerTrait implements LoggerTrait {
PRINT_INFO,
PRINT_WARNINGS,
}
static SummaryLogger create(ConsoleLogger sink, boolean printInfo, boolean printWarnings) {
if (!printInfo) {
sink = sink.discardOut();
}
if (!printWarnings) {
sink = sink.discardErr();
}
return new Details.DefaultLogger(sink.out(), sink.err());
}
static SummaryLogger create(System.Logger sink) {
return new Details.DefaultLogger(
Utils.toStringConsumer(sink, System.Logger.Level.INFO),
Utils.toStringConsumer(sink, System.Logger.Level.WARNING));
}
static final class Details {
private Details() {
}
private record DefaultLogger(Consumer<String> sinkInfo, Consumer<String> sinkWarnings) implements SummaryLogger {
DefaultLogger {
Objects.requireNonNull(sinkInfo);
Objects.requireNonNull(sinkWarnings);
}
@Override
public void summary(Summary summary) {
summary.print(sinkInfo, sinkWarnings);
}
}
}
static final SummaryLogger DISCARDING_LOGGER = Utils.discardingLogger(SummaryLogger.class);
}

View File

@ -0,0 +1,34 @@
/*
* 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. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* 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.internal.log;
import java.util.Objects;
record SystemLoggerTrait(System.Logger logger) implements LoggerTrait {
SystemLoggerTrait {
Objects.requireNonNull(logger);
}
}

View File

@ -0,0 +1,128 @@
/*
* 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. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* 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.internal.log;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Objects;
public interface TraceLogger extends Logger {
void trace(String msg);
default void trace(String format, Object... args) {
if (enabled()) {
trace(String.format(format, args));
}
}
void trace(Throwable t, String msg);
default void trace(Throwable t, String format, Object... args) {
if (enabled()) {
trace(t, String.format(format, args));
}
}
void trace(Throwable t);
static TraceLogger create(ConsoleLogger sink) {
return new Details.Console(sink.addTimestampsToOut().addTimestampsToErr());
}
static TraceLogger create(System.Logger sink) {
return new Details.SystemLogger(sink);
}
static final class Details {
private Details() {
}
private record Console(ConsoleLogger sink) implements TraceLogger {
Console {
Objects.requireNonNull(sink);
}
@Override
public void trace(String msg) {
Objects.requireNonNull(msg);
sink.out().accept(MSG_PREFIX + msg);
}
@Override
public void trace(Throwable t, String msg) {
Objects.requireNonNull(t);
Objects.requireNonNull(msg);
var buf = new StringWriter();
buf.write(MSG_PREFIX);
buf.write(msg);
buf.write(": ");
t.printStackTrace(new PrintWriter(buf));
Utils.writeWithoutTrailingLineSeparator(buf, sink.err());
}
@Override
public void trace(Throwable t) {
Objects.requireNonNull(t);
var buf = new StringWriter();
buf.write(MSG_PREFIX);
t.printStackTrace(new PrintWriter(buf));
Utils.writeWithoutTrailingLineSeparator(buf, sink.err());
}
private static final String MSG_PREFIX = "TRACE: ";
}
private record SystemLogger(System.Logger sink) implements TraceLogger {
SystemLogger {
Objects.requireNonNull(sink);
}
@Override
public void trace(String msg) {
sink.log(System.Logger.Level.TRACE, msg);
}
@Override
public void trace(Throwable t, String msg) {
sink.log(System.Logger.Level.ERROR, msg, t);
}
@Override
public void trace(Throwable t) {
sink.log(System.Logger.Level.ERROR, "Ignored error", t);
}
}
}
static final TraceLogger DISCARDING_LOGGER = Utils.discardingLogger(TraceLogger.class);
}

View File

@ -0,0 +1,163 @@
/*
* 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. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* 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.internal.log;
import java.io.StringWriter;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import jdk.jpackage.internal.model.SelfContainedException;
import jdk.jpackage.internal.util.function.ExceptionBox;
final class Utils {
private Utils() {
}
@SuppressWarnings("unchecked")
static <T extends Logger> T discardingLogger(Class<T> type) {
return (T)Proxy.newProxyInstance(Utils.class.getClassLoader(), new Class<?>[]{type}, new LoggerHandler<>(type, false) {
@Override
protected void invokeLoggerMethod(Object proxy, Method method, Object[] args) {
throw ExceptionBox.reachedUnreachable();
}
});
}
@SuppressWarnings("unchecked")
static <T extends Logger> T teeLogger(Class<T> type, List<? extends T> loggers) {
var enabledLoggers = loggers.stream().filter(Logger::enabled).toList();
if (enabledLoggers.isEmpty()) {
return discardingLogger(type);
} else if (enabledLoggers.size() == 1) {
return enabledLoggers.getFirst();
}
return (T)Proxy.newProxyInstance(Utils.class.getClassLoader(), new Class<?>[]{type}, new LoggerHandler<>(type, true) {
@Override
protected void invokeLoggerMethod(Object proxy, Method method, Object[] args) throws Throwable {
for (var logger : enabledLoggers) {
method.invoke(logger, args);
}
}
});
}
static Consumer<String> toStringConsumer(System.Logger logger, System.Logger.Level level) {
Objects.requireNonNull(logger);
Objects.requireNonNull(level);
return str -> {
logger.log(level, str);
};
}
static boolean isSelfContained(Throwable t) {
return t.getClass().getAnnotation(SelfContainedException.class) != null;
}
static String toString(Throwable t) {
if (isSelfContained(t)) {
return t.getMessage();
} else {
return t.toString();
}
}
static void writeWithoutTrailingLineSeparator(StringWriter writer, Consumer<String> sink) {
var buf = writer.getBuffer();
var lineSeparator = System.lineSeparator();
if (buf.length() >= lineSeparator.length()) {
var tailChars = new char[lineSeparator.length()];
buf.getChars(buf.length() - tailChars.length, buf.length(), tailChars, 0);
if (Arrays.equals(tailChars, lineSeparator.toCharArray())) {
buf.setLength(buf.length() - tailChars.length);
}
}
sink.accept(buf.toString());
}
private abstract static class LoggerHandler<T extends Logger> implements InvocationHandler {
protected LoggerHandler(Class<T> loggerType, boolean loggerEnabled) {
Objects.requireNonNull(loggerType);
if (!loggerType.isInterface() ) {
throw new IllegalArgumentException(String.format("%s is not an interface", loggerType));
}
loggerMethods = unfoldInterface(loggerType).flatMap(interfaceType -> {
return Stream.of(interfaceType.getMethods());
}).collect(Collectors.toSet());
this.loggerEnabled = loggerEnabled;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (loggerMethods.contains(method)) {
var returnType = method.getReturnType();
if (method.getName().equals("enabled") && returnType.equals(boolean.class) && method.getParameterCount() == 0) {
return loggerEnabled;
} else if (returnType.equals(void.class)) {
if (loggerEnabled) {
invokeLoggerMethod(proxy, method, args);
}
return null;
} else {
throw new AssertionError(String.format("Don't know how to handle %s", method));
}
} else {
// Presumably this is java.lang.Objects's method. Redirect it to this instance.
return method.invoke(this, args);
}
}
protected abstract void invokeLoggerMethod(Object proxy, Method method, Object[] args) throws Throwable;
private final Set<Method> loggerMethods;
private final boolean loggerEnabled;
}
private static Stream<Class<?>> unfoldInterface(Class<?> interfaceType) {
return Stream.concat(
Stream.of(interfaceType),
Stream.of(interfaceType.getInterfaces()
).flatMap(Utils::unfoldInterface));
}
static final Consumer<String> DISCARDER = _ -> {};
}

View File

@ -43,9 +43,11 @@ public enum StandardPackageType implements PackageType {
}
/**
* Gets file extension of this package type.
* E.g.: <code>.msi</code>, <code>.dmg</code>, <code>.deb</code>.
* @return file extension of this package type
* Gets file extension corresponding to the package type. E.g.:
* <code>.msi</code>, <code>.dmg</code>, <code>.deb</code>.
*
* @return file extension corresponding to the package type; the value starts
* with the period (.) character.
*/
public String suffix() {
return suffix;

View File

@ -388,7 +388,34 @@ help.option.vendor=\
\ Vendor of the application
help.option.verbose=\
\ Enables verbose output
\ Configures verbose output. Where "category" is one of\n\
\ "all"\n\
\ "console"\n\
\ "log"\n\
\ "errors"\n\
\ "progress"\n\
\ "resources"\n\
\ "summary"\n\
\ "tools"\n\
\ "trace"\n\
\ "warnings"\n\
\n\
\ Suppress all console output, enable logging via System.Logger API:\n\
\ --verbose log\n\
\ Enable all message categories in the console:\n\
\ --verbose console\n\
\ Enable all message categories, but "trace" and "tools" in the console:\n\
\ --verbose console,-trace,-tools\n\
\ Enable "trace" and "tools" message categories in the console:\n\
\ --verbose trace,tools\n\
\ Enable "trace" and "tools" message categories in the console and\n\
\ enable logging via System.Logger API:\n\
\ --verbose log,trace,tools\n\
\n\
\ If the option is specified without the value, it is equivalent to\n\
\ --verbose console,-trace\n\
\ If the option is not specified it is equivalent to\n\
\ --verbose errors,warnings\n\
help.option.version=\
\ Print the product version to the output stream and exit.

View File

@ -37,6 +37,16 @@ bundle-type.linux-app=Linux Application Image
bundle-type.linux-deb=Linux DEB Package
bundle-type.linux-rpm=Linux RPM Package
summary.property.operation=Operation
summary.property.operation.format=Create {0}
summary.property.output-bundle=Output bundle
summary.property.version=Version
summary.warning=WARNING: {0}
summary.multi-line-warning=WARNING: {0}:
summary.value.disabled=Disabled
summary.value.enabled=Enabled
resource.post-app-image-script=script to run after application image is populated
message.using-default-resource=Using default package resource {0} {1} (add {2} to the resource-dir to customize).
@ -59,6 +69,9 @@ message.error-header=Error: {0}
message.advice-header=Advice to fix: {0}
message.failed-command-output-header=Command output:
progress.warning-header=WARNING: {0}
progress.warning-header2=WARNING: {0}: {1}
error.command-failed-unexpected-output=Unexpected output from executing the command {0}
error.command-failed-unexpected-exit-code=Unexpected exit code {0} from executing the command {1}
error.command-failed-timed-out=Timed-out command {0}
@ -91,6 +104,7 @@ error.parameter-not-mac-bundle=The value "{0}" provided for parameter {1} is not
error.parameter-not-mac-bundle-identifier=The value "{0}" provided for parameter {1} is not a valid macOS bundle identifier.
error.parameter-not-mac-bundle-identifier.advice=Bundle identifier must be a non-empty string containing only alphanumeric characters (A-Z, a-z, and 0-9), hyphens (-), and periods (.)
error.path-parameter-ioexception=I/O error accessing path value "{0}" of parameter {1}
error.parameter-invalid-value=Invalid value "{0}" provided for parameter {1}
error.parameter-add-launcher-malformed=The value "{0}" provided for parameter {1} does not match the pattern <name>=<file path>
error.parameter-add-launcher-not-file=The value of path to a property file "{0}" provided for additional launcher "{1}" is not a valid file path
error.properties-parameter-not-path=The value "{0}" provided for property "{1}" in "{2}" file is not a valid path
@ -113,8 +127,8 @@ error.tool-not-found.advice=Please install "{0}"
error.tool-old-version=Can not find "{0}" {1} or newer
error.tool-old-version.advice=Please install "{0}" {1} or newer
warning.tempdir.cleanup-failed=Warning: Failed to clean-up temporary directory {0}
warning.tempdir.cleanup-file-failed=Warning: Failed to delete "{0}" file in the temporary directory
warning.tempdir.cleanup-failed=Failed to clean-up temporary directory {0}
warning.tempdir.cleanup-file-failed=Failed to delete "{0}" file in the temporary directory
error.output-bundle-cannot-be-overwritten=Output package file "{0}" exists and can not be removed.

View File

@ -0,0 +1,59 @@
/*
* 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. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* 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.internal.summary;
import java.util.List;
import java.util.Map;
import jdk.internal.util.OperatingSystem;
import jdk.jpackage.internal.util.MultiResourceBundle;
import jdk.jpackage.internal.util.StringBundle;
final class I18N {
private I18N() {
}
static String getString(String key) {
return BUNDLE.getString(key);
}
static String format(String key, Object ... args) {
return BUNDLE.format(key, args);
}
private static final StringBundle BUNDLE;
static {
var prefix = "jdk.jpackage.internal.resources.";
BUNDLE = StringBundle.fromResourceBundle(MultiResourceBundle.create(
prefix + "MainResources",
Map.of(
OperatingSystem.LINUX, List.of(prefix + "LinuxResources"),
OperatingSystem.MACOS, List.of(prefix + "MacResources"),
OperatingSystem.WINDOWS, List.of(prefix + "WinResources")
)
));
}
}

View File

@ -0,0 +1,33 @@
/*
* 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. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* 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.internal.summary;
/**
* Property in summary.
*/
public non-sealed interface Property extends SummaryItem {
String formatLabel();
}

View File

@ -0,0 +1,92 @@
/*
* 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. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* 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.internal.summary;
import java.util.Objects;
import java.util.Optional;
/**
* Property for summary.
*/
public enum StandardProperty implements Property {
//
// Keep items in the order they should be printed in the summary.
//
OPERATION("summary.property.operation", "summary.property.operation.format"),
MAC_SIGN_APP_IMAGE_OPERATION("summary.property.operation", "summary.property.mac-sign-app-image.format"),
OUTPUT_BUNDLE("summary.property.output-bundle"),
LINUX_PACKAGE_NAME("summary.property.linux-package-name"),
WIN_MSI_PRODUCT_CODE("summary.property.win-product-code"),
WIN_MSI_UPGRADE_CODE("summary.property.win-upgrade-code"),
MAC_BUNDLE_IDENTIFIER("summary.property.mac-bundle-identifier"),
MAC_BUNDLE_NAME("summary.property.mac-bundle-name"),
VERSION("summary.property.version"),
WIN_WIX_VERSION("summary.property.win-wix-version"),
LINUX_DISABLE_REQUIRED_PACKAGES_SEARCH("summary.property.linux-required-packages-search", "summary.value.disabled"),
;
StandardProperty(String label, Optional<String> valueFormat) {
this.label = Objects.requireNonNull(label);
this.valueFormat = Objects.requireNonNull(valueFormat);
}
StandardProperty(String label, String valueFormatter) {
this(label, Optional.of(valueFormatter));
}
StandardProperty(String label) {
this(label, Optional.empty());
}
String label() {
return label;
}
@Override
public Optional<String> valueFormat() {
return valueFormat;
}
@Override
public String formatLabel() {
return I18N.getString(label);
}
private final String label;
private final Optional<String> valueFormat;
}

View File

@ -0,0 +1,60 @@
/*
* 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. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* 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.internal.summary;
import java.util.Optional;
/**
* Warning in summary.
*/
public enum StandardWarning implements Warning {
//
// Keep items in the order they should be printed in the summary.
//
MAC_SIGNED_PREDEFINED_APP_IMAGE_WITHOUT_PACKAGE_FILE("warning.per.user.app.image.signed"),
MAC_SIGNED_PKG_WITH_UNSIGNED_PREDEFINED_APP_IMAGE("warning.unsigned.app.image"),
MAC_NON_STANDARD_APP_CONTENT("warning.non-standard-app-content"),
MAC_BUNDLE_NAME_TOO_LONG("warning.bundle-name-too-long-warning"),
LINUX_DEB_MISSING_LICENSE_FILE("message.debs-like-licenses"),
;
StandardWarning(String valueFormat) {
this.valueFormat = Optional.of(valueFormat);
}
@Override
public Optional<String> valueFormat() {
return valueFormat;
}
private final Optional<String> valueFormat;
}

View File

@ -0,0 +1,180 @@
/*
* 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. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* 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.internal.summary;
import static jdk.jpackage.internal.util.IdentityWrapper.wrapIdentity;
import java.nio.file.Path;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import jdk.jpackage.internal.cli.StandardBundlingOperation;
import jdk.jpackage.internal.model.Application;
import jdk.jpackage.internal.model.BundleSpec;
import jdk.jpackage.internal.model.Package;
import jdk.jpackage.internal.util.IdentityWrapper;
import jdk.jpackage.internal.util.PathUtils;
/**
* Summary of the operation jpackage will perform.
*/
public final class Summary implements SummaryAccumulator {
public void print(Consumer<String> sinkInfo, Consumer<String> sinkWarnings) {
// Properties
properties.entrySet().stream().sorted(comparator()).map(e -> {
return String.format("%s: %s", e.getKey().value().formatLabel(), e.getValue());
}).forEach(sinkInfo);
// Warnings
warnings.entrySet().stream().sorted(comparator()).map(e -> {
switch (e.getValue()) {
case SingleLineContent c -> {
return I18N.format("summary.warning", c.str());
}
case MultiLineContent c -> {
return Stream.concat(
Stream.of(I18N.format("summary.multi-line-warning", c.header())),
StreamSupport.stream(c.items().spliterator(), false).map(msg -> {
// Add indentation.
return " " + msg;
})
).collect(Collectors.joining("\n"));
}
}
}).forEach(sinkWarnings);
}
@Override
public void putIfAbsent(SummaryItem k, String value) {
Objects.requireNonNull(value);
switch (k) {
case Property prop -> {
properties.putIfAbsent(wrapIdentity(prop), value);
}
case Warning warn -> {
warnings.putIfAbsent(wrapIdentity(warn), new SingleLineContent(value));
}
}
}
@Override
public void put(SummaryItem k, String value) {
Objects.requireNonNull(value);
switch (k) {
case Property prop -> {
properties.put(wrapIdentity(prop), value);
}
case Warning warn -> {
warnings.put(wrapIdentity(warn), new SingleLineContent(value));
}
}
}
@Override
public void putMultiValue(Warning k, String header, Iterable<String> items) {
if (!items.iterator().hasNext()) {
throw new IllegalArgumentException();
}
warnings.put(wrapIdentity(k), new MultiLineContent(header, items));
}
public Summary putStandardPropertiesIfAbsent(StandardBundlingOperation op, Path outputDir, BundleSpec bundle) {
Objects.requireNonNull(op);
Objects.requireNonNull(outputDir);
Objects.requireNonNull(bundle);
if (op != StandardBundlingOperation.SIGN_MAC_APP_IMAGE) {
putIfAbsent(StandardProperty.OPERATION, (Object)op.bundleType().label());
putIfAbsent(StandardProperty.OUTPUT_BUNDLE, PathUtils.normalizedAbsolutePath(outputDir.resolve(outputBundleName(bundle))));
}
putIfAbsent(StandardProperty.VERSION, version(bundle));
return this;
}
private static Path outputBundleName(BundleSpec bundle) {
return getProperty(bundle, Application::appImageDirName, pkg -> {
return Path.of(pkg.packageFileNameWithSuffix());
});
}
private static String version(BundleSpec bundle) {
return getProperty(bundle, Application::version, Package::version);
}
private static <T> T getProperty(BundleSpec bundle,
Function<Application, T> appPropertyGetter,
Function<Package, T> pkgPropertyGetter) {
Objects.requireNonNull(appPropertyGetter);
Objects.requireNonNull(pkgPropertyGetter);
switch (bundle) {
case Application app -> {
return appPropertyGetter.apply(app);
}
case Package pkg -> {
return pkgPropertyGetter.apply(pkg);
}
}
}
private static <T extends SummaryItem> Comparator<Map.Entry<IdentityWrapper<T>, ?>> comparator() {
return Comparator.comparing(
e -> {
return e.getKey().value();
},
Comparator.comparingInt(SummaryItem::ordinal)
);
}
private sealed interface Content {
}
record SingleLineContent(String str) implements Content {
SingleLineContent {
Objects.requireNonNull(str);
}
}
record MultiLineContent(String header, Iterable<String> items) implements Content {
MultiLineContent {
Objects.requireNonNull(header);
Objects.requireNonNull(items);
}
}
private final Map<IdentityWrapper<Property>, String> properties = new HashMap<>();
private final Map<IdentityWrapper<Warning>, Content> warnings = new HashMap<>();
}

View File

@ -0,0 +1,49 @@
/*
* 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. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* 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.internal.summary;
/**
* Accumulator of items for summary.
*/
public interface SummaryAccumulator {
default void putIfAbsent(SummaryItem k, Object... valueFormatArgs) {
putIfAbsent(k, k.formatValue(valueFormatArgs));
}
default void put(SummaryItem k, Object... valueFormatArgs) {
put(k, k.formatValue(valueFormatArgs));
}
default void putMultiValue(Warning k, Iterable<String> items, Object... valueFormatArgs) {
putMultiValue(k, k.formatValue(valueFormatArgs), items);
}
void put(SummaryItem k, String value);
void putIfAbsent(SummaryItem k, String value);
void putMultiValue(Warning k, String header, Iterable<String> items);
}

View File

@ -0,0 +1,45 @@
/*
* 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. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* 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.internal.summary;
import java.util.Optional;
public sealed interface SummaryItem permits Property, Warning {
int ordinal();
Optional<String> valueFormat();
default String formatValue(Object... valueFormatArgs) {
return valueFormat().map(valueFormat -> {
return I18N.format(valueFormat, valueFormatArgs);
}).orElseGet(() -> {
if (valueFormatArgs.length != 1) {
throw new IllegalArgumentException();
}
return valueFormatArgs[0].toString();
});
}
}

View File

@ -0,0 +1,31 @@
/*
* 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. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* 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.internal.summary;
/**
* Warning in summary.
*/
public non-sealed interface Warning extends SummaryItem {
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2025, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -28,11 +28,18 @@ import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
public final class SetBuilder<T> {
public static <T> SetBuilder<T> build() {
return new SetBuilder<>();
@SafeVarargs
@SuppressWarnings("varargs")
public static <T> SetBuilder<T> build(T... v) {
return new SetBuilder<T>().add(v);
}
public static <T> SetBuilder<T> build(Collection<? extends T> v) {
return new SetBuilder<T>().add(v);
}
public SetBuilder<T> set(Collection<? extends T> v) {
@ -67,6 +74,11 @@ public final class SetBuilder<T> {
return remove(List.of(v));
}
public SetBuilder<T> mutate(Consumer<SetBuilder<T>> mutator) {
mutator.accept(this);
return this;
}
public SetBuilder<T> clear() {
values.clear();
return this;

View File

@ -121,9 +121,76 @@ The `jpackage` tool will take as input a Java application and a Java run-time im
: Vendor of the application
<a id="option-verbose">`--verbose`</a>
<a id="option-verbose">`--verbose` &lt;\[-\]key(,\[-\]key)*&gt;</a>
: Enables verbose output.
: Configures verbose output. Enables and/or disables log message categories using zero or more of the keys described below separated by commas.
The key `all` enables or disables all categories (respectively); other keys enable the corresponding category, or disable it if preceded by a hyphen (`-`).
Supported values for *key* are:
- `all`: Enables console output and routing of all message categories to the logging framework through the [System.Logger API](../../api/java.base/java/lang/System.Logger.html). Equivalent to `console,log`.
- `console`: Enables console output of all message categories. Equivalent to `all,-log`.
- `errors`: Enables output of fatal errors.
If the key is specified without the `trace` key, error messages will be written to the console without the corresponding exception stacktraces.
If the key is specified with the `trace` key, error messages will be written to the console with the corresponding exception stacktraces.
- `progress`: Enables output of progress messages.
- `resources`: Enables output of messages about the use of the configurable resources.
- `summary`: Enables output of the bundle properties and the versions of the tools being used.
- `tools`: Enables output of commands being executed.
If the key is specified without the `trace` key, the jpackage will print the command lines without the output produced by executing these command lines.
Only command lines that are relevant to package customization will be written to the console.
If the key is specified with the `trace` key, the jpackage will print all command lines and their output.
- `trace`: Enables output of stack traces of suppressed exceptions and details of the jpackage execution.
When combined with other keys, it enables additional information in messages from the corresponding message categories, as described above.
- `warning`: Enables output of warnings.
- `log`: Enables routing of all message categories to the logging framework through the [System.Logger API](../../api/java.base/java/lang/System.Logger.html).
The logger name will be "jdk.jpackage".
If the option is specified without the value, it is equivalent to:
```
--verbose console,-trace
```
If the option is not specified, it is equivalent to:
```
--verbose errors,warnings
```
Sample usage:
Suppress all console output, enable logging via the [System.Logger API](../../api/java.base/java/lang/System.Logger.html):
```
--verbose log
```
Enable all message categories in the console:
```
--verbose console
```
Enable all message categories, but "trace" and "tools" in the console:
```
--verbose console,-trace,-tools
```
Enable "trace" and "tools" message categories in the console:
```
--verbose trace,tools
```
Enable "trace" and "tools" message categories in the console and enable logging via the [System.Logger API](../../api/java.base/java/lang/System.Logger.html):
```
--verbose log,trace,tools
```
<a id="option-version">`--version`</a>

View File

@ -32,6 +32,7 @@ import static jdk.jpackage.internal.cli.StandardBundlingOperation.CREATE_WIN_MSI
import static jdk.jpackage.internal.util.MemoizingSupplier.runOnce;
import jdk.jpackage.internal.cli.Options;
import jdk.jpackage.internal.summary.StandardProperty;
public class WinBundlingEnvironment extends DefaultBundlingEnvironment {
@ -47,28 +48,27 @@ public class WinBundlingEnvironment extends DefaultBundlingEnvironment {
private static void createMsiPackage(Options options, WinSystemEnvironment sysEnv) {
addWixSummary(options, sysEnv);
createNativePackage(options,
WinFromOptions.createWinMsiPackage(options),
buildEnv()::create,
WinPackagingPipeline.build(),
(env, pkg, outputDir) -> {
traceWixToolset(sysEnv);
return new WinMsiPackager(env, pkg, outputDir, sysEnv);
});
}
private static void createExePackage(Options options, WinSystemEnvironment sysEnv) {
addWixSummary(options, sysEnv);
createNativePackage(options,
WinFromOptions.createWinExePackage(options),
buildEnv()::create,
WinPackagingPipeline.build(),
(env, pkg, outputDir) -> {
traceWixToolset(sysEnv);
final var msiOutputDir = env.buildRoot().resolve("msi");
var msiPackager = new WinMsiPackager(env, pkg.msiPackage(),
@ -90,14 +90,7 @@ public class WinBundlingEnvironment extends DefaultBundlingEnvironment {
return new BuildEnvFromOptions().predefinedAppImageLayout(APPLICATION_LAYOUT);
}
private static void traceWixToolset(WinSystemEnvironment sysEnv) {
final var wixToolset = sysEnv.wixToolset();
for (var tool : wixToolset.getType().getTools()) {
Log.verbose(I18N.format("message.tool-version",
wixToolset.getToolPath(tool).getFileName(),
wixToolset.getVersion()));
}
private static void addWixSummary(Options options, WinSystemEnvironment sysEnv) {
OptionUtils.summary(options).put(StandardProperty.WIN_WIX_VERSION, sysEnv.wixToolset().getVersion());
}
}

View File

@ -50,6 +50,7 @@ import jdk.jpackage.internal.model.WinExePackage;
import jdk.jpackage.internal.model.WinLauncher;
import jdk.jpackage.internal.model.WinLauncherMixin;
import jdk.jpackage.internal.model.WinMsiPackage;
import jdk.jpackage.internal.summary.StandardProperty;
final class WinFromOptions {
@ -104,7 +105,13 @@ final class WinFromOptions {
WIN_UPGRADE_UUID.ifPresentIn(options, pkgBuilder::upgradeCode);
return pkgBuilder.create();
var pkg = pkgBuilder.create();
var summary = OptionUtils.summary(options);
summary.put(StandardProperty.WIN_MSI_PRODUCT_CODE, pkg.productCode());
summary.put(StandardProperty.WIN_MSI_UPGRADE_CODE, pkg.upgradeCode());
return pkg;
}
static WinExePackage createWinExePackage(Options options) {

View File

@ -175,6 +175,7 @@ final class WinMsiPackager implements Consumer<PackagingPipeline.Builder> {
IOUtils.copyFile(licenseFile, destFile);
RtfConverter.createSimple(licenseFile).ifPresent(toConsumer(rtfConverter -> {
Log.trace("Convert a copy of the license file [%s] to RTF", licenseFile);
destFile.toFile().setWritable(true);
rtfConverter.convert(destFile);
}));
@ -187,7 +188,7 @@ final class WinMsiPackager implements Consumer<PackagingPipeline.Builder> {
final var msiOut = outputDir.resolve(pkg.packageFileNameWithSuffix());
Log.verbose(I18N.format("message.preparing-msi-config", msiOut.toAbsolutePath()));
Log.progress(I18N.format("message.preparing-msi-config", msiOut.toAbsolutePath()));
final var wixVars = createWixVars();
@ -249,7 +250,7 @@ final class WinMsiPackager implements Consumer<PackagingPipeline.Builder> {
.filter(custom -> primaryWxlFiles.stream().noneMatch(primary ->
primary.getFileName().toString().equalsIgnoreCase(
custom.getFileName().toString())))
.peek(custom -> Log.verbose(I18N.format(
.peek(custom -> Log.useResource(I18N.format(
"message.using-custom-resource", String.format("[%s]",
I18N.getString("resource.wxl-file")),
custom.getFileName()))).toList();
@ -317,9 +318,6 @@ final class WinMsiPackager implements Consumer<PackagingPipeline.Builder> {
wixVars.put("JpProductCode", pkg.productCode().toString());
wixVars.put("JpProductUpgradeCode", pkg.upgradeCode().toString());
Log.verbose(I18N.format("message.product-code", pkg.productCode()));
Log.verbose(I18N.format("message.upgrade-code", pkg.upgradeCode()));
wixVars.define("JpAllowUpgrades");
if (!pkg.isRuntimeInstaller()) {
wixVars.define("JpAllowDowngrades");

View File

@ -30,6 +30,7 @@ import static java.util.stream.Collectors.toSet;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.util.Comparator;
import java.util.List;
@ -125,6 +126,7 @@ public enum WixTool {
.collect(toSet());
if (sameVersionTools.equals(Set.of(Candle3)) || sameVersionTools.equals(Set.of(Light3))) {
// There is only one tool from WiX v3 toolset of some version available. Discard it.
Log.trace("Discard [%s]: incomplete", sameVersionLookupResults.getFirst().info());
return false;
} else {
return true;
@ -178,6 +180,11 @@ public enum WixTool {
});
});
Log.trace("Using %s WiX Toolkit v%s", toolset.getType(), toolset.getVersion());
toolset.getType().getTools().stream().sorted().forEach(tool -> {
Log.trace("%s: %s", tool, toolset.getToolPath(tool));
});
return toolset;
}
@ -199,6 +206,12 @@ public enum WixTool {
Objects.requireNonNull(tool);
Objects.requireNonNull(lookupDir);
lookupDir.ifPresentOrElse(theLookupDir -> {
Log.trace("Look up for %s in [%s] directory", tool.toolFileName, theLookupDir);
}, () -> {
Log.trace("Look up for %s in the PATH", tool.toolFileName);
});
final Path toolPath = lookupDir.map(p -> {
return p.resolve(tool.toolFileName);
}).orElse(tool.toolFileName);
@ -256,7 +269,7 @@ public enum WixTool {
// Detect FIPS mode
var fips = false;
try {
final var result = Executor.of(toolPath.toString(), "-?").setQuiet(true).saveOutput(true).execute();
final var result = Executor.of(toolPath.toString(), "-?").quiet().saveOutput(true).execute();
final var exitCode = result.getExitCode();
if (exitCode != 0 /* 308 */) {
final var output = result.getOutput();
@ -265,12 +278,18 @@ public enum WixTool {
}
}
} catch (IOException ex) {
Log.trace(ex, "Failed to execute [%s] command with '-?' option to detect FIPS mode. Assume FIPS=false", toolPath);
}
info = new DefaultCandleInfo(info, fips);
}
Log.trace("Found [%s]", info);
return Optional.of(new ToolLookupResult(tool, info));
} else {
if (parsedVersion.find().isPresent()) {
Log.trace("Discard [%s]: failed validation", new DefaultToolInfo(toolPath, parsedVersion.get()));
}
return Optional.empty();
}
}
@ -294,7 +313,14 @@ public enum WixTool {
private static Optional<Path> getEnvVariableAsPath(String envVar) {
Objects.requireNonNull(envVar);
return Optional.ofNullable(Globals.instance().system().getenv(envVar)).flatMap(PathUtils::asPath);
return Optional.ofNullable(Globals.instance().system().getenv(envVar)).map(v -> {
try {
return Path.of(v);
} catch (InvalidPathException ex) {
Log.trace(ex, "The value of environment variable '%s' [%s] is not a path", envVar, v);
return null;
}
});
}
private static List<Path> findWixCurrentInstallDirs() {
@ -318,6 +344,7 @@ public enum WixTool {
try (var paths = Files.walk(path, 1)) {
return paths.toList();
} catch (IOException ex) {
Log.trace(ex, "Can not get a listing of [%s] directory", path);
return List.<Path>of();
}
}).flatMap(List::stream)

View File

@ -36,6 +36,10 @@ resource.launcher-as-service-wix-file=Service installer WiX project file
resource.wix-src-conv=XSLT stylesheet converting WiX sources from WiX v3 to WiX v4 format
resource.installer-exe=installer executable
summary.property.win-product-code=MSI ProductCode
summary.property.win-upgrade-code=MSI UpgradeCode
summary.property.win-wix-version=WiX Toolkit version
error.no-wix-tools=No usable WiX Toolset installation found
error.no-wix-tools.advice=Install the latest WiX v3 from https://github.com/wixtoolset/wix3/releases or WiX v4+ from https://github.com/wixtoolset/wix/releases
error.version-string-wrong-format.advice=Set value of --app-version parameter to a valid Windows Installer ProductVersion.
@ -45,7 +49,6 @@ error.msi-product-version-build-out-of-range=Build part of version must be in th
error.msi-product-version-minor-out-of-range=Minor version must be in the range [0, 255]
error.version-swap=Failed to update version information for {0}
error.icon-swap=Failed to update icon for {0}
error.invalid-envvar=Invalid value of {0} environment variable
error.lock-resource=Failed to lock: {0}
error.unlock-resource=Failed to unlock: {0}
error.read-wix-l10n-file=Failed to parse {0} file
@ -56,6 +59,4 @@ error.missing-service-installer.advice=Add 'service-installer.exe' service insta
message.icon-not-ico=The specified icon "{0}" is not an ICO file and will not be used. The default icon will be used in it's place.
message.tool-version=Detected [{0}] version [{1}].
message.product-code=MSI ProductCode: {0}.
message.upgrade-code=MSI UpgradeCode: {0}.
message.preparing-msi-config=Preparing MSI config: {0}.

View File

@ -24,7 +24,9 @@ exclusiveAccess.dirs = \
modules = \
jdk.jpackage/jdk.jpackage.internal:+open \
jdk.jpackage/jdk.jpackage.internal.cli \
jdk.jpackage/jdk.jpackage.internal.log \
jdk.jpackage/jdk.jpackage.internal.model \
jdk.jpackage/jdk.jpackage.internal.summary \
jdk.jpackage/jdk.jpackage.internal.util \
jdk.jpackage/jdk.jpackage.internal.util.function \
jdk.jpackage/jdk.jpackage.internal.resources:+open \

View File

@ -73,15 +73,45 @@ public record CannedFormattedString(BiFunction<String, Object[], String> formatt
String format();
List<Object> modelArgs();
public enum Formatter {
JPACKAGE_MAIN_STRING_BUNDLE,
MESSAGE_FORMAT,
;
}
default Formatter formatter() {
return Formatter.JPACKAGE_MAIN_STRING_BUNDLE;
}
default CannedFormattedString asCannedFormattedString(Object ... args) {
if (args.length != modelArgs().size()) {
throw new IllegalArgumentException();
}
return JPackageStringBundle.MAIN.cannedFormattedString(format(), args);
var format = Objects.requireNonNull(format());
return switch (Objects.requireNonNull(formatter())) {
case JPACKAGE_MAIN_STRING_BUNDLE -> {
yield JPackageStringBundle.MAIN.cannedFormattedString(format, args);
}
case MESSAGE_FORMAT -> {
yield CannedFormattedString.createFromMessageFormat(format, args);
}
};
}
default Pattern asPattern() {
return JPackageStringBundle.MAIN.cannedFormattedStringAsPattern(format(), modelArgs().toArray());
var format = Objects.requireNonNull(format());
var args = Objects.requireNonNull(modelArgs()).toArray();
return switch (Objects.requireNonNull(formatter())) {
case JPACKAGE_MAIN_STRING_BUNDLE -> {
yield JPackageStringBundle.MAIN.cannedFormattedStringAsPattern(format, args);
}
case MESSAGE_FORMAT -> {
yield CannedMessageFormat.create(format, args).toPattern();
}
};
}
}

View File

@ -24,6 +24,7 @@
package jdk.jpackage.test;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.mapping;
import static java.util.stream.Collectors.toCollection;
import static java.util.stream.Collectors.toList;
@ -61,10 +62,12 @@ import java.util.spi.ToolProvider;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import jdk.jpackage.internal.cli.LogConfigParser;
import jdk.jpackage.internal.model.DottedVersion;
import jdk.jpackage.internal.util.MacBundle;
import jdk.jpackage.internal.util.Result;
import jdk.jpackage.internal.util.RuntimeReleaseFile;
import jdk.jpackage.internal.util.SetBuilder;
import jdk.jpackage.internal.util.function.ThrowingConsumer;
import jdk.jpackage.internal.util.function.ThrowingFunction;
import jdk.jpackage.internal.util.function.ThrowingRunnable;
@ -85,6 +88,7 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
verifyActions = new Actions();
excludeStandardAsserts(StandardAssert.MAIN_LAUNCHER_DESCRIPTION);
removeOldOutputBundle = true;
logConfig = new LogConfig();
}
private JPackageCommand(JPackageCommand cmd, boolean immutable) {
@ -104,6 +108,7 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
winMsiLogFile = cmd.winMsiLogFile;
unpackedPackageDirectory = cmd.unpackedPackageDirectory;
explicitVersion = cmd.explicitVersion;
logConfig = cmd.logConfig;
}
JPackageCommand createImmutableCopy() {
@ -994,6 +999,38 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
return this;
}
public JPackageCommand setEnabledMessageCategories(Set<MessageCategory> categories) {
verifyMutable();
logConfig = new LogConfig(
categories,
SetBuilder.build(logConfig.remove()).remove(categories).emptyAllowed(true).create());
return this;
}
public JPackageCommand setEnabledMessageCategories(MessageCategory... categories) {
return setEnabledMessageCategories(Set.of(categories));
}
public JPackageCommand setDisabledMessageCategories(Set<MessageCategory> categories) {
verifyMutable();
logConfig = new LogConfig(
SetBuilder.build(logConfig.add()).remove(categories).emptyAllowed(true).create(),
categories);
return this;
}
public JPackageCommand setDisabledMessageCategories(MessageCategory... categories) {
return setDisabledMessageCategories(Set.of(categories));
}
public static Set<MessageCategory> messageCategoriesConsoleAll() {
return Stream.of(MessageCategory.values()).filter(MessageCategory::isConsole).collect(toSet());
}
public static Set<MessageCategory> messageCategoriesConsoleNoTrace() {
return SetBuilder.build(messageCategoriesConsoleAll()).remove(MessageCategory.TRACE).create();
}
/**
* Configures this instance to optionally remove the existing output bundle
* before running the jpackage command.
@ -1070,6 +1107,30 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
return makeAdvice(JPackageStringBundle.MAIN.cannedFormattedString(key, args));
}
public static CannedFormattedString makeProgressWarning(CannedFormattedString v) {
return v.addPrefix("progress.warning-header");
}
public static CannedFormattedString makeProgressWarning(String key, Object ... args) {
return makeProgressWarning(JPackageStringBundle.MAIN.cannedFormattedString(key, args));
}
public static CannedFormattedString makeSummaryWarning(CannedFormattedString v) {
return v.addPrefix("summary.warning");
}
public static CannedFormattedString makeSummaryWarning(String key, Object ... args) {
return makeSummaryWarning(JPackageStringBundle.MAIN.cannedFormattedString(key, args));
}
public static CannedFormattedString makeSummaryMultiLineWarning(CannedFormattedString v) {
return v.addPrefix("summary.multi-line-warning");
}
public static CannedFormattedString makeSummaryMultiLineWarning(String key, Object ... args) {
return makeSummaryMultiLineWarning(JPackageStringBundle.MAIN.cannedFormattedString(key, args));
}
public String getValue(CannedFormattedString str) {
return new CannedFormattedString(str.formatter(), str.format(), str.args().stream().map(arg -> {
if (arg instanceof CannedArgument cannedArg) {
@ -1263,7 +1324,125 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
return this;
}
public static enum Macro {
public enum MessageCategory {
SUMMARY,
WARNINGS,
ERRORS,
PROGRESS,
TRACE,
RESOURCES,
TOOLS,
SYSTEM_LOGGER,
;
MessageCategory() {
// Ensure this item has a peer with the same name in LogConfigParser.MessageCategory enum.
LogConfigParser.MessageCategory.valueOf(name());
}
static Set<MessageCategory> parseVerboseOptionValue(String str) {
return LogConfigParser.tokenize(str).stream()
.map(Enum::name)
.map(MessageCategory::valueOf)
.collect(toSet());
}
static String toVerboseOptionValue(Set<MessageCategory> categories) {
Objects.requireNonNull(categories);
String negativeRoot;
if (categories.contains(SYSTEM_LOGGER)) {
negativeRoot = "all";
} else {
negativeRoot = "console";
}
var str = categories.stream()
.map(Enum::name)
.map(String::toLowerCase)
.sorted()
.collect(joining(","));
var negateStr = Stream.concat(
Stream.of(negativeRoot),
messageCategoriesConsoleAll().stream()
.filter(Predicate.not(categories::contains))
.map(Enum::name)
.map(String::toLowerCase)
.map(v -> {
return "-" + v;
}).sorted()
).collect(joining(","));
if (str.length() < negateStr.length()) {
return str;
} else {
return negateStr;
}
}
boolean isConsole() {
switch (this) {
case SYSTEM_LOGGER -> {
return false;
}
default -> {
return true;
}
}
}
}
static {
Function<Class<? extends Enum<?>>, String[]> names = enumType -> {
return Stream.of(enumType.getEnumConstants()).map(Enum::name).toArray(String[]::new);
};
var missingEnumItems = SetBuilder.build(names.apply(LogConfigParser.MessageCategory.class))
.remove(names.apply(MessageCategory.class))
.emptyAllowed(true)
.create();
if (!missingEnumItems.isEmpty()) {
throw new AssertionError(
String.format("%s is missing items: %s", MessageCategory.class, missingEnumItems));
}
}
private record LogConfig(Set<MessageCategory> add, Set<MessageCategory> remove) {
LogConfig {
Objects.requireNonNull(add);
Objects.requireNonNull(remove);
var common = Comm.compare(add, remove).common();
if (!common.isEmpty()) {
throw new IllegalArgumentException(String.format("Overlap: %s", common));
}
add = Set.copyOf(add);
remove = Set.copyOf(remove);
}
LogConfig() {
this(Set.of(), Set.of());
}
Set<MessageCategory> filter(Set<MessageCategory> categories) {
Objects.requireNonNull(categories);
if (categories.containsAll(add) && Collections.disjoint(categories, remove)) {
return categories;
} else {
return SetBuilder.build(categories).add(add).remove(remove).emptyAllowed(true).create();
}
}
static Set<MessageCategory> quiet() {
return Set.of(MessageCategory.ERRORS, MessageCategory.WARNINGS);
}
}
public enum Macro {
APPDIR(cmd -> {
return cmd.appLayout().appDirectory().toString();
}),
@ -1818,8 +1997,19 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
addArguments("--runtime-image", defaultRuntime);
});
if (!hasArgument("--verbose") && TKit.verboseJPackage() && !ignoreDefaultVerbose) {
addArgument("--verbose");
if (!hasArgument("--verbose")) {
final Set<MessageCategory> unfilteredCategories;
if (ignoreDefaultVerbose) {
unfilteredCategories = LogConfig.quiet();
} else {
unfilteredCategories = DEFAULT_VERBOSE;
}
final var categories = logConfig.filter(unfilteredCategories);
if (!categories.equals(LogConfig.quiet())) {
setArgumentValue("--verbose", MessageCategory.toVerboseOptionValue(categories));
}
}
return this;
@ -2078,6 +2268,7 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
private Path winMsiLogFile;
private Path unpackedPackageDirectory;
private String explicitVersion;
private LogConfig logConfig;
private Set<ReadOnlyPathAssert> readOnlyPathAsserts = Set.of(ReadOnlyPathAssert.values());
private Set<StandardAssert> standardAsserts = Set.of(StandardAssert.values());
private List<Consumer<Executor.Result>> validators = new ArrayList<>();
@ -2097,6 +2288,10 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
// `--runtime-image` parameter set.
private static final Optional<Path> DEFAULT_RUNTIME_IMAGE = Optional.ofNullable(TKit.getConfigProperty("runtime-image")).map(Path::of);
private static final Set<MessageCategory> DEFAULT_VERBOSE = MessageCategory.parseVerboseOptionValue(
Optional.ofNullable(TKit.getConfigProperty("verbose")).orElse("console" /* Set max verbose level by default */)
);
public static final String DEFAULT_VERSION = "1.0";
// [HH:mm:ss.SSS]

View File

@ -1271,10 +1271,6 @@ public final class TKit {
return state().currentTest;
}
static boolean verboseJPackage() {
return state().verboseJPackage;
}
static boolean verboseTestSetup() {
return state().verboseTestSetup;
}
@ -1317,7 +1313,6 @@ public final class TKit {
Map<Object, Object> properties,
boolean trace,
boolean traceAsserts,
boolean verboseJPackage,
boolean verboseTestSetup) {
Objects.requireNonNull(os);
@ -1334,7 +1329,6 @@ public final class TKit {
this.trace = trace;
this.traceAsserts = traceAsserts;
this.verboseJPackage = verboseJPackage;
this.verboseTestSetup = verboseTestSetup;
}
@ -1379,12 +1373,10 @@ public final class TKit {
if (logOptions == null) {
trace = true;
traceAsserts = true;
verboseJPackage = true;
verboseTestSetup = true;
} else if (logOptions.contains("all")) {
trace = false;
traceAsserts = false;
verboseJPackage = false;
verboseTestSetup = false;
} else {
Predicate<Set<String>> isNonOf = options -> {
@ -1393,7 +1385,6 @@ public final class TKit {
trace = isNonOf.test(Set.of("trace", "t"));
traceAsserts = isNonOf.test(Set.of("assert", "a"));
verboseJPackage = isNonOf.test(Set.of("jpackage", "jp"));
verboseTestSetup = isNonOf.test(Set.of("init", "i"));
}
@ -1413,7 +1404,6 @@ public final class TKit {
trace = state.trace;
traceAsserts = state.traceAsserts;
verboseJPackage = state.verboseJPackage;
verboseTestSetup = state.verboseTestSetup;
return this;
@ -1462,7 +1452,6 @@ public final class TKit {
mutable ? new HashMap<>(properties) : Map.copyOf(properties),
trace,
traceAsserts,
verboseJPackage,
verboseTestSetup);
}
@ -1475,7 +1464,6 @@ public final class TKit {
private boolean trace;
private boolean traceAsserts;
private boolean verboseJPackage;
private boolean verboseTestSetup;
private boolean mutable = true;
@ -1492,7 +1480,6 @@ public final class TKit {
private final boolean trace;
private final boolean traceAsserts;
private final boolean verboseJPackage;
private final boolean verboseTestSetup;
}
}

View File

@ -43,6 +43,7 @@ import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Stream;
import jdk.jpackage.internal.model.DottedVersion;
import jdk.jpackage.internal.util.function.ThrowingRunnable;
import jdk.jpackage.test.PackageTest.PackageHandlers;
@ -241,21 +242,39 @@ public class WindowsHelper {
public enum WixType {
WIX3,
WIX4
WIX4,
;
/**
* Returns the file name of the WiX build tool outputting MSI files.
*
* @return the name of the WiX tool outputting MSI files
*/
public String buildTool() {
return switch (this) {
case WIX3 -> "light.exe";
case WIX4 -> "wix.exe";
};
}
}
public static WixType getWixTypeFromVerboseJPackageOutput(Executor.Result result) {
return result.getOutput().stream().map(str -> {
if (str.contains("[light.exe]")) {
var summaryWixVersion = JPackageStringBundle.MAIN.cannedFormattedString(
"summary.property.win-wix-version").getValue() + ": ";
return result.stdout().stream().filter(str -> {
return str.startsWith(summaryWixVersion);
}).findFirst().map(str -> {
var ver = str.substring(summaryWixVersion.length());
if (DottedVersion.compareComponents(DottedVersion.lazy(ver), DottedVersion.greedy("4.0")) < 0) {
return WixType.WIX3;
} else if (str.contains("[wix.exe]")) {
return WixType.WIX4;
} else {
return null;
return WixType.WIX4;
}
}).filter(Objects::nonNull).reduce((a, b) -> {
throw new IllegalArgumentException("Invalid input: multiple invocations of WiX tools");
}).orElseThrow(() -> new IllegalArgumentException("Invalid input: no invocations of WiX tools"));
}).orElseThrow(() -> {
return new IllegalArgumentException("Failed to detect WiX version. Likely, the input is missing the summary");
});
}
static Optional<Path> toShortPath(Path path) {

View File

@ -0,0 +1,71 @@
/*
* 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.stdmock;
public interface DebToolsMock extends EnvironmentMock {
String versionDpkg();
String versionFakeroot();
boolean withPackageLookup();
public static Builder build() {
return new Builder();
}
public static final class Builder {
public DebToolsMock create() {
return new DefaultDebToolsMock(versionDpkg, versionFakeroot, packageLookup);
}
public Builder env(DebToolsMock v) {
return versionDpkg(v.versionDpkg()).versionFakeroot(v.versionFakeroot()).withPackageLookup(v.withPackageLookup());
}
public Builder versionDpkg(String v) {
versionDpkg = v;
return this;
}
public Builder versionFakeroot(String v) {
versionFakeroot = v;
return this;
}
public Builder withPackageLookup(boolean v) {
if (v) {
packageLookup = LinuxPackageLookupMock.ENABLED;
} else {
packageLookup = LinuxPackageLookupMock.DISABLED;
}
return this;
}
private String versionDpkg;
private String versionFakeroot;
private LinuxPackageLookupMock packageLookup = LinuxPackageLookupMock.DISABLED;
}
}

View File

@ -0,0 +1,121 @@
/*
* 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.stdmock;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Stream;
import jdk.jpackage.test.LinuxHelper;
import jdk.jpackage.test.PackageType;
import jdk.jpackage.test.mock.CommandActionSpec;
import jdk.jpackage.test.mock.CommandActionSpecs;
import jdk.jpackage.test.mock.CommandMockSpec;
import jdk.jpackage.test.mock.Script;
record DefaultDebToolsMock(
String versionDpkg,
String versionFakeroot,
LinuxPackageLookupMock packageLookup) implements DebToolsMock {
DefaultDebToolsMock {
Objects.requireNonNull(versionDpkg);
Objects.requireNonNull(versionFakeroot);
if (versionDpkg.isBlank()) {
throw new IllegalArgumentException();
}
if (versionFakeroot.isBlank()) {
throw new IllegalArgumentException();
}
Objects.requireNonNull(packageLookup);
}
@Override
public List<CommandMockSpec> mocks() {
return Stream.of(dpkg(), dpkgdeb(), fakeroot()).map(action -> {
return new CommandMockSpec(action.description(), CommandActionSpecs.build().action(action).create());
}).toList();
}
@Override
public void applyTo(Script.Builder scriptBuilder) {
mocks().forEach(scriptBuilder::map);
packageLookup.applyTo(scriptBuilder);
}
@Override
public boolean withPackageLookup() {
return packageLookup == LinuxPackageLookupMock.ENABLED;
}
@Override
public String toString() {
return String.format("DEB Env %s; fakeroot=%s%s",
versionDpkg, versionFakeroot, withPackageLookup() ? "; ldd" : "");
}
private CommandActionSpec dpkg() {
var ver = versionDpkg("dpkg", versionDpkg);
return CommandActionSpec.create("dpkg", context -> {
if (context.args().contains("--version")) {
context.out().println(ver);
} else if (context.args().equals(List.of("-s", "coreutils"))) {
var out = context.out();
out.println("Package: coreutils");
out.println("Essential: yes");
out.println("Status: install ok installed");
} else if (context.args().equals(List.of("--print-architecture"))) {
context.out().println(LinuxHelper.getDefaultPackageArch(PackageType.LINUX_DEB));
}
return Optional.of(0);
});
}
private CommandActionSpec dpkgdeb() {
var ver = versionDpkg("dpkg-deb", versionDpkg);
return CommandActionSpec.create("dpkg-deb", context -> {
if (context.args().contains("--version")) {
context.out().println(versionDpkg("dpkg-deb", ver));
}
return Optional.of(0);
});
}
private CommandActionSpec fakeroot() {
var ver = String.format("fakeroot version %s", versionFakeroot);
return CommandActionSpec.create("fakeroot", context -> {
if (context.args().contains("--version")) {
context.out().println(ver);
}
return Optional.of(0);
});
}
private static String versionDpkg(String tool, String version) {
Objects.requireNonNull(tool);
Objects.requireNonNull(version);
return String.format("Debian '%s' package management program version %s (%s).",
tool, version, LinuxHelper.getDefaultPackageArch(PackageType.LINUX_DEB));
}
}

View File

@ -0,0 +1,55 @@
/*
* 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.stdmock;
import java.util.Collection;
import java.util.Optional;
import java.util.stream.Stream;
import jdk.jpackage.test.mock.CommandActionSpec;
import jdk.jpackage.test.mock.CommandActionSpecs;
import jdk.jpackage.test.mock.CommandMockSpec;
record DefaultMacToolsMock() implements MacToolsMock {
@Override
public Collection<CommandMockSpec> mocks() {
var setfile = CommandActionSpec.create("/Developer/Tools/SetFile", context -> {
if (context.args().contains("-h")) {
return Optional.of(0);
} else {
return Optional.of(1);
}
});
return Stream.of(setfile).map(action -> {
return new CommandMockSpec(action.description(), CommandActionSpecs.build().action(action).create());
}).toList();
}
@Override
public String toString() {
return "Mac Env";
}
}

View File

@ -0,0 +1,90 @@
/*
* 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.stdmock;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Stream;
import jdk.jpackage.test.LinuxHelper;
import jdk.jpackage.test.PackageType;
import jdk.jpackage.test.mock.CommandActionSpec;
import jdk.jpackage.test.mock.CommandActionSpecs;
import jdk.jpackage.test.mock.CommandMockSpec;
import jdk.jpackage.test.mock.Script;
record DefaultRpmToolsMock(String version, LinuxPackageLookupMock packageLookup) implements RpmToolsMock {
DefaultRpmToolsMock {
Objects.requireNonNull(version);
if (version.isBlank()) {
throw new IllegalArgumentException();
}
Objects.requireNonNull(packageLookup);
}
@Override
public Collection<CommandMockSpec> mocks() {
var versionString = "RPM version " + version;
var rpm = CommandActionSpec.create("rpm", context -> {
if (context.args().contains("--version") || context.args().isEmpty()) {
context.out().println(versionString);
} else if (context.args().equals(List.of("-q", "rpm"))) {
context.out().println("rpm-build");
}
return Optional.of(0);
});
var rpmbuild = CommandActionSpec.create("rpmbuild", context -> {
if (context.args().contains("--version")) {
context.out().println(versionString);
} else if (context.args().contains("--eval=%{_target_cpu}")) {
context.out().println(LinuxHelper.getDefaultPackageArch(PackageType.LINUX_RPM));
}
return Optional.of(0);
});
return Stream.of(rpm, rpmbuild).map(action -> {
return new CommandMockSpec(action.description(), CommandActionSpecs.build().action(action).create());
}).toList();
}
@Override
public void applyTo(Script.Builder scriptBuilder) {
mocks().forEach(scriptBuilder::map);
packageLookup.applyTo(scriptBuilder);
}
@Override
public boolean withPackageLookup() {
return packageLookup == LinuxPackageLookupMock.ENABLED;
}
@Override
public String toString() {
return String.format("RPM Env %s%s", version, withPackageLookup() ? "; ldd" : "");
}
}

View File

@ -0,0 +1,65 @@
/*
* 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.stdmock;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import jdk.jpackage.internal.model.DottedVersion;
import jdk.jpackage.test.mock.CommandMockSpec;
import jdk.jpackage.test.mock.CommandActionSpecs;
record DefaultWixToolsMock(String version) implements WixToolsMock {
DefaultWixToolsMock {
Objects.requireNonNull(version);
if (version.isBlank()) {
throw new IllegalArgumentException();
}
}
@Override
public Collection<CommandMockSpec> mocks() {
if (DottedVersion.compareComponents(DottedVersion.lazy(version), DottedVersion.greedy("4.0")) < 0) {
// WiX v3
return List.of(
new WixToolMock().candle(version).create(),
new WixToolMock().light(version).create(),
new CommandMockSpec("wix.exe", CommandActionSpecs.build().exit(1).create())
);
} else {
// Wix v4
return List.of(
new CommandMockSpec("candle.exe", CommandActionSpecs.build().exit(1).create()),
new CommandMockSpec("light.exe", CommandActionSpecs.build().exit(1).create()),
new WixToolMock().wix(version).create()
);
}
}
@Override
public String toString() {
return String.format("WiX Env %s", version);
}
}

View File

@ -0,0 +1,37 @@
/*
* 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.stdmock;
import java.util.Collection;
import jdk.jpackage.test.mock.CommandMockSpec;
import jdk.jpackage.test.mock.Script;
public interface EnvironmentMock {
Collection<CommandMockSpec> mocks();
default void applyTo(Script.Builder scriptBuilder) {
mocks().forEach(scriptBuilder::map);
}
}

View File

@ -0,0 +1,49 @@
/*
* 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.stdmock;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import jdk.jpackage.test.mock.CommandActionSpecs;
import jdk.jpackage.test.mock.CommandMockSpec;
import jdk.jpackage.test.mock.CommandMockExit;
public enum LinuxPackageLookupMock implements EnvironmentMock {
ENABLED(CommandMockExit.SUCCEED),
DISABLED(CommandMockExit.EXIT_1),
;
LinuxPackageLookupMock(CommandMockExit exit) {
this.exit = Objects.requireNonNull(exit);
}
@Override
public Collection<CommandMockSpec> mocks() {
return List.of(new CommandMockSpec("ldd", CommandActionSpecs.build().exit(exit).create()));
}
private final CommandMockExit exit;
}

View File

@ -0,0 +1,38 @@
/*
* 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.stdmock;
public interface MacToolsMock extends EnvironmentMock {
public static Builder build() {
return new Builder();
}
public static final class Builder {
public MacToolsMock create() {
return new DefaultMacToolsMock();
}
}
}

View File

@ -0,0 +1,63 @@
/*
* 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.stdmock;
public interface RpmToolsMock extends EnvironmentMock {
String version();
boolean withPackageLookup();
public static Builder build() {
return new Builder();
}
public static final class Builder {
public RpmToolsMock create() {
return new DefaultRpmToolsMock(version, packageLookup);
}
public Builder env(RpmToolsMock v) {
return version(v.version()).withPackageLookup(v.withPackageLookup());
}
public Builder version(String v) {
version = v;
return this;
}
public Builder withPackageLookup(boolean v) {
if (v) {
packageLookup = LinuxPackageLookupMock.ENABLED;
} else {
packageLookup = LinuxPackageLookupMock.DISABLED;
}
return this;
}
private String version;
private LinuxPackageLookupMock packageLookup = LinuxPackageLookupMock.DISABLED;
}
}

View File

@ -0,0 +1,51 @@
/*
* 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.stdmock;
public interface WixToolsMock extends EnvironmentMock {
String version();
public static Builder build() {
return new Builder();
}
public static final class Builder {
public WixToolsMock create() {
return new DefaultWixToolsMock(version);
}
public Builder env(WixToolsMock v) {
return version(v.version());
}
public Builder version(String v) {
version = v;
return this;
}
private String version;
}
}

View File

@ -9,7 +9,9 @@ lib.dirs = \
modules=jdk.jpackage/jdk.jpackage.internal:+open \
jdk.jpackage/jdk.jpackage.internal.cli:+open \
jdk.jpackage/jdk.jpackage.internal.log:+open \
jdk.jpackage/jdk.jpackage.internal.model:+open \
jdk.jpackage/jdk.jpackage.internal.summary:+open \
jdk.jpackage/jdk.jpackage.internal.pipeline:+open \
jdk.jpackage/jdk.jpackage.internal.util:+open \
jdk.jpackage/jdk.jpackage.internal.util.function:+open

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2019, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -47,7 +47,7 @@ public class BuildEnvTest {
public void testUnresolvedAppImageLayout(Path appImageDir) {
final var rootDir = Path.of("");
final var env = BuildEnv.create(rootDir, Optional.empty(), true,
final var env = BuildEnv.create(rootDir, Optional.empty(),
BuildEnvTest.class, RuntimeLayout.DEFAULT.resolveAt(appImageDir).resetRootDirectory());
assertEquals(env.appImageDir(), env.appImageLayout().rootDirectory());
@ -57,7 +57,6 @@ public class BuildEnvTest {
assertEquals(rootDir, env.buildRoot());
assertEquals(rootDir.resolve("config"), env.configDir());
assertEquals(Optional.empty(), env.resourceDir());
assertTrue(env.verbose());
}
@Test
@ -66,7 +65,7 @@ public class BuildEnvTest {
final var appImageDir = Path.of("/foo/bar");
final var layout = RuntimeLayout.DEFAULT.resolveAt(appImageDir);
final var env = BuildEnv.create(rootDir, Optional.empty(), true, BuildEnvTest.class, layout);
final var env = BuildEnv.create(rootDir, Optional.empty(), BuildEnvTest.class, layout);
assertSame(layout, env.appImageLayout());
assertEquals(env.appImageDir(), env.appImageLayout().rootDirectory());
@ -76,7 +75,6 @@ public class BuildEnvTest {
assertEquals(rootDir, env.buildRoot());
assertEquals(rootDir.resolve("config"), env.configDir());
assertEquals(Optional.empty(), env.resourceDir());
assertTrue(env.verbose());
}
@ParameterizedTest
@ -86,7 +84,7 @@ public class BuildEnvTest {
final var layout = RuntimeLayout.DEFAULT;
final var env = BuildEnv.withAppImageDir(BuildEnv.create(rootDir,
Optional.empty(), false, BuildEnvTest.class, layout), appImageDir);
Optional.empty(), BuildEnvTest.class, layout), appImageDir);
assertNotSame(layout, env.appImageLayout());
assertEquals(env.appImageDir(), env.appImageLayout().rootDirectory());
@ -96,7 +94,6 @@ public class BuildEnvTest {
assertEquals(rootDir, env.buildRoot());
assertEquals(rootDir.resolve("config"), env.configDir());
assertEquals(Optional.empty(), env.resourceDir());
assertFalse(env.verbose());
}
@ParameterizedTest
@ -114,7 +111,7 @@ public class BuildEnvTest {
}
final var env = BuildEnv.withAppImageLayout(BuildEnv.create(rootDir,
Optional.empty(), false, BuildEnvTest.class, RuntimeLayout.DEFAULT), layout);
Optional.empty(), BuildEnvTest.class, RuntimeLayout.DEFAULT), layout);
assertSame(layout, env.appImageLayout());
assertEquals(env.appImageDir(), env.appImageLayout().rootDirectory());
@ -123,18 +120,17 @@ public class BuildEnvTest {
assertEquals(rootDir, env.buildRoot());
assertEquals(rootDir.resolve("config"), env.configDir());
assertEquals(Optional.empty(), env.resourceDir());
assertFalse(env.verbose());
}
@Test
public void test_asApplicationLayout() {
final var rootDir = Path.of("r");
assertTrue(BuildEnv.create(rootDir, Optional.empty(), false,
assertTrue(BuildEnv.create(rootDir, Optional.empty(),
BuildEnvTest.class, RuntimeLayout.DEFAULT).asApplicationLayout().isEmpty());
var layout = ApplicationLayout.build().setAll("foo").create();
assertSame(layout, BuildEnv.create(rootDir, Optional.empty(), false,
assertSame(layout, BuildEnv.create(rootDir, Optional.empty(),
BuildEnvTest.class, layout).asApplicationLayout().orElseThrow());
}
}

View File

@ -668,7 +668,7 @@ public class PackagingPipelineTest {
}
private static BuildEnv dummyBuildEnv() {
return BuildEnv.create(Path.of("foo"), Optional.empty(), false, PackagingPipeline.class, RuntimeLayout.DEFAULT);
return BuildEnv.create(Path.of("foo"), Optional.empty(), PackagingPipeline.class, RuntimeLayout.DEFAULT);
}
private static PackagingPipeline.Builder buildPipeline() {

View File

@ -25,7 +25,6 @@ package jdk.jpackage.internal;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrowsExactly;
import static org.junit.jupiter.api.Assertions.assertTrue;
@ -51,10 +50,13 @@ import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Stream;
import jdk.internal.util.OperatingSystem;
import jdk.jpackage.internal.cli.LogConfigParser.MessageCategory;
import jdk.jpackage.internal.cli.Options;
import jdk.jpackage.internal.cli.StandardOption;
import jdk.jpackage.internal.log.LogEnvironment;
import jdk.jpackage.internal.util.FileUtils;
import jdk.jpackage.internal.util.RetryExecutor;
import jdk.jpackage.internal.util.SetBuilder;
import jdk.jpackage.internal.util.function.ThrowingFunction;
import jdk.jpackage.internal.util.function.ThrowingSupplier;
import jdk.jpackage.test.PathDeletionPreventer;
@ -155,10 +157,22 @@ public class TempDirectoryTest {
private void test_close_impl(CloseType closeType, Path root) throws IOException {
var logSink = new StringWriter();
var logPrintWriter = new PrintWriter(logSink, true);
Globals.instance().loggerOutputStreams(logPrintWriter, logPrintWriter);
if (closeType.isVerbose()) {
Globals.instance().loggerVerbose();
}
Globals.instance().logEnv(LogEnvironment.build()
.out(logPrintWriter)
.err(logPrintWriter)
.mutate(logEnvBuilder -> {
SetBuilder.build(MessageCategory.values())
.remove(MessageCategory.SYSTEM_LOGGER)
.mutate(b -> {
if (!closeType.isVerbose()) {
b.remove(MessageCategory.WARNINGS);
}
})
.create()
.forEach(messageCategory -> {
messageCategory.applyTo(logEnvBuilder);
});
}).create());
final var workDir = root.resolve("workdir");
Files.createDirectories(workDir);
@ -214,13 +228,13 @@ public class TempDirectoryTest {
}
logPrintWriter.flush();
var logMessages = new BufferedReader(new StringReader(logSink.toString())).lines().toList();
var logLines = new BufferedReader(new StringReader(logSink.toString())).lines().toList();
assertTrue(Files.isDirectory(root));
if (closeType.isSuccess()) {
assertFalse(Files.exists(tempDir.path()));
assertEquals(List.of(), logMessages);
assertEquals(List.of(), logLines);
} else {
assertTrue(Files.isDirectory(tempDir.path()));
assertTrue(Files.exists(leftoverPath));
@ -238,12 +252,17 @@ public class TempDirectoryTest {
throw new AssertionError();
}
}
assertEquals(List.of(I18N.format(errMessage, leftoverPath)), logMessages.subList(0, 1));
if (closeType.isVerbose()) {
// Check the log contains a stacktrace
assertNotEquals(1, logMessages.size());
assertEquals(2, logLines.size());
assertEquals(List.of(I18N.format("progress.warning-header", I18N.format(errMessage, leftoverPath))), logLines.subList(0, 1));
assertTrue(logLines.get(1).startsWith(I18N.format("progress.warning-header", "")), () -> {
return String.format("Check [%s] starts with [%s]", logLines.get(1), I18N.format("progress.warning-header", ""));
});
} else {
assertEquals(List.of(), logLines);
}
FileUtils.deleteRecursive(tempDir.path());
}
}

View File

@ -0,0 +1,693 @@
/*
* 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.internal.cli;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrowsExactly;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.time.Clock;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import jdk.jpackage.internal.log.AllLoggers;
import jdk.jpackage.internal.log.CommandLogger;
import jdk.jpackage.internal.log.ConsoleLogger;
import jdk.jpackage.internal.log.ErrorLogger;
import jdk.jpackage.internal.log.LogEnvironment;
import jdk.jpackage.internal.log.LogEnvironment.LogSink;
import jdk.jpackage.internal.log.Logger;
import jdk.jpackage.internal.log.LoggerRole;
import jdk.jpackage.internal.log.ProgressLogger;
import jdk.jpackage.internal.log.ResourceLogger;
import jdk.jpackage.internal.log.SummaryLogger;
import jdk.jpackage.internal.log.TraceLogger;
import jdk.jpackage.internal.model.JPackageException;
import jdk.jpackage.internal.summary.StandardProperty;
import jdk.jpackage.internal.summary.Summary;
import jdk.jpackage.internal.summary.Warning;
import jdk.jpackage.internal.util.SetBuilder;
import jdk.jpackage.internal.util.function.ExceptionBox;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
class LogConfigParserTest {
@ParameterizedTest
@MethodSource
void test_valueOf(TestSpec spec) {
spec.run();
}
@ParameterizedTest
@MethodSource
void test_valueOf_negative(String logEnvStr) {
assertThrowsExactly(IllegalArgumentException.class, () -> {
LogConfigParser.valueOf(logEnvStr);
});
}
@Test
void test_defaultVerbose() {
test(LogConfigParser.defaultVerbose(), MessageCategory.toLogRecords(
SetBuilder.<MessageCategory>build()
.add(MessageCategory.values())
.remove(MessageCategory.SYSTEM_LOGGER, MessageCategory.TRACE)
.create()));
}
@Test
void test_quiet() {
test(LogConfigParser.quiet(), MessageCategory.toLogRecords(
MessageCategory.ERRORS,
MessageCategory.WARNINGS));
}
private static void test(LogEnvironment.Builder logEnvBuilder, Set<LogRecord> expectedLogRecords) {
Objects.requireNonNull(logEnvBuilder);
Objects.requireNonNull(expectedLogRecords);
var consoleSink = new StringWriter();
var systemLoggerSink = new StringWriter();
var logEnv = logEnvBuilder
.out(new PrintWriter(consoleSink, true))
.err(new PrintWriter(consoleSink, true))
.consoleTimestampClock(FIXED_CONSOLE_TIMESTAMP)
.systemLoggerFactory(_ -> {
return new SimpleSystemLogger(new PrintWriter(systemLoggerSink, true)::println);
})
.create();
var logger = AllLoggers.create(logEnv);
Stream.of(LogMessage.values()).sorted(Comparator.comparing(Enum::name)).forEach(logRecordSrc -> {
var logRecordSink = new StringWriter();
var logRecords = LogRecord.findLogRecords(logRecordSrc, expectedLogRecords);
var logRecordLogger = AllLoggers.teeLogger(logRecords.stream().map(logRecord -> {
return logRecord.createLogger(logRecordSink);
}).toList());
List.of(consoleSink, systemLoggerSink).forEach(sink -> {
var buffer = sink.getBuffer();
buffer.delete(0, buffer.length());
});
// Record log messages for the logger constructed from the string value
// and for the logger constructed from the expected log record from
// the same line of code to ensure recorded stack traces will be equal.
// Don't use "Collection#forEach()" as the optimizations in the immutable list implementation in JDK27
// produce different stack traces for the first and the last items in the two-item list.
for (var l : List.of(logRecordLogger, logger)) {
logRecordSrc.applyTo(l);
}
switch (logRecords.size()) {
case 0 -> {
assertEquals("", logRecordSink.toString());
assertEquals("", consoleSink.toString());
assertEquals("", systemLoggerSink.toString());
}
case 1 -> {
StringWriter nonEmptySink;
StringWriter emptySink;
if (logRecords.getFirst().isSystemLogger()) {
nonEmptySink = systemLoggerSink;
emptySink = consoleSink;
} else {
nonEmptySink = consoleSink;
emptySink = systemLoggerSink;
}
assertEquals(logRecordSink.toString(), nonEmptySink.toString());
assertEquals("", emptySink.toString());
}
case 2 -> {
var sink = new StringBuilder();
if (logRecords.getFirst().isSystemLogger()) {
sink.append(systemLoggerSink.toString()).append(consoleSink.toString());
} else {
sink.append(consoleSink.toString()).append(systemLoggerSink.toString());
}
assertEquals(logRecordSink.toString(), sink.toString());
}
default -> {
throw ExceptionBox.reachedUnreachable();
}
}
});
}
private static Collection<TestSpec> test_valueOf() {
var testCases = new ArrayList<TestSpec>();
var allCategories = MessageCategory.values();
IntStream.range(0, 2 << (MessageCategory.values().length - 1)).parallel().mapToObj(i -> {
var bitset = BitSet.valueOf(new long[] {i});
var categories = bitset.stream().mapToObj(ordinal -> {
return allCategories[ordinal];
}).toList();
var sb = new StringBuilder();
categories.forEach(category -> {
sb.append(category.asStringValue()).append(',');
});
var logRecords = MessageCategory.toLogRecords(categories);
return new TestSpec(sb.toString(), logRecords);
}).toList().forEach(testCases::add);
testCases.addAll(test_valueOf_manual_test_cases());
return testCases;
}
private static Collection<TestSpec> test_valueOf_manual_test_cases() {
return List.of(
new TestSpec("log", MessageCategory.SYSTEM_LOGGER),
new TestSpec("log,console", MessageCategory.values()),
new TestSpec("console", MessageCategory.toLogRecords(ALL_CONSOLE_CATEGORIES)),
new TestSpec("all", MessageCategory.values()),
new TestSpec("all,console", MessageCategory.values()),
new TestSpec("-trace,trace", MessageCategory.TRACE),
new TestSpec("-trace", Set.of()),
new TestSpec("-log", Set.of()),
new TestSpec("-tools,tools,-tools", MessageCategory.TOOLS),
new TestSpec("-trace,trace,console", MessageCategory.toLogRecords(ALL_CONSOLE_CATEGORIES)),
new TestSpec("console,-tools,tools,-tools", MessageCategory.toLogRecords(ALL_CONSOLE_CATEGORIES)),
new TestSpec("-tools,console,", MessageCategory.toLogRecords(
SetBuilder.build(ALL_CONSOLE_CATEGORIES).remove(MessageCategory.TOOLS).create())),
new TestSpec(""),
new TestSpec("errors,,errors,", MessageCategory.ERRORS)
);
}
private static Collection<String> test_valueOf_negative() {
return List.of(
",",
"logerrors",
"log,error",
"log,-all",
"-all",
"-console",
",errors,,errors,"
);
}
record TestSpec(String logEnvStr, Set<LogRecord> expectedLogRecords) {
TestSpec {
Objects.requireNonNull(logEnvStr);
Objects.requireNonNull(expectedLogRecords);
}
TestSpec(String logEnvStr, MessageCategory... categories) {
this(logEnvStr, MessageCategory.toLogRecords(categories));
}
void run() {
test(LogConfigParser.valueOf(logEnvStr), expectedLogRecords);
}
@Override
public String toString() {
return String.format("<%s> => %s", logEnvStr, expectedLogRecords.stream().map(Enum::name).sorted().toList());
}
}
/**
* Log message. Each enum item should wrap an invocation of one method
* from an interface inherited from {@link Logger}.
*/
private enum LogMessage {
TRACE_STRING((TraceLogger logger) -> {
logger.trace("Ecart foo");
}),
TRACE_THROWABLE((TraceLogger logger) -> {
logger.trace(new Exception("Trace foo exception!"));
}),
TRACE_STRING_AND_THROWABLE((TraceLogger logger) -> {
logger.trace(new Exception("Trace bar exception!"), "Ecart bar");
}),
TRACE_FORMAT((TraceLogger logger) -> {
logger.trace("Ecart %s", "it");
}),
TRACE_FORMAT_AND_THROWABLE((TraceLogger logger) -> {
logger.trace(new Exception("Trace exception again!"), "Ecart %s with", "it");
}),
SUMMARY((SummaryLogger logger) -> {
var summary = new Summary();
summary.put(StandardProperty.OUTPUT_BUNDLE, "sample");
logger.summary(summary);
}),
SUMMARY_WARNING((SummaryLogger logger) -> {
var summary = new Summary();
summary.put(new Warning() {
@Override
public int ordinal() {
throw new UnsupportedOperationException();
}
@Override
public Optional<String> valueFormat() {
throw new UnsupportedOperationException();
}
}, "Foo configuration warning");
logger.summary(summary);
}),
ERROR((ErrorLogger logger) -> {
logger.reportError(new Exception("Kaput!"));
}),
ERROR_SELF_CONTAINED((ErrorLogger logger) -> {
logger.reportError(new JPackageException("Cooked!"));
}),
PROGRESS_MESSAGE((ProgressLogger logger) -> {
logger.progress("Start operation #1");
}),
PROGRESS_WARNING_EXCEPTION((ProgressLogger logger) -> {
logger.progressWarning(new Exception("Minor issue"));
}),
PROGRESS_WARNING_MESSAGE_AND_EXCEPTION((ProgressLogger logger) -> {
logger.progressWarning(new Exception("Minor issue"), "Ignoring the exception");
}),
PROGRESS_WARNING_MESSAGE((ProgressLogger logger) -> {
logger.progressWarning("Ignoring the problem");
}),
RESOURCE((ResourceLogger logger) -> {
logger.useResource("Using the resource");
}),
COMMMAND_BEFORE((CommandLogger logger) -> {
logger.beforeCommandExecuted(false, "before -abc");
}),
COMMMAND_AFTER((CommandLogger logger) -> {
logger.afterCommandExecuted(false, "after -abc", Optional.of(67L), Optional.of(0), "Hello\nGoodbye");
}),
COMMMAND_BEFORE_QUIET((CommandLogger logger) -> {
logger.beforeCommandExecuted(true, "before -x -y");
}),
COMMMAND_AFTER_QUIET((CommandLogger logger) -> {
logger.afterCommandExecuted(true, "after -x -y", Optional.of(321L), Optional.of(7), "Monday\nSunday");
}),
;
LogMessage(Consumer<? super AllLoggers> useLogger) {
this.useLogger = Objects.requireNonNull(useLogger);
}
void applyTo(AllLoggers logger) {
useLogger.accept(logger);
}
private final Consumer<? super AllLoggers> useLogger;
}
/**
* Log records. Each enum item binds one or more invocations of logging methods
* to a logger with specific configuration parameters.
*/
private enum LogRecord {
TRACE_STRING(console(TraceLogger.class, TraceLogger::create)),
TRACE_THROWABLE(TRACE_STRING),
TRACE_STRING_AND_THROWABLE(TRACE_STRING),
TRACE_FORMAT(TRACE_STRING),
TRACE_FORMAT_AND_THROWABLE(TRACE_STRING),
TRACE_SYSTEM_LOGGER(systemLogger(TraceLogger.class, TraceLogger::create),
LogMessage.TRACE_FORMAT,
LogMessage.TRACE_FORMAT_AND_THROWABLE,
LogMessage.TRACE_STRING,
LogMessage.TRACE_STRING_AND_THROWABLE,
LogMessage.TRACE_THROWABLE),
SUMMARY(console(SummaryLogger.class, sink -> {
return SummaryLogger.create(sink, true, false);
})),
SUMMARY_WARNING(console(SummaryLogger.class, sink -> {
return SummaryLogger.create(sink, false, true);
})),
SUMMARY_SYSTEM_LOGGER(systemLogger(SummaryLogger.class, SummaryLogger::create),
LogMessage.SUMMARY,
LogMessage.SUMMARY_WARNING),
ERROR(console(ErrorLogger.class, sink -> {
return ErrorLogger.create(sink, false, false);
})),
ERROR_SELF_CONTAINED(ERROR),
ERROR_SELF_CONTAINED_ALWAYS_PRINT_STACKTRACE(console(ErrorLogger.class, sink -> {
return ErrorLogger.create(sink, true, false);
}), LogMessage.ERROR_SELF_CONTAINED),
ERROR_SYSTEM_LOGGER(systemLogger(ErrorLogger.class, ErrorLogger::create),
LogMessage.ERROR,
LogMessage.ERROR_SELF_CONTAINED),
PROGRESS_MESSAGE(console(ProgressLogger.class, sink -> {
return ProgressLogger.create(sink, true, false);
})),
PROGRESS_WARNING_EXCEPTION(console(ProgressLogger.class, sink -> {
return ProgressLogger.create(sink, false, true);
})),
PROGRESS_WARNING_MESSAGE_AND_EXCEPTION(PROGRESS_WARNING_EXCEPTION),
PROGRESS_WARNING_MESSAGE(PROGRESS_WARNING_EXCEPTION),
PROGRESS_SYSTEM_LOGGER(systemLogger(ProgressLogger.class, ProgressLogger::create),
LogMessage.PROGRESS_MESSAGE,
LogMessage.PROGRESS_WARNING_EXCEPTION,
LogMessage.PROGRESS_WARNING_MESSAGE,
LogMessage.PROGRESS_WARNING_MESSAGE_AND_EXCEPTION),
RESOURCE(console(ResourceLogger.class, ResourceLogger::create)),
RESOURCE_SYSTEM_LOGGER(systemLogger(ResourceLogger.class, ResourceLogger::create),
LogMessage.RESOURCE),
COMMMAND(console(CommandLogger.class, sink -> {
return CommandLogger.create(sink, false, false);
}), LogMessage.COMMMAND_BEFORE),
COMMMAND_PRINT_QUIET_AND_RESULT(console(CommandLogger.class, sink -> {
return CommandLogger.create(sink, true, true);
}), LogMessage.COMMMAND_BEFORE,
LogMessage.COMMMAND_AFTER,
LogMessage.COMMMAND_BEFORE_QUIET,
LogMessage.COMMMAND_AFTER_QUIET),
COMMMAND_SYSTEM_LOGGER(systemLogger(CommandLogger.class, CommandLogger::create),
LogMessage.COMMMAND_BEFORE,
LogMessage.COMMMAND_AFTER,
LogMessage.COMMMAND_BEFORE_QUIET,
LogMessage.COMMMAND_AFTER_QUIET),
;
LogRecord(CannedLogger<? super AllLoggers> cannedLogger, LogMessage... logRecordSources) {
this.cannedLogger = Objects.requireNonNull(cannedLogger);
if (logRecordSources.length > 0) {
this.logRecordSources = Set.of(logRecordSources);
} else {
this.logRecordSources = Set.of(LogMessage.valueOf(name()));
}
}
LogRecord(LogRecord other) {
this(other.cannedLogger);
}
AllLoggers createLogger(StringWriter sink) {
return cannedLogger.createLoggerWithSink(sink);
}
boolean isSystemLogger() {
return cannedLogger.sinkType() == LogSink.SYSTEM_LOGGER;
}
static List<LogRecord> findLogRecords(LogMessage logRecordSrc, Collection<LogRecord> logRecords) {
Objects.requireNonNull(logRecordSrc);
Objects.requireNonNull(logRecords);
var filteredLogRecords = logRecords.stream().filter(logRecord -> {
return logRecord.logRecordSources.contains(logRecordSrc);
}).toList();
switch (filteredLogRecords.size()) {
case 0, 1 -> {
return filteredLogRecords;
}
case 2 -> {
if (filteredLogRecords.getFirst().isSystemLogger() != filteredLogRecords.getLast().isSystemLogger()) {
return filteredLogRecords;
}
}
}
throw new IllegalStateException(String.format(
"Multiple %s log records map into the %s log record source", filteredLogRecords, logRecordSrc));
}
private record CannedLogger<T extends Logger>(Class<T> type, Function<PrintWriter, T> ctor, LogSink sinkType) {
CannedLogger {
Objects.requireNonNull(type);
Objects.requireNonNull(ctor);
Objects.requireNonNull(sinkType);
}
AllLoggers createLoggerWithSink(StringWriter sink) {
Objects.requireNonNull(sink);
var emptyLogEnv = Options.concat();
return AllLoggers.create(Stream.of(LoggerRole.values()).map(LoggerRole::logger).map(ov -> {
return ov.getFrom(emptyLogEnv);
}).map(discardingLogger -> {
if (type.isInstance(discardingLogger)) {
return ctor.apply(new PrintWriter(sink));
} else {
return discardingLogger;
}
}).toList());
}
}
private static <T extends Logger> CannedLogger<T> console(
Class<T> type, Function<ConsoleLogger, T> ctor) {
Objects.requireNonNull(ctor);
return new CannedLogger<>(type, sink -> {
var timestampClock = FIXED_CONSOLE_TIMESTAMP;
return ctor.apply(new ConsoleLogger(sink::println, sink::println, timestampClock));
}, LogSink.CONSOLE);
}
private static <T extends Logger> CannedLogger<T> systemLogger(
Class<T> type, Function<System.Logger, T> ctor) {
Objects.requireNonNull(ctor);
return new CannedLogger<>(type, sink -> {
return ctor.apply(new SimpleSystemLogger(sink::println));
}, LogSink.SYSTEM_LOGGER);
}
private final CannedLogger<? super AllLoggers> cannedLogger;
private final Set<LogMessage> logRecordSources;
}
private enum MessageCategory {
SUMMARY(LogRecord.SUMMARY),
WARNINGS(
LogRecord.SUMMARY_WARNING,
LogRecord.PROGRESS_WARNING_MESSAGE,
LogRecord.PROGRESS_WARNING_EXCEPTION,
LogRecord.PROGRESS_WARNING_MESSAGE_AND_EXCEPTION),
ERRORS(LogRecord.ERROR, LogRecord.ERROR_SELF_CONTAINED),
PROGRESS(LogRecord.PROGRESS_MESSAGE),
TRACE(Stream.of(
add(
LogRecord.TRACE_STRING,
LogRecord.TRACE_STRING_AND_THROWABLE,
LogRecord.TRACE_FORMAT,
LogRecord.TRACE_FORMAT_AND_THROWABLE,
LogRecord.TRACE_THROWABLE,
LogRecord.COMMMAND_PRINT_QUIET_AND_RESULT
),
replace(
LogRecord.ERROR_SELF_CONTAINED,
LogRecord.ERROR_SELF_CONTAINED_ALWAYS_PRINT_STACKTRACE
),
replace(
LogRecord.COMMMAND,
LogRecord.COMMMAND_PRINT_QUIET_AND_RESULT
)
).flatMap(x -> x)),
RESOURCES(LogRecord.RESOURCE),
TOOLS(LogRecord.COMMMAND),
SYSTEM_LOGGER(Stream.of(LogRecord.values()).filter(LogRecord::isSystemLogger).map(AddLogRecordsMutator::new)) {
@Override
String asStringValue() {
return "log";
}
},
;
MessageCategory(Stream<LogRecordsMutator> mutators) {
this.mutators = mutators.toList();
if (this.mutators.isEmpty()) {
throw new IllegalArgumentException();
}
}
MessageCategory(LogRecord... logRecords) {
this(add(logRecords));
}
String asStringValue() {
return name().toLowerCase();
}
private Stream<LogRecordsMutator> filterMutators(Class<? extends LogRecordsMutator> mutatorType) {
return mutators.stream().filter(mutatorType::isInstance);
}
static Set<LogRecord> toLogRecords(Collection<MessageCategory> categories) {
var logRecords = new HashSet<LogRecord>();
for (var mutatorType : List.of(AddLogRecordsMutator.class, ReplaceLogRecordsMutator.class)) {
categories.stream().map(category -> {
return category.filterMutators(mutatorType);
}).flatMap(x -> x).forEach(mutator -> {
mutator.mutate(logRecords);
});
}
return logRecords;
}
static Set<LogRecord> toLogRecords(MessageCategory... categories) {
return toLogRecords(Set.of(categories));
}
private sealed interface LogRecordsMutator {
void mutate(Set<LogRecord> logRecords);
}
private record AddLogRecordsMutator(LogRecord value) implements LogRecordsMutator {
AddLogRecordsMutator {
Objects.requireNonNull(value);
}
@Override
public void mutate(Set<LogRecord> logRecords) {
logRecords.add(value);
}
}
private record ReplaceLogRecordsMutator(LogRecord from, LogRecord to) implements LogRecordsMutator {
ReplaceLogRecordsMutator {
Objects.requireNonNull(from);
Objects.requireNonNull(to);
}
@Override
public void mutate(Set<LogRecord> logRecords) {
if (logRecords.contains(from)) {
logRecords.remove(from);
logRecords.add(to);
}
}
}
private static Stream<LogRecordsMutator> add(LogRecord... values) {
return Stream.of(values).map(AddLogRecordsMutator::new);
}
private static Stream<LogRecordsMutator> replace(LogRecord from, LogRecord to) {
return Stream.of(new ReplaceLogRecordsMutator(from, to));
}
private final List<LogRecordsMutator> mutators;
}
private record SimpleSystemLogger(Consumer<String> sink) implements System.Logger {
SimpleSystemLogger {
Objects.requireNonNull(sink);
}
@Override
public String getName() {
throw new UnsupportedOperationException();
}
@Override
public boolean isLoggable(Level level) {
return true;
}
@Override
public void log(Level level, ResourceBundle bundle, String msg, Throwable thrown) {
var buf = new StringWriter();
thrown.printStackTrace(new PrintWriter(buf));
logImpl(level, String.format("%s: %s", msg, buf));
}
@Override
public void log(Level level, ResourceBundle bundle, String format, Object... params) {
logImpl(level, String.format(format, params));
}
private void logImpl(Level level, String msg) {
sink.accept(String.format("%s: %s: %s", SimpleSystemLogger.class.getSimpleName(), level, msg));
}
}
private static final Clock FIXED_CONSOLE_TIMESTAMP = Clock.fixed(Clock.systemDefaultZone().instant(), ZoneId.systemDefault());
private static final Set<MessageCategory> ALL_CONSOLE_CATEGORIES = SetBuilder.build(MessageCategory.values())
.remove(MessageCategory.SYSTEM_LOGGER)
.create();
static {
// Assert log records are unique.
Stream.of(LogRecord.values()).map(logRecord -> {
var sink = new StringWriter();
var logger = logRecord.createLogger(sink);
logRecord.logRecordSources.forEach(logRecordSource -> {
logRecordSource.applyTo(logger);
});
var str = sink.toString();
if (!str.isBlank()) {
return str;
} else {
throw new IllegalArgumentException(String.format(
"Source log record results into a blank %s log record", logRecord));
}
}).collect(Collectors.toMap(x -> x, x -> x));
}
}

View File

@ -160,6 +160,10 @@ public class MainTest extends JUnitAdapter {
}
private static Collection<TestSpec> testOutput() {
// Non-empty directory
var invalidTempValue = Path.of(System.getProperty("java.home")).toString();
return Stream.of(
// Print the tool version
build().expectShortHelp(),
@ -180,22 +184,40 @@ public class MainTest extends JUnitAdapter {
// Invalid command line requesting to print the version of the tool.
// Additional error messages may be printed if the default bundling operation
// can not be identified; don't verify these errors in the output.
build().args("foo", "--version").stderrMatchType(OutputMatchType.STARTS_WITH).expectErrors(I18N.format("error.non-option-arguments", 1))
build().args("foo", "--version").stderrMatchType(OutputMatchType.STARTS_WITH).expectErrors(I18N.format("error.non-option-arguments", 1)),
// Should print two errors: one for the invalid value of the "--type" option
// and another for the invalid value of the "--verbose" option.
build().args("--temp", invalidTempValue, "--verbose", "bar").expectErrors(
I18N.format("error.parameter-not-empty-directory", invalidTempValue, "--temp"),
I18N.format("error.parameter-invalid-value", "bar", "--verbose")),
build().args("--verbose", "bar", "--temp", invalidTempValue).expectErrors(
I18N.format("error.parameter-invalid-value", "bar", "--verbose"),
I18N.format("error.parameter-not-empty-directory", invalidTempValue, "--temp")),
// This is just for the coverage.
build().args("--verbose", "errors", "--temp", invalidTempValue).expectErrors(
I18N.format("error.parameter-not-empty-directory", invalidTempValue, "--temp")),
// Silent failure.
build().args("--verbose", "", "--temp", invalidTempValue).expectErrorExitCode(),
// If the value of the "--type" option is invalid, this is the only reported error.
build().args("--type", "foo", "--verbose", "bar").expectErrors(
I18N.format("ERR_InvalidInstallerType", "foo"))
).map(TestSpec.Builder::create).toList();
}
private static List<ErrorReporterTestSpec> test_ErrorReporter() {
var testCases = new ArrayList<ErrorReporterTestSpec>();
for (var verbose : List.of(true, false)) {
test_ErrorReporter_Exception(verbose, testCases::add);
test_ErrorReporter_UnexpectedResultException(verbose, testCases::add);
test_ErrorReporter_suppressedExceptions(verbose, testCases::add);
for (var alwaysPrintStackTrace : List.of(true, false)) {
test_ErrorReporter_Exception(alwaysPrintStackTrace, testCases::add);
for (var printCommandOutput : List.of(true, false)) {
test_ErrorReporter_UnexpectedResultException(alwaysPrintStackTrace, printCommandOutput, testCases::add);
test_ErrorReporter_suppressedExceptions(alwaysPrintStackTrace, printCommandOutput, testCases::add);
}
}
return testCases;
}
private static void test_ErrorReporter_Exception(boolean verbose, Consumer<ErrorReporterTestSpec> sink) {
private static void test_ErrorReporter_Exception(boolean alwaysPrintStackTrace, Consumer<ErrorReporterTestSpec> sink) {
for (var makeCause : List.<UnaryOperator<Exception>>of(
ex -> ex,
@ -220,6 +242,8 @@ public class MainTest extends JUnitAdapter {
for (var expect : List.of(
new IOException("I/O error"),
new NullPointerException(),
// Exception without a message
new Exception(),
new JPackageException("Kaput!"),
new ConfigException("It is broken", "Fix it!"),
new ConfigException("It is broken. No advice how to fix it", (String)null),
@ -232,13 +256,14 @@ public class MainTest extends JUnitAdapter {
}
var expectedOutput = new ArrayList<ExceptionFormatter>();
ErrorReporterTestSpec.expectExceptionFormatters(expect, verbose, expectedOutput::add);
sink.accept(ErrorReporterTestSpec.create(cause, expect, verbose, expectedOutput));
ErrorReporterTestSpec.expectExceptionFormatters(expect, alwaysPrintStackTrace, false, expectedOutput::add);
sink.accept(ErrorReporterTestSpec.create(cause, expect, alwaysPrintStackTrace, false, expectedOutput));
}
}
}
private static void test_ErrorReporter_UnexpectedResultException(boolean verbose, Consumer<ErrorReporterTestSpec> sink) {
private static void test_ErrorReporter_UnexpectedResultException(
boolean alwaysPrintStackTrace, boolean printCommandOutput, Consumer<ErrorReporterTestSpec> sink) {
var execAttrs = new CommandOutputControl.ProcessAttributes(Optional.of(12345L), List.of("foo", "--bar"));
@ -263,8 +288,8 @@ public class MainTest extends JUnitAdapter {
)) {
var cause = makeCause.apply(expect);
var expectedOutput = new ArrayList<ExceptionFormatter>();
ErrorReporterTestSpec.expectExceptionFormatters(expect, verbose, expectedOutput::add);
sink.accept(ErrorReporterTestSpec.create(cause, expect, verbose, expectedOutput));
ErrorReporterTestSpec.expectExceptionFormatters(expect, alwaysPrintStackTrace, printCommandOutput, expectedOutput::add);
sink.accept(ErrorReporterTestSpec.create(cause, expect, alwaysPrintStackTrace, printCommandOutput, expectedOutput));
}
}
}
@ -286,7 +311,8 @@ public class MainTest extends JUnitAdapter {
}
}
private static void test_ErrorReporter_suppressedExceptions(boolean verbose, Consumer<ErrorReporterTestSpec> sink) {
private static void test_ErrorReporter_suppressedExceptions(
boolean alwaysPrintStackTrace, boolean printCommandOutput, Consumer<ErrorReporterTestSpec> sink) {
var execAttrs = new CommandOutputControl.ProcessAttributes(Optional.of(567L), List.of("foo", "--bar"));
@ -329,10 +355,11 @@ public class MainTest extends JUnitAdapter {
var expectedOutput = new ArrayList<FormattedException>();
ErrorReporterTestSpec.expectOutputFragments(ExceptionBox.unbox(suppressed), verbose, expectedOutput::add);
ErrorReporterTestSpec.expectOutputFragments(main, verbose, expectedOutput::add);
ErrorReporterTestSpec.expectOutputFragments(
ExceptionBox.unbox(suppressed), alwaysPrintStackTrace, printCommandOutput, expectedOutput::add);
ErrorReporterTestSpec.expectOutputFragments(main, alwaysPrintStackTrace, printCommandOutput, expectedOutput::add);
sink.accept(new ErrorReporterTestSpec(cause, verbose, expectedOutput));
sink.accept(new ErrorReporterTestSpec(cause, alwaysPrintStackTrace, printCommandOutput, expectedOutput));
}
}
}
@ -614,7 +641,11 @@ public class MainTest extends JUnitAdapter {
}
record ErrorReporterTestSpec(Exception cause, boolean verbose, List<FormattedException> expectOutput) {
record ErrorReporterTestSpec(
Exception cause,
boolean alwaysPrintStackTrace,
boolean printCommandOutput,
List<FormattedException> expectOutput) {
ErrorReporterTestSpec {
Objects.requireNonNull(cause);
@ -625,26 +656,40 @@ public class MainTest extends JUnitAdapter {
}
static ErrorReporterTestSpec create(
Exception cause, boolean verbose, List<ExceptionFormatter> expectOutput) {
return create(cause, cause, verbose, expectOutput);
Exception cause,
boolean alwaysPrintStackTrace,
boolean printCommandOutput,
List<ExceptionFormatter> expectOutput) {
return create(cause, cause, alwaysPrintStackTrace, printCommandOutput, expectOutput);
}
static ErrorReporterTestSpec create(
Exception cause, Exception expect, boolean verbose, List<ExceptionFormatter> expectOutput) {
Exception cause,
Exception expect,
boolean alwaysPrintStackTrace,
boolean printCommandOutput,
List<ExceptionFormatter> expectOutput) {
Objects.requireNonNull(cause);
Objects.requireNonNull(expect);
return new ErrorReporterTestSpec(cause, verbose, expectOutput.stream().map(formatter -> {
return new ErrorReporterTestSpec(cause, alwaysPrintStackTrace, printCommandOutput, expectOutput.stream().map(formatter -> {
return new FormattedException(formatter, expect);
}).toList());
}
static void expectExceptionFormatters(Exception ex, boolean verbose, Consumer<ExceptionFormatter> sink) {
static void expectExceptionFormatters(
Exception ex,
boolean alwaysPrintStackTrace,
boolean printCommandOutput,
Consumer<ExceptionFormatter> sink) {
Objects.requireNonNull(ex);
Objects.requireNonNull(sink);
final var isSelfContained = (ex.getClass().getAnnotation(SelfContainedException.class) != null);
if (verbose || !(isSelfContained || ex instanceof UnexpectedResultException)) {
if (alwaysPrintStackTrace || !(isSelfContained || ex instanceof UnexpectedResultException)) {
sink.accept(ExceptionFormatter.STACK_TRACE);
}
@ -664,7 +709,10 @@ public class MainTest extends JUnitAdapter {
} else {
sink.accept(ExceptionFormatter.FAILED_COMMAND_TIMEDOUT_MESSAGE);
}
sink.accept(ExceptionFormatter.FAILED_COMMAND_OUTPUT);
if (printCommandOutput) {
sink.accept(ExceptionFormatter.FAILED_COMMAND_OUTPUT);
}
}
default -> {
if (isSelfContained) {
@ -676,9 +724,14 @@ public class MainTest extends JUnitAdapter {
}
}
static void expectOutputFragments(Exception ex, boolean verbose, Consumer<FormattedException> sink) {
static void expectOutputFragments(
Exception ex,
boolean alwaysPrintStackTrace,
boolean printCommandOutput,
Consumer<FormattedException> sink) {
Objects.requireNonNull(sink);
expectExceptionFormatters(ex, verbose, formatter -> {
expectExceptionFormatters(ex, alwaysPrintStackTrace, printCommandOutput, formatter -> {
sink.accept(formatter.bind(ex));
});
}
@ -708,8 +761,12 @@ public class MainTest extends JUnitAdapter {
}).collect(Collectors.joining("+")));
}
if (verbose) {
tokens.add("verbose");
if (alwaysPrintStackTrace) {
tokens.add("stacktrace-always");
}
if (printCommandOutput) {
tokens.add("command-output");
}
return tokens.stream().collect(Collectors.joining("; "));
@ -723,7 +780,7 @@ public class MainTest extends JUnitAdapter {
t.printStackTrace(pw);
}, msg -> {
pw.println(msg);
}, verbose).reportError(cause);
}, alwaysPrintStackTrace, printCommandOutput).reportError(cause);
}
var expected = expectOutput.stream().map(FormattedException::format).collect(Collectors.joining(""));

View File

@ -99,7 +99,7 @@ public class OptionsValidationFailTest {
var errorReporter = new Main.ErrorReporter(ex -> {
ex.printStackTrace(err);
}, err::println, false);
}, err::println, false, true);
return parse(args).peekErrors(errors -> {
final var firstErr = errors.stream().findFirst().orElseThrow();

View File

@ -64,8 +64,35 @@ Generic Options:
removed upon the task completion.
--vendor <vendor string>
Vendor of the application
--verbose
Enables verbose output
--verbose [<[-]category(,[-]category)*>]
Configures verbose output. Where "category" is one of
"all"
"console"
"log"
"errors"
"progress"
"resources"
"summary"
"tools"
"trace"
"warnings"
Suppress all console output, enable logging via System.Logger API:
--verbose log
Enable all message categories in the console:
--verbose console
Enable all message categories, but "trace" and "tools" in the console:
--verbose console,-trace,-tools
Enable "trace" and "tools" message categories in the console:
--verbose trace,tools
Enable "trace" and "tools" message categories in the console and
enable logging via System.Logger API:
--verbose log,trace,tools
If the option is specified without the value, it is equivalent to
--verbose console,-trace
If the option is not specified it is equivalent to
--verbose errors,warnings
--version
Print the product version to the output stream and exit.

View File

@ -70,8 +70,35 @@ Generic Options:
removed upon the task completion.
--vendor <vendor string>
Vendor of the application
--verbose
Enables verbose output
--verbose [<[-]category(,[-]category)*>]
Configures verbose output. Where "category" is one of
"all"
"console"
"log"
"errors"
"progress"
"resources"
"summary"
"tools"
"trace"
"warnings"
Suppress all console output, enable logging via System.Logger API:
--verbose log
Enable all message categories in the console:
--verbose console
Enable all message categories, but "trace" and "tools" in the console:
--verbose console,-trace,-tools
Enable "trace" and "tools" message categories in the console:
--verbose trace,tools
Enable "trace" and "tools" message categories in the console and
enable logging via System.Logger API:
--verbose log,trace,tools
If the option is specified without the value, it is equivalent to
--verbose console,-trace
If the option is not specified it is equivalent to
--verbose errors,warnings
--version
Print the product version to the output stream and exit.

Some files were not shown because too many files have changed in this diff Show More