diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/EnvironmentProvider.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/EnvironmentProvider.java new file mode 100644 index 00000000000..de364129256 --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/EnvironmentProvider.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jpackage.internal; + +public interface EnvironmentProvider { + + String getProperty(String propertyName); + + String getenv(String envVarName); + + public static EnvironmentProvider DEFAULT = new EnvironmentProvider() { + + @Override + public String getenv(String envVarName) { + return System.getenv(envVarName); + } + + @Override + public String getProperty(String propertyName) { + return System.getProperty(propertyName); + } + }; +} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/Globals.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/Globals.java index 0128d050c25..2fc0046fad5 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/Globals.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/Globals.java @@ -65,6 +65,14 @@ public final class Globals { return this; } + public EnvironmentProvider system() { + return this.findProperty(EnvironmentProvider.class).orElse(EnvironmentProvider.DEFAULT); + } + + public Globals system(EnvironmentProvider v) { + return setProperty(EnvironmentProvider.class, v); + } + Log.Logger logger() { return logger; } diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/PathUtils.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/PathUtils.java index a8944a67ae0..707dd2784ce 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/PathUtils.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/PathUtils.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -24,6 +24,7 @@ */ package jdk.jpackage.internal.util; +import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.util.Objects; import java.util.Optional; @@ -31,18 +32,26 @@ import java.util.function.UnaryOperator; public final class PathUtils { + private PathUtils() { + } + public static String getSuffix(Path path) { String filename = replaceSuffix(path.getFileName(), null).toString(); return path.getFileName().toString().substring(filename.length()); } public static Path addSuffix(Path path, String suffix) { + Objects.requireNonNull(path); + Objects.requireNonNull(suffix); + Path parent = path.getParent(); String filename = path.getFileName().toString() + suffix; return parent != null ? parent.resolve(filename) : Path.of(filename); } public static Path replaceSuffix(Path path, String suffix) { + Objects.requireNonNull(path); + Path parent = path.getParent(); String filename = path.getFileName().toString().replaceAll("\\.[^.]*$", "") + Optional.ofNullable(suffix).orElse(""); @@ -59,18 +68,22 @@ public final class PathUtils { } public static Path normalizedAbsolutePath(Path path) { - if (path != null) { + return mapNullablePath(_ -> { return path.normalize().toAbsolutePath(); - } else { - return null; - } + }, path); } public static String normalizedAbsolutePathString(Path path) { - if (path != null) { - return normalizedAbsolutePath(path).toString(); - } else { - return null; - } + return Optional.ofNullable(normalizedAbsolutePath(path)).map(Path::toString).orElse(null); + } + + public static Optional asPath(String value) { + return Optional.ofNullable(value).map(v -> { + try { + return Path.of(v); + } catch (InvalidPathException ex) { + return null; + } + }); } } 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 c4f8610312a..d0c5e6ca3b0 100644 --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixTool.java +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixTool.java @@ -23,14 +23,14 @@ * questions. */ package jdk.jpackage.internal; +import static java.util.stream.Collectors.groupingBy; +import static java.util.stream.Collectors.toMap; +import static java.util.stream.Collectors.toSet; import java.io.IOException; import java.nio.file.FileSystems; import java.nio.file.Files; -import java.nio.file.InvalidPathException; import java.nio.file.Path; -import java.nio.file.PathMatcher; -import java.text.MessageFormat; import java.util.Comparator; import java.util.List; import java.util.Map; @@ -38,12 +38,13 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.function.Function; -import java.util.stream.Collectors; +import java.util.function.Supplier; import java.util.stream.Stream; import jdk.jpackage.internal.WixToolset.WixToolsetType; import jdk.jpackage.internal.model.ConfigException; import jdk.jpackage.internal.model.DottedVersion; import jdk.jpackage.internal.util.PathUtils; +import jdk.jpackage.internal.util.Slot; /** * WiX tool. @@ -58,16 +59,20 @@ public enum WixTool { this.minimalVersion = minimalVersion; } - interface ToolInfo { + Path fileName() { + return toolFileName; + } + + sealed interface ToolInfo { Path path(); DottedVersion version(); } - interface CandleInfo extends ToolInfo { + sealed interface CandleInfo extends ToolInfo { boolean fips(); } - private record DefaultToolInfo(Path path, DottedVersion version) implements ToolInfo { + record DefaultToolInfo(Path path, DottedVersion version) implements ToolInfo { DefaultToolInfo { Objects.requireNonNull(path); Objects.requireNonNull(version); @@ -76,9 +81,14 @@ public enum WixTool { DefaultToolInfo(Path path, String version) { this(path, DottedVersion.lazy(version)); } + + @Override + public String toString() { + return String.format("%s|ver=%s", path, version); + } } - private record DefaultCandleInfo(Path path, DottedVersion version, boolean fips) implements CandleInfo { + record DefaultCandleInfo(Path path, DottedVersion version, boolean fips) implements CandleInfo { DefaultCandleInfo { Objects.requireNonNull(path); Objects.requireNonNull(version); @@ -87,25 +97,42 @@ public enum WixTool { DefaultCandleInfo(ToolInfo info, boolean fips) { this(info.path(), info.version(), fips); } + + @Override + public String toString() { + var sb = new StringBuffer(); + sb.append(path); + if (fips) { + sb.append("|fips"); + } + sb.append("|ver=").append(version); + return sb.toString(); + } } static WixToolset createToolset() { + return createToolset(WixTool::findWixInstallDirs, true); + } + + static WixToolset createToolset(Supplier> wixInstallDirs, boolean searchInPath) { + Function, Map> conv = lookupResults -> { - return lookupResults.stream().filter(ToolLookupResult::isValid).collect(Collectors. - groupingBy(lookupResult -> { + return lookupResults.stream().filter(ToolLookupResult::isValid).collect(groupingBy(lookupResult -> { return lookupResult.info().version().toString(); })).values().stream().filter(sameVersionLookupResults -> { - Set sameVersionTools = sameVersionLookupResults.stream().map( - ToolLookupResult::tool).collect(Collectors.toSet()); - if (sameVersionTools.equals(Set.of(Candle3)) || sameVersionTools.equals(Set.of( - Light3))) { + var sameVersionTools = sameVersionLookupResults.stream() + .map(ToolLookupResult::tool) + .collect(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; } - }).flatMap(List::stream).collect(Collectors.toMap(ToolLookupResult::tool, - ToolLookupResult::info, (ToolInfo x, ToolInfo y) -> { + }).flatMap(List::stream).collect(toMap( + ToolLookupResult::tool, + ToolLookupResult::info, + (ToolInfo x, ToolInfo y) -> { return Stream.of(x, y).sorted(Comparator.comparing((ToolInfo toolInfo) -> { return toolInfo.version().toComponentsString(); }).reversed()).findFirst().get(); @@ -115,58 +142,53 @@ public enum WixTool { 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(); + return Stream.of(WixToolsetType.values()).flatMap(toolsetType -> { + return WixToolset.create(toolsetType, tools).stream(); + }).findFirst(); }; - var toolsInPath = Stream.of(values()).map(tool -> { - return ToolLookupResult.lookup(tool, Optional.empty()); - }).filter(Optional::isPresent).map(Optional::get).toList(); + final List toolsInPath; + if (searchInPath) { + toolsInPath = Stream.of(values()).flatMap(tool -> { + return ToolLookupResult.lookup(tool, Optional.empty()).stream(); + }).toList(); + } else { + toolsInPath = List.of(); + } // Try to build a toolset from tools in the PATH first. - var toolset = createToolset.apply(toolsInPath); - if (toolset.isPresent()) { - return toolset.get(); - } + var toolset = createToolset.apply(toolsInPath).orElseGet(() -> { + // Look up for WiX tools in known locations. + var toolsInKnownWiXDirs = wixInstallDirs.get().stream().flatMap(dir -> { + return Stream.of(values()).flatMap(tool -> { + return ToolLookupResult.lookup(tool, Optional.of(dir)).stream(); + }); + }).toList(); - // Look up for WiX tools in known locations. - var toolsInKnownWiXDirs = findWixInstallDirs().stream().map(dir -> { - return Stream.of(values()).map(tool -> { - return ToolLookupResult.lookup(tool, Optional.of(dir)); + // Build a toolset found in the PATH and in known locations. + var allValidFoundTools = Stream.of(toolsInPath, toolsInKnownWiXDirs) + .flatMap(List::stream) + .filter(ToolLookupResult::isValid) + .toList(); + + return createToolset.apply(allValidFoundTools).orElseThrow(() -> { + return new ConfigException( + I18N.getString("error.no-wix-tools"), + I18N.getString("error.no-wix-tools.advice")); }); - }).flatMap(Function.identity()).filter(Optional::isPresent).map(Optional::get).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.info().path(), - lookupResult.info().version(), lookupResult.tool().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")); - } - } + return toolset; } - private record ToolLookupResult(WixTool tool, ToolInfo info, boolean versionTooOld) { + static List findWixInstallDirs() { + return Stream.of( + findWixCurrentInstallDirs(), + findWix3InstallDirs() + ).flatMap(List::stream).toList(); + } + + private record ToolLookupResult(WixTool tool, ToolInfo info) { ToolLookupResult { Objects.requireNonNull(tool); @@ -177,58 +199,59 @@ public enum WixTool { Objects.requireNonNull(tool); Objects.requireNonNull(lookupDir); - final Path toolPath = lookupDir.map(p -> p.resolve( - tool.toolFileName)).orElse(tool.toolFileName); + final Path toolPath = lookupDir.map(p -> { + return 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); - final var validator = new ToolValidator(toolPath) - .setMinimalVersion(tool.minimalVersion) - .setToolOldVersionErrorHandler((name, version) -> { - tooOld[0] = true; - return null; - }); - - final Function, String> versionParser; - - if (Set.of(Candle3, Light3).contains(tool)) { - final String printVersionArg; - if (tool == Candle3) { + final var printVersionArg = switch (tool) { + case Candle3 -> { // Add '-fips' to make "candle.exe" print help message and return // 0 exit code instead of returning error exit code and printing // "error CNDL0308 : The Federal Information Processing Standard (FIPS) appears to be enabled on the machine..." // error message if FIPS is enabled. // If FIPS is disabled, passing '-fips' parameter still makes // "candle.exe" print help message and return 0 exit code. - printVersionArg = "-fips"; - } else { - printVersionArg = "-?"; + yield "-fips"; } - validator.setCommandLine(printVersionArg); - versionParser = output -> { - String firstLineOfOutput = output.findFirst().orElse(""); - int separatorIdx = firstLineOfOutput.lastIndexOf(' '); - if (separatorIdx == -1) { - return null; - } - return firstLineOfOutput.substring(separatorIdx + 1); - }; - } else { - validator.setCommandLine("--version"); - versionParser = output -> { - return output.findFirst().orElse(""); - }; - } + case Light3 -> { + yield "-?"; + } + default -> { + yield "--version"; + } + }; + validator.setCommandLine(printVersionArg); + final Function, Optional> versionParser = switch (tool) { + case Candle3, Light3 -> { + yield output -> { + return output.findFirst().map(firstLineOfOutput -> { + int separatorIdx = firstLineOfOutput.lastIndexOf(' '); + if (separatorIdx == -1) { + return null; + } + return firstLineOfOutput.substring(separatorIdx + 1); + }); + }; + } + default -> { + yield output -> { + return output.findFirst(); + }; + } + }; + + final var parsedVersion = Slot.createEmpty(); validator.setVersionParser(output -> { - parsedVersion[0] = versionParser.apply(output); - return parsedVersion[0]; + versionParser.apply(output).ifPresent(parsedVersion::set); + return parsedVersion.find().orElse(null); }); if (validator.validate() == null) { // Tool found - ToolInfo info = new DefaultToolInfo(toolPath, parsedVersion[0]); + ToolInfo info = new DefaultToolInfo(toolPath, parsedVersion.get()); if (tool == Candle3) { // Detect FIPS mode var fips = false; @@ -242,63 +265,52 @@ public enum WixTool { } } } catch (IOException ex) { - Log.verbose(ex); } info = new DefaultCandleInfo(info, fips); } - return Optional.of(new ToolLookupResult(tool, info, tooOld[0])); + + return Optional.of(new ToolLookupResult(tool, info)); } else { return Optional.empty(); } } + boolean versionTooOld() { + return DottedVersion.compareComponents(info.version(), tool.minimalVersion) < 0; + } + boolean isValid() { - return !versionTooOld; + return !versionTooOld(); } } - private static Path getSystemDir(String envVar, String knownDir) { - return Optional - .ofNullable(getEnvVariableAsPath(envVar)) - .orElseGet(() -> Optional - .ofNullable(getEnvVariableAsPath("SystemDrive")) - .orElseGet(() -> Path.of("C:")).resolve(knownDir)); + private static Path getSystemDir(String envVar, Path knownDir) { + return getEnvVariableAsPath(envVar).orElseGet(() -> { + return getEnvVariableAsPath("SystemDrive").orElseGet(() -> { + return Path.of("C:"); + }).resolve(knownDir); + }); } - private static Path getEnvVariableAsPath(String envVar) { - String path = System.getenv(envVar); - if (path != null) { - try { - return Path.of(path); - } catch (InvalidPathException ex) { - Log.error(MessageFormat.format(I18N.getString( - "error.invalid-envvar"), envVar)); - } - } - return null; - } - - private static List findWixInstallDirs() { - return Stream.of(findWixCurrentInstallDirs(), findWix3InstallDirs()). - flatMap(List::stream).toList(); + private static Optional getEnvVariableAsPath(String envVar) { + Objects.requireNonNull(envVar); + return Optional.ofNullable(Globals.instance().system().getenv(envVar)).flatMap(PathUtils::asPath); } 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(); + return Stream.of( + getEnvVariableAsPath("USERPROFILE"), + Optional.ofNullable(Globals.instance().system().getProperty("user.home")).flatMap(PathUtils::asPath) + ).flatMap(Optional::stream).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*"); + var wixInstallDirMatcher = FileSystems.getDefault().getPathMatcher("glob:WiX Toolset v*"); - Path programFiles = getSystemDir("ProgramFiles", "\\Program Files"); - Path programFilesX86 = getSystemDir("ProgramFiles(x86)", - "\\Program Files (x86)"); + var programFiles = getSystemDir("ProgramFiles", Path.of("Program Files")); + var programFilesX86 = getSystemDir("ProgramFiles(x86)", Path.of("Program Files (x86)")); // Returns list of WiX install directories ordered by WiX version number. // Newer versions go first. @@ -306,13 +318,11 @@ public enum WixTool { try (var paths = Files.walk(path, 1)) { return paths.toList(); } catch (IOException ex) { - Log.verbose(ex); - List empty = List.of(); - return empty; + return List.of(); } }).flatMap(List::stream) - .filter(path -> wixInstallDirMatcher.matches(path.getFileName())). - sorted(Comparator.comparing(Path::getFileName).reversed()) + .filter(path -> wixInstallDirMatcher.matches(path.getFileName())) + .sorted(Comparator.comparing(Path::getFileName).reversed()) .map(path -> path.resolve("bin")) .toList(); } diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixToolset.java b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixToolset.java index e7bdbede368..1694503a8c8 100644 --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixToolset.java +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixToolset.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -26,14 +26,20 @@ package jdk.jpackage.internal; import java.nio.file.Path; import java.util.Map; +import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; import jdk.jpackage.internal.model.DottedVersion; -final class WixToolset { +record WixToolset(Map tools) { - static enum WixToolsetType { + WixToolset { + tools = Map.copyOf(tools); + } + + enum WixToolsetType { // Wix v4+ Wix4(WixTool.Wix4), // Wix v3+ @@ -50,10 +56,6 @@ final class WixToolset { 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()); @@ -75,16 +77,19 @@ final class WixToolset { .anyMatch(WixTool.CandleInfo::fips); } - static WixToolset create(Set requiredTools, Map allTools) { + static Optional create(WixToolsetType type, Map allTools) { + Objects.requireNonNull(type); + Objects.requireNonNull(allTools); + + var requiredTools = type.getTools(); + 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); + return Optional.of(new WixToolset(filteredTools)); } else { - return null; + return Optional.empty(); } } - - private final Map tools; } diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/WinResources.properties b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/WinResources.properties index 0f3dcab8260..a11a8a6b41e 100644 --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/WinResources.properties +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/WinResources.properties @@ -36,8 +36,8 @@ 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 resource.installer-exe=installer executable -error.no-wix-tools=Can not find WiX tools. Was looking for WiX v3 light.exe and candle.exe or WiX v4/v5 wix.exe and none was found -error.no-wix-tools.advice=Download WiX 3.0 or later from https://wixtoolset.org and add it to the PATH. +error.no-wix-tools=No usable WiX Toolset installation found +error.no-wix-tools.advice=Install the latest WiX v3 from https://github.com/wixtoolset/wix3/releases or WiX v4+ from https://github.com/wixtoolset/wix/releases error.version-string-wrong-format.advice=Set value of --app-version parameter to a valid Windows Installer ProductVersion. error.msi-product-version-components=Version string [{0}] must have between 2 and 4 components. error.msi-product-version-major-out-of-range=Major version must be in the range [0, 255] @@ -56,7 +56,6 @@ error.missing-service-installer.advice=Add 'service-installer.exe' service insta message.icon-not-ico=The specified icon "{0}" is not an ICO file and will not be used. The default icon will be used in it's place. message.tool-version=Detected [{0}] version [{1}]. -message.wrong-tool-version=Detected [{0}] version {1} but version {2} is required. message.product-code=MSI ProductCode: {0}. message.upgrade-code=MSI UpgradeCode: {0}. message.preparing-msi-config=Preparing MSI config: {0}. diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/mock/MockingToolProvider.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/mock/MockingToolProvider.java index f8c04cc3927..2c46d02e7ce 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/mock/MockingToolProvider.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/mock/MockingToolProvider.java @@ -160,5 +160,5 @@ abstract sealed class MockingToolProvider implements ToolProviderCommandMock { private final String name; private final Iterator actionIter; - static ToolProviderCommandMock UNREACHABLE = new MockingToolProvider.NonCompletable("", List.of()); + static final ToolProviderCommandMock UNREACHABLE = new MockingToolProvider.NonCompletable("", List.of()); } diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/stdmock/EnvironmentProviderMock.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/stdmock/EnvironmentProviderMock.java new file mode 100644 index 00000000000..1e12078ec66 --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/stdmock/EnvironmentProviderMock.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.jpackage.test.stdmock; + +import jdk.jpackage.internal.EnvironmentProvider; +import java.util.ArrayList; +import java.util.Map; +import java.util.Objects; + +public record EnvironmentProviderMock( + Map envVariables, + Map systemProperties) implements EnvironmentProvider { + + public EnvironmentProviderMock { + envVariables.keySet().forEach(Objects::requireNonNull); + envVariables.values().forEach(Objects::requireNonNull); + + systemProperties.keySet().forEach(Objects::requireNonNull); + systemProperties.values().forEach(Objects::requireNonNull); + } + + @Override + public String getenv(String envVarName) { + return envVariables.get(Objects.requireNonNull(envVarName)); + } + + @Override + public String getProperty(String propertyName) { + return systemProperties.get(Objects.requireNonNull(propertyName)); + } + + @Override + public String toString() { + var tokens = new ArrayList(); + if (!envVariables.isEmpty()) { + tokens.add(String.format("env=%s", envVariables)); + } + if (!systemProperties.isEmpty()) { + tokens.add(String.format("props=%s", systemProperties)); + } + return String.join(", ", tokens); + } +} diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/stdmock/WixToolMock.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/stdmock/WixToolMock.java new file mode 100644 index 00000000000..9cd00f61082 --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/stdmock/WixToolMock.java @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.jpackage.test.stdmock; + +import java.nio.file.Path; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import jdk.jpackage.internal.util.PathUtils; +import jdk.jpackage.test.mock.CommandActionSpec; +import jdk.jpackage.test.mock.CommandActionSpecs; +import jdk.jpackage.test.mock.CommandMockSpec; + +public final class WixToolMock { + + public CommandMockSpec create() { + Objects.requireNonNull(type); + Objects.requireNonNull(version); + + CommandActionSpec action = switch (type) { + case CANDLE3 -> { + yield candleAction(fips, version); + } + case LIGHT3 -> { + yield lightAction(version); + } + case WIX4 -> { + yield wixAction(version); + } + }; + + var toolPath = Optional.ofNullable(dir).map(d -> { + return d.resolve(type.fileName); + }).orElse(type.fileName); + var mockName = PathUtils.replaceSuffix(toolPath, ""); + + return new CommandMockSpec(toolPath, mockName, CommandActionSpecs.build().action(action).create()); + } + + public WixToolMock fips(Boolean v) { + fips = v; + return this; + } + + public WixToolMock fips() { + return fips(true); + } + + public WixToolMock dir(Path v) { + dir = v; + return this; + } + + public WixToolMock version(String v) { + version = v; + return this; + } + + public WixToolMock candle(String version) { + return type(WixTool.CANDLE3).version(version); + } + + public WixToolMock light(String version) { + return type(WixTool.LIGHT3).version(version); + } + + public WixToolMock wix(String version) { + return type(WixTool.WIX4).version(version); + } + + private WixToolMock type(WixTool v) { + type = v; + return this; + } + + private static CommandActionSpec candleAction(boolean fips, String version) { + Objects.requireNonNull(version); + var sb = new StringBuilder(); + sb.append(version); + if (fips) { + sb.append("; fips"); + } + return CommandActionSpec.create(sb.toString(), context -> { + if (List.of("-?").equals(context.args())) { + if (fips) { + context.err().println("error CNDL0308 : The Federal Information Processing Standard (FIPS) appears to be enabled on the machine"); + return Optional.of(308); + } + } else if (!List.of("-fips").equals(context.args())) { + throw context.unexpectedArguments(); + } + + var out = context.out(); + List.of( + "Windows Installer XML Toolset Compiler version " + version, + "Copyright (c) .NET Foundation and contributors. All rights reserved.", + "", + " usage: candle.exe [-?] [-nologo] [-out outputFile] sourceFile [sourceFile ...] [@responseFile]" + ).forEach(out::println); + + return Optional.of(0); + }); + } + + private static CommandActionSpec lightAction(String version) { + Objects.requireNonNull(version); + return CommandActionSpec.create(version, context -> { + if (List.of("-?").equals(context.args())) { + var out = context.out(); + List.of( + "Windows Installer XML Toolset Linker version " + version, + "Copyright (c) .NET Foundation and contributors. All rights reserved.", + "", + " usage: light.exe [-?] [-b bindPath] [-nologo] [-out outputFile] objectFile [objectFile ...] [@responseFile]" + ).forEach(out::println); + return Optional.of(0); + } else { + throw context.unexpectedArguments(); + } + }); + } + + private static CommandActionSpec wixAction(String version) { + Objects.requireNonNull(version); + return CommandActionSpec.create(version, context -> { + if (List.of("--version").equals(context.args())) { + context.out().println(version); + return Optional.of(0); + } else { + throw context.unexpectedArguments(); + } + }); + } + + private enum WixTool { + CANDLE3("candle"), + LIGHT3("light"), + WIX4("wix"), + ; + + WixTool(String name) { + this.fileName = Path.of(Objects.requireNonNull(name) + ".exe"); + } + + final Path fileName; + } + + private Path dir; + private WixTool type; + private String version; + private boolean fips; +} diff --git a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/util/PathUtilsTest.java b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/util/PathUtilsTest.java new file mode 100644 index 00000000000..7ed679ea3b5 --- /dev/null +++ b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/util/PathUtilsTest.java @@ -0,0 +1,213 @@ +/* + * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.jpackage.internal.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrowsExactly; + +import java.nio.file.Path; +import java.util.Optional; +import java.util.function.UnaryOperator; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + + +class PathUtilsTest { + + @ParameterizedTest + @CsvSource({ + "foo,''", + "foo.bar,.bar", + "foo..bar,.bar", + ".bar,.bar", + "foo.bar.buz,.buz", + ".,.", + "...,.", + "..,.", + }) + void test_getSuffix(Path path, String expected) { + + var suffix = PathUtils.getSuffix(path); + + assertEquals(expected, suffix); + } + + @Test + void test_getSuffix_null() { + assertThrowsExactly(NullPointerException.class, () -> { + PathUtils.getSuffix(null); + }); + } + + @ParameterizedTest + @CsvSource({ + "foo,'',foo", + "a/b/foo.exe,.ico,a/b/foo.exe.ico", + "foo,bar,foobar", + "'',bar,bar", + ".,bar,.bar", + }) + void test_addSuffix(Path path, String suffix, Path expected) { + + var newPath = PathUtils.addSuffix(path, suffix); + + assertEquals(expected, newPath); + } + + @Test + void test_addSuffix_null() { + assertThrowsExactly(NullPointerException.class, () -> { + PathUtils.addSuffix(null, "foo"); + }); + assertThrowsExactly(NullPointerException.class, () -> { + PathUtils.addSuffix(Path.of("foo"), null); + }); + } + + @ParameterizedTest + @CsvSource({ + "foo.exe,.ico,foo.ico", + "foo.exe,,foo", + "foo.exe,'',foo", + "a/b/foo.exe,.ico,a/b/foo.ico", + "foo,'',foo", + "foo,bar,foobar", + "'',bar,bar", + ".,bar,bar", + ".,.bar,.bar", + }) + void test_replaceSuffix(Path path, String newSuffix, Path expected) { + + var newPath = PathUtils.replaceSuffix(path, newSuffix); + + assertEquals(expected, newPath); + } + + @Test + void test_replaceSuffix_null() { + assertThrowsExactly(NullPointerException.class, () -> { + PathUtils.replaceSuffix(null, "foo"); + }); + + assertEquals(Path.of("foo"), PathUtils.replaceSuffix(Path.of("foo.a"), null)); + } + + @ParameterizedTest + @CsvSource({ + "IDENTITY,a,a", + "IDENTITY,,", + "RETURN_NULL,a,", + "RETURN_NULL,,", + "FOO,a,foo", + "FOO,,", + }) + void test_mapNullablePath(PathMapper mapper, Path path, Path expected) { + + var newPath = PathUtils.mapNullablePath(mapper, path); + + assertEquals(expected, newPath); + } + + @Test + void test_mapNullablePath_null() { + assertThrowsExactly(NullPointerException.class, () -> { + PathUtils.mapNullablePath(null, Path.of("")); + }); + } + + @ParameterizedTest + @CsvSource(nullValues = {"N/A"}, value = { + "foo.exe", + "N/A", + }) + void test_normalizedAbsolutePath(Path path) { + + var newPath = PathUtils.normalizedAbsolutePath(path); + + var expected = Optional.ofNullable(path).map(v -> { + return v.normalize().toAbsolutePath(); + }).orElse(null); + + assertEquals(expected, newPath); + } + + @ParameterizedTest + @CsvSource(nullValues = {"N/A"}, value = { + "foo.exe", + "N/A", + }) + void test_normalizedAbsolutePathString(Path path) { + + var newPath = PathUtils.normalizedAbsolutePathString(path); + + var expected = Optional.ofNullable(path).map(v -> { + return v.normalize().toAbsolutePath().toString(); + }).orElse(null); + + assertEquals(expected, newPath); + } + + @ParameterizedTest + @CsvSource(nullValues = {"N/A"}, value = { + "N/A", + "foo", + "*", + ":", + }) + void test_asPath(String str) { + + var path = PathUtils.asPath(str); + + var expected = Optional.ofNullable(str).flatMap(v -> { + return Result.of(() -> { + return Path.of(v); + }).value(); + }); + + assertEquals(expected, path); + } + + enum PathMapper implements UnaryOperator { + IDENTITY { + @Override + public Path apply(Path path) { + return path; + } + }, + RETURN_NULL { + @Override + public Path apply(Path path) { + return null; + } + }, + FOO { + @Override + public Path apply(Path path) { + return Path.of("foo"); + } + }, + ; + } +} diff --git a/test/jdk/tools/jpackage/junit/windows/jdk.jpackage/jdk/jpackage/internal/WixToolTest.java b/test/jdk/tools/jpackage/junit/windows/jdk.jpackage/jdk/jpackage/internal/WixToolTest.java new file mode 100644 index 00000000000..d4881069844 --- /dev/null +++ b/test/jdk/tools/jpackage/junit/windows/jdk.jpackage/jdk/jpackage/internal/WixToolTest.java @@ -0,0 +1,754 @@ +/* + * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.jpackage.internal; + +import static java.util.stream.Collectors.toMap; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import jdk.jpackage.internal.WixTool.ToolInfo; +import jdk.jpackage.internal.WixToolset.WixToolsetType; +import jdk.jpackage.internal.model.ConfigException; +import jdk.jpackage.internal.util.TokenReplace; +import jdk.jpackage.test.CannedFormattedString; +import jdk.jpackage.test.JPackageStringBundle; +import jdk.jpackage.test.mock.CommandActionSpecs; +import jdk.jpackage.test.mock.CommandMock; +import jdk.jpackage.test.mock.CommandMockSpec; +import jdk.jpackage.test.mock.Script; +import jdk.jpackage.test.stdmock.EnvironmentProviderMock; +import jdk.jpackage.test.stdmock.JPackageMockUtils; +import jdk.jpackage.test.stdmock.WixToolMock; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + + +class WixToolTest { + + @ParameterizedTest + @MethodSource + void testLookup(TestSpec spec, @TempDir Path workDir) throws IOException { + spec.run(workDir); + } + + @ParameterizedTest + @MethodSource + void testLookupDirs(EnvironmentTestSpec spec, @TempDir Path workDir) throws IOException { + spec.run(workDir); + } + + private static Collection testLookup() { + + List testCases = new ArrayList<>(); + + Consumer appendTestCases = builder -> { + testCases.add(builder.create()); + }; + + Stream.of( + // Simple WiX3 of a minimal acceptable version + TestSpec.build() + .expect(toolset().version("3.0").put(WixToolsetType.Wix3, "foo")) + .tool(tool("foo").candle("3.0")) + .tool(tool("foo").light("3.0")), + // Simple WiX3 with FIPS + TestSpec.build() + .expect(toolset().version("3.14.1.8722").put(WixToolsetType.Wix3, "foo").fips()) + .tool(tool("foo").candle("3.14.1.8722").fips()) + .tool(tool("foo").light("3.14.1.8722")), + // Simple WiX4+ of a minimal acceptable version + TestSpec.build() + .expect(toolset().version("4.0.4").put(WixToolsetType.Wix4, "foo")) + .tool(tool("foo").wix("4.0.4")), + // WiX3 with light and candle from different directories and non-existent directory + TestSpec.build() + .expect(toolset().version("3.11.2").put(WixTool.Candle3, "foo").put(WixTool.Light3, "bar")) + .lookupDir("buz") + .tool(tool("foo").candle("3.11.2")) + .tool(tool("bar").light("3.11.2")) + .tool(tool("bar").candle("3.11.1")) + .tool(tool("foo").light("3.11.1")), + // WiX3, WiX4+ same directory + TestSpec.build() + .expect(toolset().version("5.0.2+aa65968c").put(WixToolsetType.Wix4, "foo")) + .tool(tool("foo").candle("3.14.1.8722")) + .tool(tool("foo").light("3.14.1.8722")) + .tool(tool("foo").wix("5.0.2+aa65968c")), + // WiX3 (good), WiX4+ (bad version) + TestSpec.build() + .expect(toolset().version("3.14.1.8722").put(WixToolsetType.Wix3, "foo")) + .tool(tool("foo").candle("3.14.1.8722")) + .tool(tool("foo").light("3.14.1.8722")) + .tool(tool("foo").wix("Blah-blah-blah")), + // WiX3 (incomplete), WiX4+ (good) + TestSpec.build() + .expect(toolset().version("5.0").put(WixToolsetType.Wix4, "foo")) + .tool(tool("foo").candle("3.14.1.8722")) + .tool(tool("foo").wix("5.0")), + // WiX5 in the PATH and in the directory, same version; PATH always wins + TestSpec.build() + .expect(toolset().version("5.0").put(WixToolsetType.Wix4)) + .tool(tool().wix("5.0")) + .tool(tool("foo").wix("5.0")), + // WiX5 in the PATH and in the directory; the one in the directory is newer; PATH always wins + TestSpec.build() + .expect(toolset().version("5.0").put(WixToolsetType.Wix4)) + .tool(tool().wix("5.0")) + .tool(tool("foo").wix("5.1")), + // WiX5 in the PATH and in the directory; the one in the PATH is newer; PATH always wins + TestSpec.build() + .expect(toolset().version("5.1").put(WixToolsetType.Wix4)) + .tool(tool().wix("5.1")) + .tool(tool("foo").wix("5.0")), + // WiX3 in the PATH, WiX3 in the directory; PATH always wins + TestSpec.build() + .expect(toolset().version("3.20").put(WixToolsetType.Wix3)) + .tool(tool().candle("3.20")) + .tool(tool().light("3.20")) + .tool(tool("foo").wix("5.0")), + // Old WiX3 in the PATH, WiX3 in the directory + TestSpec.build() + .expect(toolset().version("3.20").put(WixToolsetType.Wix3, "foo")) + .tool(tool().candle("2.9")) + .tool(tool().light("2.9")) + .tool(tool("foo").candle("3.20")) + .tool(tool("foo").light("3.20")) + ).forEach(appendTestCases); + + for (var oldLightStatus : ToolStatus.values()) { + for (var oldCandleStatus : ToolStatus.values()) { + for (var newLightStatus : ToolStatus.values()) { + for (var newCandleStatus : ToolStatus.values()) { + boolean newGood = ToolStatus.isAllGood(newLightStatus, newCandleStatus); + if (!ToolStatus.isAllGood(oldLightStatus, oldCandleStatus) && !newGood) { + continue; + } + + var builder = TestSpec.build(); + if (newGood) { + builder.expect(toolset().version("3.14").put(WixToolsetType.Wix3, "new")); + } else { + builder.expect(toolset().version("3.11").put(WixToolsetType.Wix3, "old")); + } + + oldCandleStatus.map(tool("old").candle("3.11")).ifPresent(builder::tool); + oldLightStatus.map(tool("old").light("3.11")).ifPresent(builder::tool); + + newCandleStatus.map(tool("new").candle("3.14")).ifPresent(builder::tool); + newLightStatus.map(tool("new").light("3.14")).ifPresent(builder::tool); + + appendTestCases.accept(builder); + } + } + } + } + + Stream.of( + // No WiX tools + TestSpec.build(), + TestSpec.build() + .lookupDir("foo"), + TestSpec.build() + .lookupDir(LOOKUP_IN_PATH), + // Incomplete WiX3: missing candle.exe + TestSpec.build() + .tool(tool("foo").light("3.14.1.8722")), + // Incomplete WiX3: missing light.exe + TestSpec.build() + .tool(tool("foo").candle("3.14.1.8722")), + // Incomplete WiX3: version mismatch of light.exe and candle.exe + TestSpec.build() + .tool(tool("foo").candle("3.14")) + .tool(tool("foo").light("3.15")), + // WiX3 too old + TestSpec.build() + .tool(tool("foo").candle("2.9")) + .tool(tool("foo").light("2.9")), + // WiX4+ too old + TestSpec.build() + .tool(tool("foo").wix("4.0.3")) + ).forEach(appendTestCases); + + return testCases; + } + + private static Collection testLookupDirs() { + + List testCases = new ArrayList<>(); + + Stream.of( + EnvironmentTestSpec.build() + .env(EnvironmentVariable.USERPROFILE, "@@/foo") + .expect("@USERPROFILE@/.dotnet/tools"), + EnvironmentTestSpec.build() + .env(SystemProperty.USER_HOME, "@@/bar") + .expect("@user.home@/.dotnet/tools"), + // "USERPROFILE" environment variable and "user.home" system property set to different values, + // the order should be "USERPROFILE" followed by "user.home". + EnvironmentTestSpec.build() + .env(EnvironmentVariable.USERPROFILE, "@@/foo") + .env(SystemProperty.USER_HOME, "@@/bar") + .expect("@USERPROFILE@/.dotnet/tools") + .expect("@user.home@/.dotnet/tools"), + // "USERPROFILE" environment variable and "user.home" system property set to the same value. + EnvironmentTestSpec.build() + .env(EnvironmentVariable.USERPROFILE, "@@/buz") + .env(SystemProperty.USER_HOME, "@@/buz") + .expect("@USERPROFILE@/.dotnet/tools"), + // WiX3: newer versions first; 32bit after 64bit + EnvironmentTestSpec.build() + .standardEnv(EnvironmentVariable.PROGRAM_FILES_X86) + .standardEnv(EnvironmentVariable.PROGRAM_FILES) + .expect(String.format("@%s@/WiX Toolset v3.11/bin", EnvironmentVariable.PROGRAM_FILES_X86.variableName())) + .expect(String.format("@%s@/WiX Toolset v3.10/bin", EnvironmentVariable.PROGRAM_FILES.variableName())) + .expect(String.format("@%s@/WiX Toolset v3.10/bin", EnvironmentVariable.PROGRAM_FILES_X86.variableName())), + // Malformed installation directory should be accepted + EnvironmentTestSpec.build() + .standardEnv(EnvironmentVariable.PROGRAM_FILES_X86) + .expect(String.format("@%s@/WiX Toolset vb/bin", EnvironmentVariable.PROGRAM_FILES_X86.variableName())) + .expect(String.format("@%s@/WiX Toolset va/bin", EnvironmentVariable.PROGRAM_FILES_X86.variableName())) + .expect(String.format("@%s@/WiX Toolset v/bin", EnvironmentVariable.PROGRAM_FILES_X86.variableName())), + // No directories + EnvironmentTestSpec.build() + ).map(EnvironmentTestSpec.Builder::create).forEach(testCases::add); + + return testCases; + } + + private enum ToolStatus { + GOOD, + MISSING, + UNEXPECTED_STDOUT, + ; + + static boolean isAllGood(ToolStatus... status) { + return Stream.of(status).allMatch(Predicate.isEqual(GOOD)); + } + + Optional map(WixToolMock builder) { + return switch (this) { + case MISSING -> { + yield Optional.empty(); + } + case UNEXPECTED_STDOUT -> { + var mock = builder.create(); + yield Optional.of(new CommandMockSpec( + mock.name(), + mock.mockName(), + CommandActionSpecs.build().stdout("Blah-Blah-Blah").exit().create())); + } + case GOOD -> { + yield Optional.of(builder.create()); + } + }; + } + } + + record TestSpec( + Optional expected, + List lookupDirs, + boolean lookupInPATH, + Collection mocks, + List expectedErrors) { + + TestSpec { + Objects.requireNonNull(expected); + lookupDirs.forEach(Objects::requireNonNull); + mocks.forEach(Objects::requireNonNull); + expectedErrors.forEach(Objects::requireNonNull); + + if (expected.isEmpty() == expectedErrors.isEmpty()) { + // It should be either toolset or errors, not both or non both. + throw new IllegalArgumentException(); + } + + lookupDirs.forEach(WixToolTest::assertIsRelative); + + lookupDirs.forEach(path -> { + assertNotEquals(LOOKUP_IN_PATH, path); + }); + + // Ensure tool paths are unique. + mocks.stream().map(CommandMockSpec::name).collect(toMap(x -> x, x -> x)); + } + + @Override + public String toString() { + var tokens = new ArrayList(); + expected.map(Object::toString).ifPresent(tokens::add); + if (!expectedErrors.isEmpty()) { + tokens.add(String.format("errors=%s", expectedErrors)); + } + + List lookupPaths; + if (lookupInPATH) { + lookupPaths = new ArrayList<>(); + lookupPaths.add(Path.of("${PATH}")); + lookupPaths.addAll(lookupDirs); + } else { + lookupPaths = lookupDirs; + } + + if (!lookupPaths.isEmpty()) { + tokens.add(String.format("lookup-dirs=%s", lookupPaths)); + } + if (!mocks.isEmpty()) { + tokens.add(mocks.toString()); + } + return String.join(", ", tokens); + } + + void run(Path workDir) { + var scriptBuilder = Script.build().commandMockBuilderMutator(CommandMock.Builder::repeatInfinitely); + mocks.stream().map(mockSpec -> { + Path toolPath = mockSpec.name(); + if (toolPath.getNameCount() > 1) { + toolPath = workDir.resolve(toolPath); + } + return new CommandMockSpec(toolPath, mockSpec.mockName(), mockSpec.actions()); + }).forEach(scriptBuilder::map); + + scriptBuilder.map(_ -> true, CommandMock.ioerror("non-existent")); + + var script = scriptBuilder.createLoop(); + + Supplier createToolset = () -> { + return WixTool.createToolset(() -> { + return lookupDirs.stream().map(workDir::resolve).toList(); + }, lookupInPATH()); + }; + + Globals.main(() -> { + JPackageMockUtils.buildJPackage() + .script(script) + .listener(System.out::println) + .applyToGlobals(); + + expected.ifPresentOrElse(expectedToolset -> { + var toolset = createToolset.get(); + assertEquals(resolveAt(expectedToolset, workDir), toolset); + }, () -> { + var ex = assertThrows(RuntimeException.class, createToolset::get); + assertEquals(expectedErrors.getFirst().getValue(), ex.getMessage()); + if (ex instanceof ConfigException cfgEx) { + assertEquals(expectedErrors.getLast().getValue(), cfgEx.getAdvice()); + assertEquals(2, expectedErrors.size()); + } else { + assertEquals(1, expectedErrors.size()); + } + }); + + return 0; + }); + } + + static Builder build() { + return new Builder(); + } + + static final class Builder { + + TestSpec create() { + if (expected == null && expectedErrors.isEmpty()) { + return copy() + .expect("error.no-wix-tools") + .expect("error.no-wix-tools.advice") + .create(); + } else { + var allLookupDirs = Stream.concat( + lookupDirs.stream(), + tools.stream().map(CommandMockSpec::name).map(toolPath -> { + if (toolPath.getNameCount() == 1) { + return LOOKUP_IN_PATH; + } else { + return toolPath.getParent(); + } + }) + ).distinct().collect(Collectors.toCollection(ArrayList::new)); + + var lookupInPATH = allLookupDirs.contains(LOOKUP_IN_PATH); + if (lookupInPATH) { + allLookupDirs.remove(LOOKUP_IN_PATH); + } + + return new TestSpec( + Optional.ofNullable(expected), + Collections.unmodifiableList(allLookupDirs), + lookupInPATH, + List.copyOf(tools), + List.copyOf(expectedErrors)); + } + } + + Builder copy() { + return new Builder(this); + } + + private Builder() { + expectedErrors = new ArrayList<>(); + lookupDirs = new ArrayList<>(); + tools = new ArrayList<>(); + } + + private Builder(Builder other) { + expected = other.expected; + expectedErrors = new ArrayList<>(other.expectedErrors); + lookupDirs = new ArrayList<>(other.lookupDirs); + tools = new ArrayList<>(other.tools); + } + + Builder expect(WixToolset v) { + expected = v; + return this; + } + + Builder expect(String formatKey, Object ... args) { + expectedErrors.add(JPackageStringBundle.MAIN.cannedFormattedString(formatKey, args)); + return this; + } + + Builder expect(WixToolsetBuilder builder) { + return expect(builder.create()); + } + + Builder lookupDir(String v) { + return lookupDir(Path.of(v)); + } + + Builder lookupDir(Path v) { + lookupDirs.add(Objects.requireNonNull(v)); + return this; + } + + Builder tool(CommandMockSpec v) { + tools.add(Objects.requireNonNull(v)); + return this; + } + + Builder tool(WixToolMock v) { + return tool(v.create()); + } + + private WixToolset expected; + private final List expectedErrors; + private final List lookupDirs; + private final List tools; + } + } + + private static final class WixToolsetBuilder { + + WixToolset create() { + return new WixToolset(tools.entrySet().stream().collect(toMap(Map.Entry::getKey, e -> { + ToolInfo toolInfo = new WixTool.DefaultToolInfo(e.getValue(), version); + if (e.getKey() == WixTool.Candle3) { + toolInfo = new WixTool.DefaultCandleInfo(toolInfo, fips); + } + return toolInfo; + }))); + } + + WixToolsetBuilder version(String v) { + version = v; + return this; + } + + WixToolsetBuilder put(WixTool tool, String path) { + return put(tool, Path.of(path)); + } + + WixToolsetBuilder put(WixTool tool, Path path) { + tools.put(Objects.requireNonNull(tool), path.resolve(tool.fileName())); + return this; + } + + WixToolsetBuilder put(WixTool tool) { + return put(tool, LOOKUP_IN_PATH); + } + + WixToolsetBuilder put(WixToolsetType type, Path path) { + type.getTools().forEach(tool -> { + put(tool, path); + }); + return this; + } + + WixToolsetBuilder put(WixToolsetType type, String path) { + return put(type, Path.of(path)); + } + + WixToolsetBuilder put(WixToolsetType type) { + return put(type, LOOKUP_IN_PATH); + } + + WixToolsetBuilder fips(boolean v) { + fips = true; + return this; + } + + WixToolsetBuilder fips() { + return fips(true); + } + + private Map tools = new HashMap<>(); + private boolean fips; + private String version; + } + + enum EnvironmentVariable { + USERPROFILE("USERPROFILE"), + PROGRAM_FILES("ProgramFiles"), + PROGRAM_FILES_X86("ProgramFiles(x86)"), + SYSTEM_DRIVE("SystemDrive"), + ; + + EnvironmentVariable(String variableName) { + this.variableName = Objects.requireNonNull(variableName); + } + + String variableName() { + return variableName; + } + + private final String variableName; + } + + enum SystemProperty { + USER_HOME("user.home"), + ; + + SystemProperty(String propertyName) { + this.propertyName = Objects.requireNonNull(propertyName); + } + + String propertyName() { + return propertyName; + } + + private final String propertyName; + } + + record EnvironmentTestSpec(EnvironmentProviderMock env, List expectedDirs) { + + EnvironmentTestSpec { + Objects.requireNonNull(env); + expectedDirs.forEach(dir -> { + if (dir.isAbsolute()) { + throw new IllegalArgumentException(); + } + }); + } + + @Override + public String toString() { + var tokens = new ArrayList(); + tokens.add(String.format("expect=%s", expectedDirs)); + tokens.add(env.toString()); + return String.join(", ", tokens); + } + + void run(Path workDir) throws IOException { + var allResolved = resolve(workDir, Stream.of( + env.envVariables().entrySet().stream(), + env.systemProperties().entrySet().stream(), + expectedDirs.stream().map(Path::toString).map(dir -> { + return Map.entry(dir, dir); + }) + ).flatMap(x -> x).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))); + + Function>, Map> filterAllResolved = filterSupplier -> { + var filter = filterSupplier.get(); + return allResolved.entrySet().stream().filter(e -> { + return filter.containsKey(e.getKey()); + }).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + }; + + var resolvedEnv = new EnvironmentProviderMock( + filterAllResolved.apply(env::envVariables), + filterAllResolved.apply(env::systemProperties)); + + var resolvedDirs = expectedDirs.stream().map(Path::toString).map(allResolved::get).map(Path::of).toList(); + + for (var dir : resolvedDirs) { + Files.createDirectories(dir); + } + + Globals.main(() -> { + Globals.instance().system(resolvedEnv); + assertEquals(resolvedDirs, WixTool.findWixInstallDirs()); + return 0; + }); + } + + static Builder build() { + return new Builder(); + } + + static final class Builder { + + EnvironmentTestSpec create() { + var env = envVariables.entrySet().stream().collect(Collectors.toMap(e -> { + return e.getKey().variableName(); + }, Map.Entry::getValue)); + + var props = systemProperties.entrySet().stream().collect(Collectors.toMap(e -> { + return e.getKey().propertyName(); + }, Map.Entry::getValue)); + + return new EnvironmentTestSpec(new EnvironmentProviderMock(env, props), List.copyOf(expectedDirs)); + } + + Builder expect(List dirs) { + expectedDirs.addAll(dirs); + return this; + } + + Builder expect(Path... dirs) { + return expect(List.of(dirs)); + } + + Builder expect(String... dirs) { + return expect(List.of(dirs).stream().map(Path::of).toList()); + } + + Builder env(SystemProperty k, String v) { + systemProperties.put(Objects.requireNonNull(k), Objects.requireNonNull(v)); + return this; + } + + Builder env(EnvironmentVariable k, String v) { + envVariables.put(Objects.requireNonNull(k), Objects.requireNonNull(v)); + return this; + } + + Builder standardEnv(EnvironmentVariable k) { + var value = switch (k) { + case PROGRAM_FILES -> "Program Files"; + case PROGRAM_FILES_X86 -> "Program Files(x86)"; + default -> { + throw new IllegalArgumentException(); + } + }; + return env(k, "@@/" + value); + } + + private final Map envVariables = new HashMap<>(); + private final Map systemProperties = new HashMap<>(); + private final List expectedDirs = new ArrayList<>(); + } + + private static Map resolve(Path workDir, Map props) { + + var tokens = new ArrayList(); + + Stream.of( + Stream.of(EnvironmentVariable.values()).map(EnvironmentVariable::variableName), + Stream.of(SystemProperty.values()).map(SystemProperty::propertyName) + ).flatMap(x -> x).map(str -> { + return String.format("@%s@", str); + }).forEach(tokens::add); + + tokens.add(TOKEN_WORKDIR); + + var tokenReplace = new TokenReplace(tokens.toArray(String[]::new)); + + return props.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> { + return tokenReplace.recursiveApplyTo(e.getValue(), token -> { + if (token.equals(TOKEN_WORKDIR)) { + return workDir; + } else { + return Objects.requireNonNull(props.get(token.substring(1, token.length() - 1)), () -> { + return String.format("Unrecognized token: [%s]", token); + }); + } + }); + })); + } + + static final String TOKEN_WORKDIR = "@@"; + } + + private static WixToolsetBuilder toolset() { + return new WixToolsetBuilder(); + } + + private static WixToolMock tool() { + return new WixToolMock(); + } + + private static WixToolMock tool(Path dir) { + return tool().dir(dir); + } + + private static WixToolMock tool(String dir) { + return tool(Path.of(dir)); + } + + private static WixToolset resolveAt(WixToolset toolset, Path root) { + return new WixToolset(toolset.tools().entrySet().stream().collect(toMap(Map.Entry::getKey, e -> { + var toolInfo = e.getValue(); + + assertIsRelative(toolInfo.path()); + + if (toolInfo.path().getNameCount() == 1) { + // The tool is picked from the PATH. + return toolInfo; + } + + ToolInfo newToolInfo = new WixTool.DefaultToolInfo(root.resolve(toolInfo.path()), toolInfo.version()); + if (toolInfo instanceof WixTool.CandleInfo candleInfo) { + newToolInfo = new WixTool.DefaultCandleInfo(newToolInfo, candleInfo.fips()); + } + return newToolInfo; + }))); + } + + private static void assertIsRelative(Path path) { + if (path.isAbsolute()) { + throw new IllegalArgumentException(); + } + } + + static final Path LOOKUP_IN_PATH = Path.of(""); +} + diff --git a/test/jdk/tools/jpackage/junit/windows/junit.java b/test/jdk/tools/jpackage/junit/windows/junit.java index 1a1d0d58f7e..8c290c2c87f 100644 --- a/test/jdk/tools/jpackage/junit/windows/junit.java +++ b/test/jdk/tools/jpackage/junit/windows/junit.java @@ -45,3 +45,15 @@ * jdk/jpackage/internal/wixui/UISpecTest.java * @run junit jdk.jpackage/jdk.jpackage.internal.wixui.UISpecTest */ + +/* @test + * @summary Test WiX Toolset lookup algorithm + * @requires (os.family == "windows") + * @library /test/jdk/tools/jpackage/helpers + * @build jdk.jpackage.test.* + * @build jdk.jpackage.test.mock.* + * @build jdk.jpackage.test.stdmock.* + * @compile/module=jdk.jpackage -Xlint:all -Werror + * jdk/jpackage/internal/WixToolTest.java + * @run junit jdk.jpackage/jdk.jpackage.internal.WixToolTest + */