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 0ec6a77e683..7c1f06f54a3 100644 --- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxDebPackager.java +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxDebPackager.java @@ -147,8 +147,6 @@ final class LinuxDebPackager extends LinuxPackager { Path debFile = outputPackageFile(); - Log.verbose(I18N.format("message.outputting-to-location", debFile.toAbsolutePath())); - List cmdline = new ArrayList<>(); Stream.of(sysEnv.fakeroot(), sysEnv.dpkgdeb()).map(Path::toString).forEach(cmdline::add); if (Log.isVerbose()) { @@ -159,8 +157,6 @@ final class LinuxDebPackager extends LinuxPackager { // run dpkg Executor.of(cmdline).retryOnKnownErrorMessage( "semop(1): encountered an error: Invalid argument").execute(); - - Log.verbose(I18N.format("message.output-to-location", debFile.toAbsolutePath())); } @Override 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 60355d0d1a2..e22b9c24fdd 100644 --- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxRpmPackager.java +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxRpmPackager.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 @@ -133,8 +133,6 @@ final class LinuxRpmPackager extends LinuxPackager { Path rpmFile = outputPackageFile(); - Log.verbose(I18N.format("message.outputting-bundle-location", rpmFile.getParent())); - //run rpmbuild Executor.of(sysEnv.rpmbuild().toString(), "-bb", specFile().toAbsolutePath().toString(), @@ -147,8 +145,6 @@ final class LinuxRpmPackager extends LinuxPackager { env.buildRoot().toAbsolutePath()), "--define", String.format("%%_rpmfilename %s", rpmFile.getFileName()) ).executeExpectSuccess(); - - Log.verbose(I18N.format("message.output-bundle-location", rpmFile.getParent())); } private Path installPrefix() { 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 3aabe1f4ba5..3aa0e0e92a0 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 @@ -1,5 +1,5 @@ # -# Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2017, 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 @@ -49,11 +49,7 @@ error.rpm-arch-not-detected="Failed to detect RPM arch" message.icon-not-png=The specified icon "{0}" is not a PNG file and will not be used. The default icon will be used in it's place. message.test-for-tool=Test for [{0}]. Result: {1} -message.outputting-to-location=Generating DEB for installer to: {0}. -message.output-to-location=Package (.deb) saved to: {0}. message.debs-like-licenses=Debian packages should specify a license. The absence of a license will cause some linux distributions to complain about the quality of the application. -message.outputting-bundle-location=Generating RPM for installer to: {0}. -message.output-bundle-location=Package (.rpm) saved to: {0}. message.ldd-not-available=ldd command not found. Package dependencies will not be generated. message.deb-ldd-not-available.advice=Install "libc-bin" DEB package to get ldd. 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 0531559e052..224ea20f249 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacBundlingEnvironment.java +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacBundlingEnvironment.java @@ -54,7 +54,6 @@ public class MacBundlingEnvironment extends DefaultBundlingEnvironment { buildEnv()::create, MacBundlingEnvironment::buildPipeline, (env, pkg, outputDir) -> { - Log.verbose(I18N.format("message.building-dmg", pkg.app().name())); return new MacDmgPackager(env, pkg, outputDir, sysEnv); }); } @@ -64,10 +63,7 @@ public class MacBundlingEnvironment extends DefaultBundlingEnvironment { MacFromOptions.createMacPkgPackage(options), buildEnv()::create, MacBundlingEnvironment::buildPipeline, - (env, pkg, outputDir) -> { - Log.verbose(I18N.format("message.building-pkg", pkg.app().name())); - return new MacPkgPackager(env, pkg, outputDir); - }); + MacPkgPackager::new); } private static void signAppImage(Options options) { 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 20a687487ef..82bb9fc4dad 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacDmgPackager.java +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacDmgPackager.java @@ -235,17 +235,6 @@ record MacDmgPackager(BuildEnv env, MacDmgPackage pkg, Path outputDir, final Path srcFolder = env.appImageDir(); - Log.verbose(MessageFormat.format(I18N.getString( - "message.creating-dmg-file"), finalDMG.toAbsolutePath())); - - try { - Files.deleteIfExists(finalDMG); - } catch (IOException ex) { - throw new IOException(MessageFormat.format(I18N.getString( - "message.dmg-cannot-be-overwritten"), - finalDMG.toAbsolutePath())); - } - Files.createDirectories(protoDMG.getParent()); Files.createDirectories(finalDMG.getParent()); @@ -383,11 +372,6 @@ record MacDmgPackager(BuildEnv env, MacDmgPackage pkg, Path outputDir, } catch (IOException ex) { // Don't care if fails } - - Log.verbose(MessageFormat.format(I18N.getString( - "message.output-to-location"), - pkg.app().name(), normalizedAbsolutePathString(finalDMG))); - } private void detachVolume() throws IOException { 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 a53df7f83c2..4e63f6db178 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPackagingPipeline.java +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPackagingPipeline.java @@ -217,6 +217,11 @@ final class MacPackagingPipeline { enum SignAppImagePackageType implements PackageType { VALUE; + + @Override + public String label() { + throw new UnsupportedOperationException(); + } } static Package createSignAppImagePackage(MacApplication app, BuildEnv env) { 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 240e82dcc9b..ceeab587f66 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 @@ -57,12 +57,7 @@ message.version-string-first-number-not-zero=The first number in an app-version message.keychain.error=Unable to get keychain list. message.invalid-identifier=Invalid mac bundle identifier [{0}]. message.invalid-identifier.advice=specify identifier with "--mac-package-identifier". -message.building-dmg=Building DMG package for {0}. message.preparing-dmg-setup=Preparing dmg setup: {0}. -message.creating-dmg-file=Creating DMG file: {0}. -message.dmg-cannot-be-overwritten=Dmg file exists [{0}] and can not be removed. -message.output-to-location=Result DMG installer for {0}: {1}. -message.building-pkg=Building PKG package for {0}. message.preparing-scripts=Preparing package scripts. message.preparing-distribution-dist=Preparing distribution.dist: {0}. message.signing.pkg=Warning: For signing PKG, you might need to set "Always Trust" for your certificate using "Keychain Access" tool. 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 e4473b1e5ce..3a99dfb04da 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/DefaultBundlingEnvironment.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/DefaultBundlingEnvironment.java @@ -42,17 +42,15 @@ import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; -import jdk.internal.util.OperatingSystem; +import jdk.jpackage.internal.PackagingPipeline.PackageTaskID; import jdk.jpackage.internal.cli.CliBundlingEnvironment; import jdk.jpackage.internal.cli.Options; import jdk.jpackage.internal.cli.StandardBundlingOperation; -import jdk.jpackage.internal.model.AppImagePackageType; import jdk.jpackage.internal.model.Application; import jdk.jpackage.internal.model.BundlingOperationDescriptor; import jdk.jpackage.internal.model.JPackageException; import jdk.jpackage.internal.model.Package; -import jdk.jpackage.internal.model.PackageType; -import jdk.jpackage.internal.model.StandardPackageType; +import jdk.jpackage.internal.util.PathUtils; import jdk.jpackage.internal.util.Result; class DefaultBundlingEnvironment implements CliBundlingEnvironment { @@ -134,7 +132,9 @@ class DefaultBundlingEnvironment implements CliBundlingEnvironment { Objects.requireNonNull(app); Objects.requireNonNull(pipelineBuilder); - final var outputDir = OptionUtils.outputDir(options).resolve(app.appImageDirName()); + final var outputDir = PathUtils.normalizedAbsolutePath(OptionUtils.outputDir(options).resolve(app.appImageDirName())); + + Log.verbose(I18N.getString("message.create-app-image")); IOUtils.writableOutputDir(outputDir.getParent()); @@ -142,14 +142,14 @@ class DefaultBundlingEnvironment implements CliBundlingEnvironment { .predefinedAppImageLayout(app.asApplicationLayout().orElseThrow()) .create(options, app); - Log.verbose(I18N.format("message.creating-app-bundle", outputDir.getFileName(), outputDir.toAbsolutePath().getParent())); - if (Files.exists(outputDir)) { - throw new JPackageException(I18N.format("error.root-exists", outputDir.toAbsolutePath())); + throw new JPackageException(I18N.format("error.root-exists", outputDir)); } pipelineBuilder.excludeDirFromCopying(outputDir.getParent()) .create().execute(BuildEnv.withAppImageDir(env, outputDir), app); + + Log.verbose(I18N.getString("message.app-image-created")); } static void createNativePackage(Options options, @@ -174,11 +174,20 @@ class DefaultBundlingEnvironment implements CliBundlingEnvironment { Objects.requireNonNull(createPipelineBuilder); Objects.requireNonNull(pipelineBuilderMutatorFactory); + var pipelineBuilder = Objects.requireNonNull(createPipelineBuilder.apply(pkg)); + + // Delete an old output package file (if any) before creating a new one. + pipelineBuilder.task(PackageTaskID.DELETE_OLD_PACKAGE_FILE) + .addDependencies(pipelineBuilder.taskGraphSnapshot().getTailsOf(PackageTaskID.CREATE_PACKAGE_FILE)) + .addDependent(PackageTaskID.CREATE_PACKAGE_FILE) + .packageAction(PackagingPipeline::deleteOutputBundle) + .add(); + Packager.build().pkg(pkg) - .outputDir(OptionUtils.outputDir(options)) - .env(Objects.requireNonNull(createBuildEnv.apply(options, pkg))) - .pipelineBuilderMutatorFactory(pipelineBuilderMutatorFactory) - .execute(Objects.requireNonNull(createPipelineBuilder.apply(pkg))); + .outputDir(OptionUtils.outputDir(options)) + .env(Objects.requireNonNull(createBuildEnv.apply(options, pkg))) + .pipelineBuilderMutatorFactory(pipelineBuilderMutatorFactory) + .execute(pipelineBuilder); } @Override @@ -195,10 +204,6 @@ class DefaultBundlingEnvironment implements CliBundlingEnvironment { permanentWorkDirectory = Optional.of(tempDir.path()); } bundler.accept(tempDir.options()); - - var packageType = OptionUtils.bundlingOperation(cmdline).packageType(); - - Log.verbose(I18N.format("message.bundle-created", I18N.getString(bundleTypeDescription(packageType, op.os())))); } catch (IOException ex) { throw new UncheckedIOException(ex); } finally { @@ -219,55 +224,6 @@ class DefaultBundlingEnvironment implements CliBundlingEnvironment { }); } - private String bundleTypeDescription(PackageType type, OperatingSystem os) { - switch (type) { - case StandardPackageType stdType -> { - switch (stdType) { - case WIN_MSI -> { - return "bundle-type.win-msi"; - } - case WIN_EXE -> { - return "bundle-type.win-exe"; - } - case LINUX_DEB -> { - return "bundle-type.linux-deb"; - } - case LINUX_RPM -> { - return "bundle-type.linux-rpm"; - } - case MAC_DMG -> { - return "bundle-type.mac-dmg"; - } - case MAC_PKG -> { - return "bundle-type.mac-pkg"; - } - default -> { - throw new AssertionError(); - } - } - } - case AppImagePackageType appImageType -> { - switch (os) { - case WINDOWS -> { - return "bundle-type.win-app"; - } - case LINUX -> { - return "bundle-type.linux-app"; - } - case MACOS -> { - return "bundle-type.mac-app"; - } - default -> { - throw new AssertionError(); - } - } - } - default -> { - throw new AssertionError(); - } - } - } - private static final class CachingSupplier implements Supplier { 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 c94dada9262..f15768b2cbf 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/PackagingPipeline.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/PackagingPipeline.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 @@ -29,6 +29,7 @@ import static java.util.stream.Collectors.toMap; import static jdk.jpackage.internal.model.AppImageLayout.toPathGroup; import java.io.IOException; +import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.Path; import java.util.ArrayList; @@ -39,12 +40,16 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.concurrent.Callable; +import java.util.function.BiConsumer; +import java.util.function.Function; import java.util.function.Predicate; +import java.util.function.Supplier; import java.util.function.UnaryOperator; import java.util.stream.Stream; import jdk.jpackage.internal.model.AppImageLayout; import jdk.jpackage.internal.model.Application; import jdk.jpackage.internal.model.ApplicationLayout; +import jdk.jpackage.internal.model.JPackageException; import jdk.jpackage.internal.model.Package; import jdk.jpackage.internal.pipeline.DirectedEdge; import jdk.jpackage.internal.pipeline.FixedDAG; @@ -97,7 +102,7 @@ final class PackagingPipeline { /** * The way to access packaging build environment before building a package in a pipeline. */ - interface StartupParameters { + sealed interface StartupParameters { BuildEnv packagingEnv(); } @@ -127,6 +132,7 @@ final class PackagingPipeline { enum PackageTaskID implements TaskID { RUN_POST_IMAGE_USER_SCRIPT, CREATE_CONFIG_FILES, + DELETE_OLD_PACKAGE_FILE, CREATE_PACKAGE_FILE } @@ -183,9 +189,11 @@ final class PackagingPipeline { void execute() throws IOException; } - record TaskConfig(Optional action) { + record TaskConfig(Optional action, Optional beforeAction, Optional afterAction) { TaskConfig { Objects.requireNonNull(action); + Objects.requireNonNull(beforeAction); + Objects.requireNonNull(afterAction); } } @@ -196,47 +204,64 @@ final class PackagingPipeline { final class TaskBuilder extends TaskSpecBuilder { - private TaskBuilder(TaskID id) { - super(id); - } - - private TaskBuilder(TaskID id, TaskConfig config) { - this(id); - config.action().ifPresent(this::setAction); - } - - private TaskBuilder setAction(TaskAction v) { - action = v; - return this; - } - TaskBuilder noaction() { - action = null; - return this; + return setAction(ActionRole.WORKLOAD, null); } TaskBuilder applicationAction(ApplicationImageTaskAction action) { - return setAction(action); + return applicationAction(ActionRole.WORKLOAD, action); } TaskBuilder appImageAction(AppImageTaskAction action) { - return setAction(action); + return appImageAction(ActionRole.WORKLOAD, action); } TaskBuilder copyAction(CopyAppImageTaskAction action) { - return setAction(action); + return copyAction(ActionRole.WORKLOAD, action); } TaskBuilder packageAction(PackageTaskAction action) { - return setAction(action); + return packageAction(ActionRole.WORKLOAD, action); } TaskBuilder action(NoArgTaskAction action) { - return setAction(action); + return action(ActionRole.WORKLOAD, action); + } + + TaskBuilder logAppImageActionBegin(String keyId, Function, Object[]> formatArgsSupplier) { + return logAppImageAction(ActionRole.BEFORE, keyId, formatArgsSupplier); + } + + TaskBuilder logAppImageActionEnd(String keyId, Function, Object[]> formatArgsSupplier) { + return logAppImageAction(ActionRole.AFTER, keyId, formatArgsSupplier); + } + + TaskBuilder logPackageActionBegin(String keyId, Function, Object[]> argsSupplier) { + return logPackageAction(ActionRole.BEFORE, keyId, argsSupplier); + } + + TaskBuilder logPackageActionEnd(String keyId, Function, Object[]> argsSupplier) { + return logPackageAction(ActionRole.AFTER, keyId, argsSupplier); + } + + TaskBuilder logActionBegin(String keyId, Supplier formatArgsSupplier) { + return logAction(ActionRole.BEFORE, keyId, formatArgsSupplier); + } + + TaskBuilder logActionBegin(String keyId, Object... formatArgsSupplier) { + return logAction(ActionRole.BEFORE, keyId, () -> formatArgsSupplier); + } + + TaskBuilder logActionEnd(String keyId, Supplier formatArgsSupplier) { + return logAction(ActionRole.AFTER, keyId, formatArgsSupplier); + } + + TaskBuilder logActionEnd(String keyId, Object... formatArgsSupplier) { + return logAction(ActionRole.AFTER, keyId, () -> formatArgsSupplier); } boolean hasAction() { - return action != null; + return workloadAction != null; } @Override @@ -272,13 +297,109 @@ final class PackagingPipeline { } Builder add() { - final var config = new TaskConfig(Optional.ofNullable(action)); + final var config = new TaskConfig( + Optional.ofNullable(workloadAction), + Optional.ofNullable(beforeAction), + Optional.ofNullable(afterAction)); taskConfig.put(task(), config); createLinks().forEach(Builder.this::linkTasks); return Builder.this; } - private TaskAction action; + + private enum ActionRole { + WORKLOAD(TaskBuilder::setWorkloadAction), + BEFORE(TaskBuilder::setBeforeAction), + AFTER(TaskBuilder::setAfterAction), + ; + + ActionRole(BiConsumer actionSetter) { + this.actionSetter = Objects.requireNonNull(actionSetter); + } + + TaskBuilder setAction(TaskBuilder taskBuilder, TaskAction action) { + actionSetter.accept(taskBuilder, action); + return taskBuilder; + } + + private final BiConsumer actionSetter; + } + + + private TaskBuilder(TaskID id) { + super(id); + } + + private TaskBuilder(TaskID id, TaskConfig config) { + this(id); + config.action().ifPresent(this::setWorkloadAction); + config.beforeAction().ifPresent(this::setBeforeAction); + config.afterAction().ifPresent(this::setAfterAction); + } + + private TaskBuilder setAction(ActionRole role, TaskAction v) { + return role.setAction(this, v); + } + + private TaskBuilder setWorkloadAction(TaskAction v) { + workloadAction = v; + return this; + } + + private TaskBuilder setBeforeAction(TaskAction v) { + beforeAction = v; + return this; + } + + private TaskBuilder setAfterAction(TaskAction v) { + afterAction = v; + return this; + } + + private TaskBuilder applicationAction(ActionRole role, ApplicationImageTaskAction action) { + return setAction(role, action); + } + + private TaskBuilder appImageAction(ActionRole role, AppImageTaskAction action) { + return setAction(role, action); + } + + private TaskBuilder copyAction(ActionRole role, CopyAppImageTaskAction action) { + return setAction(role, action); + } + + private TaskBuilder packageAction(ActionRole role, PackageTaskAction action) { + return setAction(role, action); + } + + private TaskBuilder action(ActionRole role, NoArgTaskAction action) { + return setAction(role, action); + } + + 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))); + }); + } + + 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))); + }); + } + + private TaskBuilder logAction(ActionRole role, String keyId, Supplier formatArgsSupplier) { + Objects.requireNonNull(keyId); + return action(role, () -> { + Log.verbose(I18N.format(keyId, formatArgsSupplier.get())); + }); + } + + private TaskAction workloadAction; + private TaskAction beforeAction; + private TaskAction afterAction; } Builder linkTasks(DirectedEdge edge) { @@ -294,7 +415,11 @@ final class PackagingPipeline { } TaskBuilder task(TaskID id) { - return new TaskBuilder(id); + return Optional.ofNullable(taskConfig.get(id)).map(taskConfig -> { + return new TaskBuilder(id, taskConfig); + }).orElseGet(() -> { + return new TaskBuilder(id); + }); } Stream configuredTasks() { @@ -392,6 +517,8 @@ final class PackagingPipeline { builder.task(PackageTaskID.CREATE_PACKAGE_FILE) .addDependent(PrimaryTaskID.PACKAGE) + .logActionBegin("message.create-package") + .logActionEnd("message.package-created") .add(); builder.task(PrimaryTaskID.PACKAGE).add(); @@ -425,6 +552,17 @@ final class PackagingPipeline { .run(env.env(), env.pkg().app().name()); } + static void deleteOutputBundle(PackageBuildEnv env) throws IOException { + + var outputBundle = env.outputDir().resolve(env.pkg().packageFileNameWithSuffix()); + + try { + Files.deleteIfExists(outputBundle); + } catch (IOException ex) { + throw new JPackageException(I18N.format("error.output-bundle-cannot-be-overwritten", outputBundle.toAbsolutePath()), ex); + } + } + private PackagingPipeline(FixedDAG taskGraph, Map taskConfig, UnaryOperator contextMapper) { this.taskGraph = Objects.requireNonNull(taskGraph); @@ -645,7 +783,13 @@ final class PackagingPipeline { } if (accepted) { + if (config.beforeAction.isPresent()) { + context.execute(config.beforeAction.orElseThrow()); + } context.execute(config.action.orElseThrow()); + if (config.afterAction.isPresent()) { + context.execute(config.afterAction.orElseThrow()); + } } return null; diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/OptionsAnalyzer.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/OptionsAnalyzer.java index 67e87d55d8b..363aa1b863e 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/OptionsAnalyzer.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/OptionsAnalyzer.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 @@ -56,7 +56,7 @@ import jdk.jpackage.internal.model.BundlingEnvironment; import jdk.jpackage.internal.model.BundlingOperationDescriptor; import jdk.jpackage.internal.model.ConfigException; import jdk.jpackage.internal.model.JPackageException; -import jdk.jpackage.internal.model.PackageType; +import jdk.jpackage.internal.model.BundleType; /** * Analyzes jpackage command line structure. @@ -250,7 +250,7 @@ final class OptionsAnalyzer { return error("ERR_NoInstallerEntryPoint", mapFormatArguments(optionSpec)); } else { return error("ERR_InvalidTypeOption", mapFormatArguments( - optionSpec, bundlingOperation.packageTypeValue())); + optionSpec, bundlingOperation.bundleTypeValue())); } } @@ -267,30 +267,31 @@ final class OptionsAnalyzer { final var typeOption = TYPE.getOption(); return cmdline.find(typeOption).map(obj -> { - if (obj instanceof PackageType packageType) { - return packageType; + if (obj instanceof BundleType bundleType) { + return bundleType; } else { - return typeOption.spec() + var spec = new StandardOptionContext(os).mapOptionSpec(typeOption.spec()); + return spec .converter().orElseThrow() - .convert(typeOption.spec().name(), StringToken.of(((String[])obj)[0])) + .convert(spec.name(), StringToken.of(((String[])obj)[0])) .orElseThrow(); } - }).map(packageType -> { - // Find standard bundling operations producing the given package type. + }).map(bundleType -> { + // Find standard bundling operations producing the given bundle type. var bundlingOperations = Stream.of(StandardBundlingOperation.values()).filter(op -> { - return op.packageType().equals(packageType); + return op.bundleType().equals(bundleType); }).toList(); if (bundlingOperations.isEmpty()) { // jpackage internal error: none of the standard bundling operations produce - // bundles of the `packageType`. + // bundles of the `bundleType`. throw new AssertionError(String.format( "None of the standard bundling operations produce bundles of type [%s]", - packageType)); + bundleType)); } else if (bundlingOperations.size() == 1) { return bundlingOperations.getFirst(); } else { - // Multiple standard bundling operations produce the `packageType` package type. + // Multiple standard bundling operations produce the `bundleType` bundle type. // Filter those that belong to the current OS bundlingOperations = bundlingOperations.stream().filter(op -> { return op.os().equals(OperatingSystem.current()); @@ -298,10 +299,10 @@ final class OptionsAnalyzer { if (bundlingOperations.isEmpty()) { // jpackage internal error: none of the standard bundling operations produce - // bundles of the `packageType` on the current OS. + // bundles of the `bundleType` on the current OS. throw new AssertionError(String.format( "None of the standard bundling operations produce bundles of type [%s] on %s", - packageType, OperatingSystem.current())); + bundleType, OperatingSystem.current())); } else if (bundlingOperations.size() == 1) { return bundlingOperations.getFirst(); } else if (StandardBundlingOperation.MACOS_APP_IMAGE.containsAll(bundlingOperations)) { @@ -316,7 +317,7 @@ final class OptionsAnalyzer { } } }).orElseGet(() -> { - // No package type specified, use the default bundling operation in the given environment. + // No bundle type specified, use the default bundling operation in the given environment. return env.defaultOperation().map(descriptor -> { return Stream.of(StandardBundlingOperation.values()).filter(op -> { return descriptor.equals(op.descriptor()); diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/StandardBundlingOperation.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/StandardBundlingOperation.java index 45f9194db0b..f6fcd68a6d8 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/StandardBundlingOperation.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/StandardBundlingOperation.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -24,8 +24,6 @@ */ package jdk.jpackage.internal.cli; -import static jdk.jpackage.internal.model.AppImagePackageType.APP_IMAGE; - import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -34,6 +32,8 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; import jdk.internal.util.OperatingSystem; +import jdk.jpackage.internal.model.AppImageBundleType; +import jdk.jpackage.internal.model.BundleType; import jdk.jpackage.internal.model.BundlingOperationDescriptor; import jdk.jpackage.internal.model.PackageType; import jdk.jpackage.internal.model.StandardPackageType; @@ -44,16 +44,16 @@ import jdk.jpackage.internal.util.SetBuilder; * Standard jpackage operations. */ public enum StandardBundlingOperation implements BundlingOperationOptionScope { - CREATE_WIN_APP_IMAGE(APP_IMAGE, "^(?!(linux-|mac-|win-exe-|win-msi-))", OperatingSystem.WINDOWS), - CREATE_LINUX_APP_IMAGE(APP_IMAGE, "^(?!(win-|mac-|linux-rpm-|linux-deb-))", OperatingSystem.LINUX), - CREATE_MAC_APP_IMAGE(APP_IMAGE, "^(?!(linux-|win-|mac-dmg-|mac-pkg-))", OperatingSystem.MACOS), + CREATE_WIN_APP_IMAGE(AppImageBundleType.WIN_APP_IMAGE, "^(?!(linux-|mac-|win-exe-|win-msi-))", OperatingSystem.WINDOWS), + CREATE_LINUX_APP_IMAGE(AppImageBundleType.LINUX_APP_IMAGE, "^(?!(win-|mac-|linux-rpm-|linux-deb-))", OperatingSystem.LINUX), + CREATE_MAC_APP_IMAGE(AppImageBundleType.MAC_APP_IMAGE, "^(?!(linux-|win-|mac-dmg-|mac-pkg-))", OperatingSystem.MACOS), CREATE_WIN_EXE(StandardPackageType.WIN_EXE, "^(?!(linux-|mac-|win-msi-))", OperatingSystem.WINDOWS), CREATE_WIN_MSI(StandardPackageType.WIN_MSI, "^(?!(linux-|mac-|win-exe-))", OperatingSystem.WINDOWS), CREATE_LINUX_RPM(StandardPackageType.LINUX_RPM, "^(?!(win-|mac-|linux-deb-))", OperatingSystem.LINUX), CREATE_LINUX_DEB(StandardPackageType.LINUX_DEB, "^(?!(win-|mac-|linux-rpm-))", OperatingSystem.LINUX), CREATE_MAC_PKG(StandardPackageType.MAC_PKG, "^(?!(linux-|win-|mac-dmg-))", OperatingSystem.MACOS), CREATE_MAC_DMG(StandardPackageType.MAC_DMG, "^(?!(linux-|win-|mac-pkg-))", OperatingSystem.MACOS), - SIGN_MAC_APP_IMAGE(APP_IMAGE, OperatingSystem.MACOS, Verb.SIGN); + SIGN_MAC_APP_IMAGE(AppImageBundleType.MAC_APP_IMAGE, OperatingSystem.MACOS, Verb.SIGN); /** * Supported values of the {@link BundlingOperationDescriptor#verb()} property. @@ -78,19 +78,19 @@ public enum StandardBundlingOperation implements BundlingOperationOptionScope { private final String value; } - StandardBundlingOperation(PackageType packageType, String optionNameRegexp, OperatingSystem os, Verb descriptorVerb) { - this.packageType = Objects.requireNonNull(packageType); + StandardBundlingOperation(BundleType bundleType, String optionNameRegexp, OperatingSystem os, Verb descriptorVerb) { + this.bundleType = Objects.requireNonNull(bundleType); optionNamePredicate = Pattern.compile(optionNameRegexp).asPredicate(); this.os = Objects.requireNonNull(os); this.descriptorVerb = Objects.requireNonNull(descriptorVerb); } - StandardBundlingOperation(PackageType packageType, String optionNameRegexp, OperatingSystem os) { - this(packageType, optionNameRegexp, os, Verb.CREATE); + StandardBundlingOperation(BundleType bundleType, String optionNameRegexp, OperatingSystem os) { + this(bundleType, optionNameRegexp, os, Verb.CREATE); } - StandardBundlingOperation(PackageType packageType, OperatingSystem os, Verb descriptorVerb) { - this.packageType = Objects.requireNonNull(packageType); + StandardBundlingOperation(BundleType bundleType, OperatingSystem os, Verb descriptorVerb) { + this.bundleType = Objects.requireNonNull(bundleType); optionNamePredicate = v -> false; this.os = Objects.requireNonNull(os); this.descriptorVerb = Objects.requireNonNull(descriptorVerb); @@ -100,16 +100,20 @@ public enum StandardBundlingOperation implements BundlingOperationOptionScope { return os; } - public String packageTypeValue() { - if (packageType.equals(APP_IMAGE)) { + public String bundleTypeValue() { + if (bundleType instanceof AppImageBundleType) { return "app-image"; } else { - return ((StandardPackageType)packageType).suffix().substring(1); + return ((StandardPackageType)bundleType).suffix().substring(1); } } + public BundleType bundleType() { + return bundleType; + } + public PackageType packageType() { - return packageType; + return (PackageType)bundleType(); } /** @@ -122,7 +126,7 @@ public enum StandardBundlingOperation implements BundlingOperationOptionScope { @Override public BundlingOperationDescriptor descriptor() { - return new BundlingOperationDescriptor(os(), packageTypeValue(), descriptorVerb.value()); + return new BundlingOperationDescriptor(os(), bundleTypeValue(), descriptorVerb.value()); } public static Optional valueOf(BundlingOperationDescriptor descriptor) { @@ -199,6 +203,6 @@ public enum StandardBundlingOperation implements BundlingOperationOptionScope { private final Predicate optionNamePredicate; private final OperatingSystem os; - private final PackageType packageType; + private final BundleType bundleType; private final Verb descriptorVerb; } 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 dddaef8399b..9c828705a4d 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 @@ -55,11 +55,12 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Stream; import jdk.internal.util.OperatingSystem; +import jdk.jpackage.internal.model.AppImageBundleType; +import jdk.jpackage.internal.model.BundleType; import jdk.jpackage.internal.model.BundlingOperationDescriptor; import jdk.jpackage.internal.model.JPackageException; import jdk.jpackage.internal.model.LauncherShortcut; import jdk.jpackage.internal.model.LauncherShortcutStartupDirectory; -import jdk.jpackage.internal.model.PackageType; import jdk.jpackage.internal.util.SetBuilder; /** @@ -103,18 +104,18 @@ public final class StandardOption { public static final OptionValue VERBOSE = auxilaryOption("verbose").create(); - public static final OptionValue TYPE = option("type", PackageType.class).addAliases("t") + public static final OptionValue TYPE = option("type", BundleType.class).addAliases("t") .scope(StandardBundlingOperation.values()).inScope(NOT_BUILDING_APP_IMAGE) .converterExceptionFactory(ERROR_WITH_VALUE).converterExceptionFormatString("ERR_InvalidInstallerType") .converter(str -> { - Objects.requireNonNull(str); - return Stream.of(StandardBundlingOperation.values()).filter(bundlingOperation -> { - return bundlingOperation.packageTypeValue().equals(str); - }).map(StandardBundlingOperation::packageType).findFirst().orElseThrow(IllegalArgumentException::new); + return parseBundleType(str, OperatingSystem.current()); }) .description("help.option.type" + resourceKeySuffix(OperatingSystem.current())) .mutate(createOptionSpecBuilderMutator((b, context) -> { b.description("help.option.type" + resourceKeySuffix(context.os())); + b.converter(str -> { + return parseBundleType(str, context.os()); + }); })).create(); public static final OptionValue INPUT = directoryOption("input").addAliases("i") @@ -665,6 +666,23 @@ public final class StandardOption { }).defaultArrayValue(new AdditionalLauncher[0]).createArray(); } + private static BundleType parseBundleType(String str, OperatingSystem appImageOS) { + Objects.requireNonNull(str); + Objects.requireNonNull(appImageOS); + + return Stream.of(StandardBundlingOperation.values()).filter(bundlingOperation -> { + return bundlingOperation.bundleTypeValue().equals(str); + }) + .filter(bundlingOperation -> { + // Skip app image bundle type if it is from another platform. + return !(bundlingOperation.bundleType() instanceof AppImageBundleType) + || (bundlingOperation.os() == appImageOS); + }) + .map(StandardBundlingOperation::bundleType) + .findFirst() + .orElseThrow(IllegalArgumentException::new); + } + private static String resourceKeySuffix(OperatingSystem os) { switch (os) { case LINUX -> { diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/AppImageBundleType.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/AppImageBundleType.java new file mode 100644 index 00000000000..c5d2f0d1569 --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/AppImageBundleType.java @@ -0,0 +1,51 @@ +/* + * 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 + * 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.model; + +import java.util.Objects; + +/** + * App image bundle type. + * + * @see StandardPackageType + */ +public enum AppImageBundleType implements BundleType { + + WIN_APP_IMAGE("bundle-type.win-app"), + LINUX_APP_IMAGE("bundle-type.linux-app"), + MAC_APP_IMAGE("bundle-type.mac-app"), + ; + + private AppImageBundleType(String key) { + this.key = Objects.requireNonNull(key); + } + + @Override + public String label() { + return I18N.getString(key); + } + + private final String key; +} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/AppImagePackageType.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/BundleType.java similarity index 77% rename from src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/AppImagePackageType.java rename to src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/BundleType.java index 4e28bb05aef..009725f3e92 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/AppImagePackageType.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/BundleType.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * 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 @@ -22,20 +22,18 @@ * or visit www.oracle.com if you need additional information or have any * questions. */ + package jdk.jpackage.internal.model; -/** - * App image packaging type. - * - * @see StandardPackageType - */ -public final class AppImagePackageType implements PackageType { - private AppImagePackageType() { - } +/** + * Generic bundle type. E.g.: application image, rpm, msi are all bundle types. + */ +public sealed interface BundleType permits PackageType, AppImageBundleType { /** - * Singleton + * Returns a user-facing label of this bundle type. + * @return a user-facing label of this bundle type. */ - public static final AppImagePackageType APP_IMAGE = new AppImagePackageType(); + String label(); } diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/PackageType.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/PackageType.java index d0a4fd010e6..e7273d27ba5 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/PackageType.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/model/PackageType.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 @@ -27,8 +27,8 @@ package jdk.jpackage.internal.model; /** - * Generic package type. E.g.: application image, rpm, msi are all package types. + * Native package type. E.g.: dmg, rpm, msi are all package types. * * @see jdk.jpackage.internal.model.Package */ -public interface PackageType {} +public non-sealed interface PackageType extends BundleType {} 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 ccdeceb4a04..6fadc748ecc 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -24,19 +24,22 @@ */ package jdk.jpackage.internal.model; +import java.util.Objects; + /** * Standard native package types. */ public enum StandardPackageType implements PackageType { - WIN_MSI(".msi"), - WIN_EXE(".exe"), - LINUX_DEB(".deb"), - LINUX_RPM(".rpm"), - MAC_PKG(".pkg"), - MAC_DMG(".dmg"); + WIN_MSI("bundle-type.win-msi", ".msi"), + WIN_EXE("bundle-type.win-exe", ".exe"), + LINUX_DEB("bundle-type.linux-deb", ".deb"), + LINUX_RPM("bundle-type.linux-rpm", ".rpm"), + MAC_PKG("bundle-type.mac-pkg", ".pkg"), + MAC_DMG("bundle-type.mac-dmg", ".dmg"); - StandardPackageType(String suffix) { - this.suffix = suffix; + StandardPackageType(String key, String suffix) { + this.key = Objects.requireNonNull(key); + this.suffix = Objects.requireNonNull(suffix); } /** @@ -48,5 +51,11 @@ public enum StandardPackageType implements PackageType { return suffix; } + @Override + public String label() { + return I18N.getString(key); + } + + private final String key; private final String suffix; } 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 245d3b892da..588a3702839 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 @@ -28,14 +28,14 @@ param.copyright.default=Copyright (C) {0,date,YYYY} param.vendor.default=Unknown bundle-type.win-app=Windows Application Image -bundle-type.win-exe=EXE Installer Package -bundle-type.win-msi=MSI Installer Package +bundle-type.win-exe=Windows EXE Installer +bundle-type.win-msi=Windows MSI Installer bundle-type.mac-app=Mac Application Image bundle-type.mac-dmg=Mac DMG Package bundle-type.mac-pkg=Mac PKG Package bundle-type.linux-app=Linux Application Image -bundle-type.linux-deb=DEB Bundle -bundle-type.linux-rpm=RPM Bundle +bundle-type.linux-deb=Linux DEB Package +bundle-type.linux-rpm=Linux RPM Package resource.post-app-image-script=script to run after application image is populated @@ -43,9 +43,14 @@ message.using-default-resource=Using default package resource {0} {1} (add {2} t message.no-default-resource=No default package resource {0} (add {1} to the resource-dir to customize). message.using-custom-resource-from-file=Using custom package resource {0} (loaded from file {1}). message.using-custom-resource=Using custom package resource {0} (loaded from {1}). -message.creating-app-bundle=Creating app package: {0} in {1} + +message.create-package=Building output package file... +message.create-app-image=Building output application image directory... +message.package-created=Succeeded in building output package file +message.app-image-created=Succeeded in building output application image directory + message.debug-working-directory=Kept working directory for debug: {0} -message.bundle-created=Succeeded in building {0} package + message.module-version=Using version "{0}" from module "{1}" as application version message.error-header=Error: {0} @@ -97,6 +102,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 +error.output-bundle-cannot-be-overwritten=Output package file "{0}" exists and can not be removed. + error.blocked.option=jlink option [{0}] is not permitted in --jlink-options error.no.name=Name not specified with --name and cannot infer one from app-image error.no.name.advice=Specify name with --name diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinExePackager.java b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinExePackager.java index 9a13a0f954d..fd86331e2f1 100644 --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinExePackager.java +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinExePackager.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 @@ -79,8 +79,6 @@ final record WinExePackager(BuildEnv env, WinExePackage pkg, Path outputDir, Pat private void wrapMsiInExe() throws IOException { - Log.verbose(I18N.format("message.outputting-to-location", outputDir.toAbsolutePath())); - final var msi = msi(); // Copy template msi wrapper next to msi file @@ -102,7 +100,5 @@ final record WinExePackager(BuildEnv env, WinExePackage pkg, Path outputDir, Pat Files.copy(exePath, dstExePath, StandardCopyOption.REPLACE_EXISTING); dstExePath.toFile().setExecutable(true); - - Log.verbose(I18N.format("message.output-location", outputDir.toAbsolutePath())); } } 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 915d034bd82..c52be726fd2 100644 --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinMsiPackager.java +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinMsiPackager.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 @@ -315,7 +315,6 @@ final class WinMsiPackager implements Consumer { private void buildPackage() throws IOException { final var msiOut = outputDir.resolve(pkg.packageFileNameWithSuffix()); - Log.verbose(I18N.format("message.generating-msi", msiOut.toAbsolutePath())); wixPipeline.buildMsi(msiOut.toAbsolutePath()); } 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 1f485e6c6c8..38d0bd02bbb 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 @@ -1,5 +1,5 @@ # -# Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2017, 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 @@ -56,13 +56,9 @@ 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.potential.windows.defender.issue=Warning: Windows Defender may prevent jpackage from functioning. If there is an issue, it can be addressed by either disabling realtime monitoring, or adding an exclusion for the directory "{0}". -message.outputting-to-location=Generating EXE for installer to: {0}. -message.output-location=Installer (.exe) saved to: {0} message.tool-version=Detected [{0}] version [{1}]. message.wrong-tool-version=Detected [{0}] version {1} but version {2} is required. message.use-wix36-features=WiX {0} detected. Enabling advanced cleanup action. message.product-code=MSI ProductCode: {0}. message.upgrade-code=MSI UpgradeCode: {0}. message.preparing-msi-config=Preparing MSI config: {0}. -message.generating-msi=Generating MSI: {0}. - 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 d2b423b2ed2..9b8b05af93b 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java @@ -80,6 +80,7 @@ public class JPackageCommand extends CommandArguments { prerequisiteActions = new Actions(); verifyActions = new Actions(); excludeStandardAsserts(StandardAssert.MAIN_LAUNCHER_DESCRIPTION); + removeOldOutputBundle = true; } private JPackageCommand(JPackageCommand cmd, boolean immutable) { @@ -91,6 +92,7 @@ public class JPackageCommand extends CommandArguments { suppressOutput = cmd.suppressOutput; ignoreDefaultRuntime = cmd.ignoreDefaultRuntime; ignoreDefaultVerbose = cmd.ignoreDefaultVerbose; + removeOldOutputBundle = cmd.removeOldOutputBundle; this.immutable = immutable; prerequisiteActions = new Actions(cmd.prerequisiteActions); verifyActions = new Actions(cmd.verifyActions); @@ -844,6 +846,28 @@ public class JPackageCommand extends CommandArguments { return this; } + /** + * Configures this instance to optionally remove the existing output bundle + * before running the jpackage command. + * + * @param v {@code true} to remove existing output bundle before running the + * jpackage command, and {@code false} otherwise + * @return this + */ + public JPackageCommand removeOldOutputBundle(boolean v) { + verifyMutable(); + removeOldOutputBundle = v; + return this; + } + + /** + * Returns {@code true} if this instance will remove existing output bundle + * before running the jpackage command, and {@code false} otherwise. + */ + public boolean isRemoveOldOutputBundle() { + return removeOldOutputBundle; + } + public JPackageCommand validateOutput(TKit.TextStreamVerifier validator) { return validateOutput(validator::apply); } @@ -946,21 +970,18 @@ public class JPackageCommand extends CommandArguments { verifyMutable(); executePrerequisiteActions(); - if (hasArgument("--dest")) { - nullableOutputBundle().ifPresent(path -> { - ThrowingRunnable.toRunnable(() -> { - if (Files.isDirectory(path)) { - TKit.deleteDirectoryRecursive(path, String.format( - "Delete [%s] folder before running jpackage", - path)); - } else if (TKit.deleteIfExists(path)) { - TKit.trace(String.format( - "Deleted [%s] file before running jpackage", - path)); - } - }).run(); - }); - } + nullableOutputBundle().filter(_ -> { + return removeOldOutputBundle; + }).ifPresent(path -> { + ThrowingRunnable.toRunnable(() -> { + if (Files.isDirectory(path)) { + TKit.deleteDirectoryRecursive(path, + String.format("Delete [%s] folder before running jpackage", path)); + } else if (TKit.deleteIfExists(path)) { + TKit.trace(String.format("Deleted [%s] file before running jpackage", path)); + } + }).run(); + }); Path resourceDir = getArgumentValue("--resource-dir", () -> null, Path::of); if (resourceDir != null && Files.isDirectory(resourceDir)) { @@ -1090,7 +1111,7 @@ public class JPackageCommand extends CommandArguments { private final Map> snapshots; } - public static enum ReadOnlyPathAssert{ + public static enum ReadOnlyPathAssert { APP_IMAGE(new Builder("--app-image").enable(cmd -> { // External app image should be R/O unless it is an app image signing on macOS. return !(TKit.isOSX() && MacHelper.signPredefinedAppImage(cmd)); @@ -1774,6 +1795,7 @@ public class JPackageCommand extends CommandArguments { private boolean suppressOutput; private boolean ignoreDefaultRuntime; private boolean ignoreDefaultVerbose; + private boolean removeOldOutputBundle; private boolean immutable; private final Actions prerequisiteActions; private final Actions verifyActions; diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java index 2e4f11d056f..2baf6683fdf 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.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 @@ -726,12 +726,30 @@ public final class PackageTest extends RunnablePackageTest { } case CREATE -> { - Executor.Result result = cmd.execute(expectedJPackageExitCode); + var nullableOutputBundle = cmd.nullableOutputBundle(); + + var oldOutputBundleSnapshot = nullableOutputBundle + .filter(Files::exists) + .filter(_ -> { + return !cmd.isRemoveOldOutputBundle(); + }) + .map(TKit.PathSnapshot::new); + + var result = cmd.execute(expectedJPackageExitCode); + if (expectedJPackageExitCode == 0) { TKit.assertFileExists(cmd.outputBundle()); } else { - cmd.nullableOutputBundle().ifPresent(outputBundle -> { - TKit.assertPathExists(outputBundle, false); + nullableOutputBundle.ifPresent(outputBundle -> { + oldOutputBundleSnapshot.ifPresentOrElse(snapshot -> { + // jpackage failed, but the output bundle exists. + // This output bundle existed before the jpackage was invoked. + // Verify jpackage didn't modify it. + new TKit.PathSnapshot(outputBundle).assertEquals(snapshot, String.format( + "Check jpackage didn't modify the old output bundle [%s]", outputBundle)); + }, () -> { + TKit.assertPathExists(outputBundle, false); + }); }); } verifyPackageBundle(cmd, result, expectedJPackageExitCode); diff --git a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/DefaultBundlingEnvironmentTest.java b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/DefaultBundlingEnvironmentTest.java index 1a14330fe6e..709f0f8413b 100644 --- a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/DefaultBundlingEnvironmentTest.java +++ b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/DefaultBundlingEnvironmentTest.java @@ -41,7 +41,7 @@ import java.util.spi.ToolProvider; import java.util.stream.Stream; import jdk.internal.util.OperatingSystem; import jdk.jpackage.internal.cli.StandardBundlingOperation; -import jdk.jpackage.internal.model.AppImagePackageType; +import jdk.jpackage.internal.model.AppImageBundleType; import jdk.jpackage.internal.model.BundlingOperationDescriptor; import jdk.jpackage.internal.model.PackageType; import jdk.jpackage.internal.model.StandardPackageType; @@ -119,9 +119,9 @@ public class DefaultBundlingEnvironmentTest extends JUnitAdapter { // #2 - jpackage should bail out earlier). // - final var type = op.packageTypeValue(); + final var type = op.bundleTypeValue(); final int iterationCount; - if (op.packageType() instanceof AppImagePackageType) { + if (op.bundleType() instanceof AppImageBundleType) { iterationCount = 1; } else { iterationCount = 2; @@ -165,7 +165,7 @@ public class DefaultBundlingEnvironmentTest extends JUnitAdapter { private static Script createMockScript(StandardBundlingOperation op) { - if (op.packageType() instanceof AppImagePackageType) { + if (op.bundleType() instanceof AppImageBundleType) { return Script.build().createSequence(); } 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 721e0802d16..86a6cb075d0 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 @@ -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 @@ -633,7 +633,12 @@ public class PackagingPipelineTest { Package create() { return new Package.Stub( app, - new PackageType() {}, + new PackageType() { + @Override + public String label() { + throw new UnsupportedOperationException(); + } + }, "the-package", "My package", "1.0", diff --git a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/StandardOptionTest.java b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/StandardOptionTest.java index 4aa3d5f72c1..0b70f4151cc 100644 --- a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/StandardOptionTest.java +++ b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/StandardOptionTest.java @@ -53,6 +53,8 @@ import jdk.internal.util.OperatingSystem; import jdk.jpackage.internal.cli.JOptSimpleOptionsBuilder.ConvertedOptionsBuilder; import jdk.jpackage.internal.cli.JOptSimpleOptionsBuilder.OptionsBuilder; import jdk.jpackage.internal.cli.StandardOption.LauncherProperty; +import jdk.jpackage.internal.model.AppImageBundleType; +import jdk.jpackage.internal.model.BundleType; import jdk.jpackage.internal.model.JPackageException; import jdk.jpackage.internal.model.LauncherShortcut; import jdk.jpackage.internal.model.LauncherShortcutStartupDirectory; @@ -175,16 +177,21 @@ public class StandardOptionTest extends JUnitAdapter.TestSrcInitializer { assertEquals(DirectoryNotEmptyException.class, ex.getCause().getClass()); } + @ParameterizedTest + @EnumSource(names = {"WINDOWS", "LINUX", "MACOS"}) + public void test_TYPE_valid(OperatingSystem appImageOS) { + + var spec = new StandardOptionContext(appImageOS).mapOptionSpec(StandardOption.TYPE.getSpec()); + + test_TYPE_valid(spec, appImageOS); + } + @Test public void test_TYPE_valid() { var spec = StandardOption.TYPE.getSpec(); - Stream.of(StandardBundlingOperation.values()).forEach(bundlingOperation -> { - var pkgTypeStr = bundlingOperation.packageTypeValue(); - var pkgType = spec.converter().orElseThrow().convert(spec.name(), StringToken.of(pkgTypeStr)).orElseThrow(); - assertSame(bundlingOperation.packageType(), pkgType); - }); + test_TYPE_valid(spec, OperatingSystem.current()); } @ParameterizedTest @@ -336,6 +343,18 @@ public class StandardOptionTest extends JUnitAdapter.TestSrcInitializer { assertEquals(expectedOptionTable, optionTable); } + private void test_TYPE_valid(OptionSpec spec, OperatingSystem appImageOS) { + Stream.of(StandardBundlingOperation.values()).filter(bundlingOperation -> { + // Skip app image bundle type if it is from another platform. + return !(bundlingOperation.bundleType() instanceof AppImageBundleType) + || (bundlingOperation.os() == appImageOS); + }).forEach(bundlingOperation -> { + var bundleTypeStr = bundlingOperation.bundleTypeValue(); + var bundleType = spec.converter().orElseThrow().convert(spec.name(), StringToken.of(bundleTypeStr)).orElseThrow(); + assertSame(bundlingOperation.bundleType(), bundleType); + }); + } + private static Collection test_ARGUMENTS() { return List.of( Arguments.of("abc", List.of("abc")), diff --git a/test/jdk/tools/jpackage/share/BasicTest.java b/test/jdk/tools/jpackage/share/BasicTest.java index afa847f6e1b..977b7c7c057 100644 --- a/test/jdk/tools/jpackage/share/BasicTest.java +++ b/test/jdk/tools/jpackage/share/BasicTest.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 @@ -22,6 +22,7 @@ */ +import static jdk.jpackage.test.RunnablePackageTest.Action.CREATE; import static jdk.jpackage.test.RunnablePackageTest.Action.CREATE_AND_UNPACK; import java.io.IOException; @@ -32,6 +33,7 @@ import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; import java.util.regex.Pattern; @@ -41,6 +43,7 @@ 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.Executor; import jdk.jpackage.test.HelloApp; import jdk.jpackage.test.JPackageCommand; @@ -176,57 +179,74 @@ public final class BasicTest { } @Test - @SuppressWarnings("unchecked") - public void testVerbose() { - JPackageCommand cmd = JPackageCommand.helloAppImage() - // Disable default logic adding `--verbose` option - // to jpackage command line. - .ignoreDefaultVerbose(true) - .saveConsoleOutput(true) - .setFakeRuntime().executePrerequisiteActions(); + @Parameter("false") + @Parameter("true") + public void testQuiet(boolean appImage) { - List expectedVerboseOutputStrings = new ArrayList<>(); - expectedVerboseOutputStrings.add("Creating app package:"); - if (TKit.isWindows()) { - expectedVerboseOutputStrings.add( - "Succeeded in building Windows Application Image package"); - } else if (TKit.isLinux()) { - expectedVerboseOutputStrings.add( - "Succeeded in building Linux Application Image package"); - } else if (TKit.isOSX()) { - expectedVerboseOutputStrings.add("Preparing Info.plist:"); - expectedVerboseOutputStrings.add( - "Succeeded in building Mac Application Image package"); + ConfigurationTarget target; + if (appImage) { + target = new ConfigurationTarget(JPackageCommand.helloAppImage()); } else { - TKit.throwUnknownPlatformError(); + target = new ConfigurationTarget(new PackageTest().configureHelloApp()); } - TKit.deleteDirectoryContentsRecursive(cmd.outputDir()); - List nonVerboseOutput = cmd.execute().getOutput(); - List[] verboseOutput = (List[])new List[1]; - - // Directory clean up is not 100% reliable on Windows because of - // antivirus software that can lock .exe files. Setup - // different output directory instead of cleaning the default one for - // verbose jpackage run. - TKit.withTempDirectory("verbose-output", tempDir -> { - cmd.setArgumentValue("--dest", tempDir); - cmd.addArgument("--verbose"); - verboseOutput[0] = cmd.execute().getOutput(); + target.addInitializer(cmd -> { + // Disable the default logic adding `--verbose` option to jpackage command line. + cmd.ignoreDefaultVerbose(true) + .useToolProvider(true) + .saveConsoleOutput(true) + .setFakeRuntime(); }); - TKit.assertTrue(nonVerboseOutput.size() < verboseOutput[0].size(), - "Check verbose output is longer than regular"); + Consumer asserter = result -> { + TKit.assertStringListEquals(List.of(), result.getOutput(), "Check output is empty"); + }; - expectedVerboseOutputStrings.forEach(str -> { - TKit.assertTextStream(str).label("regular output") - .predicate(String::contains).negate() - .apply(nonVerboseOutput); + target.cmd().map(JPackageCommand::execute).ifPresent(asserter); + target.test().ifPresent(test -> { + test.addBundleVerifier((_, result) -> { + asserter.accept(result); + }).run(CREATE); + }); + } + + @Test + @Parameter("false") + @Parameter("true") + public void testVerbose(boolean appImage) { + + ConfigurationTarget target; + if (appImage) { + target = new ConfigurationTarget(JPackageCommand.helloAppImage()); + } else { + target = new ConfigurationTarget(new PackageTest().configureHelloApp()); + } + + target.addInitializer(cmd -> { + // Disable the default logic adding `--verbose` option to jpackage command line. + cmd.ignoreDefaultVerbose(true) + .useToolProvider(true) + .addArgument("--verbose") + .saveConsoleOutput(true) + .setFakeRuntime(); + + List verboseContent; + if (appImage) { + verboseContent = List.of( + JPackageStringBundle.MAIN.cannedFormattedString("message.create-app-image"), + JPackageStringBundle.MAIN.cannedFormattedString("message.app-image-created")); + } else { + verboseContent = List.of( + JPackageStringBundle.MAIN.cannedFormattedString("message.create-package"), + JPackageStringBundle.MAIN.cannedFormattedString("message.package-created")); + } + + cmd.validateOutput(verboseContent.toArray(CannedFormattedString[]::new)); }); - expectedVerboseOutputStrings.forEach(str -> { - TKit.assertTextStream(str).label("verbose output") - .apply(verboseOutput[0]); + target.cmd().ifPresent(JPackageCommand::execute); + target.test().ifPresent(test -> { + test.run(CREATE); }); } diff --git a/test/jdk/tools/jpackage/share/OutputErrorTest.java b/test/jdk/tools/jpackage/share/OutputErrorTest.java new file mode 100644 index 00000000000..110e86e67d9 --- /dev/null +++ b/test/jdk/tools/jpackage/share/OutputErrorTest.java @@ -0,0 +1,119 @@ +/* + * 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. + */ + + +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Objects; +import java.util.spi.ToolProvider; +import jdk.internal.util.OperatingSystem; +import jdk.jpackage.test.Annotations.Parameter; +import jdk.jpackage.test.Annotations.Test; +import jdk.jpackage.test.JPackageCommand; +import jdk.jpackage.test.JPackageStringBundle; +import jdk.jpackage.test.JavaTool; +import jdk.jpackage.test.PackageTest; +import jdk.jpackage.test.TKit; + +/* + * @test + * @summary Test how jpackage handles errors writing output bundle + * @library /test/jdk/tools/jpackage/helpers + * @build jdk.jpackage.test.* + * @compile -Xlint:all -Werror OutputErrorTest.java + * @run main/othervm/timeout=1440 -Xmx512m jdk.jpackage.test.Main + * --jpt-run=OutputErrorTest + */ + +public final class OutputErrorTest { + + @Test + @Parameter("DIR") + // "Locked file error" reliably works only on Windows + @Parameter(value = "LOCKED_FILE", ifOS = OperatingSystem.WINDOWS) + public void testPackage(ExistingOutputBundleType existingOutputBundleType) { + + new PackageTest().configureHelloApp().addInitializer(cmd -> { + + cmd.setFakeRuntime(); + cmd.setArgumentValue("--dest", TKit.createTempDirectory("output")); + cmd.removeOldOutputBundle(false); + cmd.validateOutput(JPackageCommand.makeError(JPackageStringBundle.MAIN.cannedFormattedString( + "error.output-bundle-cannot-be-overwritten", cmd.outputBundle().toAbsolutePath()))); + + var outputBundle = cmd.outputBundle(); + + switch (existingOutputBundleType) { + case DIR -> { + Files.createDirectories(outputBundle); + Files.writeString(outputBundle.resolve("foo.txt"), "Hello"); + } + case LOCKED_FILE -> { + Files.writeString(outputBundle, "Hello"); + cmd.useToolProvider(createToolProviderWithLockedFile( + JavaTool.JPACKAGE.asToolProvider(), outputBundle)); + } + } + + }).setExpectedExitCode(1).run(); + } + + enum ExistingOutputBundleType { + DIR, + LOCKED_FILE, + ; + } + + private static ToolProvider createToolProviderWithLockedFile(ToolProvider tp, Path lockedFile) { + Objects.requireNonNull(tp); + if (!Files.isRegularFile(lockedFile)) { + throw new IllegalArgumentException(); + } + + return new ToolProvider() { + + @Override + public String name() { + return "jpackage-mock"; + } + + @SuppressWarnings("try") + @Override + public int run(PrintWriter out, PrintWriter err, String... args) { + try { + var lastModifiedTime = Files.getLastModifiedTime(lockedFile); + try (var fos = new FileOutputStream(lockedFile.toFile()); var lock = fos.getChannel().lock()) { + Files.setLastModifiedTime(lockedFile, lastModifiedTime); + return tp.run(out, err, args); + } + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + }; + } +}