8381384: jpackage: add test coverage to WiX discovery

Reviewed-by: almatvee
This commit is contained in:
Alexey Semenyuk 2026-04-01 00:29:44 +00:00
parent 3f6271b2b9
commit c76381996a
12 changed files with 1459 additions and 163 deletions

View File

@ -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);
}
};
}

View File

@ -65,6 +65,14 @@ public final class Globals {
return this;
}
public EnvironmentProvider system() {
return this.<EnvironmentProvider>findProperty(EnvironmentProvider.class).orElse(EnvironmentProvider.DEFAULT);
}
public Globals system(EnvironmentProvider v) {
return setProperty(EnvironmentProvider.class, v);
}
Log.Logger logger() {
return logger;
}

View File

@ -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<Path> asPath(String value) {
return Optional.ofNullable(value).map(v -> {
try {
return Path.of(v);
} catch (InvalidPathException ex) {
return null;
}
});
}
}

View File

@ -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<List<Path>> wixInstallDirs, boolean searchInPath) {
Function<List<ToolLookupResult>, Map<WixTool, ToolInfo>> 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<WixTool> 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<List<ToolLookupResult>, Optional<WixToolset>> createToolset = lookupResults -> {
var tools = conv.apply(lookupResults);
// Try to build a toolset found in the PATH and in known locations.
return Stream.of(WixToolsetType.values()).map(toolsetType -> {
return WixToolset.create(toolsetType.getTools(), tools);
}).filter(Objects::nonNull).findFirst();
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<ToolLookupResult> 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<Path> 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<Stream<String>, 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<Stream<String>, Optional<String>> 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.<String>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<Path> findWixInstallDirs() {
return Stream.of(findWixCurrentInstallDirs(), findWix3InstallDirs()).
flatMap(List::stream).toList();
private static Optional<Path> getEnvVariableAsPath(String envVar) {
Objects.requireNonNull(envVar);
return Optional.ofNullable(Globals.instance().system().getenv(envVar)).flatMap(PathUtils::asPath);
}
private static List<Path> findWixCurrentInstallDirs() {
return Stream.of(getEnvVariableAsPath("USERPROFILE"), Optional.ofNullable(System.
getProperty("user.home")).map(Path::of).orElse(null)).filter(Objects::nonNull).map(
path -> {
return path.resolve(".dotnet/tools");
}).filter(Files::isDirectory).distinct().toList();
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<Path> 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<Path> empty = List.of();
return empty;
return List.<Path>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();
}

View File

@ -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<WixTool, WixTool.ToolInfo> 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<WixTool> tools;
}
private WixToolset(Map<WixTool, WixTool.ToolInfo> tools) {
this.tools = tools;
}
WixToolsetType getType() {
return Stream.of(WixToolsetType.values()).filter(toolsetType -> {
return toolsetType.getTools().equals(tools.keySet());
@ -75,16 +77,19 @@ final class WixToolset {
.anyMatch(WixTool.CandleInfo::fips);
}
static WixToolset create(Set<WixTool> requiredTools, Map<WixTool, WixTool.ToolInfo> allTools) {
static Optional<WixToolset> create(WixToolsetType type, Map<WixTool, WixTool.ToolInfo> 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<WixTool, WixTool.ToolInfo> tools;
}

View File

@ -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}.

View File

@ -160,5 +160,5 @@ abstract sealed class MockingToolProvider implements ToolProviderCommandMock {
private final String name;
private final Iterator<CommandAction> actionIter;
static ToolProviderCommandMock UNREACHABLE = new MockingToolProvider.NonCompletable("<unreachable>", List.of());
static final ToolProviderCommandMock UNREACHABLE = new MockingToolProvider.NonCompletable("<unreachable>", List.of());
}

View File

@ -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<String, String> envVariables,
Map<String, String> 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<String>();
if (!envVariables.isEmpty()) {
tokens.add(String.format("env=%s", envVariables));
}
if (!systemProperties.isEmpty()) {
tokens.add(String.format("props=%s", systemProperties));
}
return String.join(", ", tokens);
}
}

View File

@ -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;
}

View File

@ -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<Path> {
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");
}
},
;
}
}

View File

@ -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<TestSpec> testLookup() {
List<TestSpec> testCases = new ArrayList<>();
Consumer<TestSpec.Builder> 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<EnvironmentTestSpec> testLookupDirs() {
List<EnvironmentTestSpec> 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<CommandMockSpec> 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<WixToolset> expected,
List<Path> lookupDirs,
boolean lookupInPATH,
Collection<CommandMockSpec> mocks,
List<CannedFormattedString> 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<String>();
expected.map(Object::toString).ifPresent(tokens::add);
if (!expectedErrors.isEmpty()) {
tokens.add(String.format("errors=%s", expectedErrors));
}
List<Path> 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<WixToolset> 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<CannedFormattedString> expectedErrors;
private final List<Path> lookupDirs;
private final List<CommandMockSpec> 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<WixTool, Path> 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<Path> expectedDirs) {
EnvironmentTestSpec {
Objects.requireNonNull(env);
expectedDirs.forEach(dir -> {
if (dir.isAbsolute()) {
throw new IllegalArgumentException();
}
});
}
@Override
public String toString() {
var tokens = new ArrayList<String>();
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<Supplier<Map<String, String>>, Map<String, String>> 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<Path> 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<EnvironmentVariable, String> envVariables = new HashMap<>();
private final Map<SystemProperty, String> systemProperties = new HashMap<>();
private final List<Path> expectedDirs = new ArrayList<>();
}
private static Map<String, String> resolve(Path workDir, Map<String, String> props) {
var tokens = new ArrayList<String>();
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("");
}

View File

@ -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
*/