8319457: Update jpackage to support WiX v4 and v5 on Windows

Reviewed-by: almatvee
This commit is contained in:
Alexey Semenyuk 2024-06-12 13:37:03 +00:00
parent 2c9185eb81
commit ba67ad63ae
22 changed files with 1396 additions and 292 deletions

View File

@ -27,6 +27,6 @@ DISABLED_WARNINGS_java += dangling-doc-comments
COPY += .gif .png .txt .spec .script .prerm .preinst \ COPY += .gif .png .txt .spec .script .prerm .preinst \
.postrm .postinst .list .sh .desktop .copyright .control .plist .template \ .postrm .postinst .list .sh .desktop .copyright .control .plist .template \
.icns .scpt .wxs .wxl .wxi .ico .bmp .tiff .service .icns .scpt .wxs .wxl .wxi .ico .bmp .tiff .service .xsl
CLEAN += .properties CLEAN += .properties

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2012, 2022, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2012, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* *
* This code is free software; you can redistribute it and/or modify it * This code is free software; you can redistribute it and/or modify it
@ -67,6 +67,7 @@ import static jdk.jpackage.internal.StandardBundlerParam.RESOURCE_DIR;
import static jdk.jpackage.internal.StandardBundlerParam.TEMP_ROOT; import static jdk.jpackage.internal.StandardBundlerParam.TEMP_ROOT;
import static jdk.jpackage.internal.StandardBundlerParam.VENDOR; import static jdk.jpackage.internal.StandardBundlerParam.VENDOR;
import static jdk.jpackage.internal.StandardBundlerParam.VERSION; import static jdk.jpackage.internal.StandardBundlerParam.VERSION;
import jdk.jpackage.internal.WixToolset.WixToolsetType;
import org.w3c.dom.Document; import org.w3c.dom.Document;
import org.w3c.dom.NodeList; import org.w3c.dom.NodeList;
import org.xml.sax.SAXException; import org.xml.sax.SAXException;
@ -253,7 +254,7 @@ public class WinMsiBundler extends AbstractBundler {
public boolean supported(boolean platformInstaller) { public boolean supported(boolean platformInstaller) {
try { try {
if (wixToolset == null) { if (wixToolset == null) {
wixToolset = WixTool.toolset(); wixToolset = WixTool.createToolset();
} }
return true; return true;
} catch (ConfigException ce) { } catch (ConfigException ce) {
@ -300,7 +301,7 @@ public class WinMsiBundler extends AbstractBundler {
appImageBundler.validate(params); appImageBundler.validate(params);
if (wixToolset == null) { if (wixToolset == null) {
wixToolset = WixTool.toolset(); wixToolset = WixTool.createToolset();
} }
try { try {
@ -309,16 +310,17 @@ public class WinMsiBundler extends AbstractBundler {
throw new ConfigException(ex); throw new ConfigException(ex);
} }
for (var toolInfo: wixToolset.values()) { for (var tool : wixToolset.getType().getTools()) {
Log.verbose(MessageFormat.format(I18N.getString( Log.verbose(MessageFormat.format(I18N.getString(
"message.tool-version"), toolInfo.path.getFileName(), "message.tool-version"), wixToolset.getToolPath(tool).
toolInfo.version)); getFileName(), wixToolset.getVersion()));
} }
wixFragments.forEach(wixFragment -> wixFragment.setWixVersion( wixFragments.forEach(wixFragment -> wixFragment.setWixVersion(wixToolset.getVersion(),
wixToolset.get(WixTool.Light).version)); wixToolset.getType()));
wixFragments.get(0).logWixFeatures(); wixFragments.stream().map(WixFragmentBuilder::getLoggableWixFeatures).flatMap(
List::stream).distinct().toList().forEach(Log::verbose);
/********* validate bundle parameters *************/ /********* validate bundle parameters *************/
@ -512,22 +514,6 @@ public class WinMsiBundler extends AbstractBundler {
data.put("JpIsSystemWide", "yes"); data.put("JpIsSystemWide", "yes");
} }
// Copy standard l10n files.
for (String loc : Arrays.asList("de", "en", "ja", "zh_CN")) {
String fname = "MsiInstallerStrings_" + loc + ".wxl";
createResource(fname, params)
.setCategory(I18N.getString("resource.wxl-file"))
.saveToFile(configDir.resolve(fname));
}
createResource("main.wxs", params)
.setCategory(I18N.getString("resource.main-wix-file"))
.saveToFile(configDir.resolve("main.wxs"));
createResource("overrides.wxi", params)
.setCategory(I18N.getString("resource.overrides-wix-file"))
.saveToFile(configDir.resolve("overrides.wxi"));
return data; return data;
} }
@ -542,13 +528,11 @@ public class WinMsiBundler extends AbstractBundler {
.toString())); .toString()));
WixPipeline wixPipeline = new WixPipeline() WixPipeline wixPipeline = new WixPipeline()
.setToolset(wixToolset.entrySet().stream().collect( .setToolset(wixToolset)
Collectors.toMap( .setWixObjDir(TEMP_ROOT.fetchFrom(params).resolve("wixobj"))
entry -> entry.getKey(), .setWorkDir(WIN_APP_IMAGE.fetchFrom(params))
entry -> entry.getValue().path))) .addSource(CONFIG_ROOT.fetchFrom(params).resolve("main.wxs"),
.setWixObjDir(TEMP_ROOT.fetchFrom(params).resolve("wixobj")) wixVars);
.setWorkDir(WIN_APP_IMAGE.fetchFrom(params))
.addSource(CONFIG_ROOT.fetchFrom(params).resolve("main.wxs"), wixVars);
for (var wixFragment : wixFragments) { for (var wixFragment : wixFragments) {
wixFragment.configureWixPipeline(wixPipeline); wixFragment.configureWixPipeline(wixPipeline);
@ -557,16 +541,46 @@ public class WinMsiBundler extends AbstractBundler {
Log.verbose(MessageFormat.format(I18N.getString( Log.verbose(MessageFormat.format(I18N.getString(
"message.generating-msi"), msiOut.toAbsolutePath().toString())); "message.generating-msi"), msiOut.toAbsolutePath().toString()));
wixPipeline.addLightOptions("-sice:ICE27"); switch (wixToolset.getType()) {
case Wix3 -> {
wixPipeline.addLightOptions("-sice:ICE27");
if (!MSI_SYSTEM_WIDE.fetchFrom(params)) { if (!MSI_SYSTEM_WIDE.fetchFrom(params)) {
wixPipeline.addLightOptions("-sice:ICE91"); wixPipeline.addLightOptions("-sice:ICE91");
}
}
case Wix4 -> {
}
default -> {
throw new IllegalArgumentException();
}
} }
final Path configDir = CONFIG_ROOT.fetchFrom(params);
var primaryWxlFiles = Stream.of("de", "en", "ja", "zh_CN").map(loc -> {
return configDir.resolve("MsiInstallerStrings_" + loc + ".wxl");
}).toList();
var wixResources = new WixSourceConverter.ResourceGroup(wixToolset.getType());
// Copy standard l10n files.
for (var path : primaryWxlFiles) {
var name = path.getFileName().toString();
wixResources.addResource(createResource(name, params).setPublicName(name).setCategory(
I18N.getString("resource.wxl-file")), path);
}
wixResources.addResource(createResource("main.wxs", params).setPublicName("main.wxs").
setCategory(I18N.getString("resource.main-wix-file")), configDir.resolve("main.wxs"));
wixResources.addResource(createResource("overrides.wxi", params).setPublicName(
"overrides.wxi").setCategory(I18N.getString("resource.overrides-wix-file")),
configDir.resolve("overrides.wxi"));
// Filter out custom l10n files that were already used to // Filter out custom l10n files that were already used to
// override primary l10n files. Ignore case filename comparison, // override primary l10n files. Ignore case filename comparison,
// both lists are expected to be short. // both lists are expected to be short.
List<Path> primaryWxlFiles = getWxlFilesFromDir(params, CONFIG_ROOT);
List<Path> customWxlFiles = getWxlFilesFromDir(params, RESOURCE_DIR).stream() List<Path> customWxlFiles = getWxlFilesFromDir(params, RESOURCE_DIR).stream()
.filter(custom -> primaryWxlFiles.stream().noneMatch(primary -> .filter(custom -> primaryWxlFiles.stream().noneMatch(primary ->
primary.getFileName().toString().equalsIgnoreCase( primary.getFileName().toString().equalsIgnoreCase(
@ -577,6 +591,17 @@ public class WinMsiBundler extends AbstractBundler {
custom.getFileName().toString()))) custom.getFileName().toString())))
.toList(); .toList();
// Copy custom l10n files.
for (var path : customWxlFiles) {
var name = path.getFileName().toString();
wixResources.addResource(createResource(name, params).setPublicName(name).
setSourceOrder(OverridableResource.Source.ResourceDir).setCategory(I18N.
getString("resource.wxl-file")), configDir.resolve(name));
}
// Save all WiX resources into config dir.
wixResources.saveResources();
// All l10n files are supplied to WiX with "-loc", but only // All l10n files are supplied to WiX with "-loc", but only
// Cultures from custom files and a single primary Culture are // Cultures from custom files and a single primary Culture are
// included into "-cultures" list // included into "-cultures" list
@ -586,6 +611,7 @@ public class WinMsiBundler extends AbstractBundler {
List<String> cultures = new ArrayList<>(); List<String> cultures = new ArrayList<>();
for (var wxl : customWxlFiles) { for (var wxl : customWxlFiles) {
wxl = configDir.resolve(wxl.getFileName());
wixPipeline.addLightOptions("-loc", wxl.toAbsolutePath().normalize().toString()); wixPipeline.addLightOptions("-loc", wxl.toAbsolutePath().normalize().toString());
cultures.add(getCultureFromWxlFile(wxl)); cultures.add(getCultureFromWxlFile(wxl));
} }
@ -598,8 +624,20 @@ public class WinMsiBundler extends AbstractBundler {
// Build ordered list of unique cultures. // Build ordered list of unique cultures.
Set<String> uniqueCultures = new LinkedHashSet<>(); Set<String> uniqueCultures = new LinkedHashSet<>();
uniqueCultures.addAll(cultures); uniqueCultures.addAll(cultures);
wixPipeline.addLightOptions(uniqueCultures.stream().collect( switch (wixToolset.getType()) {
Collectors.joining(";", "-cultures:", ""))); case Wix3 -> {
wixPipeline.addLightOptions(uniqueCultures.stream().collect(Collectors.joining(";",
"-cultures:", "")));
}
case Wix4 -> {
uniqueCultures.forEach(culture -> {
wixPipeline.addLightOptions("-culture", culture);
});
}
default -> {
throw new IllegalArgumentException();
}
}
wixPipeline.buildMsi(msiOut.toAbsolutePath()); wixPipeline.buildMsi(msiOut.toAbsolutePath());
@ -751,7 +789,7 @@ public class WinMsiBundler extends AbstractBundler {
} }
private Path installerIcon; private Path installerIcon;
private Map<WixTool, WixTool.ToolInfo> wixToolset; private WixToolset wixToolset;
private AppImageBundler appImageBundler; private AppImageBundler appImageBundler;
private final List<WixFragmentBuilder> wixFragments; private final List<WixFragmentBuilder> wixFragments;
} }

View File

@ -64,6 +64,7 @@ import static jdk.jpackage.internal.StandardBundlerParam.VERSION;
import static jdk.jpackage.internal.WinMsiBundler.MSI_SYSTEM_WIDE; import static jdk.jpackage.internal.WinMsiBundler.MSI_SYSTEM_WIDE;
import static jdk.jpackage.internal.WinMsiBundler.SERVICE_INSTALLER; import static jdk.jpackage.internal.WinMsiBundler.SERVICE_INSTALLER;
import static jdk.jpackage.internal.WinMsiBundler.WIN_APP_IMAGE; import static jdk.jpackage.internal.WinMsiBundler.WIN_APP_IMAGE;
import jdk.jpackage.internal.WixToolset.WixToolsetType;
import org.w3c.dom.NodeList; import org.w3c.dom.NodeList;
/** /**
@ -152,6 +153,16 @@ class WixAppImageFragmentBuilder extends WixFragmentBuilder {
super.addFilesToConfigRoot(); super.addFilesToConfigRoot();
} }
@Override
List<String> getLoggableWixFeatures() {
if (isWithWix36Features()) {
return List.of(MessageFormat.format(I18N.getString("message.use-wix36-features"),
getWixVersion()));
} else {
return List.of();
}
}
@Override @Override
protected Collection<XmlConsumer> getFragmentWriters() { protected Collection<XmlConsumer> getFragmentWriters() {
return List.of( return List.of(
@ -314,12 +325,25 @@ class WixAppImageFragmentBuilder extends WixFragmentBuilder {
return cfg.isFile; return cfg.isFile;
} }
static void startElement(XMLStreamWriter xml, String componentId, static void startElement(WixToolsetType wixType, XMLStreamWriter xml, String componentId,
String componentGuid) throws XMLStreamException, IOException { String componentGuid) throws XMLStreamException, IOException {
xml.writeStartElement("Component"); xml.writeStartElement("Component");
xml.writeAttribute("Win64", is64Bit() ? "yes" : "no"); switch (wixType) {
case Wix3 -> {
xml.writeAttribute("Win64", is64Bit() ? "yes" : "no");
xml.writeAttribute("Guid", componentGuid);
}
case Wix4 -> {
xml.writeAttribute("Bitness", is64Bit() ? "always64" : "always32");
if (!componentGuid.equals("*")) {
xml.writeAttribute("Guid", componentGuid);
}
}
default -> {
throw new IllegalArgumentException();
}
}
xml.writeAttribute("Id", componentId); xml.writeAttribute("Id", componentId);
xml.writeAttribute("Guid", componentGuid);
} }
private static final class Config { private static final class Config {
@ -370,22 +394,31 @@ class WixAppImageFragmentBuilder extends WixFragmentBuilder {
directoryRefPath = path; directoryRefPath = path;
} }
xml.writeStartElement("DirectoryRef"); startDirectoryElement(xml, "DirectoryRef", directoryRefPath);
xml.writeAttribute("Id", Id.Folder.of(directoryRefPath));
final String componentId = "c" + role.idOf(path); final String componentId = "c" + role.idOf(path);
Component.startElement(xml, componentId, String.format("{%s}", Component.startElement(getWixType(), xml, componentId, String.format(
role.guidOf(path))); "{%s}", role.guidOf(path)));
if (role == Component.Shortcut) { if (role == Component.Shortcut) {
xml.writeStartElement("Condition");
String property = shortcutFolders.stream().filter(shortcutFolder -> { String property = shortcutFolders.stream().filter(shortcutFolder -> {
return path.startsWith(shortcutFolder.root); return path.startsWith(shortcutFolder.root);
}).map(shortcutFolder -> { }).map(shortcutFolder -> {
return shortcutFolder.property; return shortcutFolder.property;
}).findFirst().get(); }).findFirst().get();
xml.writeCharacters(property); switch (getWixType()) {
xml.writeEndElement(); case Wix3 -> {
xml.writeStartElement("Condition");
xml.writeCharacters(property);
xml.writeEndElement();
}
case Wix4 -> {
xml.writeAttribute("Condition", property);
}
default -> {
throw new IllegalArgumentException();
}
}
} }
boolean isRegistryKeyPath = !systemWide || role.isRegistryKeyPath(); boolean isRegistryKeyPath = !systemWide || role.isRegistryKeyPath();
@ -442,7 +475,7 @@ class WixAppImageFragmentBuilder extends WixFragmentBuilder {
private void addShortcutComponentGroup(XMLStreamWriter xml) throws private void addShortcutComponentGroup(XMLStreamWriter xml) throws
XMLStreamException, IOException { XMLStreamException, IOException {
List<String> componentIds = new ArrayList<>(); List<String> componentIds = new ArrayList<>();
Set<ShortcutsFolder> defineShortcutFolders = new HashSet<>(); Set<Path> defineShortcutFolders = new HashSet<>();
for (var launcher : launchers) { for (var launcher : launchers) {
for (var folder : shortcutFolders) { for (var folder : shortcutFolders) {
Path launcherPath = addExeSuffixToPath(installedAppImage Path launcherPath = addExeSuffixToPath(installedAppImage
@ -457,16 +490,27 @@ class WixAppImageFragmentBuilder extends WixFragmentBuilder {
folder); folder);
if (componentId != null) { if (componentId != null) {
defineShortcutFolders.add(folder); Path folderPath = folder.getPath(this);
boolean defineFolder;
switch (getWixType()) {
case Wix3 ->
defineFolder = true;
case Wix4 ->
defineFolder = !SYSTEM_DIRS.contains(folderPath);
default ->
throw new IllegalArgumentException();
}
if (defineFolder) {
defineShortcutFolders.add(folderPath);
}
componentIds.add(componentId); componentIds.add(componentId);
} }
} }
} }
} }
for (var folder : defineShortcutFolders) { for (var folderPath : defineShortcutFolders) {
Path path = folder.getPath(this); componentIds.addAll(addRootBranch(xml, folderPath));
componentIds.addAll(addRootBranch(xml, path));
} }
addComponentGroup(xml, "Shortcuts", componentIds); addComponentGroup(xml, "Shortcuts", componentIds);
@ -546,13 +590,18 @@ class WixAppImageFragmentBuilder extends WixFragmentBuilder {
throw throwInvalidPathException(path); throw throwInvalidPathException(path);
} }
Function<Path, String> createDirectoryName = dir -> null;
boolean sysDir = true; boolean sysDir = true;
int levels = 1; int levels;
var dirIt = path.iterator(); var dirIt = path.iterator();
xml.writeStartElement("DirectoryRef");
xml.writeAttribute("Id", dirIt.next().toString()); if (getWixType() != WixToolsetType.Wix3 && TARGETDIR.equals(path.getName(0))) {
levels = 0;
dirIt.next();
} else {
levels = 1;
xml.writeStartElement("DirectoryRef");
xml.writeAttribute("Id", dirIt.next().toString());
}
path = path.getName(0); path = path.getName(0);
while (dirIt.hasNext()) { while (dirIt.hasNext()) {
@ -562,21 +611,11 @@ class WixAppImageFragmentBuilder extends WixFragmentBuilder {
if (sysDir && !SYSTEM_DIRS.contains(path)) { if (sysDir && !SYSTEM_DIRS.contains(path)) {
sysDir = false; sysDir = false;
createDirectoryName = dir -> dir.getFileName().toString();
} }
final String directoryId; startDirectoryElement(xml, "Directory", path);
if (!sysDir && path.equals(installDir)) { if (!sysDir) {
directoryId = INSTALLDIR.toString(); xml.writeAttribute("Name", path.getFileName().toString());
} else {
directoryId = Id.Folder.of(path);
}
xml.writeStartElement("Directory");
xml.writeAttribute("Id", directoryId);
String directoryName = createDirectoryName.apply(path);
if (directoryName != null) {
xml.writeAttribute("Name", directoryName);
} }
} }
@ -584,9 +623,37 @@ class WixAppImageFragmentBuilder extends WixFragmentBuilder {
xml.writeEndElement(); xml.writeEndElement();
} }
List<String> componentIds = new ArrayList<>(); return List.of();
}
return componentIds; private void startDirectoryElement(XMLStreamWriter xml, String wix3ElementName, Path path) throws XMLStreamException {
final String elementName;
switch (getWixType()) {
case Wix3 -> {
elementName = wix3ElementName;
}
case Wix4 -> {
if (SYSTEM_DIRS.contains(path)) {
elementName = "StandardDirectory";
} else {
elementName = wix3ElementName;
}
}
default -> {
throw new IllegalArgumentException();
}
}
final String directoryId;
if (path.equals(installDir)) {
directoryId = INSTALLDIR.toString();
} else {
directoryId = Id.Folder.of(path);
}
xml.writeStartElement(elementName);
xml.writeAttribute("Id", directoryId);
} }
private String addRemoveDirectoryComponent(XMLStreamWriter xml, Path path) private String addRemoveDirectoryComponent(XMLStreamWriter xml, Path path)
@ -785,7 +852,7 @@ class WixAppImageFragmentBuilder extends WixFragmentBuilder {
xml.writeStartElement("RegistryKey"); xml.writeStartElement("RegistryKey");
xml.writeAttribute("Root", regRoot); xml.writeAttribute("Root", regRoot);
xml.writeAttribute("Key", registryKeyPath); xml.writeAttribute("Key", registryKeyPath);
if (DottedVersion.compareComponents(getWixVersion(), DottedVersion.lazy("3.6")) < 0) { if (!isWithWix36Features()) {
xml.writeAttribute("Action", "createAndRemoveOnUninstall"); xml.writeAttribute("Action", "createAndRemoveOnUninstall");
} }
xml.writeStartElement("RegistryValue"); xml.writeStartElement("RegistryValue");
@ -799,7 +866,7 @@ class WixAppImageFragmentBuilder extends WixFragmentBuilder {
private String addDirectoryCleaner(XMLStreamWriter xml, Path path) throws private String addDirectoryCleaner(XMLStreamWriter xml, Path path) throws
XMLStreamException, IOException { XMLStreamException, IOException {
if (DottedVersion.compareComponents(getWixVersion(), DottedVersion.lazy("3.6")) < 0) { if (!isWithWix36Features()) {
return null; return null;
} }
@ -821,14 +888,13 @@ class WixAppImageFragmentBuilder extends WixFragmentBuilder {
xml.writeStartElement("DirectoryRef"); xml.writeStartElement("DirectoryRef");
xml.writeAttribute("Id", INSTALLDIR.toString()); xml.writeAttribute("Id", INSTALLDIR.toString());
Component.startElement(xml, componentId, "*"); Component.startElement(getWixType(), xml, componentId, "*");
addRegistryKeyPath(xml, INSTALLDIR, () -> propertyId, () -> { addRegistryKeyPath(xml, INSTALLDIR, () -> propertyId, () -> {
return toWixPath(path); return toWixPath(path);
}); });
xml.writeStartElement( xml.writeStartElement(getWixNamespaces().get(WixNamespace.Util),
"http://schemas.microsoft.com/wix/UtilExtension",
"RemoveFolderEx"); "RemoveFolderEx");
xml.writeAttribute("On", "uninstall"); xml.writeAttribute("On", "uninstall");
xml.writeAttribute("Property", propertyId); xml.writeAttribute("Property", propertyId);
@ -839,6 +905,10 @@ class WixAppImageFragmentBuilder extends WixFragmentBuilder {
return componentId; return componentId;
} }
private boolean isWithWix36Features() {
return DottedVersion.compareComponents(getWixVersion(), DottedVersion.greedy("3.6")) >= 0;
}
// Does the following conversions: // Does the following conversions:
// INSTALLDIR -> [INSTALLDIR] // INSTALLDIR -> [INSTALLDIR]
// TARGETDIR/ProgramFiles64Folder/foo/bar -> [ProgramFiles64Folder]foo/bar // TARGETDIR/ProgramFiles64Folder/foo/bar -> [ProgramFiles64Folder]foo/bar

View File

@ -29,31 +29,35 @@ import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.lang.reflect.Proxy; import java.lang.reflect.Proxy;
import java.nio.file.Path; import java.nio.file.Path;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import javax.xml.stream.XMLStreamWriter; import javax.xml.stream.XMLStreamWriter;
import jdk.jpackage.internal.IOUtils.XmlConsumer; import jdk.jpackage.internal.IOUtils.XmlConsumer;
import jdk.jpackage.internal.OverridableResource.Source; import jdk.jpackage.internal.OverridableResource.Source;
import static jdk.jpackage.internal.OverridableResource.createResource;
import static jdk.jpackage.internal.StandardBundlerParam.CONFIG_ROOT; import static jdk.jpackage.internal.StandardBundlerParam.CONFIG_ROOT;
import jdk.internal.util.Architecture; import jdk.internal.util.Architecture;
import static jdk.jpackage.internal.OverridableResource.createResource;
import jdk.jpackage.internal.WixSourceConverter.ResourceGroup;
import jdk.jpackage.internal.WixToolset.WixToolsetType;
/** /**
* Creates WiX fragment. * Creates WiX fragment.
*/ */
abstract class WixFragmentBuilder { abstract class WixFragmentBuilder {
void setWixVersion(DottedVersion v) { final void setWixVersion(DottedVersion version, WixToolsetType type) {
wixVersion = v; Objects.requireNonNull(version);
Objects.requireNonNull(type);
wixVersion = version;
wixType = type;
} }
void setOutputFileName(String v) { final void setOutputFileName(String v) {
outputFileName = v; outputFileName = v;
} }
@ -65,11 +69,8 @@ abstract class WixFragmentBuilder {
Source.ResourceDir); Source.ResourceDir);
} }
void logWixFeatures() { List<String> getLoggableWixFeatures() {
if (DottedVersion.compareComponents(wixVersion, DottedVersion.lazy("3.6")) >= 0) { return List.of();
Log.verbose(MessageFormat.format(I18N.getString(
"message.use-wix36-features"), wixVersion));
}
} }
void configureWixPipeline(WixPipeline wixPipeline) { void configureWixPipeline(WixPipeline wixPipeline) {
@ -91,52 +92,84 @@ abstract class WixFragmentBuilder {
} }
if (additionalResources != null) { if (additionalResources != null) {
for (var resource : additionalResources) { additionalResources.saveResources();
resource.resource.saveToFile(configRoot.resolve(
resource.saveAsName));
}
} }
} }
DottedVersion getWixVersion() { final WixToolsetType getWixType() {
return wixType;
}
final DottedVersion getWixVersion() {
return wixVersion; return wixVersion;
} }
protected static enum WixNamespace {
Default,
Util;
}
final protected Map<WixNamespace, String> getWixNamespaces() {
switch (wixType) {
case Wix3 -> {
return Map.of(WixNamespace.Default,
"http://schemas.microsoft.com/wix/2006/wi",
WixNamespace.Util,
"http://schemas.microsoft.com/wix/UtilExtension");
}
case Wix4 -> {
return Map.of(WixNamespace.Default,
"http://wixtoolset.org/schemas/v4/wxs",
WixNamespace.Util,
"http://wixtoolset.org/schemas/v4/wxs/util");
}
default -> {
throw new IllegalArgumentException();
}
}
}
static boolean is64Bit() { static boolean is64Bit() {
return Architecture.is64bit(); return Architecture.is64bit();
} }
protected Path getConfigRoot() { final protected Path getConfigRoot() {
return configRoot; return configRoot;
} }
protected abstract Collection<XmlConsumer> getFragmentWriters(); protected abstract Collection<XmlConsumer> getFragmentWriters();
protected void defineWixVariable(String variableName) { final protected void defineWixVariable(String variableName) {
setWixVariable(variableName, "yes"); setWixVariable(variableName, "yes");
} }
protected void setWixVariable(String variableName, String variableValue) { final protected void setWixVariable(String variableName, String variableValue) {
if (wixVariables == null) { if (wixVariables == null) {
wixVariables = new WixVariables(); wixVariables = new WixVariables();
} }
wixVariables.setWixVariable(variableName, variableValue); wixVariables.setWixVariable(variableName, variableValue);
} }
protected void addResource(OverridableResource resource, String saveAsName) { final protected void addResource(OverridableResource resource, String saveAsName) {
if (additionalResources == null) { if (additionalResources == null) {
additionalResources = new ArrayList<>(); additionalResources = new ResourceGroup(getWixType());
} }
additionalResources.add(new ResourceWithName(resource, saveAsName)); additionalResources.addResource(resource, configRoot.resolve(saveAsName));
} }
static void createWixSource(Path file, XmlConsumer xmlConsumer) private void createWixSource(Path file, XmlConsumer xmlConsumer) throws IOException {
throws IOException {
IOUtils.createXml(file, xml -> { IOUtils.createXml(file, xml -> {
xml.writeStartElement("Wix"); xml.writeStartElement("Wix");
xml.writeDefaultNamespace("http://schemas.microsoft.com/wix/2006/wi"); for (var ns : getWixNamespaces().entrySet()) {
xml.writeNamespace("util", switch (ns.getKey()) {
"http://schemas.microsoft.com/wix/UtilExtension"); case Default ->
xml.writeDefaultNamespace(ns.getValue());
default ->
xml.writeNamespace(ns.getKey().name().toLowerCase(), ns.
getValue());
}
}
xmlConsumer.accept((XMLStreamWriter) Proxy.newProxyInstance( xmlConsumer.accept((XMLStreamWriter) Proxy.newProxyInstance(
XMLStreamWriter.class.getClassLoader(), new Class<?>[]{ XMLStreamWriter.class.getClassLoader(), new Class<?>[]{
@ -146,16 +179,6 @@ abstract class WixFragmentBuilder {
}); });
} }
private static class ResourceWithName {
ResourceWithName(OverridableResource resource, String saveAsName) {
this.resource = resource;
this.saveAsName = saveAsName;
}
private final OverridableResource resource;
private final String saveAsName;
}
private static class WixPreprocessorEscaper implements InvocationHandler { private static class WixPreprocessorEscaper implements InvocationHandler {
WixPreprocessorEscaper(XMLStreamWriter target) { WixPreprocessorEscaper(XMLStreamWriter target) {
@ -208,9 +231,10 @@ abstract class WixFragmentBuilder {
private final XMLStreamWriter target; private final XMLStreamWriter target;
} }
private WixToolsetType wixType;
private DottedVersion wixVersion; private DottedVersion wixVersion;
private WixVariables wixVariables; private WixVariables wixVariables;
private List<ResourceWithName> additionalResources; private ResourceGroup additionalResources;
private OverridableResource fragmentResource; private OverridableResource fragmentResource;
private String outputFileName; private String outputFileName;
private Path configRoot; private Path configRoot;

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019, 2020, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2019, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* *
* This code is free software; you can redistribute it and/or modify it * This code is free software; you can redistribute it and/or modify it
@ -22,18 +22,19 @@
* or visit www.oracle.com if you need additional information or have any * or visit www.oracle.com if you need additional information or have any
* questions. * questions.
*/ */
package jdk.jpackage.internal; package jdk.jpackage.internal;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.function.UnaryOperator; import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
/** /**
@ -45,7 +46,7 @@ public class WixPipeline {
lightOptions = new ArrayList<>(); lightOptions = new ArrayList<>();
} }
WixPipeline setToolset(Map<WixTool, Path> v) { WixPipeline setToolset(WixToolset v) {
toolset = v; toolset = v;
return this; return this;
} }
@ -79,13 +80,92 @@ public class WixPipeline {
} }
void buildMsi(Path msi) throws IOException { void buildMsi(Path msi) throws IOException {
Objects.requireNonNull(workDir);
switch (toolset.getType()) {
case Wix3 -> buildMsiWix3(msi);
case Wix4 -> buildMsiWix4(msi);
default -> throw new IllegalArgumentException();
}
}
private void addWixVariblesToCommandLine(
Map<String, String> otherWixVariables, List<String> cmdline) {
Stream.of(wixVariables, Optional.ofNullable(otherWixVariables).
orElseGet(Collections::emptyMap)).filter(Objects::nonNull).
reduce((a, b) -> {
a.putAll(b);
return a;
}).ifPresent(wixVars -> {
var entryStream = wixVars.entrySet().stream();
Stream<String> stream;
switch (toolset.getType()) {
case Wix3 -> {
stream = entryStream.map(wixVar -> {
return String.format("-d%s=%s", wixVar.getKey(), wixVar.
getValue());
});
}
case Wix4 -> {
stream = entryStream.map(wixVar -> {
return Stream.of("-d", String.format("%s=%s", wixVar.
getKey(), wixVar.getValue()));
}).flatMap(Function.identity());
}
default -> {
throw new IllegalArgumentException();
}
}
stream.reduce(cmdline, (ctnr, wixVar) -> {
ctnr.add(wixVar);
return ctnr;
}, (x, y) -> {
x.addAll(y);
return x;
});
});
}
private void buildMsiWix4(Path msi) throws IOException {
var mergedSrcWixVars = sources.stream().map(wixSource -> {
return Optional.ofNullable(wixSource.variables).orElseGet(
Collections::emptyMap).entrySet().stream();
}).flatMap(Function.identity()).collect(Collectors.toMap(
Map.Entry::getKey, Map.Entry::getValue));
List<String> cmdline = new ArrayList<>(List.of(
toolset.getToolPath(WixTool.Wix4).toString(),
"build",
"-nologo",
"-pdbtype", "none",
"-intermediatefolder", wixObjDir.toAbsolutePath().toString(),
"-ext", "WixToolset.Util.wixext",
"-arch", WixFragmentBuilder.is64Bit() ? "x64" : "x86"
));
cmdline.addAll(lightOptions);
addWixVariblesToCommandLine(mergedSrcWixVars, cmdline);
cmdline.addAll(sources.stream().map(wixSource -> {
return wixSource.source.toAbsolutePath().toString();
}).toList());
cmdline.addAll(List.of("-out", msi.toString()));
execute(cmdline);
}
private void buildMsiWix3(Path msi) throws IOException {
List<Path> wixObjs = new ArrayList<>(); List<Path> wixObjs = new ArrayList<>();
for (var source : sources) { for (var source : sources) {
wixObjs.add(compile(source)); wixObjs.add(compileWix3(source));
} }
List<String> lightCmdline = new ArrayList<>(List.of( List<String> lightCmdline = new ArrayList<>(List.of(
toolset.get(WixTool.Light).toString(), toolset.getToolPath(WixTool.Light3).toString(),
"-nologo", "-nologo",
"-spdb", "-spdb",
"-ext", "WixUtilExtension", "-ext", "WixUtilExtension",
@ -99,31 +179,20 @@ public class WixPipeline {
execute(lightCmdline); execute(lightCmdline);
} }
private Path compile(WixSource wixSource) throws IOException { private Path compileWix3(WixSource wixSource) throws IOException {
UnaryOperator<Path> adjustPath = path -> { Path wixObj = wixObjDir.toAbsolutePath().resolve(IOUtils.replaceSuffix(
return workDir != null ? path.toAbsolutePath() : path;
};
Path wixObj = adjustPath.apply(wixObjDir).resolve(IOUtils.replaceSuffix(
IOUtils.getFileName(wixSource.source), ".wixobj")); IOUtils.getFileName(wixSource.source), ".wixobj"));
List<String> cmdline = new ArrayList<>(List.of( List<String> cmdline = new ArrayList<>(List.of(
toolset.get(WixTool.Candle).toString(), toolset.getToolPath(WixTool.Candle3).toString(),
"-nologo", "-nologo",
adjustPath.apply(wixSource.source).toString(), wixSource.source.toAbsolutePath().toString(),
"-ext", "WixUtilExtension", "-ext", "WixUtilExtension",
"-arch", WixFragmentBuilder.is64Bit() ? "x64" : "x86", "-arch", WixFragmentBuilder.is64Bit() ? "x64" : "x86",
"-out", wixObj.toAbsolutePath().toString() "-out", wixObj.toAbsolutePath().toString()
)); ));
Map<String, String> appliedVaribales = new HashMap<>(); addWixVariblesToCommandLine(wixSource.variables, cmdline);
Stream.of(wixVariables, wixSource.variables)
.filter(Objects::nonNull)
.forEachOrdered(appliedVaribales::putAll);
appliedVaribales.entrySet().stream().map(wixVar -> String.format("-d%s=%s",
wixVar.getKey(), wixVar.getValue())).forEachOrdered(
cmdline::add);
execute(cmdline); execute(cmdline);
@ -131,8 +200,8 @@ public class WixPipeline {
} }
private void execute(List<String> cmdline) throws IOException { private void execute(List<String> cmdline) throws IOException {
Executor.of(new ProcessBuilder(cmdline).directory( Executor.of(new ProcessBuilder(cmdline).directory(workDir.toFile())).
workDir != null ? workDir.toFile() : null)).executeExpectSuccess(); executeExpectSuccess();
} }
private static final class WixSource { private static final class WixSource {
@ -140,7 +209,7 @@ public class WixPipeline {
Map<String, String> variables; Map<String, String> variables;
} }
private Map<WixTool, Path> toolset; private WixToolset toolset;
private Map<String, String> wixVariables; private Map<String, String> wixVariables;
private List<String> lightOptions; private List<String> lightOptions;
private Path wixObjDir; private Path wixObjDir;

View File

@ -0,0 +1,420 @@
/*
* Copyright (c) 2024, 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.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.AbstractMap;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import javax.xml.XMLConstants;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stax.StAXResult;
import javax.xml.transform.stream.StreamSource;
import jdk.jpackage.internal.WixToolset.WixToolsetType;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;
/**
* Converts WiX v3 source file into WiX v4 format.
*/
final class WixSourceConverter {
enum Status {
SavedAsIs,
SavedAsIsMalfromedXml,
Transformed,
}
WixSourceConverter(Path resourceDir) throws IOException {
var buf = new ByteArrayOutputStream();
new OverridableResource("wix3-to-wix4-conv.xsl")
.setPublicName("wix-conv.xsl")
.setResourceDir(resourceDir)
.setCategory(I18N.getString("resource.wix-src-conv"))
.saveToStream(buf);
var xslt = new StreamSource(new ByteArrayInputStream(buf.toByteArray()));
var tf = TransformerFactory.newInstance();
try {
this.transformer = tf.newTransformer(xslt);
} catch (TransformerException ex) {
// Should never happen
throw new RuntimeException(ex);
}
this.outputFactory = XMLOutputFactory.newInstance();
}
Status appyTo(OverridableResource resource, Path resourceSaveAsFile) throws IOException {
// Save the resource into DOM tree and read xml namespaces from it.
// If some namespaces are not recognized by this converter, save the resource as is.
// If all detected namespaces are recognized, run transformation of the DOM tree and save
// output into destination file.
var buf = saveResourceInMemory(resource);
Document inputXmlDom;
try {
inputXmlDom = IOUtils.initDocumentBuilder().parse(new ByteArrayInputStream(buf));
} catch (SAXException ex) {
// Malformed XML, don't run converter, save as is.
resource.saveToFile(resourceSaveAsFile);
return Status.SavedAsIsMalfromedXml;
}
try {
var nc = new NamespaceCollector();
TransformerFactory.newInstance().newTransformer().
transform(new DOMSource(inputXmlDom), new StAXResult((XMLStreamWriter) Proxy.
newProxyInstance(XMLStreamWriter.class.getClassLoader(),
new Class<?>[]{XMLStreamWriter.class}, nc)));
if (!nc.isOnlyKnownNamespacesUsed()) {
// Unsupported namespaces detected in input XML, don't run converter, save as is.
resource.saveToFile(resourceSaveAsFile);
return Status.SavedAsIs;
}
} catch (TransformerException ex) {
// Should never happen
throw new RuntimeException(ex);
}
Supplier<Source> inputXml = () -> {
// Should be "new DOMSource(inputXmlDom)", but no transfromation is applied in this case!
return new StreamSource(new ByteArrayInputStream(buf));
};
var nc = new NamespaceCollector();
try {
// Run transfomation to collect namespaces from the output XML.
transformer.transform(inputXml.get(), new StAXResult((XMLStreamWriter) Proxy.
newProxyInstance(XMLStreamWriter.class.getClassLoader(),
new Class<?>[]{XMLStreamWriter.class}, nc)));
} catch (TransformerException ex) {
// Should never happen
throw new RuntimeException(ex);
}
try (var outXml = new ByteArrayOutputStream()) {
transformer.transform(inputXml.get(), new StAXResult((XMLStreamWriter) Proxy.
newProxyInstance(XMLStreamWriter.class.getClassLoader(),
new Class<?>[]{XMLStreamWriter.class}, new NamespaceCleaner(nc.
getPrefixToUri(), outputFactory.createXMLStreamWriter(outXml)))));
Files.createDirectories(IOUtils.getParent(resourceSaveAsFile));
Files.copy(new ByteArrayInputStream(outXml.toByteArray()), resourceSaveAsFile,
StandardCopyOption.REPLACE_EXISTING);
} catch (TransformerException | XMLStreamException ex) {
// Should never happen
throw new RuntimeException(ex);
}
return Status.Transformed;
}
private static byte[] saveResourceInMemory(OverridableResource resource) throws IOException {
var buf = new ByteArrayOutputStream();
resource.saveToStream(buf);
return buf.toByteArray();
}
final static class ResourceGroup {
ResourceGroup(WixToolsetType wixToolsetType) {
this.wixToolsetType = wixToolsetType;
}
void addResource(OverridableResource resource, Path resourceSaveAsFile) {
resources.put(resourceSaveAsFile, resource);
}
void saveResources() throws IOException {
switch (wixToolsetType) {
case Wix3 -> {
for (var e : resources.entrySet()) {
e.getValue().saveToFile(e.getKey());
}
}
case Wix4 -> {
var resourceDir = resources.values().stream().filter(res -> {
return null != res.getResourceDir();
}).findAny().map(OverridableResource::getResourceDir).orElse(null);
var conv = new WixSourceConverter(resourceDir);
for (var e : resources.entrySet()) {
conv.appyTo(e.getValue(), e.getKey());
}
}
default -> {
throw new IllegalArgumentException();
}
}
}
private final Map<Path, OverridableResource> resources = new HashMap<>();
private final WixToolsetType wixToolsetType;
}
//
// Default JDK XSLT v1.0 processor is not handling well default namespace mappings.
// Running generic template:
//
// <xsl:template match="wix3loc:*">
// <xsl:element name="{local-name()}" namespace="http://wixtoolset.org/schemas/v4/wxl">
// <xsl:apply-templates select="@*|node()"/>
// </xsl:element>
// </xsl:template>
//
// produces:
//
// <ns0:WixLocalization xmlns:ns0="http://wixtoolset.org/schemas/v4/wxl" Culture="en-us" Codepage="1252">
// <ns1:String xmlns:ns1="http://wixtoolset.org/schemas/v4/wxl" Value="The folder [INSTALLDIR] already exist. Would you like to install to that folder anyway?" Id="message.install.dir.exist"/>
// <ns2:String xmlns:ns2="http://wixtoolset.org/schemas/v4/wxl" Value="Main Feature" Id="MainFeatureTitle"/>
// ...
// <ns12:String xmlns:ns12="http://wixtoolset.org/schemas/v4/wxl" Value="Open with [ProductName]" Id="ContextMenuCommandLabel"/>
// </ns0:WixLocalization>
//
// which is conformant XML but WiX4 doesn't like it:
//
// wix.exe : error WIX0202: The {http://wixtoolset.org/schemas/v4/wxl}String element contains an unsupported extension attribute '{http://www.w3.org/2000/xmlns/}ns1'. The {http://wixtoolset.org/schemas/v4/wxl}String element does not currently support extension attributes. Is the {http://www.w3.org/2000/xmlns/}ns1 attribute using the correct XML namespace?
// wix.exe : error WIX0202: The {http://wixtoolset.org/schemas/v4/wxl}String element contains an unsupported extension attribute '{http://www.w3.org/2000/xmlns/}ns2'. The {http://wixtoolset.org/schemas/v4/wxl}String element does not currently support extension attributes. Is the {http://www.w3.org/2000/xmlns/}ns2 attribute using the correct XML namespace?
// wix.exe : error WIX0202: The {http://wixtoolset.org/schemas/v4/wxl}String element contains an unsupported extension attribute '{http://www.w3.org/2000/xmlns/}ns3'. The {http://wixtoolset.org/schemas/v4/wxl}String element does not currently support extension attributes. Is the {http://www.w3.org/2000/xmlns/}ns3 attribute using the correct XML namespace?
//
// Someone hit this issue long ago - https://stackoverflow.com/questions/26904623/replace-default-namespace-using-xsl and they suggested to use different XSLT processor.
// Two online XSLT processors used in testing produce clean XML with this template indeed:
//
// <WixLocalization xmlns="http://wixtoolset.org/schemas/v4/wxl" Codepage="1252" Culture="en-us">
// <String Value="The folder [INSTALLDIR] already exist. Would you like to install to that folder anyway?" Id="message.install.dir.exist"/>
// <String Value="Main Feature" Id="MainFeatureTitle"/>
// ...
// <String Value="Open with [ProductName]" Id="ContextMenuCommandLabel"/>
// </WixLocalization>
//
// To workaround default JDK's XSLT processor limitations we do additionl postprocessing of output XML with NamespaceCleaner class.
//
private static class NamespaceCleaner implements InvocationHandler {
NamespaceCleaner(Map<String, String> prefixToUri, XMLStreamWriter target) {
this.uriToPrefix = prefixToUri.entrySet().stream().collect(Collectors.toMap(
Map.Entry::getValue, e -> {
return new Prefix(e.getKey());
}, (x, y) -> x));
this.prefixToUri = prefixToUri;
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
switch (method.getName()) {
case "writeNamespace" -> {
final String uri = (String) args[1];
var prefixObj = uriToPrefix.get(uri);
if (!prefixObj.written) {
prefixObj.written = true;
target.writeNamespace(prefixObj.name, uri);
}
return null;
}
case "writeStartElement", "writeEmptyElement" -> {
final String name;
switch (args.length) {
case 1 ->
name = (String) args[0];
case 2, 3 ->
name = (String) args[1];
default ->
throw new IllegalArgumentException();
}
final String prefix;
final String localName;
final String[] tokens = name.split(":", 2);
if (tokens.length == 2) {
prefix = tokens[0];
localName = tokens[1];
} else {
localName = name;
switch (args.length) {
case 3 ->
prefix = (String) args[0];
case 2 ->
prefix = uriToPrefix.get((String) args[0]).name;
default ->
prefix = null;
}
}
if (prefix != null && !XMLConstants.DEFAULT_NS_PREFIX.equals(prefix)) {
final String uri = prefixToUri.get(prefix);
var prefixObj = uriToPrefix.get(uri);
if (prefixObj.written) {
var writeName = String.join(":", prefixObj.name, localName);
if ("writeStartElement".equals(method.getName())) {
target.writeStartElement(writeName);
} else {
target.writeEmptyElement(writeName);
}
return null;
} else {
prefixObj.written = (args.length > 1);
args = Arrays.copyOf(args, args.length, Object[].class);
if (localName.equals(name)) {
// No prefix in the name
if (args.length == 3) {
args[0] = prefixObj.name;
}
} else {
var writeName = String.join(":", prefixObj.name, localName);
switch (args.length) {
case 1 ->
args[0] = writeName;
case 2 -> {
args[0] = uri;
args[1] = writeName;
}
case 3 -> {
args[0] = prefixObj.name;
args[1] = writeName;
args[2] = uri;
}
}
}
}
}
}
}
return method.invoke(target, args);
}
static class Prefix {
Prefix(String name) {
this.name = name;
}
private final String name;
private boolean written;
}
private final Map<String, Prefix> uriToPrefix;
private final Map<String, String> prefixToUri;
private final XMLStreamWriter target;
}
private static class NamespaceCollector implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
switch (method.getName()) {
case "setPrefix", "writeNamespace" -> {
var prefix = (String) args[0];
var namespace = prefixToUri.computeIfAbsent(prefix, k -> createValue(args[1]));
if (XMLConstants.XMLNS_ATTRIBUTE.equals(prefix)) {
namespace.setValue(true);
}
}
case "writeStartElement", "writeEmptyElement" -> {
switch (args.length) {
case 3 ->
prefixToUri.computeIfAbsent((String) args[0], k -> createValue(
(String) args[2])).setValue(true);
case 2 ->
initFromElementName((String) args[1], (String) args[0]);
case 1 ->
initFromElementName((String) args[0], null);
}
}
}
return null;
}
boolean isOnlyKnownNamespacesUsed() {
return prefixToUri.values().stream().filter(namespace -> {
return namespace.getValue();
}).allMatch(namespace -> {
if (!namespace.getValue()) {
return true;
} else {
return KNOWN_NAMESPACES.contains(namespace.getKey());
}
});
}
Map<String, String> getPrefixToUri() {
return prefixToUri.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey,
e -> {
return e.getValue().getKey();
}));
}
private void initFromElementName(String name, String namespace) {
final String[] tokens = name.split(":", 2);
if (tokens.length == 2) {
if (namespace != null) {
prefixToUri.computeIfAbsent(tokens[0], k -> createValue(namespace)).setValue(
true);
} else {
prefixToUri.computeIfPresent(tokens[0], (k, v) -> {
v.setValue(true);
return v;
});
}
}
}
private Map.Entry<String, Boolean> createValue(Object prefix) {
return new AbstractMap.SimpleEntry<String, Boolean>((String) prefix, false);
}
private final Map<String, Map.Entry<String, Boolean>> prefixToUri = new HashMap<>();
}
private final Transformer transformer;
private final XMLOutputFactory outputFactory;
// The list of WiX v3 namespaces this converter can handle
private final static Set<String> KNOWN_NAMESPACES = Set.of(
"http://schemas.microsoft.com/wix/2006/localization",
"http://schemas.microsoft.com/wix/2006/wi");
}

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019, 2021, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2019, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* *
* This code is free software; you can redistribute it and/or modify it * This code is free software; you can redistribute it and/or modify it
@ -22,7 +22,6 @@
* or visit www.oracle.com if you need additional information or have any * or visit www.oracle.com if you need additional information or have any
* questions. * questions.
*/ */
package jdk.jpackage.internal; package jdk.jpackage.internal;
import java.io.IOException; import java.io.IOException;
@ -32,105 +31,198 @@ import java.nio.file.InvalidPathException;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.PathMatcher; import java.nio.file.PathMatcher;
import java.text.MessageFormat; import java.text.MessageFormat;
import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.function.Supplier; import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import jdk.jpackage.internal.WixToolset.WixToolsetType;
/** /**
* WiX tool. * WiX tool.
*/ */
public enum WixTool { public enum WixTool {
Candle, Light; Candle3("candle", DottedVersion.lazy("3.0")),
Light3("light", DottedVersion.lazy("3.0")),
Wix4("wix", DottedVersion.lazy("4.0.4"));
WixTool(String commandName, DottedVersion minimalVersion) {
this.toolFileName = IOUtils.addSuffix(Path.of(commandName), ".exe");
this.minimalVersion = minimalVersion;
}
static final class ToolInfo { static final class ToolInfo {
ToolInfo(Path path, String version) { ToolInfo(Path path, String version) {
this.path = path; this.path = path;
this.version = new DottedVersion(version); this.version = DottedVersion.lazy(version);
} }
final Path path; final Path path;
final DottedVersion version; final DottedVersion version;
} }
static Map<WixTool, ToolInfo> toolset() throws ConfigException { static WixToolset createToolset() throws ConfigException {
Map<WixTool, ToolInfo> toolset = new HashMap<>(); Function<List<ToolLookupResult>, Map<WixTool, ToolInfo>> conv = lookupResults -> {
for (var tool : values()) { return lookupResults.stream().filter(ToolLookupResult::isValid).collect(Collectors.
toolset.put(tool, tool.find()); groupingBy(lookupResult -> {
} return lookupResult.getInfo().version.toString();
return toolset; })).values().stream().filter(sameVersionLookupResults -> {
} Set<WixTool> sameVersionTools = sameVersionLookupResults.stream().map(
ToolLookupResult::getTool).collect(Collectors.toSet());
ToolInfo find() throws ConfigException { if (sameVersionTools.equals(Set.of(Candle3)) || sameVersionTools.equals(Set.of(
final Path toolFileName = IOUtils.addSuffix( Light3))) {
Path.of(name().toLowerCase()), ".exe"); // There is only one tool from WiX v3 toolset of some version available. Discard it.
return false;
String[] version = new String[1]; } else {
ConfigException reason = createToolValidator(toolFileName, version).get(); return true;
if (version[0] != null) {
if (reason == null) {
// Found in PATH.
return new ToolInfo(toolFileName, version[0]);
}
// Found in PATH, but something went wrong.
throw reason;
}
for (var dir : findWixInstallDirs()) {
Path path = dir.resolve(toolFileName);
if (Files.exists(path)) {
reason = createToolValidator(path, version).get();
if (reason != null) {
throw reason;
} }
return new ToolInfo(path, version[0]); }).flatMap(List::stream).collect(Collectors.toMap(ToolLookupResult::getTool,
} ToolLookupResult::getInfo, (ToolInfo x, ToolInfo y) -> {
return Stream.of(x, y).sorted(Comparator.comparing((ToolInfo toolInfo) -> {
return toolInfo.version.toComponentsString();
}).reversed()).findFirst().get();
}));
};
Function<List<ToolLookupResult>, Optional<WixToolset>> createToolset = lookupResults -> {
var tools = conv.apply(lookupResults);
// Try to build a toolset found in the PATH and in known locations.
return Stream.of(WixToolsetType.values()).map(toolsetType -> {
return WixToolset.create(toolsetType.getTools(), tools);
}).filter(Objects::nonNull).findFirst();
};
var toolsInPath = Stream.of(values()).map(tool -> {
return new ToolLookupResult(tool, null);
}).toList();
// Try to build a toolset from tools in the PATH first.
var toolset = createToolset.apply(toolsInPath);
if (toolset.isPresent()) {
return toolset.get();
} }
throw reason; // Look up for WiX tools in known locations.
var toolsInKnownWiXDirs = findWixInstallDirs().stream().map(dir -> {
return Stream.of(values()).map(tool -> {
return new ToolLookupResult(tool, dir);
});
}).flatMap(Function.identity()).toList();
// Build a toolset found in the PATH and in known locations.
var allFoundTools = Stream.of(toolsInPath, toolsInKnownWiXDirs).flatMap(List::stream).filter(
ToolLookupResult::isValid).toList();
toolset = createToolset.apply(allFoundTools);
if (toolset.isPresent()) {
return toolset.get();
} else if (allFoundTools.isEmpty()) {
throw new ConfigException(I18N.getString("error.no-wix-tools"), I18N.getString(
"error.no-wix-tools.advice"));
} else {
var toolOldVerErr = allFoundTools.stream().map(lookupResult -> {
if (lookupResult.versionTooOld) {
return new ConfigException(MessageFormat.format(I18N.getString(
"message.wrong-tool-version"), lookupResult.getInfo().path,
lookupResult.getInfo().version, lookupResult.getTool().minimalVersion),
I18N.getString("error.no-wix-tools.advice"));
} else {
return null;
}
}).filter(Objects::nonNull).findAny();
if (toolOldVerErr.isPresent()) {
throw toolOldVerErr.get();
} else {
throw new ConfigException(I18N.getString("error.no-wix-tools"), I18N.getString(
"error.no-wix-tools.advice"));
}
}
} }
private static Supplier<ConfigException> createToolValidator(Path toolPath, private static class ToolLookupResult {
String[] versionCtnr) {
return new ToolValidator(toolPath) ToolLookupResult(WixTool tool, Path lookupDir) {
.setCommandLine("/?")
.setMinimalVersion(MINIMAL_VERSION) final Path toolPath = Optional.ofNullable(lookupDir).map(p -> p.resolve(
.setToolNotFoundErrorHandler( tool.toolFileName)).orElse(tool.toolFileName);
(name, ex) -> new ConfigException(
I18N.getString("error.no-wix-tools"), final boolean[] tooOld = new boolean[1];
I18N.getString("error.no-wix-tools.advice"))) final String[] parsedVersion = new String[1];
.setToolOldVersionErrorHandler(
(name, version) -> new ConfigException( final var validator = new ToolValidator(toolPath).setMinimalVersion(tool.minimalVersion).
MessageFormat.format(I18N.getString( setToolNotFoundErrorHandler((name, ex) -> {
"message.wrong-tool-version"), name, return new ConfigException("", "");
version, MINIMAL_VERSION), }).setToolOldVersionErrorHandler((name, version) -> {
I18N.getString("error.no-wix-tools.advice"))) tooOld[0] = true;
.setVersionParser(output -> { return null;
versionCtnr[0] = ""; });
final Function<Stream<String>, String> versionParser;
if (Set.of(Candle3, Light3).contains(tool)) {
validator.setCommandLine("/?");
versionParser = output -> {
String firstLineOfOutput = output.findFirst().orElse(""); String firstLineOfOutput = output.findFirst().orElse("");
int separatorIdx = firstLineOfOutput.lastIndexOf(' '); int separatorIdx = firstLineOfOutput.lastIndexOf(' ');
if (separatorIdx == -1) { if (separatorIdx == -1) {
return null; return null;
} }
versionCtnr[0] = firstLineOfOutput.substring(separatorIdx + 1); return firstLineOfOutput.substring(separatorIdx + 1);
return versionCtnr[0]; };
})::validate; } else {
validator.setCommandLine("--version");
versionParser = output -> {
return output.findFirst().orElse("");
};
}
validator.setVersionParser(output -> {
parsedVersion[0] = versionParser.apply(output);
return parsedVersion[0];
});
this.tool = tool;
if (validator.validate() == null) {
// Tool found
this.versionTooOld = tooOld[0];
this.info = new ToolInfo(toolPath, parsedVersion[0]);
} else {
this.versionTooOld = false;
this.info = null;
}
}
WixTool getTool() {
return tool;
}
ToolInfo getInfo() {
return info;
}
boolean isValid() {
return info != null && !versionTooOld;
}
boolean isVersionTooOld() {
return versionTooOld;
}
private final WixTool tool;
private final ToolInfo info;
private final boolean versionTooOld;
} }
private static final DottedVersion MINIMAL_VERSION = DottedVersion.lazy("3.0"); private static Path getSystemDir(String envVar, String knownDir) {
static Path getSystemDir(String envVar, String knownDir) {
return Optional return Optional
.ofNullable(getEnvVariableAsPath(envVar)) .ofNullable(getEnvVariableAsPath(envVar))
.orElseGet(() -> Optional .orElseGet(() -> Optional
.ofNullable(getEnvVariableAsPath("SystemDrive")) .ofNullable(getEnvVariableAsPath("SystemDrive"))
.orElseGet(() -> Path.of("C:")).resolve(knownDir)); .orElseGet(() -> Path.of("C:")).resolve(knownDir));
} }
private static Path getEnvVariableAsPath(String envVar) { private static Path getEnvVariableAsPath(String envVar) {
@ -147,8 +239,22 @@ public enum WixTool {
} }
private static List<Path> findWixInstallDirs() { private static List<Path> findWixInstallDirs() {
PathMatcher wixInstallDirMatcher = FileSystems.getDefault().getPathMatcher( return Stream.of(findWixCurrentInstallDirs(), findWix3InstallDirs()).
"glob:WiX Toolset v*"); flatMap(List::stream).toList();
}
private static List<Path> findWixCurrentInstallDirs() {
return Stream.of(getEnvVariableAsPath("USERPROFILE"), Optional.ofNullable(System.
getProperty("user.home")).map(Path::of).orElse(null)).filter(Objects::nonNull).map(
path -> {
return path.resolve(".dotnet/tools");
}).filter(Files::isDirectory).distinct().toList();
}
private static List<Path> findWix3InstallDirs() {
PathMatcher wixInstallDirMatcher = FileSystems.getDefault().
getPathMatcher(
"glob:WiX Toolset v*");
Path programFiles = getSystemDir("ProgramFiles", "\\Program Files"); Path programFiles = getSystemDir("ProgramFiles", "\\Program Files");
Path programFilesX86 = getSystemDir("ProgramFiles(x86)", Path programFilesX86 = getSystemDir("ProgramFiles(x86)",
@ -157,18 +263,20 @@ public enum WixTool {
// Returns list of WiX install directories ordered by WiX version number. // Returns list of WiX install directories ordered by WiX version number.
// Newer versions go first. // Newer versions go first.
return Stream.of(programFiles, programFilesX86).map(path -> { return Stream.of(programFiles, programFilesX86).map(path -> {
List<Path> result;
try (var paths = Files.walk(path, 1)) { try (var paths = Files.walk(path, 1)) {
result = paths.toList(); return paths.toList();
} catch (IOException ex) { } catch (IOException ex) {
Log.verbose(ex); Log.verbose(ex);
result = Collections.emptyList(); List<Path> empty = List.of();
return empty;
} }
return result;
}).flatMap(List::stream) }).flatMap(List::stream)
.filter(path -> wixInstallDirMatcher.matches(path.getFileName())) .filter(path -> wixInstallDirMatcher.matches(path.getFileName())).
.sorted(Comparator.comparing(Path::getFileName).reversed()) sorted(Comparator.comparing(Path::getFileName).reversed())
.map(path -> path.resolve("bin")) .map(path -> path.resolve("bin"))
.toList(); .toList();
} }
private final Path toolFileName;
private final DottedVersion minimalVersion;
} }

View File

@ -0,0 +1,82 @@
/*
* Copyright (c) 2024, 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.nio.file.Path;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
final class WixToolset {
static enum WixToolsetType {
// Wix v4+
Wix4(WixTool.Wix4),
// Wix v3+
Wix3(WixTool.Candle3, WixTool.Light3);
WixToolsetType(WixTool... tools) {
this.tools = Set.of(tools);
}
Set<WixTool> getTools() {
return tools;
}
private final Set<WixTool> tools;
}
private WixToolset(Map<WixTool, WixTool.ToolInfo> tools) {
this.tools = tools;
}
WixToolsetType getType() {
return Stream.of(WixToolsetType.values()).filter(toolsetType -> {
return toolsetType.getTools().equals(tools.keySet());
}).findAny().get();
}
Path getToolPath(WixTool tool) {
return tools.get(tool).path;
}
DottedVersion getVersion() {
return tools.values().iterator().next().version;
}
static WixToolset create(Set<WixTool> requiredTools, Map<WixTool, WixTool.ToolInfo> allTools) {
var filteredTools = allTools.entrySet().stream().filter(e -> {
return requiredTools.contains(e.getKey());
}).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
if (filteredTools.keySet().equals(requiredTools)) {
return new WixToolset(filteredTools);
} else {
return null;
}
}
private final Map<WixTool, WixTool.ToolInfo> tools;
}

View File

@ -43,6 +43,7 @@ import jdk.jpackage.internal.IOUtils.XmlConsumer;
import static jdk.jpackage.internal.OverridableResource.createResource; import static jdk.jpackage.internal.OverridableResource.createResource;
import static jdk.jpackage.internal.StandardBundlerParam.LICENSE_FILE; import static jdk.jpackage.internal.StandardBundlerParam.LICENSE_FILE;
import jdk.jpackage.internal.WixAppImageFragmentBuilder.ShortcutsFolder; import jdk.jpackage.internal.WixAppImageFragmentBuilder.ShortcutsFolder;
import jdk.jpackage.internal.WixToolset.WixToolsetType;
/** /**
* Creates UI WiX fragment. * Creates UI WiX fragment.
@ -100,7 +101,13 @@ final class WixUiFragmentBuilder extends WixFragmentBuilder {
super.configureWixPipeline(wixPipeline); super.configureWixPipeline(wixPipeline);
if (withShortcutPromptDlg || withInstallDirChooserDlg || withLicenseDlg) { if (withShortcutPromptDlg || withInstallDirChooserDlg || withLicenseDlg) {
wixPipeline.addLightOptions("-ext", "WixUIExtension"); 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 // Only needed if we using CA dll, so Wix can find it
@ -148,15 +155,14 @@ final class WixUiFragmentBuilder extends WixFragmentBuilder {
xml.writeEndElement(); // WixVariable xml.writeEndElement(); // WixVariable
} }
xml.writeStartElement("UI");
xml.writeAttribute("Id", "JpUI");
var ui = getUI(); var ui = getUI();
if (ui != null) { if (ui != null) {
ui.write(this, xml); ui.write(getWixType(), this, xml);
} else {
xml.writeStartElement("UI");
xml.writeAttribute("Id", "JpUI");
xml.writeEndElement();
} }
xml.writeEndElement(); // UI
} }
private UI getUI() { private UI getUI() {
@ -187,12 +193,43 @@ final class WixUiFragmentBuilder extends WixFragmentBuilder {
this.dialogPairsSupplier = dialogPairsSupplier; this.dialogPairsSupplier = dialogPairsSupplier;
} }
void write(WixUiFragmentBuilder outer, XMLStreamWriter xml) throws void write(WixToolsetType wixType, WixUiFragmentBuilder outer, XMLStreamWriter xml) throws XMLStreamException, IOException {
XMLStreamException, IOException { switch (wixType) {
xml.writeStartElement("UIRef"); case Wix3 -> {}
xml.writeAttribute("Id", wixUIRef); case Wix4 -> {
xml.writeEndElement(); // UIRef // 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) { if (dialogIdsSupplier != null) {
List<Dialog> dialogIds = dialogIdsSupplier.apply(outer); List<Dialog> dialogIds = dialogIdsSupplier.apply(outer);
Map<DialogPair, List<Publish>> dialogPairs = dialogPairsSupplier.get(); Map<DialogPair, List<Publish>> dialogPairs = dialogPairsSupplier.get();
@ -210,7 +247,7 @@ final class WixUiFragmentBuilder extends WixFragmentBuilder {
DialogPair pair = new DialogPair(firstId, secondId); DialogPair pair = new DialogPair(firstId, secondId);
for (var curPair : List.of(pair, pair.flip())) { for (var curPair : List.of(pair, pair.flip())) {
for (var publish : dialogPairs.get(curPair)) { for (var publish : dialogPairs.get(curPair)) {
writePublishDialogPair(xml, publish, curPair); writePublishDialogPair(wixType, xml, publish, curPair);
} }
} }
firstId = secondId; firstId = secondId;
@ -218,6 +255,17 @@ final class WixUiFragmentBuilder extends WixFragmentBuilder {
} }
} }
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 String wixUIRef;
private final Function<WixUiFragmentBuilder, List<Dialog>> dialogIdsSupplier; private final Function<WixUiFragmentBuilder, List<Dialog>> dialogIdsSupplier;
private final Supplier<Map<DialogPair, List<Publish>>> dialogPairsSupplier; private final Supplier<Map<DialogPair, List<Publish>>> dialogPairsSupplier;
@ -441,9 +489,8 @@ final class WixUiFragmentBuilder extends WixFragmentBuilder {
return new PublishBuilder(publish); return new PublishBuilder(publish);
} }
private static void writePublishDialogPair(XMLStreamWriter xml, private static void writePublishDialogPair(WixToolsetType wixType, XMLStreamWriter xml,
Publish publish, DialogPair dialogPair) throws IOException, Publish publish, DialogPair dialogPair) throws IOException, XMLStreamException {
XMLStreamException {
xml.writeStartElement("Publish"); xml.writeStartElement("Publish");
xml.writeAttribute("Dialog", dialogPair.firstId); xml.writeAttribute("Dialog", dialogPair.firstId);
xml.writeAttribute("Control", publish.control); xml.writeAttribute("Control", publish.control);
@ -452,7 +499,11 @@ final class WixUiFragmentBuilder extends WixFragmentBuilder {
if (publish.order != 0) { if (publish.order != 0) {
xml.writeAttribute("Order", String.valueOf(publish.order)); xml.writeAttribute("Order", String.valueOf(publish.order));
} }
xml.writeCharacters(publish.condition); switch (wixType) {
case Wix3 -> xml.writeCharacters(publish.condition);
case Wix4 -> xml.writeAttribute("Condition", publish.condition);
default -> throw new IllegalArgumentException();
}
xml.writeEndElement(); xml.writeEndElement();
} }
@ -463,9 +514,8 @@ final class WixUiFragmentBuilder extends WixFragmentBuilder {
this.wxsFileName = wxsFileName; this.wxsFileName = wxsFileName;
this.wixVariables = new WixVariables(); this.wixVariables = new WixVariables();
addResource( addResource(createResource(wxsFileName, params).setCategory(category).setPublicName(
createResource(wxsFileName, params).setCategory(category), wxsFileName), wxsFileName);
wxsFileName);
} }
void addToWixPipeline(WixPipeline wixPipeline) { void addToWixPipeline(WixPipeline wixPipeline) {

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- <!--
/* /*
* Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* *
* This code is free software; you can redistribute it and/or modify it * This code is free software; you can redistribute it and/or modify it
@ -41,9 +41,7 @@
<Control Id="No" Type="PushButton" X="150" Y="55" Width="50" Height="15" Default="yes" Cancel="yes" Text="!(loc.WixUINo)"> <Control Id="No" Type="PushButton" X="150" Y="55" Width="50" Height="15" Default="yes" Cancel="yes" Text="!(loc.WixUINo)">
<Publish Event="NewDialog" Value="InstallDirDlg">1</Publish> <Publish Event="NewDialog" Value="InstallDirDlg">1</Publish>
</Control> </Control>
<Control Id="Text" Type="Text" X="25" Y="15" Width="250" Height="30" TabSkip="no"> <Control Id="Text" Type="Text" X="25" Y="15" Width="250" Height="30" TabSkip="no" Text="!(loc.InstallDirNotEmptyDlgInstallDirExistMessage)"/>
<Text>!(loc.message.install.dir.exist)</Text>
</Control>
</Dialog> </Dialog>
<Publish Dialog="InstallDirDlg" Control="Next" Event="DoAction" Value="JpCheckInstallDir" Order="3">1</Publish> <Publish Dialog="InstallDirDlg" Control="Next" Event="DoAction" Value="JpCheckInstallDir" Order="3">1</Publish>

View File

@ -1,6 +1,5 @@
<?xml version = '1.0' encoding = 'utf-8'?> <?xml version = '1.0' encoding = 'utf-8'?>
<WixLocalization Culture="de-de" xmlns="http://schemas.microsoft.com/wix/2006/localization" Codepage="1252"> <WixLocalization Culture="de-de" xmlns="http://schemas.microsoft.com/wix/2006/localization" Codepage="1252">
<String Id="message.install.dir.exist">Der Ordner [INSTALLDIR] ist bereits vorhanden. Möchten Sie diesen Ordner trotzdem installieren?</String>
<String Id="MainFeatureTitle">Hauptfeature</String> <String Id="MainFeatureTitle">Hauptfeature</String>
<String Id="DowngradeErrorMessage">Eine höhere Version von [ProductName] ist bereits installiert. Downgrades sind deaktiviert. Setup wird jetzt beendet.</String> <String Id="DowngradeErrorMessage">Eine höhere Version von [ProductName] ist bereits installiert. Downgrades sind deaktiviert. Setup wird jetzt beendet.</String>
<String Id="DisallowUpgradeErrorMessage">Eine niedrigere Version von [ProductName] ist bereits installiert. Upgrades sind deaktiviert. Setup wird jetzt beendet.</String> <String Id="DisallowUpgradeErrorMessage">Eine niedrigere Version von [ProductName] ist bereits installiert. Upgrades sind deaktiviert. Setup wird jetzt beendet.</String>
@ -11,6 +10,9 @@
<String Id="ShortcutPromptDlgDescription">Wählen Sie die zu erstellenden Verknüpfungen aus.</String> <String Id="ShortcutPromptDlgDescription">Wählen Sie die zu erstellenden Verknüpfungen aus.</String>
<String Id="ShortcutPromptDlgDesktopShortcutControlLabel">Desktopverknüpfung(en) erstellen</String> <String Id="ShortcutPromptDlgDesktopShortcutControlLabel">Desktopverknüpfung(en) erstellen</String>
<String Id="ShortcutPromptDlgStartMenuShortcutControlLabel">Startmenüverknüpfung(en) erstellen</String> <String Id="ShortcutPromptDlgStartMenuShortcutControlLabel">Startmenüverknüpfung(en) erstellen</String>
<String Id="InstallDirNotEmptyDlg_Title">[ProductName]-Setup</String> <String Id="InstallDirNotEmptyDlg_Title">[ProductName]-Setup</String>
<String Id="InstallDirNotEmptyDlgInstallDirExistMessage">Der Ordner [INSTALLDIR] ist bereits vorhanden. Möchten Sie diesen Ordner trotzdem installieren?</String>
<String Id="ContextMenuCommandLabel">Mit [ProductName] öffnen</String> <String Id="ContextMenuCommandLabel">Mit [ProductName] öffnen</String>
</WixLocalization> </WixLocalization>

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<WixLocalization Culture="en-us" xmlns="http://schemas.microsoft.com/wix/2006/localization" Codepage="1252"> <WixLocalization Culture="en-us" xmlns="http://schemas.microsoft.com/wix/2006/localization" Codepage="1252">
<String Id="message.install.dir.exist">The folder [INSTALLDIR] already exist. Would you like to install to that folder anyway?</String>
<String Id="MainFeatureTitle">Main Feature</String> <String Id="MainFeatureTitle">Main Feature</String>
<String Id="DowngradeErrorMessage">A higher version of [ProductName] is already installed. Downgrades disabled. Setup will now exit.</String> <String Id="DowngradeErrorMessage">A higher version of [ProductName] is already installed. Downgrades disabled. Setup will now exit.</String>
<String Id="DisallowUpgradeErrorMessage">A lower version of [ProductName] is already installed. Upgrades disabled. Setup will now exit.</String> <String Id="DisallowUpgradeErrorMessage">A lower version of [ProductName] is already installed. Upgrades disabled. Setup will now exit.</String>
@ -11,6 +10,9 @@
<String Id="ShortcutPromptDlgDescription">Select shortcuts to create.</String> <String Id="ShortcutPromptDlgDescription">Select shortcuts to create.</String>
<String Id="ShortcutPromptDlgDesktopShortcutControlLabel">Create desktop shortcut(s)</String> <String Id="ShortcutPromptDlgDesktopShortcutControlLabel">Create desktop shortcut(s)</String>
<String Id="ShortcutPromptDlgStartMenuShortcutControlLabel">Create start menu shortcut(s)</String> <String Id="ShortcutPromptDlgStartMenuShortcutControlLabel">Create start menu shortcut(s)</String>
<String Id="InstallDirNotEmptyDlg_Title">[ProductName] Setup</String> <String Id="InstallDirNotEmptyDlg_Title">[ProductName] Setup</String>
<String Id="InstallDirNotEmptyDlgInstallDirExistMessage">The folder [INSTALLDIR] already exist. Would you like to install to that folder anyway?</String>
<String Id="ContextMenuCommandLabel">Open with [ProductName]</String> <String Id="ContextMenuCommandLabel">Open with [ProductName]</String>
</WixLocalization> </WixLocalization>

View File

@ -1,6 +1,5 @@
<?xml version = '1.0' encoding = 'utf-8'?> <?xml version = '1.0' encoding = 'utf-8'?>
<WixLocalization Culture="ja-jp" xmlns="http://schemas.microsoft.com/wix/2006/localization" Codepage="932"> <WixLocalization Culture="ja-jp" xmlns="http://schemas.microsoft.com/wix/2006/localization" Codepage="932">
<String Id="message.install.dir.exist">フォルダ[INSTALLDIR]はすでに存在します。そのフォルダにインストールしますか?</String>
<String Id="MainFeatureTitle">主な機能</String> <String Id="MainFeatureTitle">主な機能</String>
<String Id="DowngradeErrorMessage">[ProductName]のより上位のバージョンがすでにインストールされています。ダウングレードは無効です。セットアップを終了します。</String> <String Id="DowngradeErrorMessage">[ProductName]のより上位のバージョンがすでにインストールされています。ダウングレードは無効です。セットアップを終了します。</String>
<String Id="DisallowUpgradeErrorMessage">[ProductName]のより下位のバージョンがすでにインストールされています。アップグレードは無効です。セットアップを終了します。</String> <String Id="DisallowUpgradeErrorMessage">[ProductName]のより下位のバージョンがすでにインストールされています。アップグレードは無効です。セットアップを終了します。</String>
@ -11,6 +10,9 @@
<String Id="ShortcutPromptDlgDescription">作成するショートカットを選択します。</String> <String Id="ShortcutPromptDlgDescription">作成するショートカットを選択します。</String>
<String Id="ShortcutPromptDlgDesktopShortcutControlLabel">デスクトップ・ショートカットの作成</String> <String Id="ShortcutPromptDlgDesktopShortcutControlLabel">デスクトップ・ショートカットの作成</String>
<String Id="ShortcutPromptDlgStartMenuShortcutControlLabel">スタート・メニューのショートカットの作成</String> <String Id="ShortcutPromptDlgStartMenuShortcutControlLabel">スタート・メニューのショートカットの作成</String>
<String Id="InstallDirNotEmptyDlg_Title">[ProductName]セットアップ</String> <String Id="InstallDirNotEmptyDlg_Title">[ProductName]セットアップ</String>
<String Id="InstallDirNotEmptyDlgInstallDirExistMessage">フォルダ[INSTALLDIR]はすでに存在します。そのフォルダにインストールしますか?</String>
<String Id="ContextMenuCommandLabel">[ProductName]で開く</String> <String Id="ContextMenuCommandLabel">[ProductName]で開く</String>
</WixLocalization> </WixLocalization>

View File

@ -1,6 +1,5 @@
<?xml version = '1.0' encoding = 'utf-8'?> <?xml version = '1.0' encoding = 'utf-8'?>
<WixLocalization Culture="zh-cn" xmlns="http://schemas.microsoft.com/wix/2006/localization" Codepage="936"> <WixLocalization Culture="zh-cn" xmlns="http://schemas.microsoft.com/wix/2006/localization" Codepage="936">
<String Id="message.install.dir.exist">文件夹 [INSTALLDIR] 已存在。是否仍要安装到该文件夹?</String>
<String Id="MainFeatureTitle">主要功能</String> <String Id="MainFeatureTitle">主要功能</String>
<String Id="DowngradeErrorMessage">已安装更高版本的 [ProductName]。降级已禁用。现在将退出安装。</String> <String Id="DowngradeErrorMessage">已安装更高版本的 [ProductName]。降级已禁用。现在将退出安装。</String>
<String Id="DisallowUpgradeErrorMessage">已安装更低版本的 [ProductName]。升级已禁用。现在将退出安装。</String> <String Id="DisallowUpgradeErrorMessage">已安装更低版本的 [ProductName]。升级已禁用。现在将退出安装。</String>
@ -11,6 +10,9 @@
<String Id="ShortcutPromptDlgDescription">选择要创建的快捷方式。</String> <String Id="ShortcutPromptDlgDescription">选择要创建的快捷方式。</String>
<String Id="ShortcutPromptDlgDesktopShortcutControlLabel">创建桌面快捷方式</String> <String Id="ShortcutPromptDlgDesktopShortcutControlLabel">创建桌面快捷方式</String>
<String Id="ShortcutPromptDlgStartMenuShortcutControlLabel">创建开始菜单快捷方式</String> <String Id="ShortcutPromptDlgStartMenuShortcutControlLabel">创建开始菜单快捷方式</String>
<String Id="InstallDirNotEmptyDlg_Title">[ProductName] 安装程序</String> <String Id="InstallDirNotEmptyDlg_Title">[ProductName] 安装程序</String>
<String Id="InstallDirNotEmptyDlgInstallDirExistMessage">文件夹 [INSTALLDIR] 已存在。是否仍要安装到该文件夹?</String>
<String Id="ContextMenuCommandLabel">使用 [ProductName] 打开</String> <String Id="ContextMenuCommandLabel">使用 [ProductName] 打开</String>
</WixLocalization> </WixLocalization>

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2017, 2022, Oracle and/or its affiliates. All rights reserved. # Copyright (c) 2017, 2024, Oracle and/or its affiliates. All rights reserved.
# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. # DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
# #
# This code is free software; you can redistribute it and/or modify it # This code is free software; you can redistribute it and/or modify it
@ -41,8 +41,9 @@ resource.overrides-wix-file=Overrides WiX project file
resource.shortcutpromptdlg-wix-file=Shortcut prompt dialog WiX project file resource.shortcutpromptdlg-wix-file=Shortcut prompt dialog WiX project file
resource.installdirnotemptydlg-wix-file=Not empty install directory dialog WiX project file resource.installdirnotemptydlg-wix-file=Not empty install directory dialog WiX project file
resource.launcher-as-service-wix-file=Service installer WiX project file resource.launcher-as-service-wix-file=Service installer WiX project file
resource.wix-src-conv=XSLT stylesheet converting WiX sources from WiX v3 to WiX v4 format
error.no-wix-tools=Can not find WiX tools (light.exe, candle.exe) error.no-wix-tools=Can not find WiX tools. Was looking for WiX v3 light.exe and candle.exe or WiX v4/v5 wix.exe and none was found
error.no-wix-tools.advice=Download WiX 3.0 or later from https://wixtoolset.org and add it to the PATH. error.no-wix-tools.advice=Download WiX 3.0 or later from https://wixtoolset.org and add it to the PATH.
error.version-string-wrong-format.advice=Set value of --app-version parameter to a valid Windows Installer ProductVersion. error.version-string-wrong-format.advice=Set value of --app-version parameter to a valid Windows Installer ProductVersion.
error.msi-product-version-components=Version string [{0}] must have between 2 and 4 components. error.msi-product-version-components=Version string [{0}] must have between 2 and 4 components.

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2017, 2022, Oracle and/or its affiliates. All rights reserved. # Copyright (c) 2017, 2024, Oracle and/or its affiliates. All rights reserved.
# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. # DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
# #
# This code is free software; you can redistribute it and/or modify it # This code is free software; you can redistribute it and/or modify it
@ -41,8 +41,9 @@ resource.overrides-wix-file=Überschreibt WiX-Projektdatei
resource.shortcutpromptdlg-wix-file=Dialogfeld für Verknüpfungs-Prompt der WiX-Projektdatei resource.shortcutpromptdlg-wix-file=Dialogfeld für Verknüpfungs-Prompt der WiX-Projektdatei
resource.installdirnotemptydlg-wix-file=Nicht leeres Installationsverzeichnis in Dialogfeld für WiX-Projektdatei resource.installdirnotemptydlg-wix-file=Nicht leeres Installationsverzeichnis in Dialogfeld für WiX-Projektdatei
resource.launcher-as-service-wix-file=WiX-Projektdatei für Serviceinstallationsprogramm resource.launcher-as-service-wix-file=WiX-Projektdatei für Serviceinstallationsprogramm
resource.wix-src-conv=XSLT stylesheet converting WiX sources from WiX v3 to WiX v4 format
error.no-wix-tools=WiX-Tools (light.exe, candle.exe) nicht gefunden error.no-wix-tools=Can not find WiX tools. Was looking for WiX v3 light.exe and candle.exe or WiX v4/v5 wix.exe and none was found
error.no-wix-tools.advice=Laden Sie WiX 3.0 oder höher von https://wixtoolset.org herunter, und fügen Sie es zu PATH hinzu. error.no-wix-tools.advice=Laden Sie WiX 3.0 oder höher von https://wixtoolset.org herunter, und fügen Sie es zu PATH hinzu.
error.version-string-wrong-format.advice=Setzen Sie den Wert des --app-version-Parameters auf eine gültige ProductVersion des Windows-Installationsprogramms. error.version-string-wrong-format.advice=Setzen Sie den Wert des --app-version-Parameters auf eine gültige ProductVersion des Windows-Installationsprogramms.
error.msi-product-version-components=Versionszeichenfolge [{0}] muss zwischen 2 und 4 Komponenten aufweisen. error.msi-product-version-components=Versionszeichenfolge [{0}] muss zwischen 2 und 4 Komponenten aufweisen.

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2017, 2022, Oracle and/or its affiliates. All rights reserved. # Copyright (c) 2017, 2024, Oracle and/or its affiliates. All rights reserved.
# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. # DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
# #
# This code is free software; you can redistribute it and/or modify it # This code is free software; you can redistribute it and/or modify it
@ -41,8 +41,9 @@ resource.overrides-wix-file=WiXプロジェクト・ファイルのオーバー
resource.shortcutpromptdlg-wix-file=ショートカット・プロンプト・ダイアログWiXプロジェクト・ファイル resource.shortcutpromptdlg-wix-file=ショートカット・プロンプト・ダイアログWiXプロジェクト・ファイル
resource.installdirnotemptydlg-wix-file=インストール・ディレクトリ・ダイアログのWiXプロジェクト・ファイルが空ではありません resource.installdirnotemptydlg-wix-file=インストール・ディレクトリ・ダイアログのWiXプロジェクト・ファイルが空ではありません
resource.launcher-as-service-wix-file=サービス・インストーラWiXプロジェクト・ファイル resource.launcher-as-service-wix-file=サービス・インストーラWiXプロジェクト・ファイル
resource.wix-src-conv=XSLT stylesheet converting WiX sources from WiX v3 to WiX v4 format
error.no-wix-tools=WiXツール(light.exe、candle.exe)が見つかりません error.no-wix-tools=Can not find WiX tools. Was looking for WiX v3 light.exe and candle.exe or WiX v4/v5 wix.exe and none was found
error.no-wix-tools.advice=WiX 3.0以降をhttps://wixtoolset.orgからダウンロードし、PATHに追加します。 error.no-wix-tools.advice=WiX 3.0以降をhttps://wixtoolset.orgからダウンロードし、PATHに追加します。
error.version-string-wrong-format.advice=--app-versionパラメータの値を有効なWindows Installer ProductVersionに設定します。 error.version-string-wrong-format.advice=--app-versionパラメータの値を有効なWindows Installer ProductVersionに設定します。
error.msi-product-version-components=バージョン文字列[{0}]には、2から4つのコンポーネントが含まれている必要があります。 error.msi-product-version-components=バージョン文字列[{0}]には、2から4つのコンポーネントが含まれている必要があります。

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2017, 2022, Oracle and/or its affiliates. All rights reserved. # Copyright (c) 2017, 2024, Oracle and/or its affiliates. All rights reserved.
# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. # DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
# #
# This code is free software; you can redistribute it and/or modify it # This code is free software; you can redistribute it and/or modify it
@ -41,8 +41,9 @@ resource.overrides-wix-file=覆盖 WiX 项目文件
resource.shortcutpromptdlg-wix-file=快捷方式提示对话框 WiX 项目文件 resource.shortcutpromptdlg-wix-file=快捷方式提示对话框 WiX 项目文件
resource.installdirnotemptydlg-wix-file=安装目录对话框 WiX 项目文件非空 resource.installdirnotemptydlg-wix-file=安装目录对话框 WiX 项目文件非空
resource.launcher-as-service-wix-file=服务安装程序 WiX 项目文件 resource.launcher-as-service-wix-file=服务安装程序 WiX 项目文件
resource.wix-src-conv=XSLT stylesheet converting WiX sources from WiX v3 to WiX v4 format
error.no-wix-tools=找不到 WiX 工具 (light.exe, candle.exe) error.no-wix-tools=Can not find WiX tools. Was looking for WiX v3 light.exe and candle.exe or WiX v4/v5 wix.exe and none was found
error.no-wix-tools.advice=从 https://wixtoolset.org 下载 WiX 3.0 或更高版本,然后将其添加到 PATH。 error.no-wix-tools.advice=从 https://wixtoolset.org 下载 WiX 3.0 或更高版本,然后将其添加到 PATH。
error.version-string-wrong-format.advice=将 --app-version 参数的值设置为有效的 Windows Installer ProductVersion。 error.version-string-wrong-format.advice=将 --app-version 参数的值设置为有效的 Windows Installer ProductVersion。
error.msi-product-version-components=版本字符串 [{0}] 必须包含 2 到 4 个组成部分。 error.msi-product-version-components=版本字符串 [{0}] 必须包含 2 到 4 个组成部分。

View File

@ -28,4 +28,4 @@ to disable (the value doesn't matter).
Should be defined to enable upgrades and undefined to disable upgrades. Should be defined to enable upgrades and undefined to disable upgrades.
By default it is defined, use <?undef JpAllowUpgrades?> to disable. By default it is defined, use <?undef JpAllowUpgrades?> to disable.
--> -->
<Include/> <Include xmlns="http://schemas.microsoft.com/wix/2006/wi"/>

View File

@ -0,0 +1,183 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
/*
* Copyright (c) 2024, 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.
*/
-->
<!--
This stylesheet can be applied to Wix3 .wxl, .wxs, and .wsi source files.
-->
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:wix3loc="http://schemas.microsoft.com/wix/2006/localization"
xmlns:wix3="http://schemas.microsoft.com/wix/2006/wi"
>
<!-- Wix4 complains about xml declaration in input files. Turn it off -->
<xsl:output method="xml" omit-xml-declaration="yes" indent="yes"/>
<!--
Remap xmlns="http://schemas.microsoft.com/wix/2006/localization"
to xmlns="http://wixtoolset.org/schemas/v4/wxl"
-->
<xsl:template match="wix3loc:*">
<xsl:element name="{local-name()}" namespace="http://wixtoolset.org/schemas/v4/wxl">
<xsl:apply-templates select="@*|node()"/>
</xsl:element>
</xsl:template>
<!--
Remap xmlns="http://schemas.microsoft.com/wix/2006/localization"
to xmlns="http://wixtoolset.org/schemas/v4/wxs"
-->
<xsl:template match="wix3:*">
<xsl:element name="{local-name()}" namespace="http://wixtoolset.org/schemas/v4/wxs">
<xsl:apply-templates select="@*|node()"/>
</xsl:element>
</xsl:template>
<!--
From <String Id="foo">Bar</String> to <String Id="foo" Value="Bar"/>
-->
<xsl:template match="wix3loc:WixLocalization/wix3loc:String">
<xsl:element name="{local-name()}" namespace="http://wixtoolset.org/schemas/v4/wxl">
<xsl:attribute name="Value">
<xsl:value-of select="text()"/>
</xsl:attribute>
<xsl:apply-templates select="@*"/>
</xsl:element>
</xsl:template>
<!--
Wix3 Product (https://wixtoolset.org/docs/v3/xsd/wix/product/):
Id
Codepage
Language
Manufacturer
Name
UpgradeCode
Version
Wix3 Package (https://wixtoolset.org/docs/v3/xsd/wix/package/):
AdminImage
Comments
Compressed
Description
Id
InstallerVersion
InstallPrivileges
InstallScope
Keywords
Languages
Manufacturer
Platform
Platforms
ReadOnly
ShortNames
SummaryCodepage
Wix4 Package (https://wixtoolset.org/docs/schema/wxs/package/):
Codepage <- Wix3:Product/@Codepage
Compressed <- Wix3:@Compressed
InstallerVersion <- Wix3:@InstallerVersion
Language <- Wix3:Product/@Language
Manufacturer <- Wix3:Product/@Manufacturer
Name <- Wix3:Product/@Name
ProductCode <- Wix3:Product/@Id
Scope <- Wix3:@InstallScope
ShortNames <- Wix3:@ShortNames
UpgradeCode <- Wix3:Product/@UpgradeCode
UpgradeStrategy <-
Version <- Wix3:Product/@Version
Wix4 SummaryInformation (https://wixtoolset.org/docs/schema/wxs/summaryinformation/):
Codepage <- Wix3:Product/@Codepage
Comments <- Wix3:@Comments
Description <- Wix3:@Description
Keywords <- Wix3:@Keywords
Manufacturer <- Wix3:Product/@Manufacturer
-->
<xsl:template match="wix3:Product">
<xsl:element name="Package" namespace="http://wixtoolset.org/schemas/v4/wxs">
<xsl:apply-templates select="@Codepage|wix3:Package/@Compressed|wix3:Package/@InstallerVersion|@Language|@Manufacturer|@Name|@Id|wix3:Package/@InstallScope|wix3:Package/@ShortNames|@UpgradeCode|@Version"/>
<xsl:if test="@Id">
<xsl:attribute name="ProductCode">
<xsl:value-of select="@Id"/>
</xsl:attribute>
</xsl:if>
<xsl:if test="wix3:Package/@InstallScope">
<xsl:attribute name="Scope">
<xsl:value-of select="wix3:Package/@InstallScope"/>
</xsl:attribute>
</xsl:if>
<xsl:element name="SummaryInformation" namespace="http://wixtoolset.org/schemas/v4/wxs">
<xsl:apply-templates select="@Codepage|wix3:Package/@Comments|wix3:Package/@Description|wix3:Package/@Keywords|@Manufacturer"/>
</xsl:element>
<xsl:apply-templates select="node()"/>
</xsl:element>
</xsl:template>
<xsl:template match="wix3:Package|wix3:Product/@Id|wix3:Package/@InstallScope"/>
<xsl:template match="wix3:CustomAction/@BinaryKey">
<xsl:attribute name="BinaryRef">
<xsl:value-of select="."/>
</xsl:attribute>
</xsl:template>
<xsl:template match="wix3:Custom|wix3:Publish">
<xsl:element name="{local-name()}" namespace="http://wixtoolset.org/schemas/v4/wxs">
<xsl:apply-templates select="@*"/>
<xsl:if test="text()">
<xsl:attribute name="Condition">
<xsl:value-of select="text()"/>
</xsl:attribute>
</xsl:if>
<xsl:apply-templates select="node()"/>
</xsl:element>
</xsl:template>
<xsl:template match="wix3:Custom/text()|wix3:Publish/text()"/>
<xsl:template match="wix3:Directory[@Id='TARGETDIR']"/>
<!--
Identity transform
-->
<xsl:template match="@*|node()">
<xsl:copy>
<xsl:apply-templates select="@*|node()"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019, 2022, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2019, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* *
* This code is free software; you can redistribute it and/or modify it * This code is free software; you can redistribute it and/or modify it
@ -139,6 +139,30 @@ public class WindowsHelper {
String.format("TARGETDIR=\"%s\"", String.format("TARGETDIR=\"%s\"",
unpackDir.toAbsolutePath().normalize()))))); unpackDir.toAbsolutePath().normalize())))));
runMsiexecWithRetries(Executor.of("cmd", "/c", unpackBat.toString())); runMsiexecWithRetries(Executor.of("cmd", "/c", unpackBat.toString()));
//
// WiX3 uses "." as the value of "DefaultDir" field for "ProgramFiles64Folder" folder in msi's Directory table
// WiX4 uses "PFiles64" as the value of "DefaultDir" field for "ProgramFiles64Folder" folder in msi's Directory table
// msiexec creates "Program Files/./<App Installation Directory>" from WiX3 msi which translates to "Program Files/<App Installation Directory>"
// msiexec creates "Program Files/PFiles64/<App Installation Directory>" from WiX4 msi
// So for WiX4 msi we need to transform "Program Files/PFiles64/<App Installation Directory>" into "Program Files/<App Installation Directory>"
//
// WiX4 does the same thing for %LocalAppData%.
//
for (var extraPathComponent : List.of("PFiles64", "LocalApp")) {
if (Files.isDirectory(unpackDir.resolve(extraPathComponent))) {
Path installationSubDirectory = getInstallationSubDirectory(cmd);
Path from = Path.of(extraPathComponent).resolve(installationSubDirectory);
Path to = installationSubDirectory;
TKit.trace(String.format("Convert [%s] into [%s] in [%s] directory", from, to,
unpackDir));
ThrowingRunnable.toRunnable(() -> {
Files.createDirectories(unpackDir.resolve(to).getParent());
Files.move(unpackDir.resolve(from), unpackDir.resolve(to));
TKit.deleteDirectoryRecursive(unpackDir.resolve(extraPathComponent));
}).run();
}
}
return destinationDir; return destinationDir;
}; };
return msi; return msi;

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2020, 2022, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2020, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* *
* This code is free software; you can redistribute it and/or modify it * This code is free software; you can redistribute it and/or modify it
@ -33,6 +33,7 @@ import jdk.jpackage.test.Annotations.Parameters;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import jdk.jpackage.test.Executor; import jdk.jpackage.test.Executor;
@ -55,11 +56,11 @@ import static jdk.jpackage.test.WindowsHelper.getTempDirectory;
public class WinL10nTest { public class WinL10nTest {
public WinL10nTest(WixFileInitializer wxlFileInitializers[], public WinL10nTest(WixFileInitializer wxlFileInitializers[],
String expectedCulture, String expectedErrorMessage, String[] expectedCultures, String expectedErrorMessage,
String userLanguage, String userCountry, String userLanguage, String userCountry,
boolean enableWixUIExtension) { boolean enableWixUIExtension) {
this.wxlFileInitializers = wxlFileInitializers; this.wxlFileInitializers = wxlFileInitializers;
this.expectedCulture = expectedCulture; this.expectedCultures = expectedCultures;
this.expectedErrorMessage = expectedErrorMessage; this.expectedErrorMessage = expectedErrorMessage;
this.userLanguage = userLanguage; this.userLanguage = userLanguage;
this.userCountry = userCountry; this.userCountry = userCountry;
@ -69,56 +70,65 @@ public class WinL10nTest {
@Parameters @Parameters
public static List<Object[]> data() { public static List<Object[]> data() {
return List.of(new Object[][]{ return List.of(new Object[][]{
{null, "en-us", null, null, null, false}, {null, new String[] {"en-us"}, null, null, null, false},
{null, "en-us", null, "en", "US", false}, {null, new String[] {"en-us"}, null, "en", "US", false},
{null, "en-us", null, "en", "US", true}, {null, new String[] {"en-us"}, null, "en", "US", true},
{null, "de-de", null, "de", "DE", false}, {null, new String[] {"de-de"}, null, "de", "DE", false},
{null, "de-de", null, "de", "DE", true}, {null, new String[] {"de-de"}, null, "de", "DE", true},
{null, "ja-jp", null, "ja", "JP", false}, {null, new String[] {"ja-jp"}, null, "ja", "JP", false},
{null, "ja-jp", null, "ja", "JP", true}, {null, new String[] {"ja-jp"}, null, "ja", "JP", true},
{null, "zh-cn", null, "zh", "CN", false}, {null, new String[] {"zh-cn"}, null, "zh", "CN", false},
{null, "zh-cn", null, "zh", "CN", true}, {null, new String[] {"zh-cn"}, null, "zh", "CN", true},
{new WixFileInitializer[] { {new WixFileInitializer[] {
WixFileInitializer.create("a.wxl", "en-us") WixFileInitializer.create("a.wxl", "en-us")
}, "en-us", null, null, null, false}, }, new String[] {"en-us"}, null, null, null, false},
{new WixFileInitializer[] { {new WixFileInitializer[] {
WixFileInitializer.create("a.wxl", "fr") WixFileInitializer.create("a.wxl", "fr")
}, "fr;en-us", null, null, null, false}, }, new String[] {"fr", "en-us"}, null, null, null, false},
{new WixFileInitializer[] { {new WixFileInitializer[] {
WixFileInitializer.create("a.wxl", "fr"), WixFileInitializer.create("a.wxl", "fr"),
WixFileInitializer.create("b.wxl", "fr") WixFileInitializer.create("b.wxl", "fr")
}, "fr;en-us", null, null, null, false}, }, new String[] {"fr", "en-us"}, null, null, null, false},
{new WixFileInitializer[] { {new WixFileInitializer[] {
WixFileInitializer.create("a.wxl", "it"), WixFileInitializer.create("a.wxl", "it"),
WixFileInitializer.create("b.wxl", "fr") WixFileInitializer.create("b.wxl", "fr")
}, "it;fr;en-us", null, null, null, false}, }, new String[] {"it", "fr", "en-us"}, null, null, null, false},
{new WixFileInitializer[] { {new WixFileInitializer[] {
WixFileInitializer.create("c.wxl", "it"), WixFileInitializer.create("c.wxl", "it"),
WixFileInitializer.create("b.wxl", "fr") WixFileInitializer.create("b.wxl", "fr")
}, "fr;it;en-us", null, null, null, false}, }, new String[] {"fr", "it", "en-us"}, null, null, null, false},
{new WixFileInitializer[] { {new WixFileInitializer[] {
WixFileInitializer.create("a.wxl", "fr"), WixFileInitializer.create("a.wxl", "fr"),
WixFileInitializer.create("b.wxl", "it"), WixFileInitializer.create("b.wxl", "it"),
WixFileInitializer.create("c.wxl", "fr"), WixFileInitializer.create("c.wxl", "fr"),
WixFileInitializer.create("d.wxl", "it") WixFileInitializer.create("d.wxl", "it")
}, "fr;it;en-us", null, null, null, false}, }, new String[] {"fr", "it", "en-us"}, null, null, null, false},
{new WixFileInitializer[] { {new WixFileInitializer[] {
WixFileInitializer.create("c.wxl", "it"), WixFileInitializer.create("c.wxl", "it"),
WixFileInitializer.createMalformed("b.wxl") WixFileInitializer.createMalformed("b.wxl")
}, null, null, null, null, false}, }, null, null, null, null, false},
{new WixFileInitializer[] { {new WixFileInitializer[] {
WixFileInitializer.create("MsiInstallerStrings_de.wxl", "de") WixFileInitializer.create("MsiInstallerStrings_de.wxl", "de")
}, "en-us", null, null, null, false} }, new String[] {"en-us"}, null, null, null, false}
}); });
} }
private static Stream<String> getLightCommandLine( private static Stream<String> getBuildCommandLine(Executor.Result result) {
Executor.Result result) { return result.getOutput().stream().filter(createToolCommandLinePredicate("light").or(
return result.getOutput().stream().filter(s -> { createToolCommandLinePredicate("wix")));
}
private static boolean isWix3(Executor.Result result) {
return result.getOutput().stream().anyMatch(createToolCommandLinePredicate("light"));
}
private final static Predicate<String> createToolCommandLinePredicate(String wixToolName) {
var toolFileName = wixToolName + ".exe";
return (s) -> {
s = s.trim(); s = s.trim();
return s.startsWith("light.exe") || ((s.contains("\\light.exe ") return s.startsWith(toolFileName) || ((s.contains(String.format("\\%s ", toolFileName)) && s.
&& s.contains(" -out "))); contains(" -out ")));
}); };
} }
private static List<TKit.TextStreamVerifier> createDefaultL10nFilesLocVerifiers(Path tempDir) { private static List<TKit.TextStreamVerifier> createDefaultL10nFilesLocVerifiers(Path tempDir) {
@ -148,14 +158,23 @@ public class WinL10nTest {
// 2. Instruct test to save jpackage output. // 2. Instruct test to save jpackage output.
cmd.setFakeRuntime().saveConsoleOutput(true); cmd.setFakeRuntime().saveConsoleOutput(true);
boolean withJavaOptions = false;
// Set JVM default locale that is used to select primary l10n file. // Set JVM default locale that is used to select primary l10n file.
if (userLanguage != null) { if (userLanguage != null) {
withJavaOptions = true;
cmd.addArguments("-J-Duser.language=" + userLanguage); cmd.addArguments("-J-Duser.language=" + userLanguage);
} }
if (userCountry != null) { if (userCountry != null) {
withJavaOptions = true;
cmd.addArguments("-J-Duser.country=" + userCountry); cmd.addArguments("-J-Duser.country=" + userCountry);
} }
if (withJavaOptions) {
// Use jpackage as a command to allow "-J" options come through
cmd.useToolProvider(false);
}
// Cultures handling is affected by the WiX extensions used. // Cultures handling is affected by the WiX extensions used.
// By default only WixUtilExtension is used, this flag // By default only WixUtilExtension is used, this flag
// additionally enables WixUIExtension. // additionally enables WixUIExtension.
@ -169,9 +188,16 @@ public class WinL10nTest {
cmd.addArguments("--temp", tempDir.toString()); cmd.addArguments("--temp", tempDir.toString());
}) })
.addBundleVerifier((cmd, result) -> { .addBundleVerifier((cmd, result) -> {
if (expectedCulture != null) { if (expectedCultures != null) {
TKit.assertTextStream("-cultures:" + expectedCulture).apply( String expected;
getLightCommandLine(result)); if (isWix3(result)) {
expected = "-cultures:" + String.join(";", expectedCultures);
} else {
expected = Stream.of(expectedCultures).map(culture -> {
return String.join(" ", "-culture", culture);
}).collect(Collectors.joining(" "));
}
TKit.assertTextStream(expected).apply(getBuildCommandLine(result));
} }
if (expectedErrorMessage != null) { if (expectedErrorMessage != null) {
@ -180,24 +206,24 @@ public class WinL10nTest {
} }
if (wxlFileInitializers != null) { if (wxlFileInitializers != null) {
var wixSrcDir = Path.of(cmd.getArgumentValue("--temp")).resolve("config");
if (allWxlFilesValid) { if (allWxlFilesValid) {
for (var v : wxlFileInitializers) { for (var v : wxlFileInitializers) {
if (!v.name.startsWith("MsiInstallerStrings_")) { if (!v.name.startsWith("MsiInstallerStrings_")) {
v.createCmdOutputVerifier(resourceDir).apply( v.createCmdOutputVerifier(wixSrcDir).apply(getBuildCommandLine(result));
getLightCommandLine(result));
} }
} }
Path tempDir = getTempDirectory(cmd, tempRoot).toAbsolutePath(); Path tempDir = getTempDirectory(cmd, tempRoot).toAbsolutePath();
for (var v : createDefaultL10nFilesLocVerifiers(tempDir)) { for (var v : createDefaultL10nFilesLocVerifiers(tempDir)) {
v.apply(getLightCommandLine(result)); v.apply(getBuildCommandLine(result));
} }
} else { } else {
Stream.of(wxlFileInitializers) Stream.of(wxlFileInitializers)
.filter(Predicate.not(WixFileInitializer::isValid)) .filter(Predicate.not(WixFileInitializer::isValid))
.forEach(v -> v.createCmdOutputVerifier( .forEach(v -> v.createCmdOutputVerifier(
resourceDir).apply(result.getOutput().stream())); wixSrcDir).apply(result.getOutput().stream()));
TKit.assertFalse( TKit.assertFalse(getBuildCommandLine(result).findAny().isPresent(),
getLightCommandLine(result).findAny().isPresent(),
"Check light.exe was not invoked"); "Check light.exe was not invoked");
} }
} }
@ -223,7 +249,7 @@ public class WinL10nTest {
} }
final private WixFileInitializer[] wxlFileInitializers; final private WixFileInitializer[] wxlFileInitializers;
final private String expectedCulture; final private String[] expectedCultures;
final private String expectedErrorMessage; final private String expectedErrorMessage;
final private String userLanguage; final private String userLanguage;
final private String userCountry; final private String userCountry;