From 96f6ffbff49e39d6efd5bddd8f8b0a55ea696aa7 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Thu, 19 Mar 2026 21:59:55 +0000 Subject: [PATCH] 8278591: Jpackage post installation information message Reviewed-by: almatvee, erikj --- make/modules/jdk.jpackage/Java.gmk | 4 +- .../jpackage/internal/cli/StandardOption.java | 2 + .../resources/HelpResources.properties | 4 +- src/jdk.jpackage/share/man/jpackage.md | 6 +- .../jdk/jpackage/internal/MsiMutator.java | 56 ++ .../jdk/jpackage/internal/WinFromOptions.java | 2 + .../internal/WinMsiPackageBuilder.java | 9 +- .../jdk/jpackage/internal/WinMsiPackager.java | 37 +- .../jpackage/internal/WixFragmentBuilder.java | 14 +- .../jdk/jpackage/internal/WixPipeline.java | 179 +++--- .../internal/WixUiFragmentBuilder.java | 520 ++++++------------ .../jdk/jpackage/internal/WixVariables.java | 97 +++- .../internal/model/WinMsiPackageMixin.java | 18 +- .../internal/resources/msi-disable-actions.js | 79 +++ .../jdk/jpackage/internal/wixui/Control.java | 36 ++ .../jpackage/internal/wixui/CustomDialog.java | 38 ++ .../jdk/jpackage/internal/wixui/Dialog.java | 36 ++ .../jpackage/internal/wixui/DialogPair.java | 49 ++ .../jdk/jpackage/internal/wixui/Publish.java | 92 ++++ .../internal/wixui/ShowActionSuppresser.java | 72 +++ .../internal/wixui/StandardControl.java | 47 ++ .../jdk/jpackage/internal/wixui/UIConfig.java | 79 +++ .../jdk/jpackage/internal/wixui/UISpec.java | 293 ++++++++++ .../jpackage/internal/wixui/WixDialog.java | 43 ++ .../jdk/jpackage/internal/wixui/WixUI.java | 46 ++ .../jdk/jpackage/test/MsiDatabase.java | 135 ++++- .../jdk/jpackage/test/WindowsHelper.java | 9 + .../jpackage/internal/cli/help-windows.txt | 2 + .../jpackage/internal/cli/jpackage-options.md | 1 + .../jpackage/internal/WixVariablesTest.java | 269 +++++++++ .../jpackage/internal/wixui/UISpecTest.java | 85 +++ .../tools/jpackage/junit/windows/junit.java | 19 +- .../ControlEvents.md | 8 + .../InstallUISequence.md | 3 + .../dir_chooser+license/ControlEvents.md | 4 + .../dir_chooser+license/InstallUISequence.md | 3 + .../ControlEvents.md | 10 + .../InstallUISequence.md | 3 + .../dir_chooser/ControlEvents.md | 6 + .../dir_chooser/InstallUISequence.md | 3 + .../license+shortcut_prompt/ControlEvents.md | 7 + .../InstallUISequence.md | 3 + .../license/ControlEvents.md | 2 + .../license/InstallUISequence.md | 4 + .../shortcut_prompt/ControlEvents.md | 7 + .../shortcut_prompt/InstallUISequence.md | 3 + .../WinInstallerUiTest/ui/ControlEvents.md | 2 + .../ui/InstallUISequence.md | 4 + .../tools/jpackage/resources/msi-export.js | 4 +- .../jpackage/windows/WinInstallerUiTest.java | 332 ++++++++--- .../tools/jpackage/windows/WinL10nTest.java | 31 +- 51 files changed, 2219 insertions(+), 598 deletions(-) create mode 100644 src/jdk.jpackage/windows/classes/jdk/jpackage/internal/MsiMutator.java create mode 100644 src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/msi-disable-actions.js create mode 100644 src/jdk.jpackage/windows/classes/jdk/jpackage/internal/wixui/Control.java create mode 100644 src/jdk.jpackage/windows/classes/jdk/jpackage/internal/wixui/CustomDialog.java create mode 100644 src/jdk.jpackage/windows/classes/jdk/jpackage/internal/wixui/Dialog.java create mode 100644 src/jdk.jpackage/windows/classes/jdk/jpackage/internal/wixui/DialogPair.java create mode 100644 src/jdk.jpackage/windows/classes/jdk/jpackage/internal/wixui/Publish.java create mode 100644 src/jdk.jpackage/windows/classes/jdk/jpackage/internal/wixui/ShowActionSuppresser.java create mode 100644 src/jdk.jpackage/windows/classes/jdk/jpackage/internal/wixui/StandardControl.java create mode 100644 src/jdk.jpackage/windows/classes/jdk/jpackage/internal/wixui/UIConfig.java create mode 100644 src/jdk.jpackage/windows/classes/jdk/jpackage/internal/wixui/UISpec.java create mode 100644 src/jdk.jpackage/windows/classes/jdk/jpackage/internal/wixui/WixDialog.java create mode 100644 src/jdk.jpackage/windows/classes/jdk/jpackage/internal/wixui/WixUI.java create mode 100644 test/jdk/tools/jpackage/junit/windows/jdk.jpackage/jdk/jpackage/internal/WixVariablesTest.java create mode 100644 test/jdk/tools/jpackage/junit/windows/jdk.jpackage/jdk/jpackage/internal/wixui/UISpecTest.java create mode 100644 test/jdk/tools/jpackage/resources/WinInstallerUiTest/dir_chooser+license+shortcut_prompt/ControlEvents.md create mode 100644 test/jdk/tools/jpackage/resources/WinInstallerUiTest/dir_chooser+license+shortcut_prompt/InstallUISequence.md create mode 100644 test/jdk/tools/jpackage/resources/WinInstallerUiTest/dir_chooser+license/ControlEvents.md create mode 100644 test/jdk/tools/jpackage/resources/WinInstallerUiTest/dir_chooser+license/InstallUISequence.md create mode 100644 test/jdk/tools/jpackage/resources/WinInstallerUiTest/dir_chooser+shortcut_prompt/ControlEvents.md create mode 100644 test/jdk/tools/jpackage/resources/WinInstallerUiTest/dir_chooser+shortcut_prompt/InstallUISequence.md create mode 100644 test/jdk/tools/jpackage/resources/WinInstallerUiTest/dir_chooser/ControlEvents.md create mode 100644 test/jdk/tools/jpackage/resources/WinInstallerUiTest/dir_chooser/InstallUISequence.md create mode 100644 test/jdk/tools/jpackage/resources/WinInstallerUiTest/license+shortcut_prompt/ControlEvents.md create mode 100644 test/jdk/tools/jpackage/resources/WinInstallerUiTest/license+shortcut_prompt/InstallUISequence.md create mode 100644 test/jdk/tools/jpackage/resources/WinInstallerUiTest/license/ControlEvents.md create mode 100644 test/jdk/tools/jpackage/resources/WinInstallerUiTest/license/InstallUISequence.md create mode 100644 test/jdk/tools/jpackage/resources/WinInstallerUiTest/shortcut_prompt/ControlEvents.md create mode 100644 test/jdk/tools/jpackage/resources/WinInstallerUiTest/shortcut_prompt/InstallUISequence.md create mode 100644 test/jdk/tools/jpackage/resources/WinInstallerUiTest/ui/ControlEvents.md create mode 100644 test/jdk/tools/jpackage/resources/WinInstallerUiTest/ui/InstallUISequence.md diff --git a/make/modules/jdk.jpackage/Java.gmk b/make/modules/jdk.jpackage/Java.gmk index da66fc14009..1fd4d527217 100644 --- a/make/modules/jdk.jpackage/Java.gmk +++ b/make/modules/jdk.jpackage/Java.gmk @@ -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 diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/StandardOption.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/StandardOption.java index f554fc12ec6..c2338b87fad 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/StandardOption.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/StandardOption.java @@ -400,6 +400,8 @@ public final class StandardOption { public static final OptionValue WIN_INSTALLDIR_CHOOSER = booleanOption("win-dir-chooser").scope(nativeBundling()).create(); + public static final OptionValue WIN_WITH_UI = booleanOption("win-with-ui").scope(nativeBundling()).create(); + public static final OptionValue WIN_UPGRADE_UUID = uuidOption("win-upgrade-uuid").scope(nativeBundling()).create(); public static final OptionValue WIN_CONSOLE_HINT = booleanOption("win-console") diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/resources/HelpResources.properties b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/resources/HelpResources.properties index 466f58ee68e..0b2ca83a7d7 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/resources/HelpResources.properties +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/resources/HelpResources.properties @@ -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 diff --git a/src/jdk.jpackage/share/man/jpackage.md b/src/jdk.jpackage/share/man/jpackage.md index f78bec9808c..9ba5949866f 100644 --- a/src/jdk.jpackage/share/man/jpackage.md +++ b/src/jdk.jpackage/share/man/jpackage.md @@ -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 +`--win-with-ui` + +: Enforces the installer to have UI + #### Linux platform options (available only when running on Linux): `--linux-package-name` *name* diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/MsiMutator.java b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/MsiMutator.java new file mode 100644 index 00000000000..592cc55c0df --- /dev/null +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/MsiMutator.java @@ -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); + } +} diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinFromOptions.java b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinFromOptions.java index f6080523e89..59701777396 100644 --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinFromOptions.java +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinFromOptions.java @@ -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 -> { diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinMsiPackageBuilder.java b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinMsiPackageBuilder.java index 7695cf04ac1..c2564028ecb 100644 --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinMsiPackageBuilder.java +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinMsiPackageBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -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; diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinMsiPackager.java b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinMsiPackager.java index a53d083847a..3eb7b10b846 100644 --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinMsiPackager.java +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinMsiPackager.java @@ -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 { wixPipeline.buildMsi(msiOut.toAbsolutePath()); } - private Map createWixVars() throws IOException { - Map 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 getWxlFilesFromDir(Path dir) { diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixFragmentBuilder.java b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixFragmentBuilder.java index 46894699d98..c842c366f63 100644 --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixFragmentBuilder.java +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixFragmentBuilder.java @@ -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 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) { diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixPipeline.java b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixPipeline.java index 40160192862..a09db389ca5 100644 --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixPipeline.java +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixPipeline.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -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 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 v) { - wixVariables.clear(); + Builder putWixVariables(WixVariables v) { wixVariables.putAll(v); return this; } - Builder addSource(Path source, Map wixVariables) { - sources.add(new WixSource(source, wixVariables)); + Builder putWixVariables(Map 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 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 wixVariables = new HashMap<>(); + private final WixVariables wixVariables = new WixVariables(); private final List lightOptions = new ArrayList<>(); private final List sources = new ArrayList<>(); + private final List msiMutators = new ArrayList<>(); } static Builder build() { return new Builder(); } - private WixPipeline(WixToolset toolset, Path workDir, Path wixObjDir, - Map wixVariables, List lightOptions, - List 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 lightOptions, + List sources, + List 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 otherWixVariables, List 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 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 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 variables) { - WixSource overridePath(Path path) { + private void addWixVariablesToCommandLine(Stream wixSources, Consumer> 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 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 wixVariables; + private final WixVariables wixVariables; private final List lightOptions; private final Path wixObjDir; private final Path workDir; private final List sources; + private final List msiMutators; } diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixUiFragmentBuilder.java b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixUiFragmentBuilder.java index 4a2a0756dbd..4748dcfb827 100644 --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixUiFragmentBuilder.java +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixUiFragmentBuilder.java @@ -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 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> dialogIdsSupplier, - Supplier>> 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 dialogIds = dialogIdsSupplier.apply(outer); - Map> 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> dialogIdsSupplier; - private final Supplier>> dialogPairsSupplier; - } - - private List dialogSequenceForWixUI_InstallDir() { - List 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> createPair(Dialog firstId, - Dialog secondId, List nextBuilders, - List 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> createPair(Dialog firstId, - Dialog secondId, List builders) { - return createPair(firstId, secondId, builders, builders); - } - - static Map> createPairsForWixUI_InstallDir() { - Map> 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 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 createResource, String category, - String wxsFileName) { + CustomDialog(Function 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; private boolean withCustomActionsDll = true; private List customDialogs; } diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixVariables.java b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixVariables.java index 36ed1d99738..88a25810182 100644 --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixVariables.java +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixVariables.java @@ -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 values) { + this.values = values; + this.isImmutable = true; } - Map getValues() { - return values; + WixVariables define(String variableName) { + return put(variableName, "yes"); } - private final Map 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 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 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 values; + private boolean allowOverrides; + private boolean isImmutable; + + static final WixVariables EMPTY = new WixVariables().createdImmutableCopy(); } diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/model/WinMsiPackageMixin.java b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/model/WinMsiPackageMixin.java index 94d7c30fe57..ec004da2cee 100644 --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/model/WinMsiPackageMixin.java +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/model/WinMsiPackageMixin.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -36,6 +36,8 @@ public interface WinMsiPackageMixin { boolean withShortcutPrompt(); + boolean withUI(); + Optional helpURL(); Optional updateURL(); @@ -50,8 +52,16 @@ public interface WinMsiPackageMixin { Optional serviceInstaller(); - record Stub(DottedVersion msiVersion, boolean withInstallDirChooser, boolean withShortcutPrompt, - Optional helpURL, Optional updateURL, String startMenuGroupName, - boolean isSystemWideInstall, UUID upgradeCode, UUID productCode, + record Stub( + DottedVersion msiVersion, + boolean withInstallDirChooser, + boolean withShortcutPrompt, + boolean withUI, + Optional helpURL, + Optional updateURL, + String startMenuGroupName, + boolean isSystemWideInstall, + UUID upgradeCode, + UUID productCode, Optional serviceInstaller) implements WinMsiPackageMixin {} } diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/msi-disable-actions.js b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/msi-disable-actions.js new file mode 100644 index 00000000000..2f0f3a5d019 --- /dev/null +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/msi-disable-actions.js @@ -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) + }) +})() diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/wixui/Control.java b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/wixui/Control.java new file mode 100644 index 00000000000..d41fc435126 --- /dev/null +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/wixui/Control.java @@ -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 DEFAULT_COMPARATOR = Comparator.comparing(Control::id); +} diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/wixui/CustomDialog.java b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/wixui/CustomDialog.java new file mode 100644 index 00000000000..7325685469c --- /dev/null +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/wixui/CustomDialog.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. 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(); + } +} diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/wixui/Dialog.java b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/wixui/Dialog.java new file mode 100644 index 00000000000..a43648042a9 --- /dev/null +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/wixui/Dialog.java @@ -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 DEFAULT_COMPARATOR = Comparator.comparing(Dialog::id); +} diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/wixui/DialogPair.java b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/wixui/DialogPair.java new file mode 100644 index 00000000000..7f0f3bb6090 --- /dev/null +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/wixui/DialogPair.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jpackage.internal.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 DEFAULT_COMPARATOR = + comparing(DialogPair::first, Dialog.DEFAULT_COMPARATOR) + .thenComparing(comparing(DialogPair::second, Dialog.DEFAULT_COMPARATOR)); +} diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/wixui/Publish.java b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/wixui/Publish.java new file mode 100644 index 00000000000..7741ad823bb --- /dev/null +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/wixui/Publish.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jpackage.internal.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; + } +} diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/wixui/ShowActionSuppresser.java b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/wixui/ShowActionSuppresser.java new file mode 100644 index 00000000000..f89b15987ee --- /dev/null +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/wixui/ShowActionSuppresser.java @@ -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 DEFAULT_COMPARATOR = + comparing(ShowActionSuppresser::dialog, Dialog.DEFAULT_COMPARATOR) + .thenComparing(comparing(ShowActionSuppresser::anchor, Dialog.DEFAULT_COMPARATOR)) + .thenComparing(comparing(ShowActionSuppresser::order)); +} diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/wixui/StandardControl.java b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/wixui/StandardControl.java new file mode 100644 index 00000000000..43c5c5a7e9e --- /dev/null +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/wixui/StandardControl.java @@ -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; +} diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/wixui/UIConfig.java b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/wixui/UIConfig.java new file mode 100644 index 00000000000..c695694788f --- /dev/null +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/wixui/UIConfig.java @@ -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; + } +} diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/wixui/UISpec.java b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/wixui/UISpec.java new file mode 100644 index 00000000000..38cf3775c2b --- /dev/null +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/wixui/UISpec.java @@ -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. + *

+ * UI is based on one of the standard WiX UIs with optional alterations. + */ +public record UISpec( + WixUI wixUI, + Map wixVariables, + Map customDialogSequence, + Collection 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 v) { + customDialogSequence = v; + return this; + } + + Builder hideDialogs(Collection v) { + hideDialogs = v; + return this; + } + + Builder hideDialogs(ShowActionSuppresser... v) { + return hideDialogs(List.of(v)); + } + + private WixUI wixUI; + private final Map wixVariables = new HashMap<>(); + private Map customDialogSequence; + private Collection 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 sink, + Dialog first, + Dialog second, + Publish publishNext, + Publish publishPrev) { + + createPairNext(sink, first, second, publishNext); + createPairBack(sink, second, first, publishPrev); + } + + private static void createPairs( + BiConsumer sink, + Dialog first, + Dialog second, + Publish publish) { + createPairs(sink, first, second, publish, publish); + } + + private static void createPairNext( + BiConsumer 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 sink, + Dialog first, + Dialog second, + Publish publish) { + + var pair = new DialogPair(first, second); + + sink.accept(pair, publish.toBuilder().back().create()); + } + + private static Collection toDialogPairs(List

dialogs) { + if (dialogs.size() < 2) { + throw new IllegalArgumentException(); + } + + var pairs = new ArrayList(); + 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 installDirUiDialogs(UIConfig cfg) { + var dialogs = new ArrayList(); + + 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> overrideInstallDirDialogSequence() { + + List> entries = new ArrayList<>(); + + BiConsumer 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> DEFAULT_SPECS; + + private static final String CONDITION_ALWAYS = "1"; + private static final String CONDITION_NOT_INSTALLED = "NOT Installed"; + + static { + + var specs = new HashMap>(); + + // 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); + } +} diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/wixui/WixDialog.java b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/wixui/WixDialog.java new file mode 100644 index 00000000000..95c415dcaa0 --- /dev/null +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/wixui/WixDialog.java @@ -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(); + } +} diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/wixui/WixUI.java b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/wixui/WixUI.java new file mode 100644 index 00000000000..1b87a7fee2d --- /dev/null +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/wixui/WixUI.java @@ -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; +} diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MsiDatabase.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MsiDatabase.java index 6b3fae07d41..6c92e820f2e 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MsiDatabase.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MsiDatabase.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -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 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
FIND_PROPERTY_REQUIRED_TABLES = Set.of(PROPERTY); static final Set
LIST_SHORTCUTS_REQUIRED_TABLES = Set.of(COMPONENT, DIRECTORY, FILE, SHORTCUT); + static final Set
UI_ALTERATIONS_REQUIRED_TABLES = Set.of(CONTROL_EVENT, INSTALL_UI_SEQUENCE); } @@ -120,12 +126,7 @@ final class MsiDatabase { } Collection 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 installUISequence, Collection controlEvents) { + + public UIAlterations { + Objects.requireNonNull(installUISequence); + } + } + + private Stream actionSequence(Table tableName) { + return rows(tableName).map(row -> { + return new Action(row.apply("Action"), row.apply("Condition"), Integer.parseInt(row.apply("Sequence"))); + }); + } + + private Stream> 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 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) { diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java index c7b55ed1de7..98e9c4bfe61 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java @@ -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 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 tableNames) { Objects.requireNonNull(msiPath); try { diff --git a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/help-windows.txt b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/help-windows.txt index b72ba310f3f..7a098b847ad 100644 --- a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/help-windows.txt +++ b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/help-windows.txt @@ -203,3 +203,5 @@ Platform dependent options for creating the application package: URL of available application update information --win-upgrade-uuid UUID associated with upgrades for this package + --win-with-ui + Enforces the installer to have UI diff --git a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/jpackage-options.md b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/jpackage-options.md index 60b79451547..64d3c6c075b 100644 --- a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/jpackage-options.md +++ b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/jpackage-options.md @@ -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 | diff --git a/test/jdk/tools/jpackage/junit/windows/jdk.jpackage/jdk/jpackage/internal/WixVariablesTest.java b/test/jdk/tools/jpackage/junit/windows/jdk.jpackage/jdk/jpackage/internal/WixVariablesTest.java new file mode 100644 index 00000000000..a9d83c8d544 --- /dev/null +++ b/test/jdk/tools/jpackage/junit/windows/jdk.jpackage/jdk/jpackage/internal/WixVariablesTest.java @@ -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)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(); + + 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 expectedExceptionType, Consumer mutator, WixVariables vars) { + + var content = vars.toWixCommandLine(Wix4); + + assertThrowsExactly(expectedExceptionType, () -> { + mutator.accept(vars); + }); + + assertEquals(content, vars.toWixCommandLine(Wix4)); + } +} diff --git a/test/jdk/tools/jpackage/junit/windows/jdk.jpackage/jdk/jpackage/internal/wixui/UISpecTest.java b/test/jdk/tools/jpackage/junit/windows/jdk.jpackage/jdk/jpackage/internal/wixui/UISpecTest.java new file mode 100644 index 00000000000..08e7796ca50 --- /dev/null +++ b/test/jdk/tools/jpackage/junit/windows/jdk.jpackage/jdk/jpackage/internal/wixui/UISpecTest.java @@ -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 test() { + + var testCases = new ArrayList(); + + 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 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 e) { + this(e.getKey(), e.getValue()); + } + } +} diff --git a/test/jdk/tools/jpackage/junit/windows/junit.java b/test/jdk/tools/jpackage/junit/windows/junit.java index d27a282f0dc..1a1d0d58f7e 100644 --- a/test/jdk/tools/jpackage/junit/windows/junit.java +++ b/test/jdk/tools/jpackage/junit/windows/junit.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -28,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 + */ diff --git a/test/jdk/tools/jpackage/resources/WinInstallerUiTest/dir_chooser+license+shortcut_prompt/ControlEvents.md b/test/jdk/tools/jpackage/resources/WinInstallerUiTest/dir_chooser+license+shortcut_prompt/ControlEvents.md new file mode 100644 index 00000000000..39e6b535e69 --- /dev/null +++ b/test/jdk/tools/jpackage/resources/WinInstallerUiTest/dir_chooser+license+shortcut_prompt/ControlEvents.md @@ -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 | diff --git a/test/jdk/tools/jpackage/resources/WinInstallerUiTest/dir_chooser+license+shortcut_prompt/InstallUISequence.md b/test/jdk/tools/jpackage/resources/WinInstallerUiTest/dir_chooser+license+shortcut_prompt/InstallUISequence.md new file mode 100644 index 00000000000..e583dfa4982 --- /dev/null +++ b/test/jdk/tools/jpackage/resources/WinInstallerUiTest/dir_chooser+license+shortcut_prompt/InstallUISequence.md @@ -0,0 +1,3 @@ +| Action | Condition | +| --- | --- | +| WelcomeDlg | NOT Installed OR PATCH | diff --git a/test/jdk/tools/jpackage/resources/WinInstallerUiTest/dir_chooser+license/ControlEvents.md b/test/jdk/tools/jpackage/resources/WinInstallerUiTest/dir_chooser+license/ControlEvents.md new file mode 100644 index 00000000000..1d908b0508a --- /dev/null +++ b/test/jdk/tools/jpackage/resources/WinInstallerUiTest/dir_chooser+license/ControlEvents.md @@ -0,0 +1,4 @@ +| Dialog | Control | Event | Argument | Condition | Ordering | +| --- | --- | --- | --- | --- | --- | +| InstallDirNotEmptyDlg | No | NewDialog | InstallDirDlg | 1 | 1 | +| InstallDirNotEmptyDlg | Yes | NewDialog | VerifyReadyDlg | 1 | 1 | diff --git a/test/jdk/tools/jpackage/resources/WinInstallerUiTest/dir_chooser+license/InstallUISequence.md b/test/jdk/tools/jpackage/resources/WinInstallerUiTest/dir_chooser+license/InstallUISequence.md new file mode 100644 index 00000000000..e583dfa4982 --- /dev/null +++ b/test/jdk/tools/jpackage/resources/WinInstallerUiTest/dir_chooser+license/InstallUISequence.md @@ -0,0 +1,3 @@ +| Action | Condition | +| --- | --- | +| WelcomeDlg | NOT Installed OR PATCH | diff --git a/test/jdk/tools/jpackage/resources/WinInstallerUiTest/dir_chooser+shortcut_prompt/ControlEvents.md b/test/jdk/tools/jpackage/resources/WinInstallerUiTest/dir_chooser+shortcut_prompt/ControlEvents.md new file mode 100644 index 00000000000..93d11c944e0 --- /dev/null +++ b/test/jdk/tools/jpackage/resources/WinInstallerUiTest/dir_chooser+shortcut_prompt/ControlEvents.md @@ -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 | diff --git a/test/jdk/tools/jpackage/resources/WinInstallerUiTest/dir_chooser+shortcut_prompt/InstallUISequence.md b/test/jdk/tools/jpackage/resources/WinInstallerUiTest/dir_chooser+shortcut_prompt/InstallUISequence.md new file mode 100644 index 00000000000..e583dfa4982 --- /dev/null +++ b/test/jdk/tools/jpackage/resources/WinInstallerUiTest/dir_chooser+shortcut_prompt/InstallUISequence.md @@ -0,0 +1,3 @@ +| Action | Condition | +| --- | --- | +| WelcomeDlg | NOT Installed OR PATCH | diff --git a/test/jdk/tools/jpackage/resources/WinInstallerUiTest/dir_chooser/ControlEvents.md b/test/jdk/tools/jpackage/resources/WinInstallerUiTest/dir_chooser/ControlEvents.md new file mode 100644 index 00000000000..2fcc387732a --- /dev/null +++ b/test/jdk/tools/jpackage/resources/WinInstallerUiTest/dir_chooser/ControlEvents.md @@ -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 | diff --git a/test/jdk/tools/jpackage/resources/WinInstallerUiTest/dir_chooser/InstallUISequence.md b/test/jdk/tools/jpackage/resources/WinInstallerUiTest/dir_chooser/InstallUISequence.md new file mode 100644 index 00000000000..e583dfa4982 --- /dev/null +++ b/test/jdk/tools/jpackage/resources/WinInstallerUiTest/dir_chooser/InstallUISequence.md @@ -0,0 +1,3 @@ +| Action | Condition | +| --- | --- | +| WelcomeDlg | NOT Installed OR PATCH | diff --git a/test/jdk/tools/jpackage/resources/WinInstallerUiTest/license+shortcut_prompt/ControlEvents.md b/test/jdk/tools/jpackage/resources/WinInstallerUiTest/license+shortcut_prompt/ControlEvents.md new file mode 100644 index 00000000000..72e8b80ca08 --- /dev/null +++ b/test/jdk/tools/jpackage/resources/WinInstallerUiTest/license+shortcut_prompt/ControlEvents.md @@ -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 | diff --git a/test/jdk/tools/jpackage/resources/WinInstallerUiTest/license+shortcut_prompt/InstallUISequence.md b/test/jdk/tools/jpackage/resources/WinInstallerUiTest/license+shortcut_prompt/InstallUISequence.md new file mode 100644 index 00000000000..e583dfa4982 --- /dev/null +++ b/test/jdk/tools/jpackage/resources/WinInstallerUiTest/license+shortcut_prompt/InstallUISequence.md @@ -0,0 +1,3 @@ +| Action | Condition | +| --- | --- | +| WelcomeDlg | NOT Installed OR PATCH | diff --git a/test/jdk/tools/jpackage/resources/WinInstallerUiTest/license/ControlEvents.md b/test/jdk/tools/jpackage/resources/WinInstallerUiTest/license/ControlEvents.md new file mode 100644 index 00000000000..7a9bcca86f3 --- /dev/null +++ b/test/jdk/tools/jpackage/resources/WinInstallerUiTest/license/ControlEvents.md @@ -0,0 +1,2 @@ +| Dialog | Control | Event | Argument | Condition | Ordering | +| --- | --- | --- | --- | --- | --- | diff --git a/test/jdk/tools/jpackage/resources/WinInstallerUiTest/license/InstallUISequence.md b/test/jdk/tools/jpackage/resources/WinInstallerUiTest/license/InstallUISequence.md new file mode 100644 index 00000000000..3008e0db54b --- /dev/null +++ b/test/jdk/tools/jpackage/resources/WinInstallerUiTest/license/InstallUISequence.md @@ -0,0 +1,4 @@ +| Action | Condition | +| --- | --- | +| WelcomeDlg | Installed AND PATCH | +| WelcomeEulaDlg | NOT Installed | diff --git a/test/jdk/tools/jpackage/resources/WinInstallerUiTest/shortcut_prompt/ControlEvents.md b/test/jdk/tools/jpackage/resources/WinInstallerUiTest/shortcut_prompt/ControlEvents.md new file mode 100644 index 00000000000..a93b0ecbff1 --- /dev/null +++ b/test/jdk/tools/jpackage/resources/WinInstallerUiTest/shortcut_prompt/ControlEvents.md @@ -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 | diff --git a/test/jdk/tools/jpackage/resources/WinInstallerUiTest/shortcut_prompt/InstallUISequence.md b/test/jdk/tools/jpackage/resources/WinInstallerUiTest/shortcut_prompt/InstallUISequence.md new file mode 100644 index 00000000000..e583dfa4982 --- /dev/null +++ b/test/jdk/tools/jpackage/resources/WinInstallerUiTest/shortcut_prompt/InstallUISequence.md @@ -0,0 +1,3 @@ +| Action | Condition | +| --- | --- | +| WelcomeDlg | NOT Installed OR PATCH | diff --git a/test/jdk/tools/jpackage/resources/WinInstallerUiTest/ui/ControlEvents.md b/test/jdk/tools/jpackage/resources/WinInstallerUiTest/ui/ControlEvents.md new file mode 100644 index 00000000000..7a9bcca86f3 --- /dev/null +++ b/test/jdk/tools/jpackage/resources/WinInstallerUiTest/ui/ControlEvents.md @@ -0,0 +1,2 @@ +| Dialog | Control | Event | Argument | Condition | Ordering | +| --- | --- | --- | --- | --- | --- | diff --git a/test/jdk/tools/jpackage/resources/WinInstallerUiTest/ui/InstallUISequence.md b/test/jdk/tools/jpackage/resources/WinInstallerUiTest/ui/InstallUISequence.md new file mode 100644 index 00000000000..6e3eef39f43 --- /dev/null +++ b/test/jdk/tools/jpackage/resources/WinInstallerUiTest/ui/InstallUISequence.md @@ -0,0 +1,4 @@ +| Action | Condition | +| --- | --- | +| WelcomeDlg | Installed AND PATCH | +| WelcomeEulaDlg | 0 | diff --git a/test/jdk/tools/jpackage/resources/msi-export.js b/test/jdk/tools/jpackage/resources/msi-export.js index d639f19ca44..6cb5ab5a781 100644 --- a/test/jdk/tools/jpackage/resources/msi-export.js +++ b/test/jdk/tools/jpackage/resources/msi-export.js @@ -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 } diff --git a/test/jdk/tools/jpackage/windows/WinInstallerUiTest.java b/test/jdk/tools/jpackage/windows/WinInstallerUiTest.java index d6cebde6444..e9238c1a6f7 100644 --- a/test/jdk/tools/jpackage/windows/WinInstallerUiTest.java +++ b/test/jdk/tools/jpackage/windows/WinInstallerUiTest.java @@ -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 data() { - List 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(); + 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 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 toMarkdownTable(List header, Stream content) { + return Stream.of( + Stream.of(header.toArray(String[]::new)), + Stream.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("|"), "|"); + }).collect(Collectors.joining(" | ", "| ", " |")); + }).toList(); + } + + private static List actionSequenceToMarkdownTable(Collection actions) { + return toMarkdownTable( + List.of("Action", "Condition"), + actions.stream().map(action -> { + return toStringArray(action.action(), action.condition()); + }) + ); + } + + private static List controlEventsToMarkdownTable(Collection 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 testCases() { + var testCases = new ArrayList(); + + 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())); } diff --git a/test/jdk/tools/jpackage/windows/WinL10nTest.java b/test/jdk/tools/jpackage/windows/WinL10nTest.java index 14139013318..4d983df2598 100644 --- a/test/jdk/tools/jpackage/windows/WinL10nTest.java +++ b/test/jdk/tools/jpackage/windows/WinL10nTest.java @@ -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 "); }; }