blob: 7e4353876b43a19f0daac5d7641dfca0554bdb7c [file]
/*
* Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.jpackage.internal;
import java.io.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;
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.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;
/**
* WiX tool.
*/
public enum WixTool {
Candle3("candle", DottedVersion.lazy("3.0")),
Light3("light", DottedVersion.lazy("3.0")),
Wix4("wix", DottedVersion.lazy("4.0.4"));
WixTool(String commandName, DottedVersion minimalVersion) {
this.toolFileName = PathUtils.addSuffix(Path.of(commandName), ".exe");
this.minimalVersion = minimalVersion;
}
interface ToolInfo {
Path path();
DottedVersion version();
}
interface CandleInfo extends ToolInfo {
boolean fips();
}
private record DefaultToolInfo(Path path, DottedVersion version) implements ToolInfo {
DefaultToolInfo {
Objects.requireNonNull(path);
Objects.requireNonNull(version);
}
DefaultToolInfo(Path path, String version) {
this(path, DottedVersion.lazy(version));
}
}
private record DefaultCandleInfo(Path path, DottedVersion version, boolean fips) implements CandleInfo {
DefaultCandleInfo {
Objects.requireNonNull(path);
Objects.requireNonNull(version);
}
DefaultCandleInfo(ToolInfo info, boolean fips) {
this(info.path(), info.version(), fips);
}
}
static WixToolset createToolset() throws ConfigException {
Function<List<ToolLookupResult>, Map<WixTool, ToolInfo>> conv = lookupResults -> {
return lookupResults.stream().filter(ToolLookupResult::isValid).collect(Collectors.
groupingBy(lookupResult -> {
return lookupResult.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))) {
// 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) -> {
return Stream.of(x, y).sorted(Comparator.comparing((ToolInfo toolInfo) -> {
return toolInfo.version().toComponentsString();
}).reversed()).findFirst().get();
}));
};
Function<List<ToolLookupResult>, Optional<WixToolset>> createToolset = lookupResults -> {
var tools = conv.apply(lookupResults);
// Try to build a toolset found in the PATH and in known locations.
return Stream.of(WixToolsetType.values()).map(toolsetType -> {
return WixToolset.create(toolsetType.getTools(), tools);
}).filter(Objects::nonNull).findFirst();
};
var toolsInPath = Stream.of(values()).map(tool -> {
return ToolLookupResult.lookup(tool, Optional.empty());
}).filter(Optional::isPresent).map(Optional::get).toList();
// Try to build a toolset from tools in the PATH first.
var toolset = createToolset.apply(toolsInPath);
if (toolset.isPresent()) {
return toolset.get();
}
// 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));
});
}).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"));
}
}
}
private record ToolLookupResult(WixTool tool, ToolInfo info, boolean versionTooOld) {
ToolLookupResult {
Objects.requireNonNull(tool);
Objects.requireNonNull(info);
}
static Optional<ToolLookupResult> lookup(WixTool tool, Optional<Path> lookupDir) {
Objects.requireNonNull(tool);
Objects.requireNonNull(lookupDir);
final Path toolPath = lookupDir.map(p -> p.resolve(
tool.toolFileName)).orElse(tool.toolFileName);
final boolean[] tooOld = new boolean[1];
final String[] parsedVersion = new String[1];
final var validator = new ToolValidator(toolPath).setMinimalVersion(tool.minimalVersion).
setToolNotFoundErrorHandler((name, ex) -> {
return new ConfigException("", "");
}).setToolOldVersionErrorHandler((name, version) -> {
tooOld[0] = true;
return null;
});
final Function<Stream<String>, String> versionParser;
if (Set.of(Candle3, Light3).contains(tool)) {
final String printVersionArg;
if (tool == 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 = "-?";
}
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("");
};
}
validator.setVersionParser(output -> {
parsedVersion[0] = versionParser.apply(output);
return parsedVersion[0];
});
if (validator.validate() == null) {
// Tool found
ToolInfo info = new DefaultToolInfo(toolPath, parsedVersion[0]);
if (tool == Candle3) {
// Detect FIPS mode
var fips = false;
try {
final var exec = Executor.of(toolPath.toString(), "-?").setQuiet(true).saveOutput(true);
final var exitCode = exec.execute();
if (exitCode != 0 /* 308 */) {
final var output = exec.getOutput();
if (!output.isEmpty() && output.get(0).contains("error CNDL0308")) {
fips = true;
}
}
} catch (IOException ex) {
Log.verbose(ex);
}
info = new DefaultCandleInfo(info, fips);
}
return Optional.of(new ToolLookupResult(tool, info, tooOld[0]));
} else {
return Optional.empty();
}
}
boolean isValid() {
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 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 List<Path> findWixCurrentInstallDirs() {
return Stream.of(getEnvVariableAsPath("USERPROFILE"), Optional.ofNullable(System.
getProperty("user.home")).map(Path::of).orElse(null)).filter(Objects::nonNull).map(
path -> {
return path.resolve(".dotnet/tools");
}).filter(Files::isDirectory).distinct().toList();
}
private static List<Path> findWix3InstallDirs() {
PathMatcher wixInstallDirMatcher = FileSystems.getDefault().
getPathMatcher(
"glob:WiX Toolset v*");
Path programFiles = getSystemDir("ProgramFiles", "\\Program Files");
Path programFilesX86 = getSystemDir("ProgramFiles(x86)",
"\\Program Files (x86)");
// Returns list of WiX install directories ordered by WiX version number.
// Newer versions go first.
return Stream.of(programFiles, programFilesX86).map(path -> {
try (var paths = Files.walk(path, 1)) {
return paths.toList();
} catch (IOException ex) {
Log.verbose(ex);
List<Path> empty = List.of();
return empty;
}
}).flatMap(List::stream)
.filter(path -> wixInstallDirMatcher.matches(path.getFileName())).
sorted(Comparator.comparing(Path::getFileName).reversed())
.map(path -> path.resolve("bin"))
.toList();
}
private final Path toolFileName;
private final DottedVersion minimalVersion;
}