8375240: Make bundling progress messages issued by jpackage consistent across platforms

Reviewed-by: almatvee
This commit is contained in:
Alexey Semenyuk 2026-01-14 04:04:08 +00:00
parent 5da70b1804
commit b082a390b7
27 changed files with 639 additions and 289 deletions

View File

@ -147,8 +147,6 @@ final class LinuxDebPackager extends LinuxPackager<LinuxDebPackage> {
Path debFile = outputPackageFile();
Log.verbose(I18N.format("message.outputting-to-location", debFile.toAbsolutePath()));
List<String> 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<LinuxDebPackage> {
// 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

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2025, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -133,8 +133,6 @@ final class LinuxRpmPackager extends LinuxPackager<LinuxRpmPackage> {
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<LinuxRpmPackage> {
env.buildRoot().toAbsolutePath()),
"--define", String.format("%%_rpmfilename %s", rpmFile.getFileName())
).executeExpectSuccess();
Log.verbose(I18N.format("message.output-bundle-location", rpmFile.getParent()));
}
private Path installPrefix() {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 <T extends Package> 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.<T>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<T> implements Supplier<T> {

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2025, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -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<TaskAction> action) {
record TaskConfig(Optional<TaskAction> action, Optional<TaskAction> beforeAction, Optional<TaskAction> afterAction) {
TaskConfig {
Objects.requireNonNull(action);
Objects.requireNonNull(beforeAction);
Objects.requireNonNull(afterAction);
}
}
@ -196,47 +204,64 @@ final class PackagingPipeline {
final class TaskBuilder extends TaskSpecBuilder<TaskID> {
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);
}
<T extends Application, U extends ApplicationLayout> TaskBuilder applicationAction(ApplicationImageTaskAction<T, U> action) {
return setAction(action);
return applicationAction(ActionRole.WORKLOAD, action);
}
<T extends Application, U extends AppImageLayout> TaskBuilder appImageAction(AppImageTaskAction<T, U> action) {
return setAction(action);
return appImageAction(ActionRole.WORKLOAD, action);
}
<T extends Package> TaskBuilder copyAction(CopyAppImageTaskAction<T> action) {
return setAction(action);
return copyAction(ActionRole.WORKLOAD, action);
}
<T extends Package, U extends AppImageLayout> TaskBuilder packageAction(PackageTaskAction<T, U> action) {
return setAction(action);
return packageAction(ActionRole.WORKLOAD, action);
}
TaskBuilder action(NoArgTaskAction action) {
return setAction(action);
return action(ActionRole.WORKLOAD, action);
}
<T extends Application, U extends AppImageLayout> TaskBuilder logAppImageActionBegin(String keyId, Function<AppImageBuildEnv<T, U>, Object[]> formatArgsSupplier) {
return logAppImageAction(ActionRole.BEFORE, keyId, formatArgsSupplier);
}
<T extends Application, U extends AppImageLayout> TaskBuilder logAppImageActionEnd(String keyId, Function<AppImageBuildEnv<T, U>, Object[]> formatArgsSupplier) {
return logAppImageAction(ActionRole.AFTER, keyId, formatArgsSupplier);
}
<T extends Package, U extends AppImageLayout> TaskBuilder logPackageActionBegin(String keyId, Function<PackageBuildEnv<T, U>, Object[]> argsSupplier) {
return logPackageAction(ActionRole.BEFORE, keyId, argsSupplier);
}
<T extends Package, U extends AppImageLayout> TaskBuilder logPackageActionEnd(String keyId, Function<PackageBuildEnv<T, U>, Object[]> argsSupplier) {
return logPackageAction(ActionRole.AFTER, keyId, argsSupplier);
}
TaskBuilder logActionBegin(String keyId, Supplier<Object[]> formatArgsSupplier) {
return logAction(ActionRole.BEFORE, keyId, formatArgsSupplier);
}
TaskBuilder logActionBegin(String keyId, Object... formatArgsSupplier) {
return logAction(ActionRole.BEFORE, keyId, () -> formatArgsSupplier);
}
TaskBuilder logActionEnd(String keyId, Supplier<Object[]> 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<TaskBuilder, TaskAction> actionSetter) {
this.actionSetter = Objects.requireNonNull(actionSetter);
}
TaskBuilder setAction(TaskBuilder taskBuilder, TaskAction action) {
actionSetter.accept(taskBuilder, action);
return taskBuilder;
}
private final BiConsumer<TaskBuilder, TaskAction> 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 <T extends Application, U extends ApplicationLayout> TaskBuilder applicationAction(ActionRole role, ApplicationImageTaskAction<T, U> action) {
return setAction(role, action);
}
private <T extends Application, U extends AppImageLayout> TaskBuilder appImageAction(ActionRole role, AppImageTaskAction<T, U> action) {
return setAction(role, action);
}
private <T extends Package> TaskBuilder copyAction(ActionRole role, CopyAppImageTaskAction<T> action) {
return setAction(role, action);
}
private <T extends Package, U extends AppImageLayout> TaskBuilder packageAction(ActionRole role, PackageTaskAction<T, U> action) {
return setAction(role, action);
}
private TaskBuilder action(ActionRole role, NoArgTaskAction action) {
return setAction(role, action);
}
private <T extends Application, U extends AppImageLayout> TaskBuilder logAppImageAction(ActionRole role, String keyId, Function<AppImageBuildEnv<T, U>, Object[]> formatArgsSupplier) {
Objects.requireNonNull(keyId);
return appImageAction(role, (AppImageBuildEnv<T, U> env) -> {
Log.verbose(I18N.format(keyId, formatArgsSupplier.apply(env)));
});
}
private <T extends Package, U extends AppImageLayout> TaskBuilder logPackageAction(ActionRole role, String keyId, Function<PackageBuildEnv<T, U>, Object[]> formatArgsSupplier) {
Objects.requireNonNull(keyId);
return packageAction(role, (PackageBuildEnv<T, U> env) -> {
Log.verbose(I18N.format(keyId, formatArgsSupplier.apply(env)));
});
}
private TaskBuilder logAction(ActionRole role, String keyId, Supplier<Object[]> 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<TaskID> 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<TaskBuilder> 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<Package, AppImageLayout> 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<TaskID> taskGraph, Map<TaskID, TaskConfig> taskConfig,
UnaryOperator<TaskContext> 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;

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2025, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -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());

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2025, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -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<StandardBundlingOperation> valueOf(BundlingOperationDescriptor descriptor) {
@ -199,6 +203,6 @@ public enum StandardBundlingOperation implements BundlingOperationOptionScope {
private final Predicate<String> optionNamePredicate;
private final OperatingSystem os;
private final PackageType packageType;
private final BundleType bundleType;
private final Verb descriptorVerb;
}

View File

@ -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<Boolean> VERBOSE = auxilaryOption("verbose").create();
public static final OptionValue<PackageType> TYPE = option("type", PackageType.class).addAliases("t")
public static final OptionValue<BundleType> 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<Path> 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 -> {

View File

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

View File

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

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2025, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -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 {}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2025, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -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;
}

View File

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

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2025, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -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()));
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2025, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -315,7 +315,6 @@ final class WinMsiPackager implements Consumer<PackagingPipeline.Builder> {
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());
}

View File

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

View File

@ -80,6 +80,7 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
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<JPackageCommand> {
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<JPackageCommand> {
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<JPackageCommand> {
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<JPackageCommand> {
private final Map<ReadOnlyPathAssert, List<TKit.PathSnapshot>> 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<JPackageCommand> {
private boolean suppressOutput;
private boolean ignoreDefaultRuntime;
private boolean ignoreDefaultVerbose;
private boolean removeOldOutputBundle;
private boolean immutable;
private final Actions prerequisiteActions;
private final Actions verifyActions;

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2019, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -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);

View File

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

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2025, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -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",

View File

@ -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<BundleType> 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<Arguments> test_ARGUMENTS() {
return List.of(
Arguments.of("abc", List.of("abc")),

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2019, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -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<String> 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<String> nonVerboseOutput = cmd.execute().getOutput();
List<String>[] verboseOutput = (List<String>[])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<Executor.Result> 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<CannedFormattedString> 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);
});
}

View File

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