8278591: Jpackage post installation information message

Reviewed-by: almatvee, erikj
This commit is contained in:
Alexey Semenyuk 2026-03-19 21:59:55 +00:00
parent 615aba8257
commit 96f6ffbff4
51 changed files with 2219 additions and 598 deletions

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved.
# Copyright (c) 2020, 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,7 +29,7 @@ DISABLED_WARNINGS_java += dangling-doc-comments
COPY += .gif .png .txt .spec .script .prerm .preinst \
.postrm .postinst .list .sh .desktop .copyright .control .plist .template \
.icns .scpt .wxs .wxl .wxi .wxf .ico .bmp .tiff .service .xsl
.icns .scpt .wxs .wxl .wxi .wxf .ico .bmp .tiff .service .xsl .js
CLEAN += .properties

View File

@ -400,6 +400,8 @@ public final class StandardOption {
public static final OptionValue<Boolean> WIN_INSTALLDIR_CHOOSER = booleanOption("win-dir-chooser").scope(nativeBundling()).create();
public static final OptionValue<Boolean> WIN_WITH_UI = booleanOption("win-with-ui").scope(nativeBundling()).create();
public static final OptionValue<UUID> WIN_UPGRADE_UUID = uuidOption("win-upgrade-uuid").scope(nativeBundling()).create();
public static final OptionValue<Boolean> WIN_CONSOLE_HINT = booleanOption("win-console")

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
@ -426,3 +426,5 @@ help.option.win-update-url=\
help.option.win-upgrade-uuid=\
\ UUID associated with upgrades for this package
help.option.win-with-ui=\
\ Enforces the installer to have UI

View File

@ -1,5 +1,5 @@
---
# Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved.
# Copyright (c) 2018, 2026, Oracle and/or its affiliates. All rights reserved.
# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
#
# This code is free software; you can redistribute it and/or modify it
@ -436,6 +436,10 @@ The `jpackage` tool will take as input a Java application and a Java run-time im
: UUID associated with upgrades for this package
<a id="option-win-with-ui">`--win-with-ui`</a>
: Enforces the installer to have UI
#### Linux platform options (available only when running on Linux):
<a id="option-linux-package-name">`--linux-package-name` *name*</a>

View File

@ -0,0 +1,56 @@
/*
* Copyright (c) 2021, 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;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Objects;
import jdk.jpackage.internal.resources.ResourceLocator;
/**
* WSH script altering cooked .msi file.
*/
record MsiMutator(String scriptResourceName) {
MsiMutator {
Objects.requireNonNull(scriptResourceName);
if (Path.of(scriptResourceName).getNameCount() != 1) {
throw new IllegalArgumentException();
}
}
void addToConfigRoot(Path configRoot) throws IOException {
var scriptFile = configRoot.resolve(pathInConfigRoot());
try (var in = ResourceLocator.class.getResourceAsStream(scriptResourceName)) {
Files.createDirectories(scriptFile.getParent());
Files.copy(in, scriptFile);
}
}
Path pathInConfigRoot() {
return Path.of(scriptResourceName);
}
}

View File

@ -39,6 +39,7 @@ import static jdk.jpackage.internal.cli.StandardOption.WIN_SHORTCUT_HINT;
import static jdk.jpackage.internal.cli.StandardOption.WIN_SHORTCUT_PROMPT;
import static jdk.jpackage.internal.cli.StandardOption.WIN_UPDATE_URL;
import static jdk.jpackage.internal.cli.StandardOption.WIN_UPGRADE_UUID;
import static jdk.jpackage.internal.cli.StandardOption.WIN_WITH_UI;
import static jdk.jpackage.internal.model.StandardPackageType.WIN_MSI;
import jdk.jpackage.internal.cli.Options;
@ -93,6 +94,7 @@ final class WinFromOptions {
WIN_UPDATE_URL.ifPresentIn(options, pkgBuilder::updateURL);
WIN_INSTALLDIR_CHOOSER.ifPresentIn(options, pkgBuilder::withInstallDirChooser);
WIN_SHORTCUT_PROMPT.ifPresentIn(options, pkgBuilder::withShortcutPrompt);
WIN_WITH_UI.ifPresentIn(options, pkgBuilder::withUi);
if (app.isService()) {
RESOURCE_DIR.ifPresentIn(options, resourceDir -> {

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
@ -65,6 +65,7 @@ final class WinMsiPackageBuilder {
MsiVersion.of(pkg.version()),
withInstallDirChooser,
withShortcutPrompt,
withUi,
Optional.ofNullable(helpURL),
Optional.ofNullable(updateURL),
Optional.ofNullable(startMenuGroupName).orElseGet(DEFAULTS::startMenuGroupName),
@ -92,6 +93,11 @@ final class WinMsiPackageBuilder {
return this;
}
WinMsiPackageBuilder withUi(boolean v) {
withUi = v;
return this;
}
WinMsiPackageBuilder helpURL(String v) {
helpURL = v;
return this;
@ -131,6 +137,7 @@ final class WinMsiPackageBuilder {
private boolean withInstallDirChooser;
private boolean withShortcutPrompt;
private boolean withUi;
private String helpURL;
private String updateURL;
private String startMenuGroupName;

View File

@ -36,7 +36,6 @@ import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
@ -315,50 +314,50 @@ final class WinMsiPackager implements Consumer<PackagingPipeline.Builder> {
wixPipeline.buildMsi(msiOut.toAbsolutePath());
}
private Map<String, String> createWixVars() throws IOException {
Map<String, String> data = new HashMap<>();
private WixVariables createWixVars() throws IOException {
var wixVars = new WixVariables();
data.put("JpProductCode", pkg.productCode().toString());
data.put("JpProductUpgradeCode", pkg.upgradeCode().toString());
wixVars.put("JpProductCode", pkg.productCode().toString());
wixVars.put("JpProductUpgradeCode", pkg.upgradeCode().toString());
Log.verbose(I18N.format("message.product-code", pkg.productCode()));
Log.verbose(I18N.format("message.upgrade-code", pkg.upgradeCode()));
data.put("JpAllowUpgrades", "yes");
wixVars.define("JpAllowUpgrades");
if (!pkg.isRuntimeInstaller()) {
data.put("JpAllowDowngrades", "yes");
wixVars.define("JpAllowDowngrades");
}
data.put("JpAppName", pkg.packageName());
data.put("JpAppDescription", pkg.description());
data.put("JpAppVendor", pkg.app().vendor());
data.put("JpAppVersion", pkg.version());
wixVars.put("JpAppName", pkg.packageName());
wixVars.put("JpAppDescription", pkg.description());
wixVars.put("JpAppVendor", pkg.app().vendor());
wixVars.put("JpAppVersion", pkg.version());
if (Files.exists(installerIcon)) {
data.put("JpIcon", installerIcon.toString());
wixVars.put("JpIcon", installerIcon.toString());
}
pkg.helpURL().ifPresent(value -> {
data.put("JpHelpURL", value);
wixVars.put("JpHelpURL", value);
});
pkg.updateURL().ifPresent(value -> {
data.put("JpUpdateURL", value);
wixVars.put("JpUpdateURL", value);
});
pkg.aboutURL().ifPresent(value -> {
data.put("JpAboutURL", value);
wixVars.put("JpAboutURL", value);
});
data.put("JpAppSizeKb", Long.toString(AppImageLayout.toPathGroup(
wixVars.put("JpAppSizeKb", Long.toString(AppImageLayout.toPathGroup(
env.appImageLayout()).sizeInBytes() >> 10));
data.put("JpConfigDir", env.configDir().toAbsolutePath().toString());
wixVars.put("JpConfigDir", env.configDir().toAbsolutePath().toString());
if (pkg.isSystemWideInstall()) {
data.put("JpIsSystemWide", "yes");
wixVars.define("JpIsSystemWide");
}
return data;
return wixVars;
}
private static List<Path> getWxlFilesFromDir(Path dir) {

View File

@ -32,7 +32,6 @@ import java.nio.file.Path;
import java.util.Collection;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.stream.XMLStreamWriter;
@ -65,16 +64,14 @@ abstract class WixFragmentBuilder {
}
void initFromParams(BuildEnv env, WinMsiPackage pkg) {
wixVariables = null;
wixVariables = new WixVariables();
additionalResources = null;
configRoot = env.configDir();
fragmentResource = env.createResource(defaultResourceName).setPublicName(outputFileName);
}
void configureWixPipeline(WixPipeline.Builder wixPipeline) {
wixPipeline.addSource(configRoot.resolve(outputFileName),
Optional.ofNullable(wixVariables).map(WixVariables::getValues).orElse(
null));
wixPipeline.addSource(configRoot.resolve(outputFileName), wixVariables);
}
void addFilesToConfigRoot() throws IOException {
@ -147,14 +144,11 @@ abstract class WixFragmentBuilder {
protected abstract Collection<XmlConsumer> getFragmentWriters();
protected final void defineWixVariable(String variableName) {
setWixVariable(variableName, "yes");
wixVariables.define(variableName);
}
protected final void setWixVariable(String variableName, String variableValue) {
if (wixVariables == null) {
wixVariables = new WixVariables();
}
wixVariables.setWixVariable(variableName, variableValue);
wixVariables.put(variableName, variableValue);
}
protected final void addResource(OverridableResource resource, String saveAsName) {

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
@ -24,22 +24,18 @@
*/
package jdk.jpackage.internal;
import static jdk.jpackage.internal.ShortPathUtils.adjustPath;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Consumer;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static jdk.jpackage.internal.ShortPathUtils.adjustPath;
import jdk.jpackage.internal.util.PathUtils;
/**
@ -61,18 +57,20 @@ final class WixPipeline {
final var absWorkDir = workDir.normalize().toAbsolutePath();
final UnaryOperator<Path> normalizePath = path -> {
return path.normalize().toAbsolutePath();
};
final var absObjWorkDir = PathUtils.normalizedAbsolutePath(wixObjDir);
final var absObjWorkDir = normalizePath.apply(wixObjDir);
var relSources = sources.stream().map(source -> {
return source.overridePath(normalizePath.apply(source.path));
final var absSources = sources.stream().map(source -> {
return source.copyWithPath(PathUtils.normalizedAbsolutePath(source.path));
}).toList();
return new WixPipeline(toolset, adjustPath(absWorkDir), absObjWorkDir,
wixVariables, mapLightOptions(normalizePath), relSources);
return new WixPipeline(
toolset,
adjustPath(absWorkDir),
absObjWorkDir,
wixVariables.createdImmutableCopy(),
mapLightOptions(PathUtils::normalizedAbsolutePath),
absSources,
msiMutators);
}
Builder setWixObjDir(Path v) {
@ -85,17 +83,30 @@ final class WixPipeline {
return this;
}
Builder setWixVariables(Map<String, String> v) {
wixVariables.clear();
Builder putWixVariables(WixVariables v) {
wixVariables.putAll(v);
return this;
}
Builder addSource(Path source, Map<String, String> wixVariables) {
sources.add(new WixSource(source, wixVariables));
Builder putWixVariables(Map<String, String> v) {
wixVariables.putAll(v);
return this;
}
Builder addSource(Path source, WixVariables wixVariables) {
sources.add(new WixSource(source, wixVariables.createdImmutableCopy()));
return this;
}
Builder addMsiMutator(MsiMutator msiMutator, List<String> args) {
msiMutators.add(new MsiMutatorWithArgs(msiMutator, args));
return this;
}
Builder addSource(Path source) {
return addSource(source, WixVariables.EMPTY);
}
Builder addLightOptions(String ... v) {
lightOptions.addAll(List.of(v));
return this;
@ -119,87 +130,59 @@ final class WixPipeline {
private Path workDir;
private Path wixObjDir;
private final Map<String, String> wixVariables = new HashMap<>();
private final WixVariables wixVariables = new WixVariables();
private final List<String> lightOptions = new ArrayList<>();
private final List<WixSource> sources = new ArrayList<>();
private final List<MsiMutatorWithArgs> msiMutators = new ArrayList<>();
}
static Builder build() {
return new Builder();
}
private WixPipeline(WixToolset toolset, Path workDir, Path wixObjDir,
Map<String, String> wixVariables, List<String> lightOptions,
List<WixSource> sources) {
this.toolset = toolset;
this.workDir = workDir;
this.wixObjDir = wixObjDir;
this.wixVariables = wixVariables;
this.lightOptions = lightOptions;
this.sources = sources;
private WixPipeline(
WixToolset toolset,
Path workDir,
Path wixObjDir,
WixVariables wixVariables,
List<String> lightOptions,
List<WixSource> sources,
List<MsiMutatorWithArgs> msiMutators) {
this.toolset = Objects.requireNonNull(toolset);
this.workDir = Objects.requireNonNull(workDir);
this.wixObjDir = Objects.requireNonNull(wixObjDir);
this.wixVariables = Objects.requireNonNull(wixVariables);
this.lightOptions = Objects.requireNonNull(lightOptions);
this.sources = Objects.requireNonNull(sources);
this.msiMutators = Objects.requireNonNull(msiMutators);
}
void buildMsi(Path msi) throws IOException {
Objects.requireNonNull(workDir);
// Use short path to the output msi to workaround
// WiX limitations of handling long paths.
var transientMsi = wixObjDir.resolve("a.msi");
var configRoot = workDir.resolve(transientMsi).getParent();
for (var msiMutator : msiMutators) {
msiMutator.addToConfigRoot(configRoot);
}
switch (toolset.getType()) {
case Wix3 -> buildMsiWix3(transientMsi);
case Wix4 -> buildMsiWix4(transientMsi);
default -> throw new IllegalArgumentException();
}
for (var msiMutator : msiMutators) {
msiMutator.execute(configRoot, workDir.resolve(transientMsi));
}
IOUtils.copyFile(workDir.resolve(transientMsi), msi);
}
private void addWixVariblesToCommandLine(
Map<String, String> otherWixVariables, List<String> cmdline) {
Stream.of(wixVariables, Optional.ofNullable(otherWixVariables).
orElseGet(Collections::emptyMap)).filter(Objects::nonNull).
reduce((a, b) -> {
a.putAll(b);
return a;
}).ifPresent(wixVars -> {
var entryStream = wixVars.entrySet().stream();
Stream<String> stream;
switch (toolset.getType()) {
case Wix3 -> {
stream = entryStream.map(wixVar -> {
return String.format("-d%s=%s", wixVar.getKey(), wixVar.
getValue());
});
}
case Wix4 -> {
stream = entryStream.map(wixVar -> {
return Stream.of("-d", String.format("%s=%s", wixVar.
getKey(), wixVar.getValue()));
}).flatMap(Function.identity());
}
default -> {
throw new IllegalArgumentException();
}
}
stream.reduce(cmdline, (ctnr, wixVar) -> {
ctnr.add(wixVar);
return ctnr;
}, (x, y) -> {
x.addAll(y);
return x;
});
});
}
private void buildMsiWix4(Path msi) throws IOException {
var mergedSrcWixVars = sources.stream().map(wixSource -> {
return Optional.ofNullable(wixSource.variables).orElseGet(
Collections::emptyMap).entrySet().stream();
}).flatMap(Function.identity()).collect(Collectors.toMap(
Map.Entry::getKey, Map.Entry::getValue));
List<String> cmdline = new ArrayList<>(List.of(
toolset.getToolPath(WixTool.Wix4).toString(),
@ -213,7 +196,7 @@ final class WixPipeline {
cmdline.addAll(lightOptions);
addWixVariblesToCommandLine(mergedSrcWixVars, cmdline);
addWixVariablesToCommandLine(sources.stream(), cmdline::addAll);
cmdline.addAll(sources.stream().map(wixSource -> {
return wixSource.path.toString();
@ -241,7 +224,6 @@ final class WixPipeline {
lightCmdline.addAll(lightOptions);
wixObjs.stream().map(Path::toString).forEach(lightCmdline::add);
Files.createDirectories(msi.getParent());
execute(lightCmdline);
}
@ -262,7 +244,7 @@ final class WixPipeline {
cmdline.add("-fips");
}
addWixVariblesToCommandLine(wixSource.variables, cmdline);
addWixVariablesToCommandLine(Stream.of(wixSource), cmdline::addAll);
execute(cmdline);
@ -273,16 +255,47 @@ final class WixPipeline {
Executor.of(new ProcessBuilder(cmdline).directory(workDir.toFile())).executeExpectSuccess();
}
private record WixSource(Path path, Map<String, String> variables) {
WixSource overridePath(Path path) {
private void addWixVariablesToCommandLine(Stream<WixSource> wixSources, Consumer<List<String>> sink) {
sink.accept(wixSources.map(WixSource::variables).reduce(wixVariables, (a, b) -> {
return new WixVariables().putAll(a).putAll(b);
}).toWixCommandLine(toolset.getType()));
}
private record WixSource(Path path, WixVariables variables) {
WixSource {
Objects.requireNonNull(path);
Objects.requireNonNull(variables);
}
WixSource copyWithPath(Path path) {
return new WixSource(path, variables);
}
}
private record MsiMutatorWithArgs(MsiMutator mutator, List<String> args) {
MsiMutatorWithArgs {
Objects.requireNonNull(mutator);
Objects.requireNonNull(args);
}
void addToConfigRoot(Path configRoot) throws IOException {
mutator.addToConfigRoot(configRoot);
}
void execute(Path configRoot, Path transientMsi) throws IOException {
Executor.of("cscript", "//Nologo")
.args(PathUtils.normalizedAbsolutePathString(configRoot.resolve(mutator.pathInConfigRoot())))
.args(PathUtils.normalizedAbsolutePathString(transientMsi))
.args(args)
.executeExpectSuccess();
}
}
private final WixToolset toolset;
private final Map<String, String> wixVariables;
private final WixVariables wixVariables;
private final List<String> lightOptions;
private final Path wixObjDir;
private final Path workDir;
private final List<WixSource> sources;
private final List<MsiMutatorWithArgs> msiMutators;
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2021, 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,25 +24,32 @@
*/
package jdk.jpackage.internal;
import jdk.jpackage.internal.model.WinMsiPackage;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Stream;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
import jdk.jpackage.internal.WixAppImageFragmentBuilder.ShortcutsFolder;
import jdk.jpackage.internal.WixToolset.WixToolsetType;
import jdk.jpackage.internal.model.WinMsiPackage;
import jdk.jpackage.internal.resources.ResourceLocator;
import jdk.jpackage.internal.util.XmlConsumer;
import jdk.jpackage.internal.wixui.Dialog;
import jdk.jpackage.internal.wixui.DialogPair;
import jdk.jpackage.internal.wixui.Publish;
import jdk.jpackage.internal.wixui.ShowActionSuppresser;
import jdk.jpackage.internal.wixui.UIConfig;
import jdk.jpackage.internal.wixui.UISpec;
/**
* Creates UI WiX fragment.
@ -53,63 +60,83 @@ final class WixUiFragmentBuilder extends WixFragmentBuilder {
void initFromParams(BuildEnv env, WinMsiPackage pkg) {
super.initFromParams(env, pkg);
withLicenseDlg = pkg.licenseFile().isPresent();
if (withLicenseDlg) {
final var shortcutFolders = ShortcutsFolder.getForPackage(pkg);
uiConfig = UIConfig.build()
.withLicenseDlg(pkg.licenseFile().isPresent())
.withInstallDirChooserDlg(pkg.withInstallDirChooser())
.withShortcutPromptDlg(!shortcutFolders.isEmpty() && pkg.withShortcutPrompt())
.create();
if (!uiConfig.equals(UIConfig.build().create()) || pkg.withUI()) {
uiSpec = Optional.of(UISpec.create(uiConfig));
} else {
uiSpec = Optional.empty();
}
if (uiConfig.isWithLicenseDlg()) {
Path licenseFileName = pkg.licenseFile().orElseThrow().getFileName();
Path destFile = getConfigRoot().resolve(licenseFileName);
setWixVariable("JpLicenseRtf", destFile.toAbsolutePath().toString());
}
withInstallDirChooserDlg = pkg.withInstallDirChooser();
final var shortcutFolders = ShortcutsFolder.getForPackage(pkg);
withShortcutPromptDlg = !shortcutFolders.isEmpty() && pkg.withShortcutPrompt();
customDialogs = new ArrayList<>();
if (withShortcutPromptDlg) {
CustomDialog dialog = new CustomDialog(env::createResource, I18N.getString(
"resource.shortcutpromptdlg-wix-file"),
if (uiConfig.isWithShortcutPromptDlg()) {
CustomDialog dialog = new CustomDialog(
env::createResource,
I18N.getString("resource.shortcutpromptdlg-wix-file"),
"ShortcutPromptDlg.wxs");
for (var shortcutFolder : shortcutFolders) {
dialog.wixVariables.defineWixVariable(
dialog.wixVariables.define(
shortcutFolder.getWixVariableName());
}
customDialogs.add(dialog);
}
if (withInstallDirChooserDlg) {
CustomDialog dialog = new CustomDialog(env::createResource, I18N.getString(
"resource.installdirnotemptydlg-wix-file"),
if (uiConfig.isWithInstallDirChooserDlg()) {
CustomDialog dialog = new CustomDialog(
env::createResource,
I18N.getString("resource.installdirnotemptydlg-wix-file"),
"InstallDirNotEmptyDlg.wxs");
List<Dialog> dialogIds = getUI().dialogIdsSupplier.apply(this);
dialog.wixVariables.setWixVariable("JpAfterInstallDirDlg",
dialogIds.get(dialogIds.indexOf(Dialog.InstallDirDlg) + 1).id);
customDialogs.add(dialog);
}
}
@Override
void configureWixPipeline(WixPipeline.Builder wixPipeline) {
super.configureWixPipeline(wixPipeline);
if (withShortcutPromptDlg || withInstallDirChooserDlg || withLicenseDlg) {
final String extName;
switch (getWixType()) {
case Wix3 -> extName = "WixUIExtension";
case Wix4 -> extName = "WixToolset.UI.wixext";
default -> throw new IllegalArgumentException();
}
wixPipeline.addLightOptions("-ext", extName);
}
// Only needed if we using CA dll, so Wix can find it
if (withCustomActionsDll) {
wixPipeline.addLightOptions("-b",
getConfigRoot().toAbsolutePath().toString());
}
if (uiSpec.isEmpty()) {
return;
}
var extName = switch (getWixType()) {
case Wix3 -> "WixUIExtension";
case Wix4 -> "WixToolset.UI.wixext";
};
wixPipeline.addLightOptions("-ext", extName);
wixPipeline.putWixVariables(uiSpec.get().wixVariables());
if (!uiSpec.get().hideDialogs().isEmpty() && getWixType() == WixToolsetType.Wix3) {
// Older WiX doesn't support multiple overrides of a "ShowAction" element.
// Have to run a script to alter the msi.
var removeActions = uiSpec.get().hideDialogs().stream()
.map(ShowActionSuppresser::dialog)
.sorted(Dialog.DEFAULT_COMPARATOR)
.map(Dialog::id);
wixPipeline.addMsiMutator(
new MsiMutator("msi-disable-actions.js"),
Stream.concat(Stream.of("InstallUISequence"), removeActions).toList());
}
for (var customDialog : customDialogs) {
customDialog.addToWixPipeline(wixPipeline);
}
@ -132,26 +159,24 @@ final class WixUiFragmentBuilder extends WixFragmentBuilder {
return List.of(this::addUI);
}
private void addUI(XMLStreamWriter xml) throws XMLStreamException,
IOException {
private void addUI(XMLStreamWriter xml) throws XMLStreamException, IOException {
if (withInstallDirChooserDlg) {
if (uiConfig.isWithInstallDirChooserDlg()) {
xml.writeStartElement("Property");
xml.writeAttribute("Id", "WIXUI_INSTALLDIR");
xml.writeAttribute("Value", "INSTALLDIR");
xml.writeEndElement(); // Property
}
if (withLicenseDlg) {
if (uiConfig.isWithLicenseDlg()) {
xml.writeStartElement("WixVariable");
xml.writeAttribute("Id", "WixUILicenseRtf");
xml.writeAttribute("Value", "$(var.JpLicenseRtf)");
xml.writeEndElement(); // WixVariable
}
var ui = getUI();
if (ui != null) {
ui.write(getWixType(), this, xml);
if (uiSpec.isPresent()) {
writeNonEmptyUIElement(xml);
} else {
xml.writeStartElement("UI");
xml.writeAttribute("Id", "JpUI");
@ -159,371 +184,140 @@ final class WixUiFragmentBuilder extends WixFragmentBuilder {
}
}
private UI getUI() {
if (withInstallDirChooserDlg || withShortcutPromptDlg) {
// WixUI_InstallDir for shortcut prompt dialog too because in
// WixUI_Minimal UI sequence WelcomeEulaDlg dialog doesn't have "Next"
// button, but has "Install" button. So inserting shortcut prompt dialog
// after welcome dialog in WixUI_Minimal UI sequence would be confusing
return UI.InstallDir;
} else if (withLicenseDlg) {
return UI.Minimal;
} else {
return null;
}
}
void writeNonEmptyUIElement(XMLStreamWriter xml) throws XMLStreamException, IOException {
private enum UI {
InstallDir("WixUI_InstallDir",
WixUiFragmentBuilder::dialogSequenceForWixUI_InstallDir,
Dialog::createPairsForWixUI_InstallDir),
Minimal("WixUI_Minimal", null, null);
switch (getWixType()) {
case Wix3 -> {}
case Wix4 -> {
// https://wixtoolset.org/docs/fourthree/faqs/#converting-custom-wixui-dialog-sets
xml.writeProcessingInstruction("foreach WIXUIARCH in X86;X64;A64");
writeWix4UIRef(xml, uiSpec.get().wixUI().id(), "JpUIInternal_$(WIXUIARCH)");
xml.writeProcessingInstruction("endforeach");
UI(String wixUIRef,
Function<WixUiFragmentBuilder, List<Dialog>> dialogIdsSupplier,
Supplier<Map<DialogPair, List<Publish>>> dialogPairsSupplier) {
this.wixUIRef = wixUIRef;
this.dialogIdsSupplier = dialogIdsSupplier;
this.dialogPairsSupplier = dialogPairsSupplier;
}
void write(WixToolsetType wixType, WixUiFragmentBuilder outer, XMLStreamWriter xml) throws XMLStreamException, IOException {
switch (wixType) {
case Wix3 -> {}
case Wix4 -> {
// https://wixtoolset.org/docs/fourthree/faqs/#converting-custom-wixui-dialog-sets
xml.writeProcessingInstruction("foreach WIXUIARCH in X86;X64;A64");
writeWix4UIRef(xml, wixUIRef, "JpUIInternal_$(WIXUIARCH)");
xml.writeProcessingInstruction("endforeach");
writeWix4UIRef(xml, "JpUIInternal", "JpUI");
}
default -> {
throw new IllegalArgumentException();
}
}
xml.writeStartElement("UI");
switch (wixType) {
case Wix3 -> {
xml.writeAttribute("Id", "JpUI");
xml.writeStartElement("UIRef");
xml.writeAttribute("Id", wixUIRef);
xml.writeEndElement(); // UIRef
}
case Wix4 -> {
xml.writeAttribute("Id", "JpUIInternal");
}
default -> {
throw new IllegalArgumentException();
}
}
writeContents(wixType, outer, xml);
xml.writeEndElement(); // UI
}
private void writeContents(WixToolsetType wixType, WixUiFragmentBuilder outer,
XMLStreamWriter xml) throws XMLStreamException, IOException {
if (dialogIdsSupplier != null) {
List<Dialog> dialogIds = dialogIdsSupplier.apply(outer);
Map<DialogPair, List<Publish>> dialogPairs = dialogPairsSupplier.get();
if (dialogIds.contains(Dialog.InstallDirDlg)) {
xml.writeStartElement("DialogRef");
xml.writeAttribute("Id", "InstallDirNotEmptyDlg");
xml.writeEndElement(); // DialogRef
}
var it = dialogIds.iterator();
Dialog firstId = it.next();
while (it.hasNext()) {
Dialog secondId = it.next();
DialogPair pair = new DialogPair(firstId, secondId);
for (var curPair : List.of(pair, pair.flip())) {
for (var publish : dialogPairs.get(curPair)) {
writePublishDialogPair(wixType, xml, publish, curPair);
}
}
firstId = secondId;
}
writeWix4UIRef(xml, "JpUIInternal", "JpUI");
}
}
private static void writeWix4UIRef(XMLStreamWriter xml, String uiRef, String id) throws XMLStreamException, IOException {
// https://wixtoolset.org/docs/fourthree/faqs/#referencing-the-standard-wixui-dialog-sets
xml.writeStartElement("UI");
xml.writeAttribute("Id", id);
xml.writeStartElement("ui:WixUI");
xml.writeAttribute("Id", uiRef);
xml.writeNamespace("ui", "http://wixtoolset.org/schemas/v4/wxs/ui");
xml.writeEndElement(); // UIRef
xml.writeEndElement(); // UI
}
private final String wixUIRef;
private final Function<WixUiFragmentBuilder, List<Dialog>> dialogIdsSupplier;
private final Supplier<Map<DialogPair, List<Publish>>> dialogPairsSupplier;
}
private List<Dialog> dialogSequenceForWixUI_InstallDir() {
List<Dialog> dialogIds = new ArrayList<>(
List.of(Dialog.WixUI_WelcomeDlg));
if (withLicenseDlg) {
dialogIds.add(Dialog.WixUI_LicenseAgreementDlg);
}
if (withInstallDirChooserDlg) {
dialogIds.add(Dialog.InstallDirDlg);
}
if (withShortcutPromptDlg) {
dialogIds.add(Dialog.ShortcutPromptDlg);
}
dialogIds.add(Dialog.WixUI_VerifyReadyDlg);
return dialogIds;
}
private enum Dialog {
WixUI_WelcomeDlg,
WixUI_LicenseAgreementDlg,
InstallDirDlg,
ShortcutPromptDlg,
WixUI_VerifyReadyDlg;
Dialog() {
if (name().startsWith("WixUI_")) {
id = name().substring("WixUI_".length());
} else {
id = name();
xml.writeStartElement("UI");
switch (getWixType()) {
case Wix3 -> {
xml.writeAttribute("Id", "JpUI");
xml.writeStartElement("UIRef");
xml.writeAttribute("Id", uiSpec.get().wixUI().id());
xml.writeEndElement(); // UIRef
}
case Wix4 -> {
xml.writeAttribute("Id", "JpUIInternal");
}
}
static Map<DialogPair, List<Publish>> createPair(Dialog firstId,
Dialog secondId, List<PublishBuilder> nextBuilders,
List<PublishBuilder> prevBuilders) {
var pair = new DialogPair(firstId, secondId);
return Map.of(pair, nextBuilders.stream().map(b -> {
return buildPublish(b.create()).next().create();
}).toList(), pair.flip(),
prevBuilders.stream().map(b -> {
return buildPublish(b.create()).back().create();
}).toList());
}
static Map<DialogPair, List<Publish>> createPair(Dialog firstId,
Dialog secondId, List<PublishBuilder> builders) {
return createPair(firstId, secondId, builders, builders);
}
static Map<DialogPair, List<Publish>> createPairsForWixUI_InstallDir() {
Map<DialogPair, List<Publish>> map = new HashMap<>();
// Order is a "weight" of action. If there are multiple
// "NewDialog" action for the same dialog Id, MSI would pick the one
// with higher order value. In WixUI_InstallDir dialog sequence the
// highest order value is 4. InstallDirNotEmptyDlg adds NewDialog
// action with order 5. Setting order to 6 for all
// actions configured in this function would make them executed
// instead of corresponding default actions defined in
// WixUI_InstallDir dialog sequence.
var order = 6;
// Based on WixUI_InstallDir.wxs
var backFromVerifyReadyDlg = List.of(buildPublish().condition(
"NOT Installed").order(order));
var uncondinal = List.of(buildPublish().condition("1"));
var ifNotIstalled = List.of(
buildPublish().condition("NOT Installed").order(order));
var ifLicenseAccepted = List.of(buildPublish().condition(
"LicenseAccepted = \"1\"").order(order));
// Empty condition list for the default dialog sequence
map.putAll(createPair(WixUI_WelcomeDlg, WixUI_LicenseAgreementDlg,
List.of()));
map.putAll(
createPair(WixUI_WelcomeDlg, InstallDirDlg, ifNotIstalled));
map.putAll(createPair(WixUI_WelcomeDlg, ShortcutPromptDlg,
ifNotIstalled));
map.putAll(createPair(WixUI_LicenseAgreementDlg, InstallDirDlg,
List.of()));
map.putAll(createPair(WixUI_LicenseAgreementDlg, ShortcutPromptDlg,
ifLicenseAccepted, uncondinal));
map.putAll(createPair(WixUI_LicenseAgreementDlg,
WixUI_VerifyReadyDlg, ifLicenseAccepted,
backFromVerifyReadyDlg));
map.putAll(createPair(InstallDirDlg, ShortcutPromptDlg, List.of(),
uncondinal));
map.putAll(createPair(InstallDirDlg, WixUI_VerifyReadyDlg, List.of()));
map.putAll(createPair(ShortcutPromptDlg, WixUI_VerifyReadyDlg,
uncondinal, backFromVerifyReadyDlg));
return map;
}
private final String id;
writeUIElementContents(xml);
xml.writeEndElement(); // UI
}
private static final class DialogPair {
private void writeUIElementContents(XMLStreamWriter xml) throws XMLStreamException, IOException {
DialogPair(Dialog first, Dialog second) {
this(first.id, second.id);
if (uiConfig.isWithInstallDirChooserDlg()) {
xml.writeStartElement("DialogRef");
xml.writeAttribute("Id", "InstallDirNotEmptyDlg");
xml.writeEndElement(); // DialogRef
}
DialogPair(String firstId, String secondId) {
this.firstId = firstId;
this.secondId = secondId;
for (var e : uiSpec.get().customDialogSequence().entrySet().stream()
.sorted(Comparator.comparing(Map.Entry::getKey, DialogPair.DEFAULT_COMPARATOR))
.toList()) {
writePublishDialogPair(getWixType(), xml, e.getValue(), e.getKey());
}
DialogPair flip() {
return new DialogPair(secondId, firstId);
}
@Override
public int hashCode() {
int hash = 3;
hash = 97 * hash + Objects.hashCode(this.firstId);
hash = 97 * hash + Objects.hashCode(this.secondId);
return hash;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final DialogPair other = (DialogPair) obj;
if (!Objects.equals(this.firstId, other.firstId)) {
return false;
}
if (!Objects.equals(this.secondId, other.secondId)) {
return false;
}
return true;
}
private final String firstId;
private final String secondId;
hideDialogs(getWixType(), xml, uiSpec.get().hideDialogs());
}
private static final class Publish {
Publish(String control, String condition, int order) {
this.control = control;
this.condition = condition;
this.order = order;
}
private final String control;
private final String condition;
private final int order;
private static void writeWix4UIRef(XMLStreamWriter xml, String uiRef, String id) throws XMLStreamException, IOException {
// https://wixtoolset.org/docs/fourthree/faqs/#referencing-the-standard-wixui-dialog-sets
xml.writeStartElement("UI");
xml.writeAttribute("Id", Objects.requireNonNull(id));
xml.writeStartElement("ui:WixUI");
xml.writeAttribute("Id", Objects.requireNonNull(uiRef));
xml.writeNamespace("ui", "http://wixtoolset.org/schemas/v4/wxs/ui");
xml.writeEndElement(); // UIRef
xml.writeEndElement(); // UI
}
private static final class PublishBuilder {
private static void writePublishDialogPair(
WixToolsetType wixType,
XMLStreamWriter xml,
Publish publish,
DialogPair dialogPair) throws IOException, XMLStreamException {
PublishBuilder() {
order(0);
next();
condition("1");
}
PublishBuilder(Publish publish) {
order(publish.order);
control(publish.control);
condition(publish.condition);
}
public PublishBuilder control(String v) {
control = v;
return this;
}
public PublishBuilder next() {
return control("Next");
}
public PublishBuilder back() {
return control("Back");
}
public PublishBuilder condition(String v) {
condition = v;
return this;
}
public PublishBuilder order(int v) {
order = v;
return this;
}
Publish create() {
return new Publish(control, condition, order);
}
private String control;
private String condition;
private int order;
}
private static PublishBuilder buildPublish() {
return new PublishBuilder();
}
private static PublishBuilder buildPublish(Publish publish) {
return new PublishBuilder(publish);
}
private static void writePublishDialogPair(WixToolsetType wixType, XMLStreamWriter xml,
Publish publish, DialogPair dialogPair) throws IOException, XMLStreamException {
xml.writeStartElement("Publish");
xml.writeAttribute("Dialog", dialogPair.firstId);
xml.writeAttribute("Control", publish.control);
xml.writeAttribute("Dialog", dialogPair.first().id());
xml.writeAttribute("Control", publish.control().id());
xml.writeAttribute("Event", "NewDialog");
xml.writeAttribute("Value", dialogPair.secondId);
if (publish.order != 0) {
xml.writeAttribute("Order", String.valueOf(publish.order));
xml.writeAttribute("Value", dialogPair.second().id());
if (publish.order() != 0) {
xml.writeAttribute("Order", String.valueOf(publish.order()));
}
switch (wixType) {
case Wix3 -> xml.writeCharacters(publish.condition);
case Wix4 -> xml.writeAttribute("Condition", publish.condition);
default -> throw new IllegalArgumentException();
case Wix3 -> {
xml.writeCharacters(publish.condition());
}
case Wix4 -> {
xml.writeAttribute("Condition", publish.condition());
}
}
xml.writeEndElement();
}
private static void hideDialogs(
WixToolsetType wixType,
XMLStreamWriter xml,
Collection<? extends ShowActionSuppresser> hideDialogs) throws IOException, XMLStreamException {
if (!hideDialogs.isEmpty()) {
if (wixType == WixToolsetType.Wix4) {
xml.writeStartElement("InstallUISequence");
for (var showAction : hideDialogs.stream().sorted(ShowActionSuppresser.DEFAULT_COMPARATOR).toList()) {
writeWix4ShowAction(wixType, xml, showAction);
}
xml.writeEndElement();
}
}
}
private static void writeWix4ShowAction(
WixToolsetType wixType,
XMLStreamWriter xml,
ShowActionSuppresser hideDialog) throws IOException, XMLStreamException {
xml.writeStartElement("Show");
xml.writeAttribute("Dialog", String.format("override %s", hideDialog.dialog().id()));
xml.writeAttribute(switch (hideDialog.order()) {
case AFTER -> "After";
}, hideDialog.anchor().id());
xml.writeAttribute("Condition", "0");
xml.writeEndElement();
}
private final class CustomDialog {
CustomDialog(Function<String, OverridableResource> createResource, String category,
String wxsFileName) {
CustomDialog(Function<String, OverridableResource> createResource, String category, String wxsFileName) {
this.wxsFileName = wxsFileName;
this.wixVariables = new WixVariables();
addResource(createResource.apply(wxsFileName).setCategory(category).setPublicName(
wxsFileName), wxsFileName);
addResource(createResource.apply(wxsFileName).setCategory(category).setPublicName(wxsFileName), wxsFileName);
}
void addToWixPipeline(WixPipeline.Builder wixPipeline) {
wixPipeline.addSource(getConfigRoot().toAbsolutePath().resolve(
wxsFileName), wixVariables.getValues());
wixPipeline.addSource(getConfigRoot().toAbsolutePath().resolve(wxsFileName), wixVariables);
}
private final WixVariables wixVariables;
private final String wxsFileName;
}
private boolean withInstallDirChooserDlg;
private boolean withShortcutPromptDlg;
private boolean withLicenseDlg;
private UIConfig uiConfig;
private Optional<UISpec> uiSpec;
private boolean withCustomActionsDll = true;
private List<CustomDialog> customDialogs;
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2021, 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,22 +24,103 @@
*/
package jdk.jpackage.internal;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Stream;
import jdk.jpackage.internal.WixToolset.WixToolsetType;
final class WixVariables {
void defineWixVariable(String variableName) {
setWixVariable(variableName, "yes");
WixVariables() {
this.values = new HashMap<>();
}
void setWixVariable(String variableName, String variableValue) {
values.put(variableName, variableValue);
private WixVariables(Map<String, String> values) {
this.values = values;
this.isImmutable = true;
}
Map<String, String> getValues() {
return values;
WixVariables define(String variableName) {
return put(variableName, "yes");
}
private final Map<String, String> values = new HashMap<>();
WixVariables put(String variableName, String variableValue) {
Objects.requireNonNull(variableName);
Objects.requireNonNull(variableValue);
validateMutable();
values.compute(variableName, (k, v) -> {
if (!allowOverrides && v != null) {
throw overridingDisabled();
}
return variableValue;
});
return this;
}
WixVariables putAll(Map<String, String> values) {
Objects.requireNonNull(values);
validateMutable();
if (!allowOverrides && !Collections.disjoint(this.values.keySet(), values.keySet())) {
throw overridingDisabled();
} else {
values.entrySet().forEach(e -> {
put(e.getKey(), e.getValue());
});
}
return this;
}
WixVariables putAll(WixVariables other) {
return putAll(other.values);
}
WixVariables allowOverrides(boolean v) {
validateMutable();
allowOverrides = v;
return this;
}
WixVariables createdImmutableCopy() {
if (isImmutable) {
return this;
} else {
return new WixVariables(Map.copyOf(values));
}
}
List<String> toWixCommandLine(WixToolsetType wixType) {
var orderedWixVars = values.entrySet().stream().sorted(Comparator.comparing(Map.Entry::getKey));
return (switch (Objects.requireNonNull(wixType)) {
case Wix3 -> {
yield orderedWixVars.map(wixVar -> {
return String.format("-d%s=%s", wixVar.getKey(), wixVar.getValue());
});
}
case Wix4 -> {
yield orderedWixVars.flatMap(wixVar -> {
return Stream.of("-d", String.format("%s=%s", wixVar.getKey(), wixVar.getValue()));
});
}
}).toList();
}
private void validateMutable() {
if (isImmutable) {
throw new IllegalStateException("WiX variables container is immutable");
}
}
private static IllegalStateException overridingDisabled() {
return new IllegalStateException("Overriding variables is unsupported");
}
private final Map<String, String> values;
private boolean allowOverrides;
private boolean isImmutable;
static final WixVariables EMPTY = new WixVariables().createdImmutableCopy();
}

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
@ -36,6 +36,8 @@ public interface WinMsiPackageMixin {
boolean withShortcutPrompt();
boolean withUI();
Optional<String> helpURL();
Optional<String> updateURL();
@ -50,8 +52,16 @@ public interface WinMsiPackageMixin {
Optional<Path> serviceInstaller();
record Stub(DottedVersion msiVersion, boolean withInstallDirChooser, boolean withShortcutPrompt,
Optional<String> helpURL, Optional<String> updateURL, String startMenuGroupName,
boolean isSystemWideInstall, UUID upgradeCode, UUID productCode,
record Stub(
DottedVersion msiVersion,
boolean withInstallDirChooser,
boolean withShortcutPrompt,
boolean withUI,
Optional<String> helpURL,
Optional<String> updateURL,
String startMenuGroupName,
boolean isSystemWideInstall,
UUID upgradeCode,
UUID productCode,
Optional<Path> serviceInstaller) implements WinMsiPackageMixin {}
}

View File

@ -0,0 +1,79 @@
/*
* Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
function modifyMsi(msiPath, callback) {
var installer = new ActiveXObject('WindowsInstaller.Installer')
var database = installer.OpenDatabase(msiPath, 1 /* msiOpenDatabaseModeTransact */)
callback(installer, database)
database.Commit()
}
function disableActions(installer, db, sequence, actionIDs) {
var tables = {}
var view = db.OpenView("SELECT `Action`, `Condition`, `Sequence` FROM " + sequence)
view.Execute()
try {
while (true) {
var record = view.Fetch()
if (!record) {
break
}
var action = record.StringData(1)
if (actionIDs.hasOwnProperty(action)) {
WScript.Echo("Set condition of [" + action + "] action in [" + sequence + "] sequence to [0]")
var newRecord = installer.CreateRecord(3)
for (var i = 1; i !== newRecord.FieldCount + 1; i++) {
newRecord.StringData(i) = record.StringData(i)
}
newRecord.StringData(2) = "0" // Set condition value to `0`
view.Modify(3 /* msiViewModifyAssign */, newRecord) // Replace existing record
}
}
} finally {
view.Close()
}
}
(function () {
var msi = WScript.arguments(0)
var sequence = WScript.arguments(1)
var actionIDs = {}
for (var i = 0; i !== WScript.arguments.Count(); i++) {
actionIDs[WScript.arguments(i)] = true
}
modifyMsi(msi, function (installer, db) {
disableActions(installer, db, sequence, actionIDs)
})
})()

View File

@ -0,0 +1,36 @@
/*
* Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.jpackage.internal.wixui;
import java.util.Comparator;
/**
* WiX Dialog's control.
*/
public sealed interface Control permits StandardControl {
String id();
public static final Comparator<Control> DEFAULT_COMPARATOR = Comparator.comparing(Control::id);
}

View File

@ -0,0 +1,38 @@
/*
* Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. 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.wixui;
/**
* Custom jpackage dialogs.
*/
public enum CustomDialog implements Dialog {
ShortcutPromptDlg,
;
@Override
public String id() {
return name();
}
}

View File

@ -0,0 +1,36 @@
/*
* Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.jpackage.internal.wixui;
import java.util.Comparator;
/**
* WiX Dialog.
*/
public sealed interface Dialog permits WixDialog, CustomDialog {
String id();
public static final Comparator<Dialog> DEFAULT_COMPARATOR = Comparator.comparing(Dialog::id);
}

View File

@ -0,0 +1,49 @@
/*
* Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.jpackage.internal.wixui;
import static java.util.Comparator.comparing;
import java.util.Comparator;
import java.util.Objects;
public record DialogPair(Dialog first, Dialog second) {
public DialogPair {
Objects.requireNonNull(first);
Objects.requireNonNull(second);
if (first.equals(second) || first.id().equals(second.id())) {
throw new IllegalArgumentException("Dialogs must be different");
}
}
DialogPair flip() {
return new DialogPair(second, first);
}
public static final Comparator<DialogPair> DEFAULT_COMPARATOR =
comparing(DialogPair::first, Dialog.DEFAULT_COMPARATOR)
.thenComparing(comparing(DialogPair::second, Dialog.DEFAULT_COMPARATOR));
}

View File

@ -0,0 +1,92 @@
/*
* Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.jpackage.internal.wixui;
import java.util.Objects;
public record Publish(Control control, String condition, int order) {
public Publish {
Objects.requireNonNull(control);
Objects.requireNonNull(condition);
if (order < 0) {
throw new IllegalArgumentException("Negative order");
}
}
Builder toBuilder() {
return new Builder(this);
}
static Builder build() {
return new Builder();
}
static final class Builder {
private Builder() {
order(0);
next();
condition("1");
}
private Builder(Publish publish) {
order(publish.order);
control(publish.control);
condition(publish.condition);
}
Publish create() {
return new Publish(control, condition, order);
}
Builder control(Control v) {
control = v;
return this;
}
Builder next() {
return control(StandardControl.NEXT);
}
Builder back() {
return control(StandardControl.BACK);
}
Builder condition(String v) {
condition = v;
return this;
}
Builder order(int v) {
order = v;
return this;
}
private int order;
private Control control;
private String condition;
}
}

View File

@ -0,0 +1,72 @@
/*
* Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.jpackage.internal.wixui;
import static java.util.Comparator.comparing;
import java.util.Comparator;
import java.util.Objects;
public record ShowActionSuppresser(Dialog dialog, Dialog anchor, Order order) {
public enum Order {
AFTER,
;
}
public ShowActionSuppresser {
Objects.requireNonNull(order);
validate(dialog);
validate(anchor);
}
static Builder suppressShowAction(WixDialog dialog) {
return new Builder(dialog);
}
static final class Builder {
private Builder(WixDialog dialog) {
this.dialog = Objects.requireNonNull(dialog);
}
ShowActionSuppresser after(WixDialog anchor) {
return new ShowActionSuppresser(dialog, anchor, Order.AFTER);
}
private final WixDialog dialog;
}
private static void validate(Dialog v) {
if (!(Objects.requireNonNull(v) instanceof WixDialog)) {
throw new IllegalArgumentException();
}
}
public static final Comparator<ShowActionSuppresser> DEFAULT_COMPARATOR =
comparing(ShowActionSuppresser::dialog, Dialog.DEFAULT_COMPARATOR)
.thenComparing(comparing(ShowActionSuppresser::anchor, Dialog.DEFAULT_COMPARATOR))
.thenComparing(comparing(ShowActionSuppresser::order));
}

View File

@ -0,0 +1,47 @@
/*
* Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.jpackage.internal.wixui;
import java.util.Objects;
/**
* Dialog controls referenced in adjustments of the standard WiX UI.
*/
enum StandardControl implements Control {
NEXT("Next"),
BACK("Back"),
;
StandardControl(String id) {
this.id = Objects.requireNonNull(id);
}
@Override
public String id() {
return id;
}
private final String id;
}

View File

@ -0,0 +1,79 @@
/*
* Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.jpackage.internal.wixui;
/**
* UI config.
*/
public record UIConfig(
boolean isWithInstallDirChooserDlg,
boolean isWithShortcutPromptDlg,
boolean isWithLicenseDlg) {
public static Builder build() {
return new Builder();
}
public static final class Builder {
private Builder() {
}
public UIConfig create() {
return new UIConfig(isWithInstallDirChooserDlg, isWithShortcutPromptDlg, isWithLicenseDlg);
}
public Builder withInstallDirChooserDlg(boolean v) {
isWithInstallDirChooserDlg = v;
return this;
}
public Builder withInstallDirChooserDlg() {
return withInstallDirChooserDlg(true);
}
public Builder withShortcutPromptDlg(boolean v) {
isWithShortcutPromptDlg = v;
return this;
}
public Builder withShortcutPromptDlg() {
return withShortcutPromptDlg(true);
}
public Builder withLicenseDlg(boolean v) {
isWithLicenseDlg = v;
return this;
}
public Builder withLicenseDlg() {
return withLicenseDlg(true);
}
private boolean isWithInstallDirChooserDlg;
private boolean isWithShortcutPromptDlg;
private boolean isWithLicenseDlg;
}
}

View File

@ -0,0 +1,293 @@
/*
* Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.jpackage.internal.wixui;
import static jdk.jpackage.internal.wixui.CustomDialog.ShortcutPromptDlg;
import static jdk.jpackage.internal.wixui.ShowActionSuppresser.suppressShowAction;
import static jdk.jpackage.internal.wixui.WixDialog.InstallDirDlg;
import static jdk.jpackage.internal.wixui.WixDialog.LicenseAgreementDlg;
import static jdk.jpackage.internal.wixui.WixDialog.ProgressDlg;
import static jdk.jpackage.internal.wixui.WixDialog.VerifyReadyDlg;
import static jdk.jpackage.internal.wixui.WixDialog.WelcomeDlg;
import static jdk.jpackage.internal.wixui.WixDialog.WelcomeEulaDlg;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.BiConsumer;
import java.util.function.Supplier;
import java.util.stream.Collectors;
/**
* UI spec.
* <p>
* UI is based on one of the standard WiX UIs with optional alterations.
*/
public record UISpec(
WixUI wixUI,
Map<String, String> wixVariables,
Map<DialogPair, Publish> customDialogSequence,
Collection<ShowActionSuppresser> hideDialogs) {
public UISpec {
Objects.requireNonNull(wixUI);
Objects.requireNonNull(wixVariables);
Objects.requireNonNull(customDialogSequence);
Objects.requireNonNull(hideDialogs);
}
static Builder build(WixUI wixUI) {
return new Builder().wixUI(wixUI);
}
static final class Builder {
private Builder() {
}
UISpec create() {
return new UISpec(
wixUI,
Optional.ofNullable(wixVariables).map(Collections::unmodifiableMap).orElseGet(Map::of),
Optional.ofNullable(customDialogSequence).map(Collections::unmodifiableMap).orElseGet(Map::of),
Optional.ofNullable(hideDialogs).map(List::copyOf).orElseGet(List::of));
}
Builder wixUI(WixUI v) {
wixUI = v;
return this;
}
Builder setWixVariable(String name, String value) {
wixVariables.put(Objects.requireNonNull(name), Objects.requireNonNull(value));
return this;
}
Builder customDialogSequence(Map<DialogPair, Publish> v) {
customDialogSequence = v;
return this;
}
Builder hideDialogs(Collection<ShowActionSuppresser> v) {
hideDialogs = v;
return this;
}
Builder hideDialogs(ShowActionSuppresser... v) {
return hideDialogs(List.of(v));
}
private WixUI wixUI;
private final Map<String, String> wixVariables = new HashMap<>();
private Map<DialogPair, Publish> customDialogSequence;
private Collection<ShowActionSuppresser> hideDialogs;
}
public static UISpec create(UIConfig cfg) {
Objects.requireNonNull(cfg);
return Optional.ofNullable(DEFAULT_SPECS.get(cfg)).map(Supplier::get).orElseGet(() -> {
return createCustom(cfg);
});
}
private static UISpec createCustom(UIConfig cfg) {
Objects.requireNonNull(cfg);
var dialogs = installDirUiDialogs(cfg);
var dialogPairs = toDialogPairs(dialogs);
var customDialogSequence = overrideInstallDirDialogSequence().stream().filter(e -> {
return dialogPairs.contains(e.getKey());
}).collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue));
var uiSpec = build(WixUI.INSTALL_DIR).customDialogSequence(customDialogSequence);
var it = dialogs.iterator();
do {
if (it.next().equals(InstallDirDlg)) {
uiSpec.setWixVariable("JpAfterInstallDirDlg", it.next().id());
}
} while (it.hasNext());
return uiSpec.create();
}
private static void createPairs(
BiConsumer<DialogPair, Publish> sink,
Dialog first,
Dialog second,
Publish publishNext,
Publish publishPrev) {
createPairNext(sink, first, second, publishNext);
createPairBack(sink, second, first, publishPrev);
}
private static void createPairs(
BiConsumer<DialogPair, Publish> sink,
Dialog first,
Dialog second,
Publish publish) {
createPairs(sink, first, second, publish, publish);
}
private static void createPairNext(
BiConsumer<DialogPair, Publish> sink,
Dialog first,
Dialog second,
Publish publish) {
var pair = new DialogPair(first, second);
sink.accept(pair, publish.toBuilder().next().create());
}
private static void createPairBack(
BiConsumer<DialogPair, Publish> sink,
Dialog first,
Dialog second,
Publish publish) {
var pair = new DialogPair(first, second);
sink.accept(pair, publish.toBuilder().back().create());
}
private static Collection<DialogPair> toDialogPairs(List<Dialog> dialogs) {
if (dialogs.size() < 2) {
throw new IllegalArgumentException();
}
var pairs = new ArrayList<DialogPair>();
var it = dialogs.listIterator();
var prev = it.next();
do {
var next = it.next();
var pair = new DialogPair(prev, next);
pairs.add(pair);
pairs.add(pair.flip());
prev = next;
} while (it.hasNext());
return pairs;
}
private static List<Dialog> installDirUiDialogs(UIConfig cfg) {
var dialogs = new ArrayList<Dialog>();
dialogs.add(WelcomeDlg);
if (cfg.isWithLicenseDlg()) {
dialogs.add(LicenseAgreementDlg);
}
if (cfg.isWithInstallDirChooserDlg()) {
dialogs.add(InstallDirDlg);
}
if (cfg.isWithShortcutPromptDlg()) {
dialogs.add(ShortcutPromptDlg);
}
dialogs.add(VerifyReadyDlg);
return dialogs;
}
private static Collection<Map.Entry<DialogPair, Publish>> overrideInstallDirDialogSequence() {
List<Map.Entry<DialogPair, Publish>> entries = new ArrayList<>();
BiConsumer<DialogPair, Publish> acc = (pair, publish) -> {
entries.add(Map.entry(pair, publish));
};
// Order is a "weight" of action. If there are multiple
// "NewDialog" action for the same dialog Id, MSI would pick the one
// with higher order value. In WixUI_InstallDir dialog sequence the
// highest order value is 4. InstallDirNotEmptyDlg adds NewDialog
// action with order 5. Setting order to 6 for all
// actions configured in this function would make them executed
// instead of corresponding default actions defined in
// WixUI_InstallDir dialog sequence.
var order = 6;
// Based on WixUI_InstallDir.wxs
var backFromVerifyReadyDlg = Publish.build().condition(CONDITION_NOT_INSTALLED).order(order).create();
var uncondinal = Publish.build().condition(CONDITION_ALWAYS).create();
var ifNotIstalled = Publish.build().condition(CONDITION_NOT_INSTALLED).order(order).create();
var ifLicenseAccepted = Publish.build().condition("LicenseAccepted = \"1\"").order(order).create();
// Define all alternative transitions:
// - Skip standard license dialog
// - Insert shortcut prompt dialog after the standard install dir dialog
// - Replace the standard install dir dialog with the shortcut prompt dialog
createPairs(acc, WelcomeDlg, InstallDirDlg, ifNotIstalled);
createPairs(acc, WelcomeDlg, VerifyReadyDlg, ifNotIstalled);
createPairs(acc, WelcomeDlg, ShortcutPromptDlg, ifNotIstalled);
createPairs(acc, LicenseAgreementDlg, ShortcutPromptDlg, ifLicenseAccepted, uncondinal);
createPairs(acc, LicenseAgreementDlg, VerifyReadyDlg, ifLicenseAccepted, backFromVerifyReadyDlg);
createPairs(acc, InstallDirDlg, ShortcutPromptDlg, uncondinal);
createPairs(acc, ShortcutPromptDlg, VerifyReadyDlg, uncondinal, backFromVerifyReadyDlg);
return entries;
}
private static final Map<UIConfig, Supplier<UISpec>> DEFAULT_SPECS;
private static final String CONDITION_ALWAYS = "1";
private static final String CONDITION_NOT_INSTALLED = "NOT Installed";
static {
var specs = new HashMap<UIConfig, Supplier<UISpec>>();
// Verbatim WiX "Minimal" dialog set.
specs.put(UIConfig.build()
.withLicenseDlg()
.create(), () -> {
return build(WixUI.MINIMAL).create();
});
// Standard WiX "Minimal" dialog set without the license dialog.
// The license dialog is removed by overriding the default "Show"
// action with the condition that always evaluates to "FALSE".
specs.put(UIConfig.build()
.create(), () -> {
return build(WixUI.MINIMAL).hideDialogs(suppressShowAction(WelcomeEulaDlg).after(ProgressDlg)).create();
});
DEFAULT_SPECS = Collections.unmodifiableMap(specs);
}
}

View File

@ -0,0 +1,43 @@
/*
* Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.jpackage.internal.wixui;
/**
* Standard WiX dialogs.
*/
enum WixDialog implements Dialog {
InstallDirDlg,
LicenseAgreementDlg,
ProgressDlg,
VerifyReadyDlg,
WelcomeDlg,
WelcomeEulaDlg,
;
@Override
public String id() {
return name();
}
}

View File

@ -0,0 +1,46 @@
/*
* Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.jpackage.internal.wixui;
import java.util.Objects;
/**
* Standard WiX UI.
*/
public enum WixUI {
MINIMAL("WixUI_Minimal"),
INSTALL_DIR("WixUI_InstallDir"),
;
WixUI(String id) {
this.id = Objects.requireNonNull(id);
}
public String id() {
return id;
}
private final String id;
}

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
@ -30,7 +30,9 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -42,9 +44,10 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
final class MsiDatabase {
public final class MsiDatabase {
static MsiDatabase load(Path msiFile, Path idtFileOutputDir, Set<Table> tableNames) {
try {
@ -61,7 +64,7 @@ final class MsiDatabase {
.execute(0);
var tables = orderedTableNames.stream().map(tableName -> {
return Map.entry(tableName, idtFileOutputDir.resolve(tableName + ".idt"));
return Map.entry(tableName, idtFileOutputDir.resolve(tableName.tableName() + ".idt"));
}).filter(e -> {
return Files.exists(e.getValue());
}).collect(Collectors.toMap(Map.Entry::getKey, e -> {
@ -81,6 +84,8 @@ final class MsiDatabase {
FILE("File"),
PROPERTY("Property"),
SHORTCUT("Shortcut"),
CONTROL_EVENT("ControlEvent"),
INSTALL_UI_SEQUENCE("InstallUISequence"),
;
Table(String name) {
@ -95,6 +100,7 @@ final class MsiDatabase {
static final Set<Table> FIND_PROPERTY_REQUIRED_TABLES = Set.of(PROPERTY);
static final Set<Table> LIST_SHORTCUTS_REQUIRED_TABLES = Set.of(COMPONENT, DIRECTORY, FILE, SHORTCUT);
static final Set<Table> UI_ALTERATIONS_REQUIRED_TABLES = Set.of(CONTROL_EVENT, INSTALL_UI_SEQUENCE);
}
@ -120,12 +126,7 @@ final class MsiDatabase {
}
Collection<Shortcut> listShortcuts() {
var shortcuts = tables.get(Table.SHORTCUT);
if (shortcuts == null) {
return List.of();
}
return IntStream.range(0, shortcuts.rowCount()).mapToObj(i -> {
var row = shortcuts.row(i);
return rows(Table.SHORTCUT).map(row -> {
var shortcutPath = directoryPath(row.apply("Directory_")).resolve(fileNameFromFieldValue(row.apply("Name")));
var workDir = directoryPath(row.apply("WkDir"));
var shortcutTarget = Path.of(expandFormattedString(row.apply("Target")));
@ -133,6 +134,53 @@ final class MsiDatabase {
}).toList();
}
UIAlterations uiAlterations() {
var includeActions = Set.of("WelcomeEulaDlg", "WelcomeDlg");
var actions = actionSequence(Table.INSTALL_UI_SEQUENCE).filter(action -> {
return includeActions.contains(action.action());
}).sorted(Comparator.comparing(Action::sequence)).toList();
// Custom jpackage dialogs.
var jpackageDialogs = Set.of("InstallDirNotEmptyDlg", "ShortcutPromptDlg");
var includeControls = Set.of("Next", "Back");
var controlEvents = rows(Table.CONTROL_EVENT).map(row -> {
return new ControlEvent(
row.apply("Dialog_"),
row.apply("Control_"),
row.apply("Event"),
row.apply("Argument"),
row.apply("Condition"),
Integer.parseInt(row.apply("Ordering")));
}).filter(controlEvent -> {
if (jpackageDialogs.contains(controlEvent.dialog())) {
// Include controls of all custom jpackage dialogs.
return true;
}
if (controlEvent.ordering() >= 6) {
// jpackage assumes the standard WiX UI doesn't define control events
// for dialog sequences it alters with the order higher than 6.
// Include all such items.
if (includeControls.contains(controlEvent.control())) {
// Include only specific controls that jpackage alters.
return true;
}
}
return false;
}).sorted(Comparator.comparing(ControlEvent::dialog)
.thenComparing(ControlEvent::control)
.thenComparing(ControlEvent::event)
.thenComparing(ControlEvent::argument)
.thenComparing(ControlEvent::condition)
.thenComparing(Comparator.comparingInt(ControlEvent::ordering))).toList();
return new UIAlterations(actions, controlEvents);
}
record Shortcut(Path path, Path target, Path workDir) {
Shortcut {
@ -148,6 +196,49 @@ final class MsiDatabase {
}
}
public record Action(String action, String condition, int sequence) {
public Action {
Objects.requireNonNull(action);
Objects.requireNonNull(condition);
}
}
public record ControlEvent(
String dialog,
String control,
String event,
String argument,
String condition,
int ordering) {
public ControlEvent {
Objects.requireNonNull(dialog);
Objects.requireNonNull(control);
Objects.requireNonNull(event);
Objects.requireNonNull(argument);
Objects.requireNonNull(condition);
}
}
public record UIAlterations(Collection<Action> installUISequence, Collection<ControlEvent> controlEvents) {
public UIAlterations {
Objects.requireNonNull(installUISequence);
}
}
private Stream<Action> actionSequence(Table tableName) {
return rows(tableName).map(row -> {
return new Action(row.apply("Action"), row.apply("Condition"), Integer.parseInt(row.apply("Sequence")));
});
}
private Stream<Function<String, String>> rows(Table tableName) {
return Optional.ofNullable(tables.get(tableName)).stream().flatMap(table -> {
return IntStream.range(0, table.rowCount()).mapToObj(table::row);
});
}
private Path directoryPath(String directoryId) {
var table = tables.get(Table.DIRECTORY);
Path result = null;
@ -339,20 +430,28 @@ final class MsiDatabase {
var columns = headerLines.get(0).split("\t");
var header = headerLines.get(2).split("\t", 4);
if (header.length == 3) {
if (Pattern.matches("^[1-9]\\d+$", header[0])) {
charset = Charset.forName(header[0]);
} else {
throw new IllegalArgumentException(String.format(
"Unexpected charset name [%s] in [%s] file", header[0], idtFile));
}
} else if (header.length != 2) {
int tableNameIdx;
var header = headerLines.get(2).split("\t");
if (Pattern.matches("^[1-9]\\d+$", header[0])) {
charset = Charset.forName(header[0]);
tableNameIdx = 1;
} else {
tableNameIdx = 0;
}
if (header.length < (tableNameIdx + 2)) {
throw new IllegalArgumentException(String.format(
"Unexpected number of fields (%d) in the 3rd line of [%s] file",
header.length, idtFile));
}
var tableName = header[tableNameIdx];
List<String> primaryKeys = Arrays.asList(header).subList(tableNameIdx + 1, header.length);
TKit.trace(String.format("Table in [%s] file: charset=%s; name=%s; primary keys=%s",
idtFile, charset, tableName, primaryKeys));
return new IdtFileHeader(charset, List.of(columns));
} catch (IOException ex) {

View File

@ -276,6 +276,11 @@ public class WindowsHelper {
return MsiDatabaseCache.INSTANCE.findProperty(cmd.outputBundle(), propertyName).orElseThrow();
}
public static MsiDatabase.UIAlterations getUIAlterations(JPackageCommand cmd) {
cmd.verifyIsOfType(PackageType.WIN_MSI);
return MsiDatabaseCache.INSTANCE.uiAlterations(cmd.outputBundle());
}
static Collection<MsiDatabase.Shortcut> getMsiShortcuts(JPackageCommand cmd) {
cmd.verifyIsOfType(PackageType.WIN_MSI);
return MsiDatabaseCache.INSTANCE.listShortcuts(cmd.outputBundle());
@ -572,6 +577,10 @@ public class WindowsHelper {
return ensureTables(msiPath, MsiDatabase.Table.LIST_SHORTCUTS_REQUIRED_TABLES).listShortcuts();
}
MsiDatabase.UIAlterations uiAlterations(Path msiPath) {
return ensureTables(msiPath, MsiDatabase.Table.UI_ALTERATIONS_REQUIRED_TABLES).uiAlterations();
}
MsiDatabase ensureTables(Path msiPath, Set<MsiDatabase.Table> tableNames) {
Objects.requireNonNull(msiPath);
try {

View File

@ -203,3 +203,5 @@ Platform dependent options for creating the application package:
URL of available application update information
--win-upgrade-uuid <uuid>
UUID associated with upgrades for this package
--win-with-ui
Enforces the installer to have UI

View File

@ -61,3 +61,4 @@
| --win-shortcut-prompt | win-exe, win-msi | x | x | | USE_LAST |
| --win-update-url | win-exe, win-msi | x | x | | USE_LAST |
| --win-upgrade-uuid | win-exe, win-msi | x | x | | USE_LAST |
| --win-with-ui | win-exe, win-msi | x | x | | USE_LAST |

View File

@ -0,0 +1,269 @@
/*
* Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.jpackage.internal;
import static jdk.jpackage.internal.WixToolset.WixToolsetType.Wix4;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrowsExactly;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import jdk.jpackage.internal.WixToolset.WixToolsetType;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import org.junit.jupiter.params.provider.ValueSource;
class WixVariablesTest {
@Test
void test_define() {
assertEquals(List.of("-d", "foo=yes"), new WixVariables().define("foo").toWixCommandLine(Wix4));
}
@ParameterizedTest
@ValueSource(booleans = {true, false})
void test_define_null(boolean immutable) {
assertThrows(NullPointerException.class, vars -> {
vars.define(null);
}, create(immutable));
}
@Test
void test_put() {
assertEquals(List.of("-d", "foo=bar"), new WixVariables().put("foo", "bar").toWixCommandLine(Wix4));
}
@ParameterizedTest
@ValueSource(booleans = {true, false})
void test_put_null(boolean immutable) {
assertThrows(NullPointerException.class, vars -> {
vars.put("foo", null);
}, create(immutable));
assertThrows(NullPointerException.class, vars -> {
vars.put(null, "foo");
}, create(immutable));
}
@Test
void test_putAll() {
assertEquals(List.of("-d", "foo=bar"), new WixVariables().putAll(Map.of("foo", "bar")).toWixCommandLine(Wix4));
assertEquals(List.of("-d", "foo=yes"), new WixVariables().putAll(new WixVariables().define("foo")).toWixCommandLine(Wix4));
}
@ParameterizedTest
@ValueSource(booleans = {true, false})
void test_putAll_null(boolean immutable) {
assertThrows(NullPointerException.class, vars -> {
vars.putAll((Map<String, String>)null);
}, create(immutable));
assertThrows(NullPointerException.class, vars -> {
vars.putAll((WixVariables)null);
}, create(immutable));
final var expectedExceptionType = immutable ? IllegalStateException.class : NullPointerException.class;
var other = new HashMap<String, String>();
other.clear();
other.put("foo", null);
assertThrows(expectedExceptionType, vars -> {
vars.putAll(other);
}, create(immutable));
other.clear();
other.put(null, "foo");
assertThrows(expectedExceptionType, vars -> {
vars.putAll(other);
}, create(immutable));
}
@Test
void testImmutable() {
var vars = new WixVariables().define("foo").createdImmutableCopy();
assertThrows(IllegalStateException.class, _ -> {
vars.putAll(Map.of());
}, vars);
assertThrows(IllegalStateException.class, _ -> {
vars.putAll(new WixVariables());
}, vars);
assertThrows(IllegalStateException.class, _ -> {
vars.define("foo");
}, vars);
assertThrows(IllegalStateException.class, _ -> {
vars.put("foo", "bar");
}, vars);
for (var allowOverrides : List.of(true, false)) {
assertThrows(IllegalStateException.class, _ -> {
vars.allowOverrides(allowOverrides);
}, vars);
}
}
@Test
void testDefaultOverridable() {
var vars = new WixVariables().define("foo");
assertThrows(IllegalStateException.class, _ -> {
vars.define("foo");
}, vars);
assertThrows(IllegalStateException.class, _ -> {
vars.put("foo", "no");
}, vars);
assertThrows(IllegalStateException.class, _ -> {
vars.put("foo", "yes");
}, vars);
assertThrows(IllegalStateException.class, _ -> {
vars.putAll(Map.of("foo", "A", "bar", "B"));
}, vars);
assertThrows(IllegalStateException.class, _ -> {
vars.putAll(new WixVariables().putAll(Map.of("foo", "A", "bar", "B")));
}, vars);
}
@ParameterizedTest
@ValueSource(booleans = {true, false})
void testOverridable_define(boolean overridable) {
var vars = new WixVariables().allowOverrides(overridable).define("foo");
if (overridable) {
vars.define("foo");
} else {
assertThrows(IllegalStateException.class, _ -> {
vars.define("foo");
}, vars);
vars.allowOverrides(true);
vars.define("foo");
}
}
@ParameterizedTest
@ValueSource(booleans = {true, false})
void testOverridable_put(boolean overridable) {
var vars = new WixVariables().allowOverrides(overridable).define("foo");
if (overridable) {
vars.put("foo", "bar");
assertEquals(List.of("-d", "foo=bar"), vars.toWixCommandLine(Wix4));
} else {
assertThrows(IllegalStateException.class, _ -> {
vars.put("foo", "bar");
}, vars);
vars.allowOverrides(true);
vars.put("foo", "bar");
assertEquals(List.of("-d", "foo=bar"), vars.toWixCommandLine(Wix4));
}
}
@ParameterizedTest
@ValueSource(booleans = {true, false})
void testOverridable_putAll(boolean overridable) {
var vars = new WixVariables().allowOverrides(overridable).define("foo");
var other = Map.of("foo", "A", "bar", "B");
if (overridable) {
vars.putAll(other);
assertEquals(List.of("-d", "bar=B", "-d", "foo=A"), vars.toWixCommandLine(Wix4));
} else {
assertThrows(IllegalStateException.class, _ -> {
vars.putAll(other);
}, vars);
vars.allowOverrides(true);
vars.putAll(other);
assertEquals(List.of("-d", "bar=B", "-d", "foo=A"), vars.toWixCommandLine(Wix4));
}
}
@Test
void test_createdImmutableCopy() {
var vars = new WixVariables().define("foo");
var copy = vars.createdImmutableCopy();
assertNotSame(vars, copy);
assertSame(copy, copy.createdImmutableCopy());
assertEquals(List.of("-d", "foo=yes"), copy.toWixCommandLine(Wix4));
vars.allowOverrides(true).put("foo", "bar");
assertEquals(List.of("-d", "foo=bar"), vars.toWixCommandLine(Wix4));
assertEquals(List.of("-d", "foo=yes"), copy.toWixCommandLine(Wix4));
}
@ParameterizedTest
@EnumSource(WixToolsetType.class)
void test_toWixCommandLine(WixToolsetType wixType) {
var args = new WixVariables().define("foo").put("bar", "a").toWixCommandLine(wixType);
var expectedArgs = switch (wixType) {
case Wix3 -> {
yield List.of("-dbar=a", "-dfoo=yes");
}
case Wix4 -> {
yield List.of("-d", "bar=a", "-d", "foo=yes");
}
};
assertEquals(expectedArgs, args);
}
private static WixVariables create(boolean immutable) {
var vars = new WixVariables();
if (immutable) {
return vars.createdImmutableCopy();
} else {
return vars;
}
}
private static void assertThrows(
Class<? extends RuntimeException> expectedExceptionType, Consumer<WixVariables> mutator, WixVariables vars) {
var content = vars.toWixCommandLine(Wix4);
assertThrowsExactly(expectedExceptionType, () -> {
mutator.accept(vars);
});
assertEquals(content, vars.toWixCommandLine(Wix4));
}
}

View File

@ -0,0 +1,85 @@
/*
* Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.jpackage.internal.wixui;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
class UISpecTest {
@ParameterizedTest
@MethodSource
void test(UIConfig cfg) {
var uiSpec = UISpec.create(cfg);
validateCustomDialogSequence(uiSpec.customDialogSequence());
}
private static Collection<UIConfig> test() {
var testCases = new ArrayList<UIConfig>();
for (boolean withInstallDirChooserDlg : List.of(true, false)) {
for (boolean withShortcutPromptDlg : List.of(true, false)) {
for (boolean withLicenseDlg : List.of(true, false)) {
testCases.add(UIConfig.build()
.withInstallDirChooserDlg(withInstallDirChooserDlg)
.withShortcutPromptDlg(withShortcutPromptDlg)
.withLicenseDlg(withLicenseDlg)
.create());
}
}
}
return testCases;
}
static void validateCustomDialogSequence(Map<DialogPair, Publish> seq) {
seq.entrySet().stream().map(DialogControl::new).collect(Collectors.toMap(x -> x, x -> x, (a, b) -> {
throw new AssertionError(String.format(
"Dialog [%s] has multiple Publish elements associated with [%s] control", a.host(), a.hostedControl()));
}));
}
record DialogControl(Dialog host, Control hostedControl) {
DialogControl {
Objects.requireNonNull(host);
Objects.requireNonNull(hostedControl);
}
DialogControl(DialogPair pair, Publish publish) {
this(pair.first(), publish.control());
}
DialogControl(Map.Entry<DialogPair, Publish> e) {
this(e.getKey(), e.getValue());
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2025, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -28,3 +28,20 @@
* jdk/jpackage/internal/ExecutableOSVersionTest.java
* @run junit jdk.jpackage/jdk.jpackage.internal.ExecutableOSVersionTest
*/
/* @test
* @summary WixVariables unit tests
* @requires (os.family == "windows")
* @compile/module=jdk.jpackage -Xlint:all -Werror
* jdk/jpackage/internal/WixVariablesTest.java
* @run junit jdk.jpackage/jdk.jpackage.internal.WixVariablesTest
*/
/* @test
* @summary UiSpec unit tests
* @requires (os.family == "windows")
* @modules jdk.jpackage/jdk.jpackage.internal.wixui:open
* @compile/module=jdk.jpackage -Xlint:all -Werror
* jdk/jpackage/internal/wixui/UISpecTest.java
* @run junit jdk.jpackage/jdk.jpackage.internal.wixui.UISpecTest
*/

View File

@ -0,0 +1,8 @@
| Dialog | Control | Event | Argument | Condition | Ordering |
| --- | --- | --- | --- | --- | --- |
| InstallDirNotEmptyDlg | No | NewDialog | InstallDirDlg | 1 | 1 |
| InstallDirNotEmptyDlg | Yes | NewDialog | ShortcutPromptDlg | 1 | 1 |
| ShortcutPromptDlg | Back | NewDialog | InstallDirDlg | 1 | 1 |
| ShortcutPromptDlg | Cancel | SpawnDialog | CancelDlg | 1 | 1 |
| ShortcutPromptDlg | Next | NewDialog | VerifyReadyDlg | 1 | 1 |
| VerifyReadyDlg | Back | NewDialog | ShortcutPromptDlg | NOT Installed | 6 |

View File

@ -0,0 +1,3 @@
| Action | Condition |
| --- | --- |
| WelcomeDlg | NOT Installed OR PATCH |

View File

@ -0,0 +1,4 @@
| Dialog | Control | Event | Argument | Condition | Ordering |
| --- | --- | --- | --- | --- | --- |
| InstallDirNotEmptyDlg | No | NewDialog | InstallDirDlg | 1 | 1 |
| InstallDirNotEmptyDlg | Yes | NewDialog | VerifyReadyDlg | 1 | 1 |

View File

@ -0,0 +1,3 @@
| Action | Condition |
| --- | --- |
| WelcomeDlg | NOT Installed OR PATCH |

View File

@ -0,0 +1,10 @@
| Dialog | Control | Event | Argument | Condition | Ordering |
| --- | --- | --- | --- | --- | --- |
| InstallDirDlg | Back | NewDialog | WelcomeDlg | NOT Installed | 6 |
| InstallDirNotEmptyDlg | No | NewDialog | InstallDirDlg | 1 | 1 |
| InstallDirNotEmptyDlg | Yes | NewDialog | ShortcutPromptDlg | 1 | 1 |
| ShortcutPromptDlg | Back | NewDialog | InstallDirDlg | 1 | 1 |
| ShortcutPromptDlg | Cancel | SpawnDialog | CancelDlg | 1 | 1 |
| ShortcutPromptDlg | Next | NewDialog | VerifyReadyDlg | 1 | 1 |
| VerifyReadyDlg | Back | NewDialog | ShortcutPromptDlg | NOT Installed | 6 |
| WelcomeDlg | Next | NewDialog | InstallDirDlg | NOT Installed | 6 |

View File

@ -0,0 +1,3 @@
| Action | Condition |
| --- | --- |
| WelcomeDlg | NOT Installed OR PATCH |

View File

@ -0,0 +1,6 @@
| Dialog | Control | Event | Argument | Condition | Ordering |
| --- | --- | --- | --- | --- | --- |
| InstallDirDlg | Back | NewDialog | WelcomeDlg | NOT Installed | 6 |
| InstallDirNotEmptyDlg | No | NewDialog | InstallDirDlg | 1 | 1 |
| InstallDirNotEmptyDlg | Yes | NewDialog | VerifyReadyDlg | 1 | 1 |
| WelcomeDlg | Next | NewDialog | InstallDirDlg | NOT Installed | 6 |

View File

@ -0,0 +1,3 @@
| Action | Condition |
| --- | --- |
| WelcomeDlg | NOT Installed OR PATCH |

View File

@ -0,0 +1,7 @@
| Dialog | Control | Event | Argument | Condition | Ordering |
| --- | --- | --- | --- | --- | --- |
| LicenseAgreementDlg | Next | NewDialog | ShortcutPromptDlg | LicenseAccepted = "1" | 6 |
| ShortcutPromptDlg | Back | NewDialog | LicenseAgreementDlg | 1 | 1 |
| ShortcutPromptDlg | Cancel | SpawnDialog | CancelDlg | 1 | 1 |
| ShortcutPromptDlg | Next | NewDialog | VerifyReadyDlg | 1 | 1 |
| VerifyReadyDlg | Back | NewDialog | ShortcutPromptDlg | NOT Installed | 6 |

View File

@ -0,0 +1,3 @@
| Action | Condition |
| --- | --- |
| WelcomeDlg | NOT Installed OR PATCH |

View File

@ -0,0 +1,2 @@
| Dialog | Control | Event | Argument | Condition | Ordering |
| --- | --- | --- | --- | --- | --- |

View File

@ -0,0 +1,4 @@
| Action | Condition |
| --- | --- |
| WelcomeDlg | Installed AND PATCH |
| WelcomeEulaDlg | NOT Installed |

View File

@ -0,0 +1,7 @@
| Dialog | Control | Event | Argument | Condition | Ordering |
| --- | --- | --- | --- | --- | --- |
| ShortcutPromptDlg | Back | NewDialog | WelcomeDlg | NOT Installed | 6 |
| ShortcutPromptDlg | Cancel | SpawnDialog | CancelDlg | 1 | 1 |
| ShortcutPromptDlg | Next | NewDialog | VerifyReadyDlg | 1 | 1 |
| VerifyReadyDlg | Back | NewDialog | ShortcutPromptDlg | NOT Installed | 6 |
| WelcomeDlg | Next | NewDialog | ShortcutPromptDlg | NOT Installed | 6 |

View File

@ -0,0 +1,3 @@
| Action | Condition |
| --- | --- |
| WelcomeDlg | NOT Installed OR PATCH |

View File

@ -0,0 +1,2 @@
| Dialog | Control | Event | Argument | Condition | Ordering |
| --- | --- | --- | --- | --- | --- |

View File

@ -0,0 +1,4 @@
| Action | Condition |
| --- | --- |
| WelcomeDlg | Installed AND PATCH |
| WelcomeEulaDlg | 0 |

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
@ -71,7 +71,7 @@ function exportTables(db, outputDir, requestedTableNames) {
var msi = WScript.arguments(0)
var outputDir = WScript.arguments(1)
var tables = {}
for (var i = 0; i !== WScript.arguments.Count(); i++) {
for (var i = 2; i !== WScript.arguments.Count(); i++) {
tables[WScript.arguments(i)] = true
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2021, 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
@ -21,114 +21,299 @@
* questions.
*/
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import jdk.jpackage.internal.util.Slot;
import jdk.jpackage.test.Annotations.Parameters;
import jdk.jpackage.test.Annotations.ParameterSupplier;
import jdk.jpackage.test.Annotations.Test;
import jdk.jpackage.test.JPackageCommand;
import jdk.jpackage.test.MsiDatabase;
import jdk.jpackage.test.MsiDatabase.UIAlterations;
import jdk.jpackage.test.MsiDatabase.ControlEvent;
import jdk.jpackage.test.PackageTest;
import jdk.jpackage.test.PackageType;
import jdk.jpackage.test.RunnablePackageTest.Action;
import jdk.jpackage.test.TKit;
import jdk.jpackage.test.WindowsHelper;
/**
* Test all possible combinations of --win-dir-chooser, --win-shortcut-prompt
* Test combinations of --win-dir-chooser, --win-shortcut-prompt, --win-with-ui,
* and --license parameters.
*/
/*
* @test
* @summary jpackage with --win-dir-chooser, --win-shortcut-prompt and --license parameters
* @summary jpackage with --win-dir-chooser, --win-shortcut-prompt, --with-with-ui and --license parameters
* @library /test/jdk/tools/jpackage/helpers
* @key jpackagePlatformPackage
* @build jdk.jpackage.test.*
* @build WinInstallerUiTest
* @requires (os.family == "windows")
* @requires (jpackage.test.SQETest != null)
* @run main/othervm/timeout=720 -Xmx512m jdk.jpackage.test.Main
* --jpt-run=WinInstallerUiTest
* --jpt-exclude=(dir_chooser)
* --jpt-exclude=(license)
* --jpt-exclude=+ui
*/
/*
* @test
* @summary jpackage with --win-dir-chooser, --win-shortcut-prompt, --with-with-ui and --license parameters
* @library /test/jdk/tools/jpackage/helpers
* @key jpackagePlatformPackage
* @build jdk.jpackage.test.*
* @build WinInstallerUiTest
* @requires (os.family == "windows")
* @requires (jpackage.test.SQETest == null)
* @run main/othervm/timeout=720 -Xmx512m jdk.jpackage.test.Main
* --jpt-run=WinInstallerUiTest
*/
public class WinInstallerUiTest {
public WinInstallerUiTest(Boolean withDirChooser, Boolean withLicense,
Boolean withShortcutPrompt) {
this.withShortcutPrompt = withShortcutPrompt;
this.withDirChooser = withDirChooser;
this.withLicense = withLicense;
@Test
@ParameterSupplier
public void test(TestSpec spec) {
spec.run();
}
@Parameters
public static List<Object[]> data() {
List<Object[]> data = new ArrayList<>();
for (var withDirChooser : List.of(Boolean.TRUE, Boolean.FALSE)) {
for (var withLicense : List.of(Boolean.TRUE, Boolean.FALSE)) {
for (var withShortcutPrompt : List.of(Boolean.TRUE, Boolean.FALSE)) {
public static void updateExpectedMsiTables() {
for (var spec : testCases()) {
spec.createTest(true).addBundleVerifier(cmd -> {
spec.save(WindowsHelper.getUIAlterations(cmd));
}).run(Action.CREATE);
}
}
record TestSpec(
boolean withDirChooser,
boolean withLicense,
boolean withShortcutPrompt,
boolean withUi) {
TestSpec {
if (!withDirChooser && !withLicense && !withShortcutPrompt && !withUi) {
throw new IllegalArgumentException();
}
}
@Override
public String toString() {
var tokens = new ArrayList<String>();
if (withDirChooser) {
tokens.add("dir_chooser");
}
if (withShortcutPrompt) {
tokens.add("shortcut_prompt");
}
if (withLicense) {
tokens.add("license");
}
if (withUi) {
tokens.add("ui");
}
return tokens.stream().sorted().collect(Collectors.joining("+"));
}
TestSpec copyWithUi(boolean withUi) {
return new TestSpec(withDirChooser, withLicense, withShortcutPrompt, withUi);
}
TestSpec copyWithUi() {
return copyWithUi(true);
}
TestSpec copyWithoutUi() {
return copyWithUi(false);
}
void run() {
createTest(false).forTypes(PackageType.WIN_MSI).addBundleVerifier(cmd -> {
var expectedFilesDir = expectedFilesDir();
var expectedInstallUISequence = Files.readAllLines(expectedFilesDir.resolve(INSTALL_UI_SEQUENCE_FILE));
var expectedControlEvents = Files.readAllLines(expectedFilesDir.resolve(CONTROL_EVENTS_FILE));
var uiAlterations = WindowsHelper.getUIAlterations(cmd);
var actualInstallUISequence = actionSequenceToMarkdownTable(uiAlterations.installUISequence());
var actualControlEvents = controlEventsToMarkdownTable(uiAlterations.controlEvents());
TKit.assertStringListEquals(expectedInstallUISequence, actualInstallUISequence,
String.format("Check alterations to the `InstallUISequence` MSI table match the contents of [%s] file",
expectedFilesDir.resolve(INSTALL_UI_SEQUENCE_FILE)));
TKit.assertStringListEquals(expectedControlEvents, actualControlEvents,
String.format("Check alterations to the `ControlEvents` MSI table match the contents of [%s] file",
expectedFilesDir.resolve(CONTROL_EVENTS_FILE)));
}).run();
}
PackageTest createTest(boolean onlyMsi) {
return new PackageTest()
.forTypes(onlyMsi ? Set.of(PackageType.WIN_MSI) : PackageType.WINDOWS)
.configureHelloApp()
.addInitializer(JPackageCommand::setFakeRuntime)
.addInitializer(this::setPackageName)
.mutate(test -> {
if (withDirChooser) {
test.addInitializer(cmd -> cmd.addArgument("--win-dir-chooser"));
}
if (withShortcutPrompt) {
test.addInitializer(cmd -> {
cmd.addArgument("--win-shortcut-prompt");
cmd.addArgument("--win-menu");
cmd.addArgument("--win-shortcut");
});
}
if (withLicense) {
setLicenseFile(test);
}
if (withUi) {
test.addInitializer(cmd -> cmd.addArgument("--win-with-ui"));
}
});
}
private void setPackageName(JPackageCommand cmd) {
StringBuilder sb = new StringBuilder(cmd.name());
sb.append("With");
if (withDirChooser) {
sb.append("Dc"); // DirChooser
}
if (withShortcutPrompt) {
sb.append("Sp"); // ShortcutPrompt
}
if (withLicense) {
sb.append("L"); // License
}
if (withUi) {
sb.append("Ui"); // UI
}
cmd.setArgumentValue("--name", sb.toString());
}
void save(UIAlterations uiAlterations) {
var expectedFilesDir = expectedFilesDir();
write(expectedFilesDir.resolve(INSTALL_UI_SEQUENCE_FILE),
actionSequenceToMarkdownTable(uiAlterations.installUISequence()));
write(expectedFilesDir.resolve(CONTROL_EVENTS_FILE),
controlEventsToMarkdownTable(uiAlterations.controlEvents()));
}
private Path expectedFilesDir() {
if ((withDirChooser || withShortcutPrompt || withLicense) && withUi) {
return copyWithoutUi().expectedFilesDir();
} else {
return EXPECTED_MSI_TABLES_ROOT.resolve(toString());
}
}
private void write(Path file, List<String> lines) {
try {
Files.createDirectories(file.getParent());
Files.write(file, lines, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
private static List<String> toMarkdownTable(List<String> header, Stream<String[]> content) {
return Stream.of(
Stream.<String[]>of(header.toArray(String[]::new)),
Stream.<String[]>of(Collections.nCopies(header.size(), "---").toArray(String[]::new)),
content
).flatMap(x -> x).map(row -> {
return Stream.of(row).map(v -> {
// Escape the pipe (|) character.
return v.replaceAll(Pattern.quote("|"), "&#124;");
}).collect(Collectors.joining(" | ", "| ", " |"));
}).toList();
}
private static List<String> actionSequenceToMarkdownTable(Collection<MsiDatabase.Action> actions) {
return toMarkdownTable(
List.of("Action", "Condition"),
actions.stream().map(action -> {
return toStringArray(action.action(), action.condition());
})
);
}
private static List<String> controlEventsToMarkdownTable(Collection<ControlEvent> controlEvents) {
return toMarkdownTable(
List.of("Dialog", "Control", "Event", "Argument", "Condition", "Ordering"),
controlEvents.stream().map(controlEvent -> {
return toStringArray(
controlEvent.dialog(),
controlEvent.control(),
controlEvent.event(),
controlEvent.argument(),
controlEvent.condition(),
Integer.toString(controlEvent.ordering()));
})
);
}
private static String[] toStringArray(String... items) {
return items;
}
private static final String CONTROL_EVENTS_FILE = "ControlEvents.md";
private static final String INSTALL_UI_SEQUENCE_FILE = "InstallUISequence.md";
}
public static Collection<?> test() {
return Stream.concat(
testCases().stream().filter(Predicate.not(TestSpec::withUi)).map(TestSpec::copyWithUi),
testCases().stream()
).map(v -> {
return new Object[] {v};
}).toList();
}
private static Collection<TestSpec> testCases() {
var testCases = new ArrayList<TestSpec>();
for (var withDirChooser : List.of(true, false)) {
for (var withLicense : List.of(true, false)) {
for (var withShortcutPrompt : List.of(true, false)) {
if (!withDirChooser && !withLicense && !withShortcutPrompt) {
// Duplicates SimplePackageTest
continue;
}
if (withDirChooser && !withLicense && !withShortcutPrompt) {
// Duplicates WinDirChooserTest
continue;
}
if (!withDirChooser && withLicense && !withShortcutPrompt) {
// Duplicates LicenseTest
continue;
}
data.add(new Object[]{withDirChooser, withLicense,
withShortcutPrompt});
testCases.add(new TestSpec(withDirChooser, withLicense, withShortcutPrompt, false));
}
}
}
return data;
}
// Enforce UI
testCases.add(new TestSpec(false, false, false, true));
@Test
public void test() {
PackageTest test = new PackageTest()
.forTypes(PackageType.WINDOWS)
.configureHelloApp();
test.addInitializer(JPackageCommand::setFakeRuntime);
test.addInitializer(this::setPackageName);
if (withDirChooser) {
test.addInitializer(cmd -> cmd.addArgument("--win-dir-chooser"));
}
if (withShortcutPrompt) {
test.addInitializer(cmd -> {
cmd.addArgument("--win-shortcut-prompt");
cmd.addArgument("--win-menu");
cmd.addArgument("--win-shortcut");
});
}
if (withLicense) {
setLicenseFile(test);
}
test.run();
}
private void setPackageName(JPackageCommand cmd) {
StringBuilder sb = new StringBuilder(cmd.name());
sb.append("With");
if (withDirChooser) {
sb.append("Dc"); // DirChooser
}
if (withShortcutPrompt) {
sb.append("Sp"); // ShortcutPrompt
}
if (withLicense) {
sb.append("L"); // License
}
cmd.setArgumentValue("--name", sb.toString());
return testCases;
}
private static void setLicenseFile(PackageTest test) {
@ -143,9 +328,8 @@ public class WinInstallerUiTest {
});
}
private final boolean withDirChooser;
private final boolean withLicense;
private final boolean withShortcutPrompt;
private static final Path LICENSE_FILE = TKit.TEST_SRC_ROOT.resolve(Path.of("resources", "license.txt"));
private static final Path EXPECTED_MSI_TABLES_ROOT = TKit.TEST_SRC_ROOT.resolve(
Path.of("resources", WinInstallerUiTest.class.getSimpleName()));
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2020, 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
@ -21,22 +21,22 @@
* questions.
*/
import static jdk.jpackage.test.WindowsHelper.getWixTypeFromVerboseJPackageOutput;
import static jdk.jpackage.test.WindowsHelper.WixType.WIX3;
import java.io.IOException;
import java.nio.file.Path;
import jdk.jpackage.test.TKit;
import jdk.jpackage.test.PackageTest;
import jdk.jpackage.test.PackageType;
import jdk.jpackage.test.Annotations.Test;
import jdk.jpackage.test.Annotations.Parameters;
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import jdk.jpackage.test.Annotations.Parameters;
import jdk.jpackage.test.Annotations.Test;
import jdk.jpackage.test.Executor;
import static jdk.jpackage.test.WindowsHelper.WixType.WIX3;
import static jdk.jpackage.test.WindowsHelper.getWixTypeFromVerboseJPackageOutput;
import jdk.jpackage.test.PackageTest;
import jdk.jpackage.test.PackageType;
import jdk.jpackage.test.TKit;
/*
* @test
@ -124,8 +124,17 @@ public class WinL10nTest {
var toolFileName = wixToolName + ".exe";
return (s) -> {
s = s.trim();
return s.startsWith(toolFileName) || ((s.contains(String.format("\\%s ", toolFileName)) && s.
contains(" -out ")));
if (s.startsWith(toolFileName)) {
return true;
}
// Accommodate for:
// 'C:\Program Files (x86)\WiX Toolset v3.14\bin\light.exe' ...
// light.exe ...
return Stream.of("\\%s ", "\\%s' ").map(format -> {
return String.format(format, toolFileName);
}).anyMatch(s::contains) && s.contains(" -out ");
};
}