diff --git a/make/modules/jdk.jpackage/Java.gmk b/make/modules/jdk.jpackage/Java.gmk index 9d31e5417e9..d60e9ac2814 100644 --- a/make/modules/jdk.jpackage/Java.gmk +++ b/make/modules/jdk.jpackage/Java.gmk @@ -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 diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinMsiBundler.java b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinMsiBundler.java index 894d41d7642..c0ae65b3b0b 100644 --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinMsiBundler.java +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinMsiBundler.java @@ -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 primaryWxlFiles = getWxlFilesFromDir(params, CONFIG_ROOT); List 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 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 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 wixToolset; + private WixToolset wixToolset; private AppImageBundler appImageBundler; private final List wixFragments; } diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixAppImageFragmentBuilder.java b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixAppImageFragmentBuilder.java index cf7338f7d0b..5bc20c1413c 100644 --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixAppImageFragmentBuilder.java +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixAppImageFragmentBuilder.java @@ -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 getLoggableWixFeatures() { + if (isWithWix36Features()) { + return List.of(MessageFormat.format(I18N.getString("message.use-wix36-features"), + getWixVersion())); + } else { + return List.of(); + } + } + @Override protected Collection 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 componentIds = new ArrayList<>(); - Set defineShortcutFolders = new HashSet<>(); + Set 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 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 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 diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixFragmentBuilder.java b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixFragmentBuilder.java index bc98899c659..0276cc96e65 100644 --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixFragmentBuilder.java +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixFragmentBuilder.java @@ -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 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 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 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 additionalResources; + private ResourceGroup additionalResources; private OverridableResource fragmentResource; private String outputFileName; private Path configRoot; diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixPipeline.java b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixPipeline.java index 58a07b6cbaf..835247ed1de 100644 --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixPipeline.java +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixPipeline.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 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 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 otherWixVariables, List cmdline) { + Stream.of(wixVariables, Optional.ofNullable(otherWixVariables). + orElseGet(Collections::emptyMap)).filter(Objects::nonNull). + reduce((a, b) -> { + a.putAll(b); + return a; + }).ifPresent(wixVars -> { + var entryStream = wixVars.entrySet().stream(); + + Stream stream; + switch (toolset.getType()) { + case Wix3 -> { + stream = entryStream.map(wixVar -> { + return String.format("-d%s=%s", wixVar.getKey(), wixVar. + getValue()); + }); + } + case Wix4 -> { + stream = entryStream.map(wixVar -> { + return Stream.of("-d", String.format("%s=%s", wixVar. + getKey(), wixVar.getValue())); + }).flatMap(Function.identity()); + } + default -> { + throw new IllegalArgumentException(); + } + } + + stream.reduce(cmdline, (ctnr, wixVar) -> { + ctnr.add(wixVar); + return ctnr; + }, (x, y) -> { + x.addAll(y); + return x; + }); + }); + } + + private void buildMsiWix4(Path msi) throws IOException { + var mergedSrcWixVars = sources.stream().map(wixSource -> { + return Optional.ofNullable(wixSource.variables).orElseGet( + Collections::emptyMap).entrySet().stream(); + }).flatMap(Function.identity()).collect(Collectors.toMap( + Map.Entry::getKey, Map.Entry::getValue)); + + List cmdline = new ArrayList<>(List.of( + toolset.getToolPath(WixTool.Wix4).toString(), + "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 wixObjs = new ArrayList<>(); for (var source : sources) { - wixObjs.add(compile(source)); + wixObjs.add(compileWix3(source)); } List 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 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 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 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 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 variables; } - private Map toolset; + private WixToolset toolset; private Map wixVariables; private List lightOptions; private Path wixObjDir; diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixSourceConverter.java b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixSourceConverter.java new file mode 100644 index 00000000000..7786d64a786 --- /dev/null +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixSourceConverter.java @@ -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 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 resources = new HashMap<>(); + private final WixToolsetType wixToolsetType; + } + + // + // Default JDK XSLT v1.0 processor is not handling well default namespace mappings. + // Running generic template: + // + // + // + // + // + // + // + // produces: + // + // + // + // + // ... + // + // + // + // 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: + // + // + // + // + // ... + // + // + // + // 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 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 uriToPrefix; + private final Map 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 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 createValue(Object prefix) { + return new AbstractMap.SimpleEntry((String) prefix, false); + } + + private final Map> 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 KNOWN_NAMESPACES = Set.of( + "http://schemas.microsoft.com/wix/2006/localization", + "http://schemas.microsoft.com/wix/2006/wi"); +} diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixTool.java b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixTool.java index 68104444b3c..f16b28edf24 100644 --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixTool.java +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixTool.java @@ -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 toolset() throws ConfigException { - Map 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, Map> conv = lookupResults -> { + return lookupResults.stream().filter(ToolLookupResult::isValid).collect(Collectors. + groupingBy(lookupResult -> { + return lookupResult.getInfo().version.toString(); + })).values().stream().filter(sameVersionLookupResults -> { + Set 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, Optional> 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 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, 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 findWixInstallDirs() { - PathMatcher wixInstallDirMatcher = FileSystems.getDefault().getPathMatcher( - "glob:WiX Toolset v*"); + return Stream.of(findWixCurrentInstallDirs(), findWix3InstallDirs()). + flatMap(List::stream).toList(); + } + + private static List 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 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 result; try (var paths = Files.walk(path, 1)) { - result = paths.toList(); + return paths.toList(); } catch (IOException ex) { Log.verbose(ex); - result = Collections.emptyList(); + List 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; } diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixToolset.java b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixToolset.java new file mode 100644 index 00000000000..ab433616f44 --- /dev/null +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixToolset.java @@ -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 getTools() { + return tools; + } + + private final Set tools; + } + + private WixToolset(Map 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 requiredTools, Map 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 tools; +} diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixUiFragmentBuilder.java b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixUiFragmentBuilder.java index 8d20d6432bf..4f39a65e3b6 100644 --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixUiFragmentBuilder.java +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixUiFragmentBuilder.java @@ -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 dialogIds = dialogIdsSupplier.apply(outer); Map> 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> dialogIdsSupplier; private final Supplier>> 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) { diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/InstallDirNotEmptyDlg.wxs b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/InstallDirNotEmptyDlg.wxs index 936984b1fff..755b2a0aab9 100644 --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/InstallDirNotEmptyDlg.wxs +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/InstallDirNotEmptyDlg.wxs @@ -1,7 +1,7 @@ - + diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/wix3-to-wix4-conv.xsl b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/wix3-to-wix4-conv.xsl new file mode 100644 index 00000000000..382ed731b5a --- /dev/null +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/wix3-to-wix4-conv.xsl @@ -0,0 +1,183 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java index 3faabcd6f8d..1a4dbd22897 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java @@ -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/./" from WiX3 msi which translates to "Program Files/" + // msiexec creates "Program Files/PFiles64/" from WiX4 msi + // So for WiX4 msi we need to transform "Program Files/PFiles64/" into "Program Files/" + // + // 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; diff --git a/test/jdk/tools/jpackage/windows/WinL10nTest.java b/test/jdk/tools/jpackage/windows/WinL10nTest.java index deb6b1a8705..a868fd5f051 100644 --- a/test/jdk/tools/jpackage/windows/WinL10nTest.java +++ b/test/jdk/tools/jpackage/windows/WinL10nTest.java @@ -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 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 getLightCommandLine( - Executor.Result result) { - return result.getOutput().stream().filter(s -> { + private static Stream 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 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 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;