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

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.
*
* 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.VENDOR;
import static jdk.jpackage.internal.StandardBundlerParam.VERSION;
import jdk.jpackage.internal.WixToolset.WixToolsetType;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
@ -253,7 +254,7 @@ public class WinMsiBundler extends AbstractBundler {
public boolean supported(boolean platformInstaller) {
try {
if (wixToolset == null) {
wixToolset = WixTool.toolset();
wixToolset = WixTool.createToolset();
}
return true;
} catch (ConfigException ce) {
@ -300,7 +301,7 @@ public class WinMsiBundler extends AbstractBundler {
appImageBundler.validate(params);
if (wixToolset == null) {
wixToolset = WixTool.toolset();
wixToolset = WixTool.createToolset();
}
try {
@ -309,16 +310,17 @@ public class WinMsiBundler extends AbstractBundler {
throw new ConfigException(ex);
}
for (var toolInfo: wixToolset.values()) {
for (var tool : wixToolset.getType().getTools()) {
Log.verbose(MessageFormat.format(I18N.getString(
"message.tool-version"), toolInfo.path.getFileName(),
toolInfo.version));
"message.tool-version"), wixToolset.getToolPath(tool).
getFileName(), wixToolset.getVersion()));
}
wixFragments.forEach(wixFragment -> wixFragment.setWixVersion(
wixToolset.get(WixTool.Light).version));
wixFragments.forEach(wixFragment -> wixFragment.setWixVersion(wixToolset.getVersion(),
wixToolset.getType()));
wixFragments.get(0).logWixFeatures();
wixFragments.stream().map(WixFragmentBuilder::getLoggableWixFeatures).flatMap(
List::stream).distinct().toList().forEach(Log::verbose);
/********* validate bundle parameters *************/
@ -512,22 +514,6 @@ public class WinMsiBundler extends AbstractBundler {
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;
}
@ -542,13 +528,11 @@ public class WinMsiBundler extends AbstractBundler {
.toString()));
WixPipeline wixPipeline = new WixPipeline()
.setToolset(wixToolset.entrySet().stream().collect(
Collectors.toMap(
entry -> entry.getKey(),
entry -> entry.getValue().path)))
.setWixObjDir(TEMP_ROOT.fetchFrom(params).resolve("wixobj"))
.setWorkDir(WIN_APP_IMAGE.fetchFrom(params))
.addSource(CONFIG_ROOT.fetchFrom(params).resolve("main.wxs"), wixVars);
.setToolset(wixToolset)
.setWixObjDir(TEMP_ROOT.fetchFrom(params).resolve("wixobj"))
.setWorkDir(WIN_APP_IMAGE.fetchFrom(params))
.addSource(CONFIG_ROOT.fetchFrom(params).resolve("main.wxs"),
wixVars);
for (var wixFragment : wixFragments) {
wixFragment.configureWixPipeline(wixPipeline);
@ -557,16 +541,46 @@ public class WinMsiBundler extends AbstractBundler {
Log.verbose(MessageFormat.format(I18N.getString(
"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)) {
wixPipeline.addLightOptions("-sice:ICE91");
if (!MSI_SYSTEM_WIDE.fetchFrom(params)) {
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
// override primary l10n files. Ignore case filename comparison,
// both lists are expected to be short.
List<Path> primaryWxlFiles = getWxlFilesFromDir(params, CONFIG_ROOT);
List<Path> customWxlFiles = getWxlFilesFromDir(params, RESOURCE_DIR).stream()
.filter(custom -> primaryWxlFiles.stream().noneMatch(primary ->
primary.getFileName().toString().equalsIgnoreCase(
@ -577,6 +591,17 @@ public class WinMsiBundler extends AbstractBundler {
custom.getFileName().toString())))
.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
// Cultures from custom files and a single primary Culture are
// included into "-cultures" list
@ -586,6 +611,7 @@ public class WinMsiBundler extends AbstractBundler {
List<String> cultures = new ArrayList<>();
for (var wxl : customWxlFiles) {
wxl = configDir.resolve(wxl.getFileName());
wixPipeline.addLightOptions("-loc", wxl.toAbsolutePath().normalize().toString());
cultures.add(getCultureFromWxlFile(wxl));
}
@ -598,8 +624,20 @@ public class WinMsiBundler extends AbstractBundler {
// Build ordered list of unique cultures.
Set<String> uniqueCultures = new LinkedHashSet<>();
uniqueCultures.addAll(cultures);
wixPipeline.addLightOptions(uniqueCultures.stream().collect(
Collectors.joining(";", "-cultures:", "")));
switch (wixToolset.getType()) {
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());
@ -751,7 +789,7 @@ public class WinMsiBundler extends AbstractBundler {
}
private Path installerIcon;
private Map<WixTool, WixTool.ToolInfo> wixToolset;
private WixToolset wixToolset;
private AppImageBundler appImageBundler;
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.SERVICE_INSTALLER;
import static jdk.jpackage.internal.WinMsiBundler.WIN_APP_IMAGE;
import jdk.jpackage.internal.WixToolset.WixToolsetType;
import org.w3c.dom.NodeList;
/**
@ -152,6 +153,16 @@ class WixAppImageFragmentBuilder extends WixFragmentBuilder {
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
protected Collection<XmlConsumer> getFragmentWriters() {
return List.of(
@ -314,12 +325,25 @@ class WixAppImageFragmentBuilder extends WixFragmentBuilder {
return cfg.isFile;
}
static void startElement(XMLStreamWriter xml, String componentId,
static void startElement(WixToolsetType wixType, XMLStreamWriter xml, String componentId,
String componentGuid) throws XMLStreamException, IOException {
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("Guid", componentGuid);
}
private static final class Config {
@ -370,22 +394,31 @@ class WixAppImageFragmentBuilder extends WixFragmentBuilder {
directoryRefPath = path;
}
xml.writeStartElement("DirectoryRef");
xml.writeAttribute("Id", Id.Folder.of(directoryRefPath));
startDirectoryElement(xml, "DirectoryRef", directoryRefPath);
final String componentId = "c" + role.idOf(path);
Component.startElement(xml, componentId, String.format("{%s}",
role.guidOf(path)));
Component.startElement(getWixType(), xml, componentId, String.format(
"{%s}", role.guidOf(path)));
if (role == Component.Shortcut) {
xml.writeStartElement("Condition");
String property = shortcutFolders.stream().filter(shortcutFolder -> {
return path.startsWith(shortcutFolder.root);
}).map(shortcutFolder -> {
return shortcutFolder.property;
}).findFirst().get();
xml.writeCharacters(property);
xml.writeEndElement();
switch (getWixType()) {
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();
@ -442,7 +475,7 @@ class WixAppImageFragmentBuilder extends WixFragmentBuilder {
private void addShortcutComponentGroup(XMLStreamWriter xml) throws
XMLStreamException, IOException {
List<String> componentIds = new ArrayList<>();
Set<ShortcutsFolder> defineShortcutFolders = new HashSet<>();
Set<Path> defineShortcutFolders = new HashSet<>();
for (var launcher : launchers) {
for (var folder : shortcutFolders) {
Path launcherPath = addExeSuffixToPath(installedAppImage
@ -457,16 +490,27 @@ class WixAppImageFragmentBuilder extends WixFragmentBuilder {
folder);
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);
}
}
}
}
for (var folder : defineShortcutFolders) {
Path path = folder.getPath(this);
componentIds.addAll(addRootBranch(xml, path));
for (var folderPath : defineShortcutFolders) {
componentIds.addAll(addRootBranch(xml, folderPath));
}
addComponentGroup(xml, "Shortcuts", componentIds);
@ -546,13 +590,18 @@ class WixAppImageFragmentBuilder extends WixFragmentBuilder {
throw throwInvalidPathException(path);
}
Function<Path, String> createDirectoryName = dir -> null;
boolean sysDir = true;
int levels = 1;
int levels;
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);
while (dirIt.hasNext()) {
@ -562,21 +611,11 @@ class WixAppImageFragmentBuilder extends WixFragmentBuilder {
if (sysDir && !SYSTEM_DIRS.contains(path)) {
sysDir = false;
createDirectoryName = dir -> dir.getFileName().toString();
}
final String directoryId;
if (!sysDir && path.equals(installDir)) {
directoryId = INSTALLDIR.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);
startDirectoryElement(xml, "Directory", path);
if (!sysDir) {
xml.writeAttribute("Name", path.getFileName().toString());
}
}
@ -584,9 +623,37 @@ class WixAppImageFragmentBuilder extends WixFragmentBuilder {
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)
@ -785,7 +852,7 @@ class WixAppImageFragmentBuilder extends WixFragmentBuilder {
xml.writeStartElement("RegistryKey");
xml.writeAttribute("Root", regRoot);
xml.writeAttribute("Key", registryKeyPath);
if (DottedVersion.compareComponents(getWixVersion(), DottedVersion.lazy("3.6")) < 0) {
if (!isWithWix36Features()) {
xml.writeAttribute("Action", "createAndRemoveOnUninstall");
}
xml.writeStartElement("RegistryValue");
@ -799,7 +866,7 @@ class WixAppImageFragmentBuilder extends WixFragmentBuilder {
private String addDirectoryCleaner(XMLStreamWriter xml, Path path) throws
XMLStreamException, IOException {
if (DottedVersion.compareComponents(getWixVersion(), DottedVersion.lazy("3.6")) < 0) {
if (!isWithWix36Features()) {
return null;
}
@ -821,14 +888,13 @@ class WixAppImageFragmentBuilder extends WixFragmentBuilder {
xml.writeStartElement("DirectoryRef");
xml.writeAttribute("Id", INSTALLDIR.toString());
Component.startElement(xml, componentId, "*");
Component.startElement(getWixType(), xml, componentId, "*");
addRegistryKeyPath(xml, INSTALLDIR, () -> propertyId, () -> {
return toWixPath(path);
});
xml.writeStartElement(
"http://schemas.microsoft.com/wix/UtilExtension",
xml.writeStartElement(getWixNamespaces().get(WixNamespace.Util),
"RemoveFolderEx");
xml.writeAttribute("On", "uninstall");
xml.writeAttribute("Property", propertyId);
@ -839,6 +905,10 @@ class WixAppImageFragmentBuilder extends WixFragmentBuilder {
return componentId;
}
private boolean isWithWix36Features() {
return DottedVersion.compareComponents(getWixVersion(), DottedVersion.greedy("3.6")) >= 0;
}
// Does the following conversions:
// INSTALLDIR -> [INSTALLDIR]
// 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.Proxy;
import java.nio.file.Path;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.stream.XMLStreamWriter;
import jdk.jpackage.internal.IOUtils.XmlConsumer;
import jdk.jpackage.internal.OverridableResource.Source;
import static jdk.jpackage.internal.OverridableResource.createResource;
import static jdk.jpackage.internal.StandardBundlerParam.CONFIG_ROOT;
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.
*/
abstract class WixFragmentBuilder {
void setWixVersion(DottedVersion v) {
wixVersion = v;
final void setWixVersion(DottedVersion version, WixToolsetType type) {
Objects.requireNonNull(version);
Objects.requireNonNull(type);
wixVersion = version;
wixType = type;
}
void setOutputFileName(String v) {
final void setOutputFileName(String v) {
outputFileName = v;
}
@ -65,11 +69,8 @@ abstract class WixFragmentBuilder {
Source.ResourceDir);
}
void logWixFeatures() {
if (DottedVersion.compareComponents(wixVersion, DottedVersion.lazy("3.6")) >= 0) {
Log.verbose(MessageFormat.format(I18N.getString(
"message.use-wix36-features"), wixVersion));
}
List<String> getLoggableWixFeatures() {
return List.of();
}
void configureWixPipeline(WixPipeline wixPipeline) {
@ -91,52 +92,84 @@ abstract class WixFragmentBuilder {
}
if (additionalResources != null) {
for (var resource : additionalResources) {
resource.resource.saveToFile(configRoot.resolve(
resource.saveAsName));
}
additionalResources.saveResources();
}
}
DottedVersion getWixVersion() {
final WixToolsetType getWixType() {
return wixType;
}
final DottedVersion getWixVersion() {
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() {
return Architecture.is64bit();
}
protected Path getConfigRoot() {
final protected Path getConfigRoot() {
return configRoot;
}
protected abstract Collection<XmlConsumer> getFragmentWriters();
protected void defineWixVariable(String variableName) {
final protected void defineWixVariable(String variableName) {
setWixVariable(variableName, "yes");
}
protected void setWixVariable(String variableName, String variableValue) {
final protected void setWixVariable(String variableName, String variableValue) {
if (wixVariables == null) {
wixVariables = new WixVariables();
}
wixVariables.setWixVariable(variableName, variableValue);
}
protected void addResource(OverridableResource resource, String saveAsName) {
final protected void addResource(OverridableResource resource, String saveAsName) {
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)
throws IOException {
private void createWixSource(Path file, XmlConsumer xmlConsumer) throws IOException {
IOUtils.createXml(file, xml -> {
xml.writeStartElement("Wix");
xml.writeDefaultNamespace("http://schemas.microsoft.com/wix/2006/wi");
xml.writeNamespace("util",
"http://schemas.microsoft.com/wix/UtilExtension");
for (var ns : getWixNamespaces().entrySet()) {
switch (ns.getKey()) {
case Default ->
xml.writeDefaultNamespace(ns.getValue());
default ->
xml.writeNamespace(ns.getKey().name().toLowerCase(), ns.
getValue());
}
}
xmlConsumer.accept((XMLStreamWriter) Proxy.newProxyInstance(
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 {
WixPreprocessorEscaper(XMLStreamWriter target) {
@ -208,9 +231,10 @@ abstract class WixFragmentBuilder {
private final XMLStreamWriter target;
}
private WixToolsetType wixType;
private DottedVersion wixVersion;
private WixVariables wixVariables;
private List<ResourceWithName> additionalResources;
private ResourceGroup additionalResources;
private OverridableResource fragmentResource;
private String outputFileName;
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.
*
* 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
* questions.
*/
package jdk.jpackage.internal;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Collections;
import java.util.List;
import java.util.Map;
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;
/**
@ -45,7 +46,7 @@ public class WixPipeline {
lightOptions = new ArrayList<>();
}
WixPipeline setToolset(Map<WixTool, Path> v) {
WixPipeline setToolset(WixToolset v) {
toolset = v;
return this;
}
@ -79,13 +80,92 @@ public class WixPipeline {
}
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<>();
for (var source : sources) {
wixObjs.add(compile(source));
wixObjs.add(compileWix3(source));
}
List<String> lightCmdline = new ArrayList<>(List.of(
toolset.get(WixTool.Light).toString(),
toolset.getToolPath(WixTool.Light3).toString(),
"-nologo",
"-spdb",
"-ext", "WixUtilExtension",
@ -99,31 +179,20 @@ public class WixPipeline {
execute(lightCmdline);
}
private Path compile(WixSource wixSource) throws IOException {
UnaryOperator<Path> adjustPath = path -> {
return workDir != null ? path.toAbsolutePath() : path;
};
Path wixObj = adjustPath.apply(wixObjDir).resolve(IOUtils.replaceSuffix(
private Path compileWix3(WixSource wixSource) throws IOException {
Path wixObj = wixObjDir.toAbsolutePath().resolve(IOUtils.replaceSuffix(
IOUtils.getFileName(wixSource.source), ".wixobj"));
List<String> cmdline = new ArrayList<>(List.of(
toolset.get(WixTool.Candle).toString(),
toolset.getToolPath(WixTool.Candle3).toString(),
"-nologo",
adjustPath.apply(wixSource.source).toString(),
wixSource.source.toAbsolutePath().toString(),
"-ext", "WixUtilExtension",
"-arch", WixFragmentBuilder.is64Bit() ? "x64" : "x86",
"-out", wixObj.toAbsolutePath().toString()
));
Map<String, String> appliedVaribales = new HashMap<>();
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);
addWixVariblesToCommandLine(wixSource.variables, cmdline);
execute(cmdline);
@ -131,8 +200,8 @@ public class WixPipeline {
}
private void execute(List<String> cmdline) throws IOException {
Executor.of(new ProcessBuilder(cmdline).directory(
workDir != null ? workDir.toFile() : null)).executeExpectSuccess();
Executor.of(new ProcessBuilder(cmdline).directory(workDir.toFile())).
executeExpectSuccess();
}
private static final class WixSource {
@ -140,7 +209,7 @@ public class WixPipeline {
Map<String, String> variables;
}
private Map<WixTool, Path> toolset;
private WixToolset toolset;
private Map<String, String> wixVariables;
private List<String> lightOptions;
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.
*
* 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
* questions.
*/
package jdk.jpackage.internal;
import java.io.IOException;
@ -32,105 +31,198 @@ import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.text.MessageFormat;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Supplier;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import jdk.jpackage.internal.WixToolset.WixToolsetType;
/**
* WiX tool.
*/
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 {
ToolInfo(Path path, String version) {
this.path = path;
this.version = new DottedVersion(version);
this.version = DottedVersion.lazy(version);
}
final Path path;
final DottedVersion version;
}
static Map<WixTool, ToolInfo> toolset() throws ConfigException {
Map<WixTool, ToolInfo> toolset = new HashMap<>();
for (var tool : values()) {
toolset.put(tool, tool.find());
}
return toolset;
}
ToolInfo find() throws ConfigException {
final Path toolFileName = IOUtils.addSuffix(
Path.of(name().toLowerCase()), ".exe");
String[] version = new String[1];
ConfigException reason = createToolValidator(toolFileName, version).get();
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;
static WixToolset createToolset() throws ConfigException {
Function<List<ToolLookupResult>, Map<WixTool, ToolInfo>> conv = lookupResults -> {
return lookupResults.stream().filter(ToolLookupResult::isValid).collect(Collectors.
groupingBy(lookupResult -> {
return lookupResult.getInfo().version.toString();
})).values().stream().filter(sameVersionLookupResults -> {
Set<WixTool> sameVersionTools = sameVersionLookupResults.stream().map(
ToolLookupResult::getTool).collect(Collectors.toSet());
if (sameVersionTools.equals(Set.of(Candle3)) || sameVersionTools.equals(Set.of(
Light3))) {
// There is only one tool from WiX v3 toolset of some version available. Discard it.
return false;
} else {
return true;
}
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,
String[] versionCtnr) {
return new ToolValidator(toolPath)
.setCommandLine("/?")
.setMinimalVersion(MINIMAL_VERSION)
.setToolNotFoundErrorHandler(
(name, ex) -> new ConfigException(
I18N.getString("error.no-wix-tools"),
I18N.getString("error.no-wix-tools.advice")))
.setToolOldVersionErrorHandler(
(name, version) -> new ConfigException(
MessageFormat.format(I18N.getString(
"message.wrong-tool-version"), name,
version, MINIMAL_VERSION),
I18N.getString("error.no-wix-tools.advice")))
.setVersionParser(output -> {
versionCtnr[0] = "";
private static class ToolLookupResult {
ToolLookupResult(WixTool tool, Path lookupDir) {
final Path toolPath = Optional.ofNullable(lookupDir).map(p -> p.resolve(
tool.toolFileName)).orElse(tool.toolFileName);
final boolean[] tooOld = new boolean[1];
final String[] parsedVersion = new String[1];
final var validator = new ToolValidator(toolPath).setMinimalVersion(tool.minimalVersion).
setToolNotFoundErrorHandler((name, ex) -> {
return new ConfigException("", "");
}).setToolOldVersionErrorHandler((name, version) -> {
tooOld[0] = true;
return null;
});
final Function<Stream<String>, String> versionParser;
if (Set.of(Candle3, Light3).contains(tool)) {
validator.setCommandLine("/?");
versionParser = output -> {
String firstLineOfOutput = output.findFirst().orElse("");
int separatorIdx = firstLineOfOutput.lastIndexOf(' ');
if (separatorIdx == -1) {
return null;
}
versionCtnr[0] = firstLineOfOutput.substring(separatorIdx + 1);
return versionCtnr[0];
})::validate;
return firstLineOfOutput.substring(separatorIdx + 1);
};
} 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");
static Path getSystemDir(String envVar, String knownDir) {
private static Path getSystemDir(String envVar, String knownDir) {
return Optional
.ofNullable(getEnvVariableAsPath(envVar))
.orElseGet(() -> Optional
.ofNullable(getEnvVariableAsPath("SystemDrive"))
.orElseGet(() -> Path.of("C:")).resolve(knownDir));
.ofNullable(getEnvVariableAsPath("SystemDrive"))
.orElseGet(() -> Path.of("C:")).resolve(knownDir));
}
private static Path getEnvVariableAsPath(String envVar) {
@ -147,8 +239,22 @@ public enum WixTool {
}
private static List<Path> findWixInstallDirs() {
PathMatcher wixInstallDirMatcher = FileSystems.getDefault().getPathMatcher(
"glob:WiX Toolset v*");
return Stream.of(findWixCurrentInstallDirs(), findWix3InstallDirs()).
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 programFilesX86 = getSystemDir("ProgramFiles(x86)",
@ -157,18 +263,20 @@ public enum WixTool {
// Returns list of WiX install directories ordered by WiX version number.
// Newer versions go first.
return Stream.of(programFiles, programFilesX86).map(path -> {
List<Path> result;
try (var paths = Files.walk(path, 1)) {
result = paths.toList();
return paths.toList();
} catch (IOException ex) {
Log.verbose(ex);
result = Collections.emptyList();
List<Path> empty = List.of();
return empty;
}
return result;
}).flatMap(List::stream)
.filter(path -> wixInstallDirMatcher.matches(path.getFileName()))
.sorted(Comparator.comparing(Path::getFileName).reversed())
.map(path -> path.resolve("bin"))
.toList();
.filter(path -> wixInstallDirMatcher.matches(path.getFileName())).
sorted(Comparator.comparing(Path::getFileName).reversed())
.map(path -> path.resolve("bin"))
.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.StandardBundlerParam.LICENSE_FILE;
import jdk.jpackage.internal.WixAppImageFragmentBuilder.ShortcutsFolder;
import jdk.jpackage.internal.WixToolset.WixToolsetType;
/**
* Creates UI WiX fragment.
@ -100,7 +101,13 @@ final class WixUiFragmentBuilder extends WixFragmentBuilder {
super.configureWixPipeline(wixPipeline);
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
@ -148,15 +155,14 @@ final class WixUiFragmentBuilder extends WixFragmentBuilder {
xml.writeEndElement(); // WixVariable
}
xml.writeStartElement("UI");
xml.writeAttribute("Id", "JpUI");
var ui = getUI();
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() {
@ -187,12 +193,43 @@ final class WixUiFragmentBuilder extends WixFragmentBuilder {
this.dialogPairsSupplier = dialogPairsSupplier;
}
void write(WixUiFragmentBuilder outer, XMLStreamWriter xml) throws
XMLStreamException, IOException {
xml.writeStartElement("UIRef");
xml.writeAttribute("Id", wixUIRef);
xml.writeEndElement(); // UIRef
void write(WixToolsetType wixType, WixUiFragmentBuilder outer, XMLStreamWriter xml) throws XMLStreamException, IOException {
switch (wixType) {
case Wix3 -> {}
case Wix4 -> {
// https://wixtoolset.org/docs/fourthree/faqs/#converting-custom-wixui-dialog-sets
xml.writeProcessingInstruction("foreach WIXUIARCH in X86;X64;A64");
writeWix4UIRef(xml, wixUIRef, "JpUIInternal_$(WIXUIARCH)");
xml.writeProcessingInstruction("endforeach");
writeWix4UIRef(xml, "JpUIInternal", "JpUI");
}
default -> {
throw new IllegalArgumentException();
}
}
xml.writeStartElement("UI");
switch (wixType) {
case Wix3 -> {
xml.writeAttribute("Id", "JpUI");
xml.writeStartElement("UIRef");
xml.writeAttribute("Id", wixUIRef);
xml.writeEndElement(); // UIRef
}
case Wix4 -> {
xml.writeAttribute("Id", "JpUIInternal");
}
default -> {
throw new IllegalArgumentException();
}
}
writeContents(wixType, outer, xml);
xml.writeEndElement(); // UI
}
private void writeContents(WixToolsetType wixType, WixUiFragmentBuilder outer,
XMLStreamWriter xml) throws XMLStreamException, IOException {
if (dialogIdsSupplier != null) {
List<Dialog> dialogIds = dialogIdsSupplier.apply(outer);
Map<DialogPair, List<Publish>> dialogPairs = dialogPairsSupplier.get();
@ -210,7 +247,7 @@ final class WixUiFragmentBuilder extends WixFragmentBuilder {
DialogPair pair = new DialogPair(firstId, secondId);
for (var curPair : List.of(pair, pair.flip())) {
for (var publish : dialogPairs.get(curPair)) {
writePublishDialogPair(xml, publish, curPair);
writePublishDialogPair(wixType, xml, publish, curPair);
}
}
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 Function<WixUiFragmentBuilder, List<Dialog>> dialogIdsSupplier;
private final Supplier<Map<DialogPair, List<Publish>>> dialogPairsSupplier;
@ -441,9 +489,8 @@ final class WixUiFragmentBuilder extends WixFragmentBuilder {
return new PublishBuilder(publish);
}
private static void writePublishDialogPair(XMLStreamWriter xml,
Publish publish, DialogPair dialogPair) throws IOException,
XMLStreamException {
private static void writePublishDialogPair(WixToolsetType wixType, XMLStreamWriter xml,
Publish publish, DialogPair dialogPair) throws IOException, XMLStreamException {
xml.writeStartElement("Publish");
xml.writeAttribute("Dialog", dialogPair.firstId);
xml.writeAttribute("Control", publish.control);
@ -452,7 +499,11 @@ final class WixUiFragmentBuilder extends WixFragmentBuilder {
if (publish.order != 0) {
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();
}
@ -463,9 +514,8 @@ final class WixUiFragmentBuilder extends WixFragmentBuilder {
this.wxsFileName = wxsFileName;
this.wixVariables = new WixVariables();
addResource(
createResource(wxsFileName, params).setCategory(category),
wxsFileName);
addResource(createResource(wxsFileName, params).setCategory(category).setPublicName(
wxsFileName), wxsFileName);
}
void addToWixPipeline(WixPipeline wixPipeline) {

View File

@ -1,7 +1,7 @@
<?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.
*
* 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)">
<Publish Event="NewDialog" Value="InstallDirDlg">1</Publish>
</Control>
<Control Id="Text" Type="Text" X="25" Y="15" Width="250" Height="30" TabSkip="no">
<Text>!(loc.message.install.dir.exist)</Text>
</Control>
<Control Id="Text" Type="Text" X="25" Y="15" Width="250" Height="30" TabSkip="no" Text="!(loc.InstallDirNotEmptyDlgInstallDirExistMessage)"/>
</Dialog>
<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'?>
<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="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>
@ -11,6 +10,9 @@
<String Id="ShortcutPromptDlgDescription">Wählen Sie die zu erstellenden Verknüpfungen aus.</String>
<String Id="ShortcutPromptDlgDesktopShortcutControlLabel">Desktopverknüpfung(en) erstellen</String>
<String Id="ShortcutPromptDlgStartMenuShortcutControlLabel">Startmenüverknüpfung(en) erstellen</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>
</WixLocalization>

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<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="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>
@ -11,6 +10,9 @@
<String Id="ShortcutPromptDlgDescription">Select shortcuts to create.</String>
<String Id="ShortcutPromptDlgDesktopShortcutControlLabel">Create desktop shortcut(s)</String>
<String Id="ShortcutPromptDlgStartMenuShortcutControlLabel">Create start menu shortcut(s)</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>
</WixLocalization>

View File

@ -1,6 +1,5 @@
<?xml version = '1.0' encoding = 'utf-8'?>
<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="DowngradeErrorMessage">[ProductName]のより上位のバージョンがすでにインストールされています。ダウングレードは無効です。セットアップを終了します。</String>
<String Id="DisallowUpgradeErrorMessage">[ProductName]のより下位のバージョンがすでにインストールされています。アップグレードは無効です。セットアップを終了します。</String>
@ -11,6 +10,9 @@
<String Id="ShortcutPromptDlgDescription">作成するショートカットを選択します。</String>
<String Id="ShortcutPromptDlgDesktopShortcutControlLabel">デスクトップ・ショートカットの作成</String>
<String Id="ShortcutPromptDlgStartMenuShortcutControlLabel">スタート・メニューのショートカットの作成</String>
<String Id="InstallDirNotEmptyDlg_Title">[ProductName]セットアップ</String>
<String Id="InstallDirNotEmptyDlgInstallDirExistMessage">フォルダ[INSTALLDIR]はすでに存在します。そのフォルダにインストールしますか?</String>
<String Id="ContextMenuCommandLabel">[ProductName]で開く</String>
</WixLocalization>

View File

@ -1,6 +1,5 @@
<?xml version = '1.0' encoding = 'utf-8'?>
<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="DowngradeErrorMessage">已安装更高版本的 [ProductName]。降级已禁用。现在将退出安装。</String>
<String Id="DisallowUpgradeErrorMessage">已安装更低版本的 [ProductName]。升级已禁用。现在将退出安装。</String>
@ -11,6 +10,9 @@
<String Id="ShortcutPromptDlgDescription">选择要创建的快捷方式。</String>
<String Id="ShortcutPromptDlgDesktopShortcutControlLabel">创建桌面快捷方式</String>
<String Id="ShortcutPromptDlgStartMenuShortcutControlLabel">创建开始菜单快捷方式</String>
<String Id="InstallDirNotEmptyDlg_Title">[ProductName] 安装程序</String>
<String Id="InstallDirNotEmptyDlgInstallDirExistMessage">文件夹 [INSTALLDIR] 已存在。是否仍要安装到该文件夹?</String>
<String Id="ContextMenuCommandLabel">使用 [ProductName] 打开</String>
</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.
#
# 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.installdirnotemptydlg-wix-file=Not empty install directory dialog 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.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.

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.
#
# 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.installdirnotemptydlg-wix-file=Nicht leeres Installationsverzeichnis in Dialogfeld für WiX-Projektdatei
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.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.

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.
#
# 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.installdirnotemptydlg-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.version-string-wrong-format.advice=--app-versionパラメータの値を有効なWindows Installer ProductVersionに設定します。
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.
#
# 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.installdirnotemptydlg-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.version-string-wrong-format.advice=将 --app-version 参数的值设置为有效的 Windows Installer ProductVersion。
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.
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.
*
* This code is free software; you can redistribute it and/or modify it
@ -139,6 +139,30 @@ public class WindowsHelper {
String.format("TARGETDIR=\"%s\"",
unpackDir.toAbsolutePath().normalize())))));
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 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.
*
* 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.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import jdk.jpackage.test.Executor;
@ -55,11 +56,11 @@ import static jdk.jpackage.test.WindowsHelper.getTempDirectory;
public class WinL10nTest {
public WinL10nTest(WixFileInitializer wxlFileInitializers[],
String expectedCulture, String expectedErrorMessage,
String[] expectedCultures, String expectedErrorMessage,
String userLanguage, String userCountry,
boolean enableWixUIExtension) {
this.wxlFileInitializers = wxlFileInitializers;
this.expectedCulture = expectedCulture;
this.expectedCultures = expectedCultures;
this.expectedErrorMessage = expectedErrorMessage;
this.userLanguage = userLanguage;
this.userCountry = userCountry;
@ -69,56 +70,65 @@ public class WinL10nTest {
@Parameters
public static List<Object[]> data() {
return List.of(new Object[][]{
{null, "en-us", null, null, null, false},
{null, "en-us", null, "en", "US", false},
{null, "en-us", null, "en", "US", true},
{null, "de-de", null, "de", "DE", false},
{null, "de-de", null, "de", "DE", true},
{null, "ja-jp", null, "ja", "JP", false},
{null, "ja-jp", null, "ja", "JP", true},
{null, "zh-cn", null, "zh", "CN", false},
{null, "zh-cn", null, "zh", "CN", true},
{null, new String[] {"en-us"}, null, null, null, false},
{null, new String[] {"en-us"}, null, "en", "US", false},
{null, new String[] {"en-us"}, null, "en", "US", true},
{null, new String[] {"de-de"}, null, "de", "DE", false},
{null, new String[] {"de-de"}, null, "de", "DE", true},
{null, new String[] {"ja-jp"}, null, "ja", "JP", false},
{null, new String[] {"ja-jp"}, null, "ja", "JP", true},
{null, new String[] {"zh-cn"}, null, "zh", "CN", false},
{null, new String[] {"zh-cn"}, null, "zh", "CN", true},
{new WixFileInitializer[] {
WixFileInitializer.create("a.wxl", "en-us")
}, "en-us", null, null, null, false},
}, new String[] {"en-us"}, null, null, null, false},
{new WixFileInitializer[] {
WixFileInitializer.create("a.wxl", "fr")
}, "fr;en-us", null, null, null, false},
}, new String[] {"fr", "en-us"}, null, null, null, false},
{new WixFileInitializer[] {
WixFileInitializer.create("a.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[] {
WixFileInitializer.create("a.wxl", "it"),
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[] {
WixFileInitializer.create("c.wxl", "it"),
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[] {
WixFileInitializer.create("a.wxl", "fr"),
WixFileInitializer.create("b.wxl", "it"),
WixFileInitializer.create("c.wxl", "fr"),
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[] {
WixFileInitializer.create("c.wxl", "it"),
WixFileInitializer.createMalformed("b.wxl")
}, null, null, null, null, false},
{new WixFileInitializer[] {
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(
Executor.Result result) {
return result.getOutput().stream().filter(s -> {
private static Stream<String> getBuildCommandLine(Executor.Result result) {
return result.getOutput().stream().filter(createToolCommandLinePredicate("light").or(
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();
return s.startsWith("light.exe") || ((s.contains("\\light.exe ")
&& s.contains(" -out ")));
});
return s.startsWith(toolFileName) || ((s.contains(String.format("\\%s ", toolFileName)) && s.
contains(" -out ")));
};
}
private static List<TKit.TextStreamVerifier> createDefaultL10nFilesLocVerifiers(Path tempDir) {
@ -148,14 +158,23 @@ public class WinL10nTest {
// 2. Instruct test to save jpackage output.
cmd.setFakeRuntime().saveConsoleOutput(true);
boolean withJavaOptions = false;
// Set JVM default locale that is used to select primary l10n file.
if (userLanguage != null) {
withJavaOptions = true;
cmd.addArguments("-J-Duser.language=" + userLanguage);
}
if (userCountry != null) {
withJavaOptions = true;
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.
// By default only WixUtilExtension is used, this flag
// additionally enables WixUIExtension.
@ -169,9 +188,16 @@ public class WinL10nTest {
cmd.addArguments("--temp", tempDir.toString());
})
.addBundleVerifier((cmd, result) -> {
if (expectedCulture != null) {
TKit.assertTextStream("-cultures:" + expectedCulture).apply(
getLightCommandLine(result));
if (expectedCultures != null) {
String expected;
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) {
@ -180,24 +206,24 @@ public class WinL10nTest {
}
if (wxlFileInitializers != null) {
var wixSrcDir = Path.of(cmd.getArgumentValue("--temp")).resolve("config");
if (allWxlFilesValid) {
for (var v : wxlFileInitializers) {
if (!v.name.startsWith("MsiInstallerStrings_")) {
v.createCmdOutputVerifier(resourceDir).apply(
getLightCommandLine(result));
v.createCmdOutputVerifier(wixSrcDir).apply(getBuildCommandLine(result));
}
}
Path tempDir = getTempDirectory(cmd, tempRoot).toAbsolutePath();
for (var v : createDefaultL10nFilesLocVerifiers(tempDir)) {
v.apply(getLightCommandLine(result));
v.apply(getBuildCommandLine(result));
}
} else {
Stream.of(wxlFileInitializers)
.filter(Predicate.not(WixFileInitializer::isValid))
.forEach(v -> v.createCmdOutputVerifier(
resourceDir).apply(result.getOutput().stream()));
TKit.assertFalse(
getLightCommandLine(result).findAny().isPresent(),
wixSrcDir).apply(result.getOutput().stream()));
TKit.assertFalse(getBuildCommandLine(result).findAny().isPresent(),
"Check light.exe was not invoked");
}
}
@ -223,7 +249,7 @@ public class WinL10nTest {
}
final private WixFileInitializer[] wxlFileInitializers;
final private String expectedCulture;
final private String[] expectedCultures;
final private String expectedErrorMessage;
final private String userLanguage;
final private String userCountry;