From 06e6539cebac907d21918b9fa2e4a06d045d8286 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Mon, 4 May 2026 21:00:35 +0000 Subject: [PATCH] 8374839: Improve jpackage information messages 8356116: [macos] Add logging of sign commands in jpackage Reviewed-by: almatvee --- .../jpackage/internal/DesktopIntegration.java | 2 +- .../jpackage/internal/LibProvidersLookup.java | 21 +- .../internal/LinuxBundlingEnvironment.java | 22 +- .../jpackage/internal/LinuxDebPackager.java | 10 +- .../jpackage/internal/LinuxFromOptions.java | 18 +- .../jpackage/internal/LinuxPackageArch.java | 2 +- .../jdk/jpackage/internal/LinuxPackager.java | 25 +- .../jpackage/internal/LinuxRpmPackager.java | 4 +- .../internal/LinuxSystemEnvironment.java | 11 +- .../resources/LinuxResources.properties | 6 +- .../jdk/jpackage/internal/AppImageSigner.java | 36 +- .../jdk/jpackage/internal/Codesign.java | 2 +- .../jdk/jpackage/internal/Keychain.java | 4 +- .../internal/MacApplicationBuilder.java | 105 ++- .../internal/MacBundlingEnvironment.java | 9 + .../internal/MacCertificateUtils.java | 2 +- .../jdk/jpackage/internal/MacDmgPackager.java | 40 +- .../internal/MacDmgSystemEnvironment.java | 4 +- .../jdk/jpackage/internal/MacFromOptions.java | 6 +- .../jpackage/internal/MacPackageBuilder.java | 21 +- .../internal/MacPackagingPipeline.java | 4 +- .../internal/MacPkgPackageBuilder.java | 20 +- .../jdk/jpackage/internal/MacPkgPackager.java | 7 +- .../resources/MacResources.properties | 16 +- .../jpackage/internal/ApplicationBuilder.java | 8 +- .../jdk/jpackage/internal/BuildEnv.java | 33 +- .../jpackage/internal/BuildEnvBuilder.java | 15 +- .../internal/BuildEnvFromOptions.java | 4 +- .../internal/DefaultBundlingEnvironment.java | 13 +- .../jdk/jpackage/internal/Executor.java | 79 +- .../jdk/jpackage/internal/Globals.java | 20 +- .../jdk/jpackage/internal/IOUtils.java | 9 + .../classes/jdk/jpackage/internal/Log.java | 117 +-- .../jdk/jpackage/internal/ModuleInfo.java | 5 +- .../jdk/jpackage/internal/OptionUtils.java | 21 + .../internal/OverridableResource.java | 10 +- .../jpackage/internal/PackagingPipeline.java | 6 +- .../jdk/jpackage/internal/TempDirectory.java | 12 +- .../jdk/jpackage/internal/ToolValidator.java | 2 +- .../internal/cli/LogConfigParser.java | 209 ++++++ .../jdk/jpackage/internal/cli/Main.java | 64 +- .../jpackage/internal/cli/OptionValue.java | 13 +- .../jpackage/internal/cli/StandardOption.java | 16 +- .../jpackage/internal/log/CommandLogger.java | 130 ++++ .../jpackage/internal/log/ConsoleLogger.java | 113 +++ .../jpackage/internal/log/ErrorLogger.java | 78 ++ .../jdk/jpackage/internal/log/I18N.java | 59 ++ .../jpackage/internal/log/LogEnvironment.java | 309 ++++++++ .../jdk/jpackage/internal/log/Logger.java | 32 + .../jdk/jpackage/internal/log/LoggerRole.java | 63 ++ .../jpackage/internal/log/LoggerTrait.java | 28 + .../jpackage/internal/log/ProgressLogger.java | 126 ++++ .../jpackage/internal/log/ResourceLogger.java | 63 ++ .../jpackage/internal/log/StandardLogger.java | 52 ++ .../jpackage/internal/log/SummaryLogger.java | 77 ++ .../internal/log/SystemLoggerTrait.java | 34 + .../jpackage/internal/log/TraceLogger.java | 128 ++++ .../jdk/jpackage/internal/log/Utils.java | 163 ++++ .../internal/model/StandardPackageType.java | 8 +- .../resources/HelpResources.properties | 29 +- .../resources/MainResources.properties | 18 +- .../jdk/jpackage/internal/summary/I18N.java | 59 ++ .../jpackage/internal/summary/Property.java | 33 + .../internal/summary/StandardProperty.java | 92 +++ .../internal/summary/StandardWarning.java | 60 ++ .../jpackage/internal/summary/Summary.java | 180 +++++ .../internal/summary/SummaryAccumulator.java | 49 ++ .../internal/summary/SummaryItem.java | 45 ++ .../jpackage/internal/summary/Warning.java | 31 + .../jpackage/internal/util/SetBuilder.java | 18 +- src/jdk.jpackage/share/man/jpackage.md | 71 +- .../internal/WinBundlingEnvironment.java | 21 +- .../jdk/jpackage/internal/WinFromOptions.java | 9 +- .../jdk/jpackage/internal/WinMsiPackager.java | 8 +- .../jdk/jpackage/internal/WixTool.java | 31 +- .../resources/WinResources.properties | 7 +- test/jdk/tools/jpackage/TEST.properties | 2 + .../jpackage/test/CannedFormattedString.java | 34 +- .../jdk/jpackage/test/JPackageCommand.java | 201 ++++- .../helpers/jdk/jpackage/test/TKit.java | 13 - .../jdk/jpackage/test/WindowsHelper.java | 37 +- .../jpackage/test/stdmock/DebToolsMock.java | 71 ++ .../test/stdmock/DefaultDebToolsMock.java | 121 +++ .../test/stdmock/DefaultMacToolsMock.java | 55 ++ .../test/stdmock/DefaultRpmToolsMock.java | 90 +++ .../test/stdmock/DefaultWixToolsMock.java | 65 ++ .../test/stdmock/EnvironmentMock.java | 37 + .../test/stdmock/LinuxPackageLookupMock.java | 49 ++ .../jpackage/test/stdmock/MacToolsMock.java | 38 + .../jpackage/test/stdmock/RpmToolsMock.java | 63 ++ .../jpackage/test/stdmock/WixToolsMock.java | 51 ++ test/jdk/tools/jpackage/junit/TEST.properties | 2 + .../jdk/jpackage/internal/BuildEnvTest.java | 18 +- .../internal/PackagingPipelineTest.java | 2 +- .../jpackage/internal/TempDirectoryTest.java | 39 +- .../internal/cli/LogConfigParserTest.java | 693 ++++++++++++++++++ .../jdk/jpackage/internal/cli/MainTest.java | 113 ++- .../cli/OptionsValidationFailTest.java | 2 +- .../jdk/jpackage/internal/cli/help-linux.txt | 31 +- .../jdk/jpackage/internal/cli/help-macos.txt | 31 +- .../jpackage/internal/cli/help-windows.txt | 31 +- .../jdk/jpackage/internal/log/AllLoggers.java | 73 ++ .../jdk/jpackage/internal/log/UtilsTest.java | 255 +++++++ .../internal/summary/StandardSummaryTest.java | 596 +++++++++++++++ .../internal/summary/SummaryTest.java | 150 ++++ .../util/CommandOutputControlTest.java | 2 +- .../jpackage/linux/LinuxResourceTest.java | 19 +- .../jpackage/macosx/MacPropertiesTest.java | 22 +- .../tools/jpackage/macosx/MacSignTest.java | 8 +- .../tools/jpackage/macosx/PkgScriptsTest.java | 2 + .../macosx/SigningPackageTwoStepTest.java | 9 +- .../tools/jpackage/share/AppContentTest.java | 92 ++- .../tools/jpackage/share/AppVersionTest.java | 76 +- test/jdk/tools/jpackage/share/BasicTest.java | 39 +- test/jdk/tools/jpackage/share/IconTest.java | 4 +- .../jpackage/share/PostImageScriptTest.java | 3 + .../tools/jpackage/windows/WinL10nTest.java | 124 +++- .../jpackage/windows/WinResourceTest.java | 9 + .../tools/jpackage/windows/WinScriptTest.java | 5 +- 119 files changed, 6087 insertions(+), 567 deletions(-) create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/LogConfigParser.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/log/CommandLogger.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/log/ConsoleLogger.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/log/ErrorLogger.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/log/I18N.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/log/LogEnvironment.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/log/Logger.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/log/LoggerRole.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/log/LoggerTrait.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/log/ProgressLogger.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/log/ResourceLogger.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/log/StandardLogger.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/log/SummaryLogger.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/log/SystemLoggerTrait.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/log/TraceLogger.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/log/Utils.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/summary/I18N.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/summary/Property.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/summary/StandardProperty.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/summary/StandardWarning.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/summary/Summary.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/summary/SummaryAccumulator.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/summary/SummaryItem.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/summary/Warning.java create mode 100644 test/jdk/tools/jpackage/helpers/jdk/jpackage/test/stdmock/DebToolsMock.java create mode 100644 test/jdk/tools/jpackage/helpers/jdk/jpackage/test/stdmock/DefaultDebToolsMock.java create mode 100644 test/jdk/tools/jpackage/helpers/jdk/jpackage/test/stdmock/DefaultMacToolsMock.java create mode 100644 test/jdk/tools/jpackage/helpers/jdk/jpackage/test/stdmock/DefaultRpmToolsMock.java create mode 100644 test/jdk/tools/jpackage/helpers/jdk/jpackage/test/stdmock/DefaultWixToolsMock.java create mode 100644 test/jdk/tools/jpackage/helpers/jdk/jpackage/test/stdmock/EnvironmentMock.java create mode 100644 test/jdk/tools/jpackage/helpers/jdk/jpackage/test/stdmock/LinuxPackageLookupMock.java create mode 100644 test/jdk/tools/jpackage/helpers/jdk/jpackage/test/stdmock/MacToolsMock.java create mode 100644 test/jdk/tools/jpackage/helpers/jdk/jpackage/test/stdmock/RpmToolsMock.java create mode 100644 test/jdk/tools/jpackage/helpers/jdk/jpackage/test/stdmock/WixToolsMock.java create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/LogConfigParserTest.java create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/log/AllLoggers.java create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/log/UtilsTest.java create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/summary/StandardSummaryTest.java create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/summary/SummaryTest.java diff --git a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/DesktopIntegration.java b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/DesktopIntegration.java index 523b6c4821c..fad901699c3 100644 --- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/DesktopIntegration.java +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/DesktopIntegration.java @@ -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; } diff --git a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LibProvidersLookup.java b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LibProvidersLookup.java index 2dccc91cf8f..5b0c518a6bd 100644 --- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LibProvidersLookup.java +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LibProvidersLookup.java @@ -66,16 +66,17 @@ public final class LibProvidersLookup { // Get the list of unique package names. List neededPackages = neededLibs.stream().map(libPath -> { try { - List 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 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 packageNames = Collections.emptyList(); - return packageNames; + Log.trace(ex, "Failed to get required packages for [%s]", libPath); + return List.of(); } }).flatMap(List::stream).sorted().distinct().toList(); @@ -83,10 +84,10 @@ public final class LibProvidersLookup { } private static List 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; diff --git a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxBundlingEnvironment.java b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxBundlingEnvironment.java index 724aeabb4b3..baf5e0bbbf0 100644 --- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxBundlingEnvironment.java +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxBundlingEnvironment.java @@ -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 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 adjustPackageArch(LinuxSystemEnvironment sysEnv, StandardPackageType type) { Objects.requireNonNull(sysEnv); Objects.requireNonNull(type); diff --git a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxDebPackager.java b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxDebPackager.java index d46ec8865c8..65efd9fc4c6 100644 --- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxDebPackager.java +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxDebPackager.java @@ -108,8 +108,9 @@ final class LinuxDebPackager extends LinuxPackager { Map 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 { List 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 { 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); diff --git a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxFromOptions.java b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxFromOptions.java index 1f954036431..7fd9fd80d7e 100644 --- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxFromOptions.java +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxFromOptions.java @@ -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()); + } } diff --git a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxPackageArch.java b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxPackageArch.java index b1df92ae312..841007a0994 100644 --- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxPackageArch.java +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxPackageArch.java @@ -47,7 +47,7 @@ record LinuxPackageArch(String value) { } private static Result 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); } diff --git a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxPackager.java b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxPackager.java index 551e1ab1af6..63f35fd6a65 100644 --- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxPackager.java +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxPackager.java @@ -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 implements Consumer implements Consumer 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 findRequiredPackages() throws IOException { @@ -160,16 +163,16 @@ abstract class LinuxPackager implements Consumer 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 implements Consumer requiredPackages = new ArrayList<>(); + private List requiredPackages; private final List customActions; } diff --git a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxRpmPackager.java b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxRpmPackager.java index 8c3e8125e9b..cc347b3b2c7 100644 --- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxRpmPackager.java +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxRpmPackager.java @@ -95,7 +95,7 @@ final class LinuxRpmPackager extends LinuxPackager { 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 { "-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); diff --git a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxSystemEnvironment.java b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxSystemEnvironment.java index e347c58ae21..d58d3e8b40d 100644 --- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxSystemEnvironment.java +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxSystemEnvironment.java @@ -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 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 diff --git a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/LinuxResources.properties b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/LinuxResources.properties index 1ab45d26a4c..dcdc96323ef 100644 --- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/LinuxResources.properties +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/LinuxResources.properties @@ -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 diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/AppImageSigner.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/AppImageSigner.java index 188430511bb..ee75e8ee36c 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/AppImageSigner.java +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/AppImageSigner.java @@ -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 codesignFile, Consumer codesignExecutableFile, Consumer codesignDir) implements Consumer { + private record Codesigners( + Consumer codesignFile, + Consumer codesignExecutableFile, + Consumer codesignMacBundle) implements Consumer { + 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> 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); } } diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/Codesign.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/Codesign.java index 984202bbfaf..154c8d01b81 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/Codesign.java +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/Codesign.java @@ -67,7 +67,7 @@ public final class Codesign { } return new Codesign(cmdline, quiet ? exec -> { - exec.setQuiet(true); + exec.quiet(); } : null); } diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/Keychain.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/Keychain.java index 8670eff2608..ddd5742b3ed 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/Keychain.java +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/Keychain.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -72,7 +72,7 @@ record Keychain(String name) { // Get the current keychain list final List 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); } diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacApplicationBuilder.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacApplicationBuilder.java index d6b816ca75e..64420d30983 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacApplicationBuilder.java +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacApplicationBuilder.java @@ -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() { 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 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> 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 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; diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacBundlingEnvironment.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacBundlingEnvironment.java index 1e60d8490f0..fdbdca95d12 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacBundlingEnvironment.java +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacBundlingEnvironment.java @@ -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; } diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacCertificateUtils.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacCertificateUtils.java index 24a236ae15d..1b5ca43ea92 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacCertificateUtils.java +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacCertificateUtils.java @@ -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() diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacDmgPackager.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacDmgPackager.java index 24474c88162..8ebfdd62cd7 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacDmgPackager.java +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacDmgPackager.java @@ -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 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); } } } diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacDmgSystemEnvironment.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacDmgSystemEnvironment.java index 11cf94ca66d..a72ff61ef31 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacDmgSystemEnvironment.java +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacDmgSystemEnvironment.java @@ -70,11 +70,11 @@ record MacDmgSystemEnvironment(Path hdiutil, Path osascript, Optional 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(() -> { diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacFromOptions.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacFromOptions.java index 2c247ed3989..e4a651ce916 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacFromOptions.java +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacFromOptions.java @@ -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 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; } diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPackageBuilder.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPackageBuilder.java index 1049496146f..5478e5cef96 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPackageBuilder.java +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPackageBuilder.java @@ -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 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; } diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPackagingPipeline.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPackagingPipeline.java index d75b7d0b9cd..25d97e15507 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPackagingPipeline.java +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPackagingPipeline.java @@ -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(); diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPkgPackageBuilder.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPkgPackageBuilder.java index 0a5cc09596f..9a4cc8136aa 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPkgPackageBuilder.java +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPkgPackageBuilder.java @@ -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 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; } diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPkgPackager.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPkgPackager.java index 369ab201611..b96e77d8960 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPkgPackager.java +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPkgPackager.java @@ -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 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 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 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 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"); diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/resources/MacResources.properties b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/resources/MacResources.properties index f181bbf79af..2586769a74b 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/resources/MacResources.properties +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/resources/MacResources.properties @@ -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. \ No newline at end of file +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 \ No newline at end of file diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ApplicationBuilder.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ApplicationBuilder.java index 0cca59f7c19..bb5f1a98a99 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ApplicationBuilder.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ApplicationBuilder.java @@ -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; }); diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/BuildEnv.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/BuildEnv.java index 527f747b229..5a793757d51 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/BuildEnv.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/BuildEnv.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -42,13 +42,6 @@ interface BuildEnv { */ Path buildRoot(); - /** - * Returns true if the build should be verbose output. - * - * @return true 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 resourceDir, boolean verbose, - Class resourceLocator, AppImageLayout appImageLayout) { - return new Internal.DefaultBuildEnv(buildRoot, resourceDir, verbose, - resourceLocator, appImageLayout); + static BuildEnv create( + Path buildRoot, + Optional resourceDir, + Class resourceLocator, + AppImageLayout appImageLayout) { + + return new Internal.DefaultBuildEnv( + buildRoot, + resourceDir, + resourceLocator, + appImageLayout); } static final class Internal { - private record DefaultBuildEnv(Path buildRoot, Optional resourceDir, - boolean verbose, Class resourceLocator, + + private record DefaultBuildEnv( + Path buildRoot, + Optional 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 diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/BuildEnvBuilder.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/BuildEnvBuilder.java index 17b805b8259..77354cb2c53 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/BuildEnvBuilder.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/BuildEnvBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -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; } diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/BuildEnvFromOptions.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/BuildEnvFromOptions.java index 8faabe97a3d..e418f957b41 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/BuildEnvFromOptions.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/BuildEnvFromOptions.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -28,7 +28,6 @@ import static 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); diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/DefaultBundlingEnvironment.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/DefaultBundlingEnvironment.java index ae4b1005318..c924b4a9973 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/DefaultBundlingEnvironment.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/DefaultBundlingEnvironment.java @@ -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 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 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())); }); } } diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/Executor.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/Executor.java index f9ccec7e3bb..a2558819b8f 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/Executor.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/Executor.java @@ -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 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 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 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) { diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/Globals.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/Globals.java index 2fc0046fad5..8755fa5fd08 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/Globals.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/Globals.java @@ -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 logger(OptionValue ov) { + return ov.getFrom(instance().logEnv); } public static int main(Supplier 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 properties = new HashMap<>(); private static final ScopedValue INSTANCE = ScopedValue.newInstance(); diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/IOUtils.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/IOUtils.java index 08cf0c10982..af428b30768 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/IOUtils.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/IOUtils.java @@ -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); + } + } } + diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/Log.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/Log.java index 5c27ef67500..bb28d39e52f 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/Log.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/Log.java @@ -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); } } diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ModuleInfo.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ModuleInfo.java index 9555423db62..15aa418e458 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ModuleInfo.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ModuleInfo.java @@ -56,13 +56,14 @@ record ModuleInfo(String name, Optional version, Optional 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(); } diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/OptionUtils.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/OptionUtils.java index b39e67a9eb7..b665656cbf9 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/OptionUtils.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/OptionUtils.java @@ -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 = OptionValue.create(); } diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/OverridableResource.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/OverridableResource.java index 73fc36d78ac..32a6f92ff60 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/OverridableResource.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/OverridableResource.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -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)); } } diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/PackagingPipeline.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/PackagingPipeline.java index 315e3bce0f1..161fa6053b0 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/PackagingPipeline.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/PackagingPipeline.java @@ -378,21 +378,21 @@ final class PackagingPipeline { private TaskBuilder logAppImageAction(ActionRole role, String keyId, Function, Object[]> formatArgsSupplier) { Objects.requireNonNull(keyId); return appImageAction(role, (AppImageBuildEnv env) -> { - Log.verbose(I18N.format(keyId, formatArgsSupplier.apply(env))); + Log.progress(I18N.format(keyId, formatArgsSupplier.apply(env))); }); } private TaskBuilder logPackageAction(ActionRole role, String keyId, Function, Object[]> formatArgsSupplier) { Objects.requireNonNull(keyId); return packageAction(role, (PackageBuildEnv 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 formatArgsSupplier) { Objects.requireNonNull(keyId); return action(role, () -> { - Log.verbose(I18N.format(keyId, formatArgsSupplier.get())); + Log.progress(I18N.format(keyId, formatArgsSupplier.get())); }); } diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/TempDirectory.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/TempDirectory.java index 345a42c5051..1fa586bee73 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/TempDirectory.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/TempDirectory.java @@ -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; } diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ToolValidator.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ToolValidator.java index 13a9e05e934..7ebeff08a73 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ToolValidator.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ToolValidator.java @@ -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()); diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/LogConfigParser.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/LogConfigParser.java new file mode 100644 index 00000000000..ff0b454161a --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/LogConfigParser.java @@ -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 tokenize(String str) { + Objects.requireNonNull(str); + + Supplier 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(); + + 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.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 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 CONSOLE_CATEGORIES = Stream.of(MessageCategory.values()) + .collect(Collectors.toUnmodifiableMap(MessageCategory::asStringValue, x -> x)); + + private static final Map> GROUPS = Map.ofEntries( + Map.entry("all", Set.of(MessageCategory.values())), + Map.entry("console", Stream.of(MessageCategory.values()) + .filter(MessageCategory::isConsole) + .collect(Collectors.toUnmodifiableSet())) + ); +} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/Main.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/Main.java index 5789203c3e9..c3277bc1a9a 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/Main.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/Main.java @@ -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(); + 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 stackTracePrinter, Consumer messagePrinter, boolean verbose) { - ErrorReporter { + public record ErrorReporter( + Consumer stackTracePrinter, + Consumer messagePrinter, + boolean alwaysPrintStackTrace, + boolean printCommandOutput) implements ErrorLogger { + + public ErrorReporter { Objects.requireNonNull(stackTracePrinter); Objects.requireNonNull(messagePrinter); } - ErrorReporter(Consumer stackTracePrinter, Consumer messagePrinter) { - this(stackTracePrinter, messagePrinter, true); + public ErrorReporter(Consumer stackTracePrinter, Consumer messagePrinter) { + this(stackTracePrinter, messagePrinter, true, false); } - void reportError(Throwable t) { + public void reportError(Throwable t) { var unfoldedExceptions = new ArrayList(); 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 advice) { var isSelfContained = isSelfContained(t); - if (!isSelfContained || verbose) { + if (!isSelfContained || alwaysPrintStackTrace) { stackTracePrinter.accept(t); } diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/OptionValue.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/OptionValue.java index fcd33826b95..1e617b8a457 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/OptionValue.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/OptionValue.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -81,7 +81,8 @@ public sealed interface OptionValue extends WithOptionIdentifier { } static final class Builder { - OptionValue create() { + + public OptionValue create() { if (conv != null) { return conv.create(Optional.ofNullable(defaultValue)); } else { @@ -92,7 +93,7 @@ public sealed interface OptionValue extends WithOptionIdentifier { } } - Builder defaultValue(T v) { + public Builder defaultValue(T v) { defaultValue = v; return this; } @@ -103,19 +104,19 @@ public sealed interface OptionValue extends WithOptionIdentifier { return this; } - Builder id(OptionIdentifier v) { + public Builder id(OptionIdentifier v) { id = v; conv = null; return this; } - Builder from(OptionValue base, Function conv) { + public Builder from(OptionValue base, Function conv) { id(null).spec(null); this.conv = new Conv<>(base, conv); return this; } - Builder to(Function conv) { + public Builder to(Function conv) { return OptionValue.build().from(create(), conv); } diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/StandardOption.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/StandardOption.java index f193cff0dc0..1cfdf326110 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/StandardOption.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/StandardOption.java @@ -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 VERSION = auxilaryOption("version").create(); - public static final OptionValue VERBOSE = auxilaryOption("verbose").create(); + static final OptionValue 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 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> nativeBundling() { return scope -> { - return new SetBuilder() - .set(scope) - .remove(new SetBuilder().set(StandardBundlingOperation.values()).remove(CREATE_NATIVE).create()) + return SetBuilder.build(scope) + .remove(SetBuilder.build(StandardBundlingOperation.values()).remove(CREATE_NATIVE).create()) .create(); }; } diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/log/CommandLogger.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/log/CommandLogger.java new file mode 100644 index 00000000000..247019fc627 --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/log/CommandLogger.java @@ -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 pid, Optional 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 sink, + Consumer 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 pid, Optional 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 sink(boolean quietCommand) { + return quietCommand ? quietCommandSink : sink; + } + + } + } + + static final CommandLogger DISCARDING_LOGGER = Utils.discardingLogger(CommandLogger.class); +} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/log/ConsoleLogger.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/log/ConsoleLogger.java new file mode 100644 index 00000000000..fbb5142d361 --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/log/ConsoleLogger.java @@ -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 out, Consumer err, Clock timestampClock) { + + public ConsoleLogger { + Objects.requireNonNull(out); + Objects.requireNonNull(err); + Objects.requireNonNull(timestampClock); + } + + public ConsoleLogger(Consumer out, Consumer 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 addTimestamps(Consumer 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 sink, Clock clock) implements Consumer { + + 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"); + } + +} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/log/ErrorLogger.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/log/ErrorLogger.java new file mode 100644 index 00000000000..01612b1732b --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/log/ErrorLogger.java @@ -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); +} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/log/I18N.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/log/I18N.java new file mode 100644 index 00000000000..8699daac81c --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/log/I18N.java @@ -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") + ) + )); + } +} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/log/LogEnvironment.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/log/LogEnvironment.java new file mode 100644 index 00000000000..6a8ba0d76a3 --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/log/LogEnvironment.java @@ -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(); + 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(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 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 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 systemLoggerFactory; + private final Set booleanTraits = new HashSet<>(); + private final Map> enabledLoggers = new HashMap<>(); + } + + static Options create(Map> 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 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 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 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 traits) { + return create( + TraceLogger.class, + traits, + TraceLogger::create, + TraceLogger::create).orElse(TraceLogger.DISCARDING_LOGGER); + } + + static ResourceLogger createResourceLogger(Collection traits) { + return create( + ResourceLogger.class, + traits, + ResourceLogger::create, + ResourceLogger::create).orElse(ResourceLogger.DISCARDING_LOGGER); + } + + static SummaryLogger createSummaryLogger(Collection 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 createConsoleLoggerSink(Collection 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 createSystemLoggerSink(Collection traits) { + return filterTraitsOfType(SystemLoggerTrait.class, traits.stream()).map(SystemLoggerTrait::logger).findFirst(); + } + + private static Optional create( + Class loggerType, + Collection traits, + Function fromConsoleSinkCtor, + Function 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 Stream filterTraitsOfType( + Class type, Stream stream) { + Objects.requireNonNull(type); + return stream.filter(type::isInstance).map(type::cast); + } + + private static Consumer toStringConsumer(Optional pw) { + return pw.>map(v -> { + return v::println; + }).orElse(Utils.DISCARDER); + } + + private LogEnvironment() { + } +} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/log/Logger.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/log/Logger.java new file mode 100644 index 00000000000..21c9ed77452 --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/log/Logger.java @@ -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; + } +} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/log/LoggerRole.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/log/LoggerRole.java new file mode 100644 index 00000000000..9f96a7ebc6c --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/log/LoggerRole.java @@ -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 logger, Function, ? extends Logger> loggerCtor) { + this.logger = Objects.requireNonNull(logger); + this.loggerCtor = Objects.requireNonNull(loggerCtor); + } + + public OptionValue logger() { + return logger; + } + + Logger createLogger(Collection traits) { + return loggerCtor.apply(traits); + } + + private final OptionValue logger; + private final Function, ? extends Logger> loggerCtor; +} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/log/LoggerTrait.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/log/LoggerTrait.java new file mode 100644 index 00000000000..8d8e30d6488 --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/log/LoggerTrait.java @@ -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 { +} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/log/ProgressLogger.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/log/ProgressLogger.java new file mode 100644 index 00000000000..540dfa99cca --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/log/ProgressLogger.java @@ -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); +} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/log/ResourceLogger.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/log/ResourceLogger.java new file mode 100644 index 00000000000..564b23d5f1c --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/log/ResourceLogger.java @@ -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 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); +} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/log/StandardLogger.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/log/StandardLogger.java new file mode 100644 index 00000000000..cabdef963c3 --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/log/StandardLogger.java @@ -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 COMMAND_LOGGER = create(CommandLogger.DISCARDING_LOGGER); + + public static final OptionValue ERROR_LOGGER = create(ErrorLogger.DISCARDING_LOGGER); + + public static final OptionValue PROGRESS_LOGGER = create(ProgressLogger.DISCARDING_LOGGER); + + public static final OptionValue RESOURCE_LOGGER = create(ResourceLogger.DISCARDING_LOGGER); + + public static final OptionValue SUMMARY_LOGGER = create(SummaryLogger.DISCARDING_LOGGER); + + public static final OptionValue TRACE_LOGGER = create(TraceLogger.DISCARDING_LOGGER); + + private static OptionValue create(T defaultValue) { + Objects.requireNonNull(defaultValue); + return OptionValue.build().defaultValue(defaultValue).create(); + } + +} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/log/SummaryLogger.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/log/SummaryLogger.java new file mode 100644 index 00000000000..d3d4d9fc7c0 --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/log/SummaryLogger.java @@ -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 sinkInfo, Consumer 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); +} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/log/SystemLoggerTrait.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/log/SystemLoggerTrait.java new file mode 100644 index 00000000000..3aa875b2321 --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/log/SystemLoggerTrait.java @@ -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); + } +} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/log/TraceLogger.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/log/TraceLogger.java new file mode 100644 index 00000000000..a33dfde726d --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/log/TraceLogger.java @@ -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); +} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/log/Utils.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/log/Utils.java new file mode 100644 index 00000000000..1d5bd037ba3 --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/log/Utils.java @@ -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 discardingLogger(Class 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 teeLogger(Class type, List 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 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 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 implements InvocationHandler { + + protected LoggerHandler(Class 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 loggerMethods; + private final boolean loggerEnabled; + } + + private static Stream> unfoldInterface(Class interfaceType) { + return Stream.concat( + Stream.of(interfaceType), + Stream.of(interfaceType.getInterfaces() + ).flatMap(Utils::unfoldInterface)); + } + + static final Consumer DISCARDER = _ -> {}; +} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/StandardPackageType.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/StandardPackageType.java index 6fadc748ecc..4cf6ead48d2 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/StandardPackageType.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/StandardPackageType.java @@ -43,9 +43,11 @@ public enum StandardPackageType implements PackageType { } /** - * Gets file extension of this package type. - * E.g.: .msi, .dmg, .deb. - * @return file extension of this package type + * Gets file extension corresponding to the package type. E.g.: + * .msi, .dmg, .deb. + * + * @return file extension corresponding to the package type; the value starts + * with the period (.) character. */ public String suffix() { return suffix; diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/resources/HelpResources.properties b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/resources/HelpResources.properties index 7f570e71330..90f4a579bfe 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/resources/HelpResources.properties +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/resources/HelpResources.properties @@ -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. diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/resources/MainResources.properties b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/resources/MainResources.properties index 9dfd13d60bb..931f0622584 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/resources/MainResources.properties +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/resources/MainResources.properties @@ -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 = 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. diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/summary/I18N.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/summary/I18N.java new file mode 100644 index 00000000000..768e32b44b7 --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/summary/I18N.java @@ -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") + ) + )); + } +} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/summary/Property.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/summary/Property.java new file mode 100644 index 00000000000..e747f86cd42 --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/summary/Property.java @@ -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(); +} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/summary/StandardProperty.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/summary/StandardProperty.java new file mode 100644 index 00000000000..6c28d127be8 --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/summary/StandardProperty.java @@ -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 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 valueFormat() { + return valueFormat; + } + + @Override + public String formatLabel() { + return I18N.getString(label); + } + + private final String label; + private final Optional valueFormat; +} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/summary/StandardWarning.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/summary/StandardWarning.java new file mode 100644 index 00000000000..8702f47ef31 --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/summary/StandardWarning.java @@ -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 valueFormat() { + return valueFormat; + } + + private final Optional valueFormat; +} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/summary/Summary.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/summary/Summary.java new file mode 100644 index 00000000000..d4c36bcd5f0 --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/summary/Summary.java @@ -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 sinkInfo, Consumer 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 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 getProperty(BundleSpec bundle, + Function appPropertyGetter, + Function 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 Comparator, ?>> 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 items) implements Content { + MultiLineContent { + Objects.requireNonNull(header); + Objects.requireNonNull(items); + } + } + + private final Map, String> properties = new HashMap<>(); + private final Map, Content> warnings = new HashMap<>(); +} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/summary/SummaryAccumulator.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/summary/SummaryAccumulator.java new file mode 100644 index 00000000000..d190046360a --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/summary/SummaryAccumulator.java @@ -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 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 items); +} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/summary/SummaryItem.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/summary/SummaryItem.java new file mode 100644 index 00000000000..1a072e514b3 --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/summary/SummaryItem.java @@ -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 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(); + }); + } +} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/summary/Warning.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/summary/Warning.java new file mode 100644 index 00000000000..2f7400af16d --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/summary/Warning.java @@ -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 { +} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/SetBuilder.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/SetBuilder.java index d3d677cba50..0c4497e4248 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/SetBuilder.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/SetBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -28,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 { - public static SetBuilder build() { - return new SetBuilder<>(); + @SafeVarargs + @SuppressWarnings("varargs") + public static SetBuilder build(T... v) { + return new SetBuilder().add(v); + } + + public static SetBuilder build(Collection v) { + return new SetBuilder().add(v); } public SetBuilder set(Collection v) { @@ -67,6 +74,11 @@ public final class SetBuilder { return remove(List.of(v)); } + public SetBuilder mutate(Consumer> mutator) { + mutator.accept(this); + return this; + } + public SetBuilder clear() { values.clear(); return this; diff --git a/src/jdk.jpackage/share/man/jpackage.md b/src/jdk.jpackage/share/man/jpackage.md index 156b05307b7..3467c53e595 100644 --- a/src/jdk.jpackage/share/man/jpackage.md +++ b/src/jdk.jpackage/share/man/jpackage.md @@ -121,9 +121,76 @@ The `jpackage` tool will take as input a Java application and a Java run-time im : Vendor of the application -`--verbose` +`--verbose` <\[-\]key(,\[-\]key)*> -: 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 + ``` `--version` diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinBundlingEnvironment.java b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinBundlingEnvironment.java index e10bfb95abf..e81e2a26dc0 100644 --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinBundlingEnvironment.java +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinBundlingEnvironment.java @@ -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()); } - } diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinFromOptions.java b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinFromOptions.java index 59701777396..63cb2ac0fe7 100644 --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinFromOptions.java +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinFromOptions.java @@ -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) { diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinMsiPackager.java b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinMsiPackager.java index 977b7057ebb..4e9f1f256b2 100644 --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinMsiPackager.java +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinMsiPackager.java @@ -175,6 +175,7 @@ final class WinMsiPackager implements Consumer { 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 { 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 { .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 { 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"); diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixTool.java b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixTool.java index d0c5e6ca3b0..55e2ea0eb6b 100644 --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixTool.java +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixTool.java @@ -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 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 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.of(); } }).flatMap(List::stream) diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/WinResources.properties b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/WinResources.properties index a11a8a6b41e..05a264e856b 100644 --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/WinResources.properties +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/WinResources.properties @@ -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}. diff --git a/test/jdk/tools/jpackage/TEST.properties b/test/jdk/tools/jpackage/TEST.properties index 99af197b989..f7007731376 100644 --- a/test/jdk/tools/jpackage/TEST.properties +++ b/test/jdk/tools/jpackage/TEST.properties @@ -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 \ diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/CannedFormattedString.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/CannedFormattedString.java index 3ad92e47005..c9852d0140f 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/CannedFormattedString.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/CannedFormattedString.java @@ -73,15 +73,45 @@ public record CannedFormattedString(BiFunction formatt String format(); List 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(); + } + }; } } diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java index 42a5096d2c0..f73f8fe6077 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java @@ -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 { 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 { winMsiLogFile = cmd.winMsiLogFile; unpackedPackageDirectory = cmd.unpackedPackageDirectory; explicitVersion = cmd.explicitVersion; + logConfig = cmd.logConfig; } JPackageCommand createImmutableCopy() { @@ -994,6 +999,38 @@ public class JPackageCommand extends CommandArguments { return this; } + public JPackageCommand setEnabledMessageCategories(Set 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 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 messageCategoriesConsoleAll() { + return Stream.of(MessageCategory.values()).filter(MessageCategory::isConsole).collect(toSet()); + } + + public static Set 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 { 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 { 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 parseVerboseOptionValue(String str) { + return LogConfigParser.tokenize(str).stream() + .map(Enum::name) + .map(MessageCategory::valueOf) + .collect(toSet()); + } + + static String toVerboseOptionValue(Set 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>, 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 add, Set 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 filter(Set 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 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 { addArguments("--runtime-image", defaultRuntime); }); - if (!hasArgument("--verbose") && TKit.verboseJPackage() && !ignoreDefaultVerbose) { - addArgument("--verbose"); + if (!hasArgument("--verbose")) { + final Set 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 { private Path winMsiLogFile; private Path unpackedPackageDirectory; private String explicitVersion; + private LogConfig logConfig; private Set readOnlyPathAsserts = Set.of(ReadOnlyPathAssert.values()); private Set standardAsserts = Set.of(StandardAssert.values()); private List> validators = new ArrayList<>(); @@ -2097,6 +2288,10 @@ public class JPackageCommand extends CommandArguments { // `--runtime-image` parameter set. private static final Optional DEFAULT_RUNTIME_IMAGE = Optional.ofNullable(TKit.getConfigProperty("runtime-image")).map(Path::of); + private static final Set 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] diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TKit.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TKit.java index 04b828677ca..b587198bc1d 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TKit.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TKit.java @@ -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 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> 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; } } diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java index 98e9c4bfe61..22031d1a7fd 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java @@ -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 toShortPath(Path path) { diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/stdmock/DebToolsMock.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/stdmock/DebToolsMock.java new file mode 100644 index 00000000000..d460442d31b --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/stdmock/DebToolsMock.java @@ -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; + } +} diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/stdmock/DefaultDebToolsMock.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/stdmock/DefaultDebToolsMock.java new file mode 100644 index 00000000000..d9d258bb3cc --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/stdmock/DefaultDebToolsMock.java @@ -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 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)); + } +} diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/stdmock/DefaultMacToolsMock.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/stdmock/DefaultMacToolsMock.java new file mode 100644 index 00000000000..ba4b4b705a2 --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/stdmock/DefaultMacToolsMock.java @@ -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 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"; + } +} diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/stdmock/DefaultRpmToolsMock.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/stdmock/DefaultRpmToolsMock.java new file mode 100644 index 00000000000..bae03afca7b --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/stdmock/DefaultRpmToolsMock.java @@ -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 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" : ""); + } +} diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/stdmock/DefaultWixToolsMock.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/stdmock/DefaultWixToolsMock.java new file mode 100644 index 00000000000..987a7e85e8e --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/stdmock/DefaultWixToolsMock.java @@ -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 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); + } +} diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/stdmock/EnvironmentMock.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/stdmock/EnvironmentMock.java new file mode 100644 index 00000000000..386a86a5697 --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/stdmock/EnvironmentMock.java @@ -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 mocks(); + + default void applyTo(Script.Builder scriptBuilder) { + mocks().forEach(scriptBuilder::map); + } +} diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/stdmock/LinuxPackageLookupMock.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/stdmock/LinuxPackageLookupMock.java new file mode 100644 index 00000000000..129efcc0505 --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/stdmock/LinuxPackageLookupMock.java @@ -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 mocks() { + return List.of(new CommandMockSpec("ldd", CommandActionSpecs.build().exit(exit).create())); + } + + private final CommandMockExit exit; +} diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/stdmock/MacToolsMock.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/stdmock/MacToolsMock.java new file mode 100644 index 00000000000..bbe24f698b4 --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/stdmock/MacToolsMock.java @@ -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(); + } + } +} diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/stdmock/RpmToolsMock.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/stdmock/RpmToolsMock.java new file mode 100644 index 00000000000..eedc4cc5cae --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/stdmock/RpmToolsMock.java @@ -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; + } +} diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/stdmock/WixToolsMock.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/stdmock/WixToolsMock.java new file mode 100644 index 00000000000..5a51f3b390d --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/stdmock/WixToolsMock.java @@ -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; + } +} diff --git a/test/jdk/tools/jpackage/junit/TEST.properties b/test/jdk/tools/jpackage/junit/TEST.properties index f979d918a68..7b120ea5b1f 100644 --- a/test/jdk/tools/jpackage/junit/TEST.properties +++ b/test/jdk/tools/jpackage/junit/TEST.properties @@ -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 diff --git a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/BuildEnvTest.java b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/BuildEnvTest.java index c71ef307f0e..0d8f5e369a2 100644 --- a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/BuildEnvTest.java +++ b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/BuildEnvTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -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()); } } diff --git a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/PackagingPipelineTest.java b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/PackagingPipelineTest.java index de0ba67fece..1db439469f3 100644 --- a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/PackagingPipelineTest.java +++ b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/PackagingPipelineTest.java @@ -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() { diff --git a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/TempDirectoryTest.java b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/TempDirectoryTest.java index 221e7d9f433..a6f7c2bd0e1 100644 --- a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/TempDirectoryTest.java +++ b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/TempDirectoryTest.java @@ -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()); } } diff --git a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/LogConfigParserTest.java b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/LogConfigParserTest.java new file mode 100644 index 00000000000..0482f50cbc4 --- /dev/null +++ b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/LogConfigParserTest.java @@ -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.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 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 test_valueOf() { + + var testCases = new ArrayList(); + + 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 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 test_valueOf_negative() { + return List.of( + ",", + "logerrors", + "log,error", + "log,-all", + "-all", + "-console", + ",errors,,errors," + ); + } + + record TestSpec(String logEnvStr, Set 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 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 useLogger) { + this.useLogger = Objects.requireNonNull(useLogger); + } + + void applyTo(AllLoggers logger) { + useLogger.accept(logger); + } + + private final Consumer 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 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 findLogRecords(LogMessage logRecordSrc, Collection 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(Class type, Function 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 CannedLogger console( + Class type, Function 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 CannedLogger systemLogger( + Class type, Function ctor) { + Objects.requireNonNull(ctor); + return new CannedLogger<>(type, sink -> { + return ctor.apply(new SimpleSystemLogger(sink::println)); + }, LogSink.SYSTEM_LOGGER); + } + + private final CannedLogger cannedLogger; + private final Set 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 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 filterMutators(Class mutatorType) { + return mutators.stream().filter(mutatorType::isInstance); + } + + static Set toLogRecords(Collection categories) { + + var logRecords = new HashSet(); + + 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 toLogRecords(MessageCategory... categories) { + return toLogRecords(Set.of(categories)); + } + + private sealed interface LogRecordsMutator { + void mutate(Set logRecords); + } + + private record AddLogRecordsMutator(LogRecord value) implements LogRecordsMutator { + AddLogRecordsMutator { + Objects.requireNonNull(value); + } + + @Override + public void mutate(Set 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 logRecords) { + if (logRecords.contains(from)) { + logRecords.remove(from); + logRecords.add(to); + } + } + } + + private static Stream add(LogRecord... values) { + return Stream.of(values).map(AddLogRecordsMutator::new); + } + + private static Stream replace(LogRecord from, LogRecord to) { + return Stream.of(new ReplaceLogRecordsMutator(from, to)); + } + + private final List mutators; + } + + private record SimpleSystemLogger(Consumer 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 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)); + + } +} diff --git a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/MainTest.java b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/MainTest.java index 1b89d23c301..93d22df4d26 100644 --- a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/MainTest.java +++ b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/MainTest.java @@ -160,6 +160,10 @@ public class MainTest extends JUnitAdapter { } private static Collection 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 test_ErrorReporter() { var testCases = new ArrayList(); - 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 sink) { + private static void test_ErrorReporter_Exception(boolean alwaysPrintStackTrace, Consumer sink) { for (var makeCause : List.>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(); - 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 sink) { + private static void test_ErrorReporter_UnexpectedResultException( + boolean alwaysPrintStackTrace, boolean printCommandOutput, Consumer 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(); - 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 sink) { + private static void test_ErrorReporter_suppressedExceptions( + boolean alwaysPrintStackTrace, boolean printCommandOutput, Consumer 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(); - 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 expectOutput) { + record ErrorReporterTestSpec( + Exception cause, + boolean alwaysPrintStackTrace, + boolean printCommandOutput, + List expectOutput) { ErrorReporterTestSpec { Objects.requireNonNull(cause); @@ -625,26 +656,40 @@ public class MainTest extends JUnitAdapter { } static ErrorReporterTestSpec create( - Exception cause, boolean verbose, List expectOutput) { - return create(cause, cause, verbose, expectOutput); + Exception cause, + boolean alwaysPrintStackTrace, + boolean printCommandOutput, + List expectOutput) { + return create(cause, cause, alwaysPrintStackTrace, printCommandOutput, expectOutput); } static ErrorReporterTestSpec create( - Exception cause, Exception expect, boolean verbose, List expectOutput) { + Exception cause, + Exception expect, + boolean alwaysPrintStackTrace, + boolean printCommandOutput, + List 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 sink) { + static void expectExceptionFormatters( + Exception ex, + boolean alwaysPrintStackTrace, + boolean printCommandOutput, + Consumer 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 sink) { + static void expectOutputFragments( + Exception ex, + boolean alwaysPrintStackTrace, + boolean printCommandOutput, + Consumer 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("")); diff --git a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/OptionsValidationFailTest.java b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/OptionsValidationFailTest.java index 3381677cb61..160048db05b 100644 --- a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/OptionsValidationFailTest.java +++ b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/OptionsValidationFailTest.java @@ -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(); diff --git a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/help-linux.txt b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/help-linux.txt index 4eb94cc8bd2..8cb5b0c17cf 100644 --- a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/help-linux.txt +++ b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/help-linux.txt @@ -64,8 +64,35 @@ Generic Options: removed upon the task completion. --vendor 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. diff --git a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/help-macos.txt b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/help-macos.txt index dd4c000b8a1..607012c16b4 100644 --- a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/help-macos.txt +++ b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/help-macos.txt @@ -70,8 +70,35 @@ Generic Options: removed upon the task completion. --vendor 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. diff --git a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/help-windows.txt b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/help-windows.txt index e30a5bb7f9b..89c235ac3a3 100644 --- a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/help-windows.txt +++ b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/help-windows.txt @@ -64,8 +64,35 @@ Generic Options: removed upon the task completion. --vendor 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. diff --git a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/log/AllLoggers.java b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/log/AllLoggers.java new file mode 100644 index 00000000000..45b4a58699a --- /dev/null +++ b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/log/AllLoggers.java @@ -0,0 +1,73 @@ +/* + * 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.log; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.stream.Stream; +import jdk.jpackage.internal.cli.Options; +import jdk.jpackage.internal.util.CompositeProxy; + +public interface AllLoggers extends + Logger, + ErrorLogger, + CommandLogger, + TraceLogger, + ResourceLogger, + ProgressLogger, + SummaryLogger { + + public static AllLoggers create(Collection slices) { + var stub = new Logger() {}; + + var loggers = new ArrayList(slices); + loggers.add(stub); + + return CompositeProxy.build() + .allowUnreferencedSlices(true) + .objectConflictResolver((_, _, method, candidates) -> { + if (!Logger.class.isAssignableFrom(method.getDeclaringClass())) { + throw new IllegalArgumentException(); + } + return stub; + }) + .methodConflictResolver((_, _, method, _) -> { + return false; + }).create(AllLoggers.class, loggers.toArray()); + } + + public static AllLoggers create(Logger... slices) { + return create(List.of(slices)); + } + + public static AllLoggers teeLogger(List loggers) { + return Utils.teeLogger(AllLoggers.class, loggers); + } + + public static AllLoggers create(Options logEnv) { + return create(Stream.of(LoggerRole.values()).map(LoggerRole::logger).map(ov -> { + return (Logger)ov.getFrom(logEnv); + }).toList()); + } +} diff --git a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/log/UtilsTest.java b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/log/UtilsTest.java new file mode 100644 index 00000000000..6966bab8012 --- /dev/null +++ b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/log/UtilsTest.java @@ -0,0 +1,255 @@ +/* + * 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.log; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class UtilsTest { + + @ParameterizedTest + @ValueSource(classes = {TestLogger.class, AlwaysEnabledLogger.class}) + void test_discardingLogger(Class type) { + assertDiscardingLogger(Utils.discardingLogger(type)); + } + + @Test + void test_discardLogMessagesLogger_negative() { + + var logger = Utils.discardingLogger(MalformedLogger.class); + + assertFalse(logger.enabled()); + + assertThrows(AssertionError.class, () -> { + logger.enabled(new Object[0]); + }); + + assertThrows(AssertionError.class, () -> { + logger.foo(); + }); + + assertThrows(AssertionError.class, () -> { + logger.bar(); + }); + } + + @Test + void test_teeLogger() { + + var buf = new StringBuilder(); + + var a = new TestLogger() { + + @Override + public void foo() { + buf.append("a"); + } + + @Override + public void foo(Consumer sink) { + sink.accept("A"); + } + + }; + + var b = new AlwaysEnabledLogger() { + + @Override + public void foo() { + buf.append("b"); + } + + @Override + public void foo(Consumer sink) { + sink.accept("B"); + } + + }; + + for (var logger : List.of(a, b)) { + assertSame(logger, Utils.teeLogger(TestLogger.class, List.of(logger))); + } + + var logger = Utils.teeLogger(TestLogger.class, List.of(a, b)); + + assertTrue(logger.enabled()); + + logger.foo(); + logger.foo(buf::append); + logger.fooIfEnabled(buf); + + assertEquals("abABxy", buf.toString()); + } + + @Test + void test_teeLogger_empty() { + assertDiscardingLogger(Utils.teeLogger(TestLogger.class, List.of())); + } + + @Test + void test_teeLogger_disabled() { + + var disabled = new TestLogger() { + + @Override + public boolean enabled() { + return false; + } + + @Override + public void foo() { + throw new AssertionError(); + } + + @Override + public void foo(Consumer sink) { + throw new AssertionError(); + } + + }; + + var buf = new StringBuilder(); + + var a = new TestLogger() { + + @Override + public void foo() { + buf.append("a"); + } + + @Override + public void foo(Consumer sink) { + sink.accept("A"); + } + + }; + + assertDiscardingLogger(Utils.teeLogger(TestLogger.class, List.of(disabled))); + assertDiscardingLogger(Utils.teeLogger(TestLogger.class, List.of(disabled, disabled))); + assertSame(a, Utils.teeLogger(TestLogger.class, List.of(disabled, a, disabled))); + + var logger = Utils.teeLogger(TestLogger.class, List.of(disabled, a, disabled, a, a)); + + logger.foo(); + logger.foo(buf::append); + logger.fooIfEnabled(buf); + + assertEquals("aaaAAAxxx", buf.toString()); + } + + @Test + void test_teeLogger_negative() { + + var loggers = new ArrayList(); + loggers.add(null); + loggers.add(new Logger() {}); + + assertThrows(NullPointerException.class, () -> { + Utils.teeLogger(Logger.class, loggers); + }); + + var a = new Logger() { + }; + + assertThrows(IllegalArgumentException.class, () -> { + Utils.teeLogger(a.getClass(), List.of()); + }); + } + + private interface TestLogger extends Logger { + + void foo(); + + void foo(Consumer sink); + + default void foo(StringBuilder sb) { + sb.append("x"); + } + + default void fooIfEnabled(StringBuilder sb) { + foo(sb); + } + } + + private interface AlwaysEnabledLogger extends TestLogger { + + @Override + default boolean enabled() { + return true; + } + + @Override + default void foo(StringBuilder sb) { + if (enabled()) { + sb.append("y"); + } else { + TestLogger.super.foo(sb); + } + } + } + + private interface MalformedLogger extends Logger { + + int foo(); + + boolean enabled(Object...args); + + default int bar() { + throw new AssertionError(); + } + } + + private void assertDiscardingLogger(TestLogger logger) { + + assertFalse(logger.enabled()); + + logger.foo(); + + do { + var buf = new StringWriter(); + logger.foo(buf::append); + assertTrue(buf.toString().isEmpty()); + } while (false); + + do { + var sb = new StringBuilder(); + logger.foo(sb); + assertTrue(sb.isEmpty()); + } while (false); + + assertDoesNotThrow(logger::hashCode); + assertDoesNotThrow(logger::toString); + } +} diff --git a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/summary/StandardSummaryTest.java b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/summary/StandardSummaryTest.java new file mode 100644 index 00000000000..2fa1a109a9e --- /dev/null +++ b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/summary/StandardSummaryTest.java @@ -0,0 +1,596 @@ +/* + * 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.summary; + +import static jdk.jpackage.internal.util.PListWriter.writeDict; +import static jdk.jpackage.internal.util.PListWriter.writePList; +import static jdk.jpackage.internal.util.PListWriter.writeString; +import static jdk.jpackage.internal.util.XmlUtils.createXml; +import static jdk.jpackage.internal.util.XmlUtils.toXmlConsumer; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.regex.Pattern; +import java.util.spi.ToolProvider; +import java.util.stream.Stream; +import jdk.internal.util.OperatingSystem; +import jdk.jpackage.internal.Globals; +import jdk.jpackage.internal.cli.CliBundlingEnvironment; +import jdk.jpackage.internal.cli.Main; +import jdk.jpackage.internal.cli.Options; +import jdk.jpackage.internal.cli.StandardBundlingOperation; +import jdk.jpackage.internal.cli.StandardOption; +import jdk.jpackage.internal.model.BundlingOperationDescriptor; +import jdk.jpackage.internal.util.MacBundle; +import jdk.jpackage.internal.util.PListReader; +import jdk.jpackage.internal.util.PathUtils; +import jdk.jpackage.internal.util.SetBuilder; +import jdk.jpackage.internal.util.function.ThrowingRunnable; +import jdk.jpackage.test.Annotations.ParameterSupplier; +import jdk.jpackage.test.Annotations.Test; +import jdk.jpackage.test.AppImageFile; +import jdk.jpackage.test.CannedArgument; +import jdk.jpackage.test.CannedFormattedString; +import jdk.jpackage.test.JPackageCommand; +import jdk.jpackage.test.JPackageOutputValidator; +import jdk.jpackage.test.JUnitAdapter; +import jdk.jpackage.test.LinuxHelper; +import jdk.jpackage.test.MacHelper; +import jdk.jpackage.test.TKit; +import jdk.jpackage.test.mock.CommandActionSpecs; +import jdk.jpackage.test.mock.CommandMock; +import jdk.jpackage.test.mock.CommandMockSpec; +import jdk.jpackage.test.mock.Script; +import jdk.jpackage.test.stdmock.DebToolsMock; +import jdk.jpackage.test.stdmock.EnvironmentMock; +import jdk.jpackage.test.stdmock.JPackageMockUtils; +import jdk.jpackage.test.stdmock.MacToolsMock; +import jdk.jpackage.test.stdmock.RpmToolsMock; +import jdk.jpackage.test.stdmock.WixToolsMock; + +public class StandardSummaryTest extends JUnitAdapter { + + @Test + @ParameterSupplier + public void test(TestSpec spec) { + spec.test(); + } + + public static Collection test() { + + Set supportedOperatingSystems = JPackageMockUtils.availableBundlingEnvironments().keySet(); + + var supportedBundlingOperations = supportedOperatingSystems.stream() + .flatMap(StandardBundlingOperation::ofPlatform) + .toList(); + + var testCases = new ArrayList(); + + for (var op : supportedBundlingOperations) { + + var testCase = testSpec().op(op).addArgs("--app-version", "45.67.1"); + + testCase.properties(StandardProperty.VERSION); + + if (op == StandardBundlingOperation.SIGN_MAC_APP_IMAGE) { + testCase.properties(StandardProperty.MAC_SIGN_APP_IMAGE_OPERATION); + testCase.addArgs("--mac-app-image-sign-identity", "foo"); + } else { + testCase.properties(StandardProperty.OPERATION); + testCase.properties(StandardProperty.OUTPUT_BUNDLE); + } + + switch (op) { + case CREATE_LINUX_DEB, CREATE_LINUX_RPM -> { + testCase.properties(StandardProperty.LINUX_PACKAGE_NAME); + testCase.addArgs("--linux-app-release", "8"); + } + case CREATE_WIN_MSI, CREATE_WIN_EXE -> { + testCase.properties(StandardProperty.WIN_MSI_PRODUCT_CODE); + testCase.properties(StandardProperty.WIN_MSI_UPGRADE_CODE); + testCase.properties(StandardProperty.WIN_WIX_VERSION); + } + case CREATE_MAC_DMG, CREATE_MAC_PKG, CREATE_MAC_APP_IMAGE -> { + testCase.addArgs("--mac-package-identifier", "foo.bar"); + } + default -> { + // NOP + } + } + + if (op.os() == OperatingSystem.MACOS) { + testCase.properties(StandardProperty.MAC_BUNDLE_IDENTIFIER); + testCase.properties(StandardProperty.MAC_BUNDLE_NAME); + } + + testCases.add(testCase); + + } + + return testCases.stream().map(TestSpec.Builder::create).map(v -> { + return new Object[] {v}; + }).toList(); + } + + enum StandardProperty { + + // + // Keep the same order as in the jdk.jpackage.internal.summary.StandardProperty enum. + // + + 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"), + LINUX_ENABLE_REQUIRED_PACKAGES_SEARCH("summary.property.linux-required-packages-search"), + ; + + StandardProperty(String mainKey, Optional valueFormmatter) { + this.mainKey = Objects.requireNonNull(mainKey); + this.valueFormmatter = Objects.requireNonNull(valueFormmatter); + } + + StandardProperty(String mainKey, String valueFormmatter) { + this(mainKey, Optional.of(valueFormmatter)); + } + + StandardProperty(String mainKey) { + this(mainKey, Optional.empty()); + } + + String format(JPackageCommand cmd, List args) { + Objects.requireNonNull(cmd); + Objects.requireNonNull(args); + if (args.isEmpty()) { + if (valueFormmatter.isEmpty()) { + return I18N.getString(mainKey); + } else { + throw new IllegalArgumentException(String.format( + "Missing arguments for the formatter of property %", mainKey)); + } + } else { + return valueFormmatter.map(f -> { + return cmd.getValue(cannedFormattedString(f, args.toArray())); + }).or(() -> { + if (args.size() == 1) { + switch (args.getFirst()) { + case JPackageCommand.CannedArgument ca -> { + return Optional.of(ca.value(cmd)); + } + case CannedArgument ca -> { + return Optional.of(ca.getValue()); + } + case Object o -> { + return Optional.of(o.toString()); + } + } + } else { + return Optional.empty(); + } + }).map(propertyValue -> { + return String.format("%s: %s", I18N.getString(mainKey), propertyValue); + }).orElseThrow(() -> { + return new IllegalArgumentException(String.format("No formatter for property %s", mainKey)); + }); + } + } + + String format(JPackageCommand cmd, Object... args) { + return format(cmd, List.of(args)); + } + + String formatKey() { + return I18N.getString(mainKey); + } + + boolean isAvailableOn(OperatingSystem os) { + if (name().startsWith("LINUX_")) { + return os == OperatingSystem.LINUX; + } else if (name().startsWith("MAC_")) { + return os == OperatingSystem.MACOS; + } else if (name().startsWith("WIN_")) { + return os == OperatingSystem.WINDOWS; + } else { + // Shared property. + return true; + } + } + + private final String mainKey; + private final Optional valueFormmatter; + } + + record TestSpec( + StandardBundlingOperation op, + Optional env, + List addArgs, + List removeArgs, + Set expectedProperties) { + + TestSpec { + Objects.requireNonNull(op); + Objects.requireNonNull(env); + Objects.requireNonNull(addArgs); + Objects.requireNonNull(removeArgs); + + Objects.requireNonNull(expectedProperties); + if (expectedProperties.isEmpty()) { + throw new IllegalArgumentException("The list of expected properties must be non-empty"); + } + } + + static final class Builder { + + Builder() { + } + + Builder op(StandardBundlingOperation v) { + op = v; + return this; + } + + Builder env(EnvironmentMock v) { + env = v; + return this; + } + + Builder setAddArgs(List v) { + addArgs.clear(); + addArgs.addAll(v); + return this; + } + + Builder setAddArgs(String... v) { + return setAddArgs(List.of(v)); + } + + Builder addArgs(List v) { + addArgs.addAll(v); + return this; + } + + Builder addArgs(String... v) { + return addArgs(List.of(v)); + } + + Builder setRemoveArgs(List v) { + removeArgs.clear(); + removeArgs.addAll(v); + return this; + } + + Builder setRemoveArgs(String... v) { + return setRemoveArgs(List.of(v)); + } + + Builder removeArgs(List v) { + removeArgs.addAll(v); + return this; + } + + Builder removeArgs(String... v) { + return removeArgs(List.of(v)); + } + + Builder properties(List v) { + expectedProperties.addAll(v); + return this; + } + + Builder properties(StandardProperty... v) { + return properties(List.of(v)); + } + + TestSpec create() { + return new TestSpec( + Objects.requireNonNull(op), + Optional.ofNullable(env).or(() -> { + return defaultEnvironment(op); + }), + List.copyOf(addArgs), + List.copyOf(removeArgs), + Set.copyOf(expectedProperties)); + } + + private static Optional defaultEnvironment(StandardBundlingOperation op) { + Objects.requireNonNull(op); + switch (op) { + case CREATE_LINUX_RPM -> { + return Optional.of(RpmToolsMock.build().version("4.15").create()); + } + case CREATE_LINUX_DEB -> { + return Optional.of(DebToolsMock.build().versionDpkg("1.18.4").versionFakeroot("1.20.2").create()); + } + case CREATE_MAC_DMG -> { + return Optional.of(MacToolsMock.build().create()); + } + case CREATE_WIN_EXE, CREATE_WIN_MSI -> { + return Optional.of(WixToolsMock.build().version("5.43+21").create()); + } + default -> { + return Optional.empty(); + } + } + } + + private StandardBundlingOperation op; + private EnvironmentMock env; + private List addArgs = new ArrayList<>(); + private List removeArgs = new ArrayList<>(); + private Set expectedProperties = new HashSet<>(); + } + + void test() { + ThrowingRunnable withOperatingSystem = () -> { + Globals.main(() -> { + env.ifPresent(e -> { + var scriptBuilder = Script.build().commandMockBuilderMutator(CommandMock.Builder::repeatInfinitely); + e.applyTo(scriptBuilder); + switch (op) { + case CREATE_LINUX_RPM -> { + // Divert the jpackage from the default DEB packaging. + scriptBuilder.map(new CommandMockSpec("dpkg", CommandActionSpecs.build().exit(1).create())); + } + default -> { + // NOP + } + } + JPackageMockUtils.buildJPackage().script(scriptBuilder.createLoop()).applyToGlobals(); + }); + testInternal(); + return 0; + }); + }; + + TKit.withOperatingSystem(withOperatingSystem, op.os()); + } + + private List orderedExpectedProperties() { + return expectedProperties.stream().sorted(Comparator.comparing(Enum::ordinal)).toList(); + } + + private void testInternal() { + + JPackageCommand cmd; + if (op != StandardBundlingOperation.SIGN_MAC_APP_IMAGE) { + cmd = JPackageCommand.helloAppImage(); + } else { + var version = new JPackageCommand().addArguments(addArgs).version(); + + cmd = new JPackageCommand(); + cmd.addArguments("--app-image", createMacAppImageMock(version)); + cmd.addArguments("--mac-sign"); + } + + removeArgs.forEach(cmd::removeArgumentWithValue); + cmd.addArguments(addArgs); + if (op == StandardBundlingOperation.SIGN_MAC_APP_IMAGE) { + cmd.removeArgumentWithValue("--app-version"); + } + + cmd.setArgumentValue("--type", op.bundleTypeValue()).removeArgument("--win-console"); + + cmd.useToolProvider(configureOnlyJPackage(op.os())); + + cmd.setArgumentValue("--verbose", "summary,warnings,errors"); + + // Look up expected properties in the output. + orderedExpectedProperties().stream().map(p -> { + return toOutputVerifier(cmd, p); + }).reduce(new JPackageOutputValidator(), + JPackageOutputValidator::add, + JPackageOutputValidator::add).applyTo(cmd); + + Function, String[]> uniquePrefixes = stream -> { + return stream.filter(prop -> { + return prop.isAvailableOn(op.os()); + }).map(StandardProperty::formatKey).sorted().distinct().toArray(String[]::new); + }; + + // Look up unexpected properties in the output. + SetBuilder.build(uniquePrefixes.apply(Stream.of(StandardProperty.values()))) + .remove(uniquePrefixes.apply(expectedProperties.stream())) + .emptyAllowed(true) + .create().stream().map(prefixStr -> { + return TKit.assertTextStream(prefixStr).negate(); + }).reduce(new JPackageOutputValidator(), + JPackageOutputValidator::add, + JPackageOutputValidator::add).applyTo(cmd); + + // Don't run output bundle validators because there is no output bundle, + // but still check that the jpackage succeeds. + cmd.executeIgnoreExitCode().assertExitCodeIsZero(); + } + + private TKit.TextStreamVerifier toOutputVerifier(JPackageCommand cmd, StandardProperty p) { + Objects.requireNonNull(p); + List formatArgs = null; + Pattern pattern = null; + switch (p) { + case OPERATION -> { + formatArgs = List.of(op.bundleType().label()); + } + case MAC_SIGN_APP_IMAGE_OPERATION -> { + formatArgs = List.of(op.bundleType().label(), PathUtils.normalizedAbsolutePath(cmd.outputBundle())); + } + case OUTPUT_BUNDLE -> { + formatArgs = List.of(PathUtils.normalizedAbsolutePath(cmd.outputBundle())); + } + case LINUX_PACKAGE_NAME -> { + formatArgs = List.of(LinuxHelper.getPackageName(cmd)); + } + case WIN_MSI_PRODUCT_CODE, WIN_MSI_UPGRADE_CODE -> { + pattern = Pattern.compile(Pattern.quote(p.formatKey()) + + ".+[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"); + } + case VERSION -> { + formatArgs = List.of(cmd.fullVersion()); + } + case WIN_WIX_VERSION -> { + var wixVersion = env.filter(WixToolsMock.class::isInstance).map(WixToolsMock.class::cast).map(WixToolsMock::version); + if (wixVersion.isPresent()) { + formatArgs = List.of(wixVersion.get()); + } + } + case LINUX_DISABLE_REQUIRED_PACKAGES_SEARCH -> { + formatArgs = List.of(I18N.getString("summary.value.disabled")); + } + case LINUX_ENABLE_REQUIRED_PACKAGES_SEARCH -> { + formatArgs = List.of(I18N.getString("summary.value.enabled")); + } + case MAC_BUNDLE_IDENTIFIER -> { + if (op == StandardBundlingOperation.SIGN_MAC_APP_IMAGE) { + PListReader plist = MacHelper.readPListFromAppImage(Path.of(cmd.getArgumentValue("--app-image"))); + formatArgs = List.of(plist.queryValue("CFBundleIdentifier")); + } else { + formatArgs = List.of(cmd.getArgumentValue("--mac-package-identifier")); + } + } + case MAC_BUNDLE_NAME -> { + if (op == StandardBundlingOperation.SIGN_MAC_APP_IMAGE) { + PListReader plist = MacHelper.readPListFromAppImage(Path.of(cmd.getArgumentValue("--app-image"))); + formatArgs = List.of(plist.queryValue("CFBundleName")); + } else { + formatArgs = List.of(cmd.name()); + } + } + default -> { + throw new AssertionError(); + } + } + + if (pattern == null && formatArgs == null) { + return TKit.assertTextStream(p.format(cmd)).predicate(String::startsWith); + } else if (formatArgs == null) { + return TKit.assertTextStream(pattern).predicate(pattern.asMatchPredicate()); + } else if (pattern == null) { + return TKit.assertTextStream(p.format(cmd, formatArgs.toArray())).predicate(String::equals); + } else { + throw new AssertionError(); + } + } + + @Override + public final String toString() { + final var sb = new StringBuilder(); + sb.append(op); + env.ifPresent(v -> { + sb.append("; [").append(v).append("]"); + }); + if (!addArgs.isEmpty()) { + sb.append("; args-add=").append(addArgs); + } + if (!removeArgs.isEmpty()) { + sb.append("; args-del=").append(removeArgs); + } + sb.append("; properties=").append(orderedExpectedProperties()); + return sb.toString(); + } + } + + private static TestSpec.Builder testSpec() { + return new TestSpec.Builder(); + } + + private static ToolProvider configureOnlyJPackage(OperatingSystem os) { + return new Main.Provider(() -> { + return new CliBundlingEnvironment() { + + @Override + public Optional defaultOperation() { + return bundlingEnv.defaultOperation(); + } + + @Override + public void createBundle(BundlingOperationDescriptor descriptor, Options cmdline) { + bundlingEnv.createBundle(descriptor, cmdline.copyWithParent(EXIT_AFTER_CONFIGURATION_PHASE)); + } + + private final CliBundlingEnvironment bundlingEnv = JPackageMockUtils.createBundlingEnvironment(os); + + private static final Options EXIT_AFTER_CONFIGURATION_PHASE = Options.of(Map.of( + StandardOption.EXIT_AFTER_CONFIGURATION_PHASE, Boolean.TRUE)); + }; + }, os); + } + + private static CannedFormattedString cannedFormattedString(String key, Object ... args) { + return new CannedFormattedString(I18N::format, key, List.of(args)); + } + + private static Path createMacAppImageMock(String version) { + Objects.requireNonNull(version); + + final var appImageRoot = TKit.createTempDirectory("appimage"); + + try { + new AppImageFile( + "foo", + Optional.of("org.foo.Hello"), + version, + false, + Map.of("foo", Map.of())).save(appImageRoot); + + var macBundle = new MacBundle(appImageRoot); + + Files.createDirectories(macBundle.macOsDir()); + + createXml(macBundle.infoPlistFile(), xml -> { + writePList(xml, toXmlConsumer(() -> { + writeDict(xml, toXmlConsumer(() -> { + writeString(xml, "CFBundleIdentifier", "com.acme"); + writeString(xml, "CFBundleName", "Hello"); + writeString(xml, "NSHumanReadableCopyright", "Copyright"); + writeString(xml, "CFBundleShortVersionString", version); + writeString(xml, "CFBundleVersion", version); + writeString(xml, "LSApplicationCategoryType", "utilities"); + })); + })); + }); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + + return appImageRoot; + } +} diff --git a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/summary/SummaryTest.java b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/summary/SummaryTest.java new file mode 100644 index 00000000000..db9d5e277aa --- /dev/null +++ b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/summary/SummaryTest.java @@ -0,0 +1,150 @@ +/* + * 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.summary; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class SummaryTest { + + @Test + void test_empty() { + assertSummary(new Summary()); + } + + @ParameterizedTest + @MethodSource + void test_stable_order(List expected, List> entries) { + + var summary = new Summary(); + + for (var e : entries) { + summary.put(e.getKey(), e.getValue()); + } + + assertSummary(summary, expected); + } + + private static Stream test_stable_order() { + var items = List.of( + Map.entry(TestProperty.FOO, "foo"), + Map.entry(TestProperty.BAR, "bar"), + Map.entry(TestWarning.WFOO, "foo!"), + Map.entry(TestWarning.WBAR, "bar!") + ); + + var expected = items.stream().map(Map.Entry::getValue).toList(); + + // Hardcoded permutations + return Stream.of( + List.of(0,1,2,3), + List.of(1,0,2,3), + List.of(2,0,1,3), + List.of(0,2,1,3), + List.of(1,2,0,3), + List.of(2,1,0,3), + List.of(2,1,3,0), + List.of(1,2,3,0), + List.of(3,2,1,0), + List.of(2,3,1,0), + List.of(1,3,2,0), + List.of(3,1,2,0), + List.of(3,0,2,1), + List.of(0,3,2,1), + List.of(2,3,0,1), + List.of(3,2,0,1), + List.of(0,2,3,1), + List.of(2,0,3,1), + List.of(1,0,3,2), + List.of(0,1,3,2), + List.of(3,1,0,2), + List.of(1,3,0,2), + List.of(0,3,1,2), + List.of(3,0,1,2) + ).map(indexes -> { + return indexes.stream().map(items::get).toList(); + }).map(v -> { + return Arguments.of(expected, v); + }); + } + + private static void assertSummary(Summary summary, String... expectedValues) { + assertSummary(summary, List.of(expectedValues)); + } + + private static void assertSummary(Summary summary, List expectedValues) { + var curExpectedTail = expectedValues.iterator(); + Consumer sink = line -> { + var expectedTail = curExpectedTail.next(); + assertTrue(line.endsWith(expectedTail), String.format("Assert summary line [%s] ends with [%s] substring", line, expectedTail)); + }; + summary.print(sink, sink); + } + + private enum TestProperty implements Property { + FOO, + BAR, + ; + + @Override + public Optional valueFormat() { + throw new UnsupportedOperationException(); + } + + @Override + public String formatValue(Object... valueFormatArgs) { + throw new UnsupportedOperationException(); + } + + @Override + public String formatLabel() { + return name(); + } + } + + private enum TestWarning implements Warning { + WFOO, + WBAR, + ; + + @Override + public Optional valueFormat() { + throw new UnsupportedOperationException(); + } + + @Override + public String formatValue(Object... valueFormatArgs) { + throw new UnsupportedOperationException(); + } + } +} diff --git a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/util/CommandOutputControlTest.java b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/util/CommandOutputControlTest.java index b179f32447f..2ddfbcb5d78 100644 --- a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/util/CommandOutputControlTest.java +++ b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/util/CommandOutputControlTest.java @@ -1248,7 +1248,7 @@ public class CommandOutputControlTest { static Function> addToSet(Set set) { return m -> { - return new SetBuilder().add(set).add(m).create(); + return SetBuilder.build(set).add(m).create(); }; } } diff --git a/test/jdk/tools/jpackage/linux/LinuxResourceTest.java b/test/jdk/tools/jpackage/linux/LinuxResourceTest.java index 503c4db77fb..9915dfd64dc 100644 --- a/test/jdk/tools/jpackage/linux/LinuxResourceTest.java +++ b/test/jdk/tools/jpackage/linux/LinuxResourceTest.java @@ -28,8 +28,8 @@ import java.nio.file.Path; import java.util.List; import java.util.Objects; import jdk.jpackage.test.Annotations.Test; -import jdk.jpackage.test.CannedFormattedString; import jdk.jpackage.test.JPackageCommand; +import jdk.jpackage.test.JPackageCommand.MessageCategory; import jdk.jpackage.test.JPackageCommand.StandardAssert; import jdk.jpackage.test.JPackageOutputValidator; import jdk.jpackage.test.LinuxHelper; @@ -59,6 +59,8 @@ public class LinuxResourceTest { cmd .setFakeRuntime() .saveConsoleOutput(true) + .setEnabledMessageCategories(MessageCategory.RESOURCES, MessageCategory.PROGRESS, MessageCategory.WARNINGS) + .setDisabledMessageCategories() .addArguments("--resource-dir", TKit.createTempDirectory("resources")); }) .forTypes(PackageType.LINUX_DEB) @@ -185,14 +187,13 @@ public class LinuxResourceTest { final var customResourcePath = customResourcePath(); - new JPackageOutputValidator() - .expectMatchingStrings( - MAIN.cannedFormattedString("error.unexpected-package-property", name, expectedValue, customValue, customResourcePath), - MAIN.cannedFormattedString("error.unexpected-package-property.advice", token, customValue, name, customResourcePath) - ) - .matchTimestamps() - .stripTimestamps() - .applyTo(cmd); + new JPackageOutputValidator().stderr().expectMatchingStrings( + JPackageCommand.makeProgressWarning("error.unexpected-package-property", name, expectedValue, customValue, customResourcePath) + ).applyTo(cmd); + + new JPackageOutputValidator().expectMatchingStrings( + MAIN.cannedFormattedString("error.unexpected-package-property.advice", token, customValue, name, customResourcePath) + ).matchTimestamps().stripTimestamps().applyTo(cmd); } private Path customResourcePath() { diff --git a/test/jdk/tools/jpackage/macosx/MacPropertiesTest.java b/test/jdk/tools/jpackage/macosx/MacPropertiesTest.java index f8638735a6a..8385c2a8127 100644 --- a/test/jdk/tools/jpackage/macosx/MacPropertiesTest.java +++ b/test/jdk/tools/jpackage/macosx/MacPropertiesTest.java @@ -21,6 +21,8 @@ * questions. */ +import static jdk.jpackage.test.JPackageCommand.MessageCategory.TRACE; + import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -76,17 +78,17 @@ public class MacPropertiesTest { } enum BundleIdentifierMessage implements CannedFormattedString.Spec { - VALUE("message.derived-bundle-identifier", "bundle-id"), + VALUE("TRACE: Derived bundle identifier: {0}", "bundle-id"), ; - BundleIdentifierMessage(String key, Object ... args) { - this.key = Objects.requireNonNull(key); + BundleIdentifierMessage(String format, Object ... args) { + this.format = Objects.requireNonNull(format); this.args = List.of(args); } @Override public String format() { - return key; + return format; } @Override @@ -94,7 +96,12 @@ public class MacPropertiesTest { return args; } - private final String key; + @Override + public CannedFormattedString.Spec.Formatter formatter() { + return CannedFormattedString.Spec.Formatter.MESSAGE_FORMAT; + } + + private final String format; private final List args; } @@ -118,10 +125,13 @@ public class MacPropertiesTest { void run() { var cmd = appDesc.map(JPackageCommand::helloAppImage).orElseGet(JPackageCommand::helloAppImage) - .setFakeRuntime().addArguments(addArgs); + .setFakeRuntime() + .setEnabledMessageCategories(TRACE); delArgs.forEach(cmd::removeArgumentWithValue); + cmd.addArguments(addArgs); + Consumer validatorMutator = validator -> { validator.matchTimestamps().stripTimestamps(); }; diff --git a/test/jdk/tools/jpackage/macosx/MacSignTest.java b/test/jdk/tools/jpackage/macosx/MacSignTest.java index 67d4258786b..60d17a841a0 100644 --- a/test/jdk/tools/jpackage/macosx/MacSignTest.java +++ b/test/jdk/tools/jpackage/macosx/MacSignTest.java @@ -77,10 +77,10 @@ public class MacSignTest { final var validator = new JPackageOutputValidator().stderr(); - validator.expectMatchingStrings(JPackageStringBundle.MAIN.cannedFormattedString( + validator.expectMatchingStrings(JPackageCommand.makeProgressWarning( "message.codesign.failed.reason.app.content")); - final var xcodeWarning = TKit.assertTextStream(JPackageStringBundle.MAIN.cannedFormattedString( + final var xcodeWarning = TKit.assertTextStream(JPackageCommand.makeProgressWarning( "message.codesign.failed.reason.xcode.tools").getValue()).predicate(String::equals); if (!MacHelper.isXcodeDevToolsInstalled()) { @@ -229,8 +229,8 @@ public class MacSignTest { * identities without validation to signing commands, signing a .pkg installer * with a name matching two signing identities succeeds. */ - new JPackageOutputValidator().stdout() - .expectMatchingStrings(JPackageStringBundle.MAIN.cannedFormattedString("warning.unsigned.app.image", "pkg")) + new JPackageOutputValidator().stderr() + .expectMatchingStrings(JPackageCommand.makeSummaryWarning("warning.unsigned.app.image", "pkg")) .validateEndOfStream() .applyTo(cmd); diff --git a/test/jdk/tools/jpackage/macosx/PkgScriptsTest.java b/test/jdk/tools/jpackage/macosx/PkgScriptsTest.java index 5b2a8b63020..00b86025782 100644 --- a/test/jdk/tools/jpackage/macosx/PkgScriptsTest.java +++ b/test/jdk/tools/jpackage/macosx/PkgScriptsTest.java @@ -32,6 +32,7 @@ import java.util.stream.Stream; import jdk.jpackage.test.Annotations.ParameterSupplier; import jdk.jpackage.test.Annotations.Test; import jdk.jpackage.test.JPackageCommand; +import jdk.jpackage.test.JPackageCommand.MessageCategory; import jdk.jpackage.test.JPackageOutputValidator; import jdk.jpackage.test.JPackageStringBundle; import jdk.jpackage.test.MacHelper; @@ -86,6 +87,7 @@ public class PkgScriptsTest { cmd.addArgument("--mac-app-store"); } cmd.addArguments("--resource-dir", TKit.createTempDirectory("resources")); + cmd.setEnabledMessageCategories(MessageCategory.RESOURCES).setDisabledMessageCategories(); customScripts.forEach(customScript -> { customScript.createFor(cmd); }); diff --git a/test/jdk/tools/jpackage/macosx/SigningPackageTwoStepTest.java b/test/jdk/tools/jpackage/macosx/SigningPackageTwoStepTest.java index d0de9c8c704..86ee185e882 100644 --- a/test/jdk/tools/jpackage/macosx/SigningPackageTwoStepTest.java +++ b/test/jdk/tools/jpackage/macosx/SigningPackageTwoStepTest.java @@ -33,7 +33,6 @@ import jdk.jpackage.test.Annotations.ParameterSupplier; import jdk.jpackage.test.Annotations.Test; import jdk.jpackage.test.CannedFormattedString; import jdk.jpackage.test.JPackageCommand; -import jdk.jpackage.test.JPackageStringBundle; import jdk.jpackage.test.MacHelper.ResolvableCertificateRequest; import jdk.jpackage.test.MacHelper.SignKeyOption; import jdk.jpackage.test.MacHelper.SignKeyOptionWithKeychain; @@ -189,12 +188,12 @@ public class SigningPackageTwoStepTest { } private static void configureOutputValidator(JPackageCommand cmd, boolean signAppImage, boolean signPackage) { - var signedPredefinedAppImageWarning = JPackageStringBundle.MAIN.cannedFormattedString( + var signedPredefinedAppImageWarning = JPackageCommand.makeSummaryWarning( "warning.per.user.app.image.signed", PackageFile.getPathInAppImage(Path.of(""))); var signedInstallerFromUnsignedPredefinedAppImageWarning = - JPackageStringBundle.MAIN.cannedFormattedString("warning.unsigned.app.image", "pkg"); + JPackageCommand.makeSummaryWarning("warning.unsigned.app.image", "pkg"); // The warnings are mutually exclusive final Optional expected; @@ -212,9 +211,9 @@ public class SigningPackageTwoStepTest { } } - expected.ifPresent(cmd::validateOut); + expected.ifPresent(cmd::validateErr); unexpected.forEach(str -> { - cmd.validateOut(TKit.assertTextStream(cmd.getValue(str)).negate()); + cmd.validateErr(TKit.assertTextStream(cmd.getValue(str)).negate()); }); } } diff --git a/test/jdk/tools/jpackage/share/AppContentTest.java b/test/jdk/tools/jpackage/share/AppContentTest.java index 46f5286e48a..66b7aaa421c 100644 --- a/test/jdk/tools/jpackage/share/AppContentTest.java +++ b/test/jdk/tools/jpackage/share/AppContentTest.java @@ -44,6 +44,7 @@ import java.util.TreeMap; import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; +import java.util.regex.Pattern; import java.util.stream.Stream; import java.util.stream.StreamSupport; import jdk.jpackage.internal.util.FileUtils; @@ -52,8 +53,12 @@ import jdk.jpackage.internal.util.function.ThrowingSupplier; import jdk.jpackage.test.Annotations.Parameter; import jdk.jpackage.test.Annotations.ParameterSupplier; import jdk.jpackage.test.Annotations.Test; +import jdk.jpackage.test.CannedFormattedString; import jdk.jpackage.test.ConfigurationTarget; +import jdk.jpackage.test.FailedCommandErrorValidator; import jdk.jpackage.test.JPackageCommand; +import jdk.jpackage.test.JPackageCommand.MessageCategory; +import jdk.jpackage.test.JPackageOutputValidator; import jdk.jpackage.test.JPackageStringBundle; import jdk.jpackage.test.PackageTest; import jdk.jpackage.test.PackageType; @@ -92,18 +97,83 @@ public class AppContentTest { } @Test(ifOS = MACOS) - @Parameter({"apps", "warning.non.standard.contents.sub.dir"}) - @Parameter({"apps/dukeplug.png", "warning.app.content.is.not.dir"}) - public void testWarnings(String testPath, String warningId) throws Exception { - final var appContentValue = TKit.TEST_SRC_ROOT.resolve(testPath); - final var expectedWarning = JPackageStringBundle.MAIN.cannedFormattedString( - warningId, appContentValue); + @Parameter("NOT_DIRECTORY") + @Parameter("NON_STANDARD_DIRECTORY_NAME") + public void testWarnings(AppContentMultiLineWarning type) throws Exception { - JPackageCommand.helloAppImage() - .addArguments("--app-content", appContentValue) - .setFakeRuntime() - .validateOut(expectedWarning) - .executeIgnoreExitCode(); + var cmd = JPackageCommand.helloAppImage() + .setFakeRuntime() + .saveConsoleOutput(true) + .setEnabledMessageCategories(MessageCategory.WARNINGS, MessageCategory.ERRORS); + for (var appContent: type.initAppContent()) { + cmd.addArguments("--app-content", appContent); + } + + var result = cmd.executeIgnoreExitCode(); + + var validator = new JPackageOutputValidator().stderr(); + + validator.expectMatchingStrings(JPackageCommand.makeSummaryMultiLineWarning("warning.non-standard-app-content")); + type.expectedWarnings().stream().map(str -> { + return TKit.assertTextStream(" " + str.getValue()).predicate(String::equals); + }).forEach(validator::add); + + // Signing is finicky when the bundle contains invalid content. + // It may pass or fail depending on the version of the codesign (macOS version?). + if (result.getExitCode() != 0) { + // Expect codesign error in the output. + var cmdlinePattern = String.format( + "^/usr/bin/codesign -s - -vvvv --force %s", + Pattern.quote(cmd.outputBundle().normalize().toAbsolutePath().toString())); + + // + // Typical codesign error: + // + // foo/output/WarningsAppContentTest.app: replacing existing signature + // foo/output/WarningsAppContentTest.app: code object is not signed at all + // In subcomponent: foo/output/WarningsAppContentTest.app/Contents/dukeplug.png + // + + var errorValidator = new FailedCommandErrorValidator(Pattern.compile(cmdlinePattern)) + .exitCode(1) + .validators( + TKit.assertTextStream(": replacing existing signature").predicate(String::endsWith), + TKit.assertTextStream(": code object is not signed at all").predicate(String::endsWith), + TKit.assertTextStream("In subcomponent: ").predicate(String::startsWith)) + .create(); + validator.add(errorValidator); + } + + validator.validateEndOfStream().applyTo(cmd, result); + } + + public enum AppContentMultiLineWarning { + NOT_DIRECTORY("warning.non-standard-app-content.not-dir", Path.of("apps/dukeplug.png")), + NON_STANDARD_DIRECTORY_NAME("warning.non-standard-app-content.non-standard-dir-name", Path.of("apps")), + ; + + AppContentMultiLineWarning(String formatKey, Path appContent) { + this.formatKey = Objects.requireNonNull(formatKey); + this.appContent = TKit.TEST_SRC_ROOT.resolve(appContent); + } + + List initAppContent() { + return List.of(appContent); + } + + List expectedWarnings() { + return switch (this) { + case NOT_DIRECTORY -> { + yield List.of(JPackageStringBundle.MAIN.cannedFormattedString(formatKey, appContent)); + } + case NON_STANDARD_DIRECTORY_NAME -> { + yield List.of(JPackageStringBundle.MAIN.cannedFormattedString(formatKey, appContent.getFileName(), appContent)); + } + }; + } + + private final String formatKey; + private Path appContent; } public static Collection test() { diff --git a/test/jdk/tools/jpackage/share/AppVersionTest.java b/test/jdk/tools/jpackage/share/AppVersionTest.java index ccafd2c7b21..dadf75cefdb 100644 --- a/test/jdk/tools/jpackage/share/AppVersionTest.java +++ b/test/jdk/tools/jpackage/share/AppVersionTest.java @@ -29,7 +29,9 @@ import static jdk.jpackage.internal.util.PListWriter.writePList; import static jdk.jpackage.internal.util.XmlUtils.createXml; import static jdk.jpackage.internal.util.XmlUtils.toXmlConsumer; import static jdk.jpackage.test.JPackageCommand.DEFAULT_VERSION; +import static jdk.jpackage.test.JPackageCommand.cannedArgument; import static jdk.jpackage.test.JPackageCommand.normalizeDerivedVersion; +import static jdk.jpackage.test.JPackageCommand.MessageCategory.TRACE; import static jdk.jpackage.test.JPackageCommand.RuntimeImageType.RUNTIME_TYPE_FAKE; import java.io.IOException; @@ -46,6 +48,7 @@ import java.util.Optional; import java.util.Set; import java.util.function.Consumer; import java.util.function.Predicate; +import java.util.stream.Stream; import jdk.internal.util.OperatingSystem; import jdk.jpackage.internal.model.DottedVersion; import jdk.jpackage.internal.util.MacBundle; @@ -279,19 +282,19 @@ public final class AppVersionTest { } enum Message implements CannedFormattedString.Spec { - VERSION_FROM_MODULE("message.module-version", "version", "module"), - VERSION_FROM_RELEASE_FILE("message.release-version", "version"), - VERSION_NORMALIZED("message.version-normalized", "version", "version"), + VERSION_FROM_MODULE("TRACE: Derive bundle version [{0}] from [{1}] module", "version", "module"), + VERSION_FROM_FILE("TRACE: Derive bundle version [{0}] from [{1}] file", "version", "file"), + VERSION_NORMALIZED("TRACE: Normalize derived bundle version from [{0}] to [{1}]", "version", "version"), ; - Message(String key, Object ... args) { - this.key = Objects.requireNonNull(key); + Message(String format, Object ... args) { + this.format = Objects.requireNonNull(format); this.args = List.of(args); } @Override public String format() { - return key; + return format; } @Override @@ -299,7 +302,12 @@ public final class AppVersionTest { return args; } - private final String key; + @Override + public CannedFormattedString.Spec.Formatter formatter() { + return CannedFormattedString.Spec.Formatter.MESSAGE_FORMAT; + } + + private final String format; private final List args; } @@ -386,6 +394,23 @@ public final class AppVersionTest { } } + record InheritPListVersionSource(String version) implements VersionSource { + + InheritPListVersionSource { + Objects.requireNonNull(version); + } + + @Override + public VersionSource copyWithVersion(String v) { + return new InheritPListVersionSource(v); + } + + @Override + public String toString() { + return String.format("plist=[%s]", version); + } + } + record Expected(String version, List messages) { Expected { @@ -468,6 +493,7 @@ public final class AppVersionTest { void applyTo(JPackageCommand cmd) { Objects.requireNonNull(cmd); + cmd.setEnabledMessageCategories(TRACE); findVersionSource(CmdlineVersionSource.class).ifPresent(ver -> { cmd.setArgumentValue("--app-version", ver.version()); }); @@ -717,7 +743,16 @@ public final class AppVersionTest { expectedBuilder.message(Message.VERSION_FROM_MODULE, ver.version(), ver.moduleName()); } case RuntimeReleaseFileVersionSource ver -> { - expectedBuilder.message(Message.VERSION_FROM_RELEASE_FILE, ver.version()); + expectedBuilder.message(Message.VERSION_FROM_FILE, ver.version(), cannedArgument(cmd -> { + var runtimeImage = Path.of(cmd.getArgumentValue("--runtime-image")); + return RuntimeReleaseFile.releaseFilePathInRuntime( + MacBundle.fromPath(runtimeImage).map(MacBundle::homeDir).orElse(runtimeImage)); + }, "RUNTIME_RELEASE_FILE")); + } + case InheritPListVersionSource ver -> { + expectedBuilder.message(Message.VERSION_FROM_FILE, ver.version(), cannedArgument(cmd -> { + return new MacBundle(Path.of(cmd.getArgumentValue("--runtime-image"))).infoPlistFile(); + }, "INFO_PLIST_FILE")); } default -> { // NOP @@ -725,7 +760,7 @@ public final class AppVersionTest { } if (!versionSource.version().equals(expectedVersion.version())) { - expectedBuilder.message(Message.VERSION_NORMALIZED, expectedVersion.version(), versionSource.version()); + expectedBuilder.message(Message.VERSION_NORMALIZED, versionSource.version(), expectedVersion.version()); } var expectedValue = expectedBuilder.create(); @@ -937,23 +972,6 @@ public final class AppVersionTest { return new StringBuilder().append(type.name().toLowerCase()).append("; ").append(spec).toString(); } - private record InheritPListVersionSource(String version) implements VersionSource { - - InheritPListVersionSource { - Objects.requireNonNull(version); - } - - @Override - public VersionSource copyWithVersion(String v) { - return new InheritPListVersionSource(v); - } - - @Override - public String toString() { - return String.format("plist=[%s]", version); - } - } - private Path createRuntime(RuntimeType type, Optional releaseFileVersion) throws IOException { Objects.requireNonNull(type); Objects.requireNonNull(releaseFileVersion); @@ -1030,4 +1048,10 @@ public final class AppVersionTest { } }; } + + static { + if (Stream.of(Message.values()).map(Message::format).distinct().count() != Message.values().length) { + throw new IllegalStateException(String.format("Multiple items of %s have the same format string", Message.class)); + } + } } diff --git a/test/jdk/tools/jpackage/share/BasicTest.java b/test/jdk/tools/jpackage/share/BasicTest.java index 7ea3f578116..b22ca5519ad 100644 --- a/test/jdk/tools/jpackage/share/BasicTest.java +++ b/test/jdk/tools/jpackage/share/BasicTest.java @@ -52,6 +52,7 @@ import jdk.jpackage.test.JPackageStringBundle; import jdk.jpackage.test.JavaAppDesc; import jdk.jpackage.test.JavaTool; import jdk.jpackage.test.PackageTest; +import jdk.jpackage.test.PackageType; import jdk.jpackage.test.TKit; import jdk.tools.jlink.internal.LinkableRuntimeImage; @@ -197,17 +198,19 @@ public final class BasicTest { .useToolProvider(true) .saveConsoleOutput(true) .setFakeRuntime(); + + new JPackageOutputValidator().validateEndOfStream().applyTo(cmd); + + var stderrValidator = new JPackageOutputValidator().stderr(); + if (cmd.packageType() == PackageType.LINUX_DEB) { + stderrValidator.expectMatchingStrings(JPackageCommand.makeSummaryWarning("message.debs-like-licenses")); + } + stderrValidator.validateEndOfStream().applyTo(cmd); }); - Consumer asserter = result -> { - TKit.assertStringListEquals(List.of(), result.getOutput(), "Check output is empty"); - }; - - target.cmd().map(JPackageCommand::execute).ifPresent(asserter); + target.cmd().map(JPackageCommand::execute); target.test().ifPresent(test -> { - test.addBundleVerifier((_, result) -> { - asserter.accept(result); - }).run(CREATE); + test.run(CREATE); }); } @@ -215,6 +218,21 @@ public final class BasicTest { @Parameter("false") @Parameter("true") public void testVerbose(boolean appImage) { + testVerbose(appImage, cmd -> { + cmd.useToolProvider(true).addArgument("--verbose"); + }); + } + + @Test + @Parameter("false") + @Parameter("true") + public void testVerboseFromEnvVar(boolean appImage) { + testVerbose(appImage, cmd -> { + cmd.useToolProvider(false).setEnvVar("JPACKAGE_DEBUG", "true"); + }); + } + + private static void testVerbose(boolean appImage, Consumer mutator) { ConfigurationTarget target; if (appImage) { @@ -226,10 +244,9 @@ public final class BasicTest { target.addInitializer(cmd -> { // Disable the default logic adding `--verbose` option to jpackage command line. cmd.ignoreDefaultVerbose(true) - .useToolProvider(true) - .addArgument("--verbose") .saveConsoleOutput(true) - .setFakeRuntime(); + .setFakeRuntime() + .mutate(mutator); List verboseContent; if (appImage) { diff --git a/test/jdk/tools/jpackage/share/IconTest.java b/test/jdk/tools/jpackage/share/IconTest.java index 17759db7192..d66a2fdebe9 100644 --- a/test/jdk/tools/jpackage/share/IconTest.java +++ b/test/jdk/tools/jpackage/share/IconTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -47,6 +47,7 @@ import jdk.jpackage.test.CannedFormattedString; import jdk.jpackage.test.ConfigurationTarget; import jdk.jpackage.test.Executor; import jdk.jpackage.test.JPackageCommand; +import jdk.jpackage.test.JPackageCommand.MessageCategory; import jdk.jpackage.test.JPackageStringBundle; import jdk.jpackage.test.LauncherIconVerifier; import jdk.jpackage.test.LinuxHelper; @@ -326,6 +327,7 @@ public class IconTest { cmd.saveConsoleOutput(true); cmd.setFakeRuntime(); cmd.addArguments(extraJPackageArgs); + cmd.setEnabledMessageCategories(MessageCategory.RESOURCES).setDisabledMessageCategories(); }); } diff --git a/test/jdk/tools/jpackage/share/PostImageScriptTest.java b/test/jdk/tools/jpackage/share/PostImageScriptTest.java index 81cdfffbf46..7f7f268fb9e 100644 --- a/test/jdk/tools/jpackage/share/PostImageScriptTest.java +++ b/test/jdk/tools/jpackage/share/PostImageScriptTest.java @@ -113,6 +113,9 @@ public class PostImageScriptTest { test.addInitializer(cmd -> { cmd.setArgumentValue("--resource-dir", TKit.createTempDirectory("resources")); + + // Ensure the full output is enabled as custom scripts write to jpackage's "out" stream. + cmd.setEnabledMessageCategories(JPackageCommand.messageCategoriesConsoleAll()); }); return test; diff --git a/test/jdk/tools/jpackage/windows/WinL10nTest.java b/test/jdk/tools/jpackage/windows/WinL10nTest.java index 4d983df2598..d63057adc44 100644 --- a/test/jdk/tools/jpackage/windows/WinL10nTest.java +++ b/test/jdk/tools/jpackage/windows/WinL10nTest.java @@ -22,21 +22,25 @@ */ import static jdk.jpackage.test.WindowsHelper.getWixTypeFromVerboseJPackageOutput; -import static jdk.jpackage.test.WindowsHelper.WixType.WIX3; import java.io.IOException; import java.nio.file.Path; import java.util.Arrays; import java.util.List; +import java.util.Objects; +import java.util.Optional; import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; import jdk.jpackage.test.Annotations.Parameters; import jdk.jpackage.test.Annotations.Test; import jdk.jpackage.test.Executor; +import jdk.jpackage.test.JPackageCommand; +import jdk.jpackage.test.JPackageCommand.MessageCategory; import jdk.jpackage.test.PackageTest; import jdk.jpackage.test.PackageType; import jdk.jpackage.test.TKit; +import jdk.jpackage.test.WindowsHelper.WixType; /* * @test @@ -111,31 +115,74 @@ public class WinL10nTest { }); } - private static Stream getWixCommandLine(Executor.Result result) { - return result.getOutput().stream().filter(createToolCommandLinePredicate("light").or( - createToolCommandLinePredicate("wix"))); - } + private record OutputAnalizer(Executor.Result result, WixType wixType, Optional wixBuildCommandLine) { - private static boolean isWix3(Executor.Result result) { - return getWixTypeFromVerboseJPackageOutput(result) == WIX3; - } + OutputAnalizer { + Objects.requireNonNull(result); + Objects.requireNonNull(wixType); + Objects.requireNonNull(wixBuildCommandLine); + } - private static final Predicate createToolCommandLinePredicate(String wixToolName) { - var toolFileName = wixToolName + ".exe"; - return (s) -> { - s = s.trim(); + OutputAnalizer(Executor.Result result) { + this(result, getWixTypeFromVerboseJPackageOutput(result)); + } - if (s.startsWith(toolFileName)) { - return true; + OutputAnalizer(Executor.Result result, WixType wixType) { + this(result, wixType, findWixBuildCommandLine(result, wixType)); + } + + String getWixBuildCommandLine() { + return wixBuildCommandLine.orElseThrow(() -> { + return new IllegalStateException("WiX build command line not found"); + }); + } + + void verifyCulturesInCmdline(String... cultures) { + if (cultures.length == 0) { + throw new IllegalArgumentException("Cultures list must be non-empty"); } - // Accommodate for: - // 'C:\Program Files (x86)\WiX Toolset v3.14\bin\light.exe' ... - // light.exe ... - return Stream.of("\\%s ", "\\%s' ").map(format -> { - return String.format(format, toolFileName); - }).anyMatch(s::contains) && s.contains(" -out "); - }; + var cmdline = getWixBuildCommandLine(); + + var expected = switch (wixType) { + case WIX3 -> { + yield "-cultures:" + String.join(";", cultures); + } + case WIX4 -> { + yield Stream.of(cultures).map(culture -> { + return String.join(" ", "-culture", culture); + }).collect(Collectors.joining(" ")); + } + }; + TKit.assertTextStream(expected).label("WiX build command line").apply(List.of(cmdline)); + } + + private static Optional findWixBuildCommandLine(Executor.Result result, WixType wixType) { + Objects.requireNonNull(result); + Objects.requireNonNull(wixType); + return result.stdout().stream() + .filter(JPackageCommand::withTimestamp) + .map(JPackageCommand::stripTimestamp) + .map(String::stripLeading) + .filter(createToolCommandLinePredicate(wixType.buildTool())) + .findFirst(); + } + + private static final Predicate createToolCommandLinePredicate(String wixToolFileName) { + Objects.requireNonNull(wixToolFileName); + return s -> { + if (!s.startsWith("Running ")) { + return false; + } + + // Accommodate for: + // 'C:\Program Files (x86)\WiX Toolset v3.14\bin\light.exe' ... + // light.exe ... + return Stream.of("Running %s ", "\\%s ", "\\%s' ").map(format -> { + return String.format(format, wixToolFileName); + }).anyMatch(s::contains) && s.contains(" -out "); + }; + } } private static List createDefaultL10nFilesLocVerifiers(Path wixSrcDir) { @@ -165,6 +212,16 @@ public class WinL10nTest { // 2. Instruct test to save jpackage output. cmd.setFakeRuntime().saveConsoleOutput(true); + // Need summary to pick WiX version. + // Need errors to pick up errors. + // Need tools to pick up tool command lines jpackage invokes + // Suppress trace as it interfers with output validators. + cmd.setEnabledMessageCategories( + MessageCategory.SUMMARY, + MessageCategory.ERRORS, + MessageCategory.TOOLS + ).setDisabledMessageCategories(MessageCategory.TRACE); + boolean withJavaOptions = false; // Set JVM default locale that is used to select primary l10n file. @@ -194,25 +251,15 @@ public class WinL10nTest { cmd.addArguments("--temp", tempDir); }) .addBundleVerifier((cmd, result) -> { - final List wixCmdline = getWixCommandLine(result).toList(); - final var isWix3 = isWix3(result); + var outputAnalizer = new OutputAnalizer(result); if (expectedCultures != null) { - String expected; - if (isWix3) { - expected = "-cultures:" + String.join(";", expectedCultures); - } else { - expected = Stream.of(expectedCultures).map(culture -> { - return String.join(" ", "-culture", culture); - }).collect(Collectors.joining(" ")); - } - TKit.assertTextStream(expected).apply(wixCmdline); + outputAnalizer.verifyCulturesInCmdline(expectedCultures); } if (expectedErrorMessage != null) { - TKit.assertTextStream(expectedErrorMessage) - .apply(result.getOutput()); + TKit.assertTextStream(expectedErrorMessage).apply(result.stderr()); } if (wxlFileInitializers != null) { @@ -222,21 +269,20 @@ public class WinL10nTest { if (allWxlFilesValid) { for (var v : wxlFileInitializers) { if (!v.name.startsWith("MsiInstallerStrings_")) { - v.createCmdOutputVerifier(wixSrcDir).apply(wixCmdline); + v.createCmdOutputVerifier(wixSrcDir).apply(List.of(outputAnalizer.getWixBuildCommandLine())); } } for (var v : createDefaultL10nFilesLocVerifiers(wixSrcDir)) { - v.apply(wixCmdline); + v.apply(List.of(outputAnalizer.getWixBuildCommandLine())); } } else { Stream.of(wxlFileInitializers) .filter(Predicate.not(WixFileInitializer::isValid)) .forEach(v -> v.createCmdOutputVerifier( wixSrcDir).apply(result.getOutput())); - TKit.assertTrue(wixCmdline.stream().findAny().isEmpty(), - String.format("Check %s.exe was not invoked", - isWix3 ? "light" : "wix")); + TKit.assertTrue(outputAnalizer.wixBuildCommandLine().isEmpty(), + String.format("Check %s was not invoked", outputAnalizer.wixType.buildTool())); } } }); diff --git a/test/jdk/tools/jpackage/windows/WinResourceTest.java b/test/jdk/tools/jpackage/windows/WinResourceTest.java index 6327b94b420..d4224fbce0e 100644 --- a/test/jdk/tools/jpackage/windows/WinResourceTest.java +++ b/test/jdk/tools/jpackage/windows/WinResourceTest.java @@ -30,6 +30,7 @@ import java.util.List; import jdk.jpackage.test.Annotations.Parameters; import jdk.jpackage.test.Annotations.Test; import jdk.jpackage.test.CannedFormattedString; +import jdk.jpackage.test.JPackageCommand.MessageCategory; import jdk.jpackage.test.JPackageOutputValidator; import jdk.jpackage.test.JPackageStringBundle; import jdk.jpackage.test.PackageTest; @@ -87,6 +88,14 @@ public class WinResourceTest { TKit.createTextFile(resourceDir.resolve(wixSource), List.of( "any string that is an invalid WiX source file")); + // Need summary to pick WiX version. + // Need errors to pick WiX error messages. + // Need resources to pick log messages on custom resource usage. + cmd.setEnabledMessageCategories( + MessageCategory.SUMMARY, + MessageCategory.ERRORS, + MessageCategory.RESOURCES).setDisabledMessageCategories(); + new JPackageOutputValidator() .matchTimestamps() .stripTimestamps() diff --git a/test/jdk/tools/jpackage/windows/WinScriptTest.java b/test/jdk/tools/jpackage/windows/WinScriptTest.java index 6dcb132cf8a..f59ae25f5c7 100644 --- a/test/jdk/tools/jpackage/windows/WinScriptTest.java +++ b/test/jdk/tools/jpackage/windows/WinScriptTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -28,6 +28,7 @@ import jdk.jpackage.test.Annotations.Parameter; import jdk.jpackage.test.Annotations.Parameters; import jdk.jpackage.test.Annotations.Test; import jdk.jpackage.test.JPackageCommand; +import jdk.jpackage.test.JPackageCommand.MessageCategory; import jdk.jpackage.test.JPackageUserScript; import jdk.jpackage.test.PackageTest; import jdk.jpackage.test.PackageType; @@ -55,6 +56,8 @@ public class WinScriptTest { .configureHelloApp() .addInitializer(cmd -> { cmd.setFakeRuntime().saveConsoleOutput(true); + // Stdout produced by custom scripts is available in jpackage's output only when tracing is enabled. + cmd.setEnabledMessageCategories(MessageCategory.TRACE).setDisabledMessageCategories(); }); }