| /* | |
| * 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.Files; | |
| import java.nio.file.Path; | |
| import java.util.ArrayList; | |
| 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.Set; | |
| import java.util.function.Function; | |
| import java.util.function.UnaryOperator; | |
| import java.util.stream.Collectors; | |
| import java.util.stream.Stream; | |
| import static jdk.jpackage.internal.ShortPathUtils.adjustPath; | |
| import jdk.jpackage.internal.util.PathUtils; | |
| /** | |
| * WiX pipeline. Compiles and links WiX sources. | |
| */ | |
| final class WixPipeline { | |
| static final class Builder { | |
| Builder() { | |
| } | |
| WixPipeline create(WixToolset toolset) { | |
| Objects.requireNonNull(toolset); | |
| Objects.requireNonNull(workDir); | |
| Objects.requireNonNull(wixObjDir); | |
| if (sources.isEmpty()) { | |
| throw new IllegalArgumentException("no sources"); | |
| } | |
| final var absWorkDir = workDir.normalize().toAbsolutePath(); | |
| final UnaryOperator<Path> normalizePath = path -> { | |
| return path.normalize().toAbsolutePath(); | |
| }; | |
| final var absObjWorkDir = normalizePath.apply(wixObjDir); | |
| var relSources = sources.stream().map(source -> { | |
| return source.overridePath(normalizePath.apply(source.path)); | |
| }).toList(); | |
| return new WixPipeline(toolset, adjustPath(absWorkDir), absObjWorkDir, | |
| wixVariables, mapLightOptions(normalizePath), relSources); | |
| } | |
| Builder setWixObjDir(Path v) { | |
| wixObjDir = v; | |
| return this; | |
| } | |
| Builder setWorkDir(Path v) { | |
| workDir = v; | |
| return this; | |
| } | |
| Builder setWixVariables(Map<String, String> v) { | |
| wixVariables.clear(); | |
| wixVariables.putAll(v); | |
| return this; | |
| } | |
| Builder addSource(Path source, Map<String, String> wixVariables) { | |
| sources.add(new WixSource(source, wixVariables)); | |
| return this; | |
| } | |
| Builder addLightOptions(String ... v) { | |
| lightOptions.addAll(List.of(v)); | |
| return this; | |
| } | |
| private List<String> mapLightOptions(UnaryOperator<Path> normalizePath) { | |
| var pathOptions = Set.of("-b", "-loc"); | |
| List<String> reply = new ArrayList<>(); | |
| boolean convPath = false; | |
| for (var opt : lightOptions) { | |
| if (convPath) { | |
| opt = normalizePath.apply(Path.of(opt)).toString(); | |
| convPath = false; | |
| } else if (pathOptions.contains(opt)) { | |
| convPath = true; | |
| } | |
| reply.add(opt); | |
| } | |
| return reply; | |
| } | |
| private Path workDir; | |
| private Path wixObjDir; | |
| private final Map<String, String> wixVariables = new HashMap<>(); | |
| private final List<String> lightOptions = new ArrayList<>(); | |
| private final List<WixSource> sources = new ArrayList<>(); | |
| } | |
| static Builder build() { | |
| return new Builder(); | |
| } | |
| private WixPipeline(WixToolset toolset, Path workDir, Path wixObjDir, | |
| Map<String, String> wixVariables, List<String> lightOptions, | |
| List<WixSource> sources) { | |
| this.toolset = toolset; | |
| this.workDir = workDir; | |
| this.wixObjDir = wixObjDir; | |
| this.wixVariables = wixVariables; | |
| this.lightOptions = lightOptions; | |
| this.sources = sources; | |
| } | |
| void buildMsi(Path msi) throws IOException { | |
| Objects.requireNonNull(workDir); | |
| // Use short path to the output msi to workaround | |
| // WiX limitations of handling long paths. | |
| var transientMsi = wixObjDir.resolve("a.msi"); | |
| switch (toolset.getType()) { | |
| case Wix3 -> buildMsiWix3(transientMsi); | |
| case Wix4 -> buildMsiWix4(transientMsi); | |
| default -> throw new IllegalArgumentException(); | |
| } | |
| IOUtils.copyFile(workDir.resolve(transientMsi), msi); | |
| } | |
| private void addWixVariblesToCommandLine( | |
| Map<String, String> otherWixVariables, List<String> cmdline) { | |
| Stream.of(wixVariables, Optional.ofNullable(otherWixVariables). | |
| orElseGet(Collections::emptyMap)).filter(Objects::nonNull). | |
| reduce((a, b) -> { | |
| a.putAll(b); | |
| return a; | |
| }).ifPresent(wixVars -> { | |
| var entryStream = wixVars.entrySet().stream(); | |
| Stream<String> stream; | |
| switch (toolset.getType()) { | |
| case Wix3 -> { | |
| stream = entryStream.map(wixVar -> { | |
| return String.format("-d%s=%s", wixVar.getKey(), wixVar. | |
| getValue()); | |
| }); | |
| } | |
| case Wix4 -> { | |
| stream = entryStream.map(wixVar -> { | |
| return Stream.of("-d", String.format("%s=%s", wixVar. | |
| getKey(), wixVar.getValue())); | |
| }).flatMap(Function.identity()); | |
| } | |
| default -> { | |
| throw new IllegalArgumentException(); | |
| } | |
| } | |
| stream.reduce(cmdline, (ctnr, wixVar) -> { | |
| ctnr.add(wixVar); | |
| return ctnr; | |
| }, (x, y) -> { | |
| x.addAll(y); | |
| return x; | |
| }); | |
| }); | |
| } | |
| private void buildMsiWix4(Path msi) throws IOException { | |
| var mergedSrcWixVars = sources.stream().map(wixSource -> { | |
| return Optional.ofNullable(wixSource.variables).orElseGet( | |
| Collections::emptyMap).entrySet().stream(); | |
| }).flatMap(Function.identity()).collect(Collectors.toMap( | |
| Map.Entry::getKey, Map.Entry::getValue)); | |
| List<String> cmdline = new ArrayList<>(List.of( | |
| toolset.getToolPath(WixTool.Wix4).toString(), | |
| "build", | |
| "-nologo", | |
| "-pdbtype", "none", | |
| "-intermediatefolder", wixObjDir.toString(), | |
| "-ext", "WixToolset.Util.wixext", | |
| "-arch", WixFragmentBuilder.is64Bit() ? "x64" : "x86" | |
| )); | |
| cmdline.addAll(lightOptions); | |
| addWixVariblesToCommandLine(mergedSrcWixVars, cmdline); | |
| cmdline.addAll(sources.stream().map(wixSource -> { | |
| return wixSource.path.toString(); | |
| }).toList()); | |
| cmdline.addAll(List.of("-out", msi.toString())); | |
| execute(cmdline); | |
| } | |
| private void buildMsiWix3(Path msi) throws IOException { | |
| List<Path> wixObjs = new ArrayList<>(); | |
| for (var source : sources) { | |
| wixObjs.add(compileWix3(source)); | |
| } | |
| List<String> lightCmdline = new ArrayList<>(List.of( | |
| toolset.getToolPath(WixTool.Light3).toString(), | |
| "-nologo", | |
| "-spdb", | |
| "-ext", "WixUtilExtension", | |
| "-out", msi.toString() | |
| )); | |
| lightCmdline.addAll(lightOptions); | |
| wixObjs.stream().map(Path::toString).forEach(lightCmdline::add); | |
| Files.createDirectories(IOUtils.getParent(msi)); | |
| execute(lightCmdline); | |
| } | |
| private Path compileWix3(WixSource wixSource) throws IOException { | |
| Path wixObj = wixObjDir.toAbsolutePath().resolve(PathUtils.replaceSuffix( | |
| wixSource.path.getFileName(), ".wixobj")); | |
| List<String> cmdline = new ArrayList<>(List.of( | |
| toolset.getToolPath(WixTool.Candle3).toString(), | |
| "-nologo", | |
| wixSource.path.toString(), | |
| "-ext", "WixUtilExtension", | |
| "-arch", WixFragmentBuilder.is64Bit() ? "x64" : "x86", | |
| "-out", wixObj.toString() | |
| )); | |
| if (toolset.needFipsParameter()) { | |
| cmdline.add("-fips"); | |
| } | |
| addWixVariblesToCommandLine(wixSource.variables, cmdline); | |
| execute(cmdline); | |
| return wixObj; | |
| } | |
| private void execute(List<String> cmdline) throws IOException { | |
| Executor.of(new ProcessBuilder(cmdline).directory(workDir.toFile())).executeExpectSuccess(); | |
| } | |
| private record WixSource(Path path, Map<String, String> variables) { | |
| WixSource overridePath(Path path) { | |
| return new WixSource(path, variables); | |
| } | |
| } | |
| private final WixToolset toolset; | |
| private final Map<String, String> wixVariables; | |
| private final List<String> lightOptions; | |
| private final Path wixObjDir; | |
| private final Path workDir; | |
| private final List<WixSource> sources; | |
| } |