8289771: jpackage: ResourceEditor error when path is overly long on Windows

Reviewed-by: almatvee
This commit is contained in:
Alexey Semenyuk 2024-11-20 16:54:51 +00:00
parent c4c6b1fe06
commit 080f1cc8cd
18 changed files with 504 additions and 101 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2020, 2023, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2020, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -30,6 +30,7 @@ import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.text.MessageFormat;
import java.util.ArrayList;
@ -40,6 +41,7 @@ import java.util.Properties;
import java.util.ResourceBundle;
import java.util.function.Supplier;
import static jdk.jpackage.internal.OverridableResource.createResource;
import static jdk.jpackage.internal.ShortPathUtils.adjustPath;
import static jdk.jpackage.internal.StandardBundlerParam.APP_NAME;
import static jdk.jpackage.internal.StandardBundlerParam.COPYRIGHT;
import static jdk.jpackage.internal.StandardBundlerParam.DESCRIPTION;
@ -112,7 +114,7 @@ final class ExecutableRebrander {
}
private void rebrandExecutable(Map<String, ? super Object> params,
Path target, UpdateResourceAction action) throws IOException {
final Path target, UpdateResourceAction action) throws IOException {
try {
String tempDirectory = TEMP_ROOT.fetchFrom(params)
.toAbsolutePath().toString();
@ -125,10 +127,11 @@ final class ExecutableRebrander {
target.toFile().setWritable(true, true);
long resourceLock = lockResource(target.toString());
var shortTargetPath = ShortPathUtils.toShortPath(target);
long resourceLock = lockResource(shortTargetPath.orElse(target).toString());
if (resourceLock == 0) {
throw new RuntimeException(MessageFormat.format(
I18N.getString("error.lock-resource"), target));
I18N.getString("error.lock-resource"), shortTargetPath.orElse(target)));
}
final boolean resourceUnlockedSuccess;
@ -144,6 +147,14 @@ final class ExecutableRebrander {
resourceUnlockedSuccess = true;
} else {
resourceUnlockedSuccess = unlockResource(resourceLock);
if (shortTargetPath.isPresent()) {
// Windows will rename the excuatble in the unlock operation.
// Should restore executable's name.
var tmpPath = target.getParent().resolve(
target.getFileName().toString() + ".restore");
Files.move(shortTargetPath.get(), tmpPath);
Files.move(tmpPath, target);
}
}
}
@ -236,6 +247,7 @@ final class ExecutableRebrander {
private static void iconSwapWrapper(long resourceLock,
String iconTarget) {
iconTarget = adjustPath(iconTarget);
if (iconSwap(resourceLock, iconTarget) != 0) {
throw new RuntimeException(MessageFormat.format(I18N.getString(
"error.icon-swap"), iconTarget));

View File

@ -0,0 +1,93 @@
/*
* Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.jpackage.internal;
import java.nio.file.Files;
import java.nio.file.Path;
import java.text.MessageFormat;
import java.util.Objects;
import java.util.Optional;
@SuppressWarnings("restricted")
final class ShortPathUtils {
static String adjustPath(String path) {
return toShortPath(path).orElse(path);
}
static Path adjustPath(Path path) {
return toShortPath(path).orElse(path);
}
static Optional<String> toShortPath(String path) {
Objects.requireNonNull(path);
return toShortPath(Path.of(path)).map(Path::toString);
}
static Optional<Path> toShortPath(Path path) {
if (!Files.exists(path)) {
throw new IllegalArgumentException(String.format("[%s] path does not exist", path));
}
var normPath = path.normalize().toAbsolutePath().toString();
if (normPath.length() > MAX_PATH) {
return Optional.of(Path.of(getShortPathWrapper(normPath)));
} else {
return Optional.empty();
}
}
private static String getShortPathWrapper(final String longPath) {
String effectivePath;
if (!longPath.startsWith(LONG_PATH_PREFIX)) {
effectivePath = LONG_PATH_PREFIX + longPath;
} else {
effectivePath = longPath;
}
return Optional.ofNullable(getShortPath(effectivePath)).orElseThrow(
() -> new ShortPathException(MessageFormat.format(I18N.getString(
"error.short-path-conv-fail"), effectivePath)));
}
static final class ShortPathException extends RuntimeException {
ShortPathException(String msg) {
super(msg);
}
private static final long serialVersionUID = 1L;
}
private static native String getShortPath(String longPath);
private static final int MAX_PATH = 240;
// See https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getshortpathnamew
private static final String LONG_PATH_PREFIX = "\\\\?\\";
static {
System.loadLibrary("jpackage");
}
}

View File

@ -526,9 +526,10 @@ public class WinMsiBundler extends AbstractBundler {
"message.preparing-msi-config"), msiOut.toAbsolutePath()
.toString()));
WixPipeline wixPipeline = new WixPipeline()
.setToolset(wixToolset)
.setWixObjDir(TEMP_ROOT.fetchFrom(params).resolve("wixobj"))
var wixObjDir = TEMP_ROOT.fetchFrom(params).resolve("wixobj");
var wixPipeline = WixPipeline.build()
.setWixObjDir(wixObjDir)
.setWorkDir(WIN_APP_IMAGE.fetchFrom(params))
.addSource(CONFIG_ROOT.fetchFrom(params).resolve("main.wxs"),
wixVars);
@ -605,13 +606,13 @@ public class WinMsiBundler extends AbstractBundler {
// Cultures from custom files and a single primary Culture are
// included into "-cultures" list
for (var wxl : primaryWxlFiles) {
wixPipeline.addLightOptions("-loc", wxl.toAbsolutePath().normalize().toString());
wixPipeline.addLightOptions("-loc", wxl.toString());
}
List<String> cultures = new ArrayList<>();
for (var wxl : customWxlFiles) {
wxl = configDir.resolve(wxl.getFileName());
wixPipeline.addLightOptions("-loc", wxl.toAbsolutePath().normalize().toString());
wixPipeline.addLightOptions("-loc", wxl.toString());
cultures.add(getCultureFromWxlFile(wxl));
}
@ -638,7 +639,8 @@ public class WinMsiBundler extends AbstractBundler {
}
}
wixPipeline.buildMsi(msiOut.toAbsolutePath());
Files.createDirectories(wixObjDir);
wixPipeline.create(wixToolset).buildMsi(msiOut.toAbsolutePath());
return msiOut;
}
@ -678,14 +680,14 @@ public class WinMsiBundler extends AbstractBundler {
if (nodes.getLength() != 1) {
throw new IOException(MessageFormat.format(I18N.getString(
"error.extract-culture-from-wix-l10n-file"),
wxlPath.toAbsolutePath()));
wxlPath.toAbsolutePath().normalize()));
}
return nodes.item(0).getNodeValue();
} catch (XPathExpressionException | ParserConfigurationException
| SAXException ex) {
throw new IOException(MessageFormat.format(I18N.getString(
"error.read-wix-l10n-file"), wxlPath.toAbsolutePath()), ex);
"error.read-wix-l10n-file"), wxlPath.toAbsolutePath().normalize()), ex);
}
}

View File

@ -74,7 +74,7 @@ abstract class WixFragmentBuilder {
return List.of();
}
void configureWixPipeline(WixPipeline wixPipeline) {
void configureWixPipeline(WixPipeline.Builder wixPipeline) {
wixPipeline.addSource(configRoot.resolve(outputFileName),
Optional.ofNullable(wixVariables).map(WixVariables::getValues).orElse(
null));

View File

@ -29,65 +29,130 @@ 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.
*/
public class WixPipeline {
WixPipeline() {
sources = new ArrayList<>();
lightOptions = new ArrayList<>();
final class WixPipeline {
static final class Builder {
Builder() {
}
WixPipeline setToolset(WixToolset v) {
toolset = v;
return this;
WixPipeline create(WixToolset toolset) {
Objects.requireNonNull(toolset);
Objects.requireNonNull(workDir);
Objects.requireNonNull(wixObjDir);
if (sources.isEmpty()) {
throw new IllegalArgumentException("no sources");
}
WixPipeline setWixVariables(Map<String, String> v) {
wixVariables = v;
return this;
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);
}
WixPipeline setWixObjDir(Path v) {
Builder setWixObjDir(Path v) {
wixObjDir = v;
return this;
}
WixPipeline setWorkDir(Path v) {
Builder setWorkDir(Path v) {
workDir = v;
return this;
}
WixPipeline addSource(Path source, Map<String, String> wixVariables) {
WixSource entry = new WixSource();
entry.source = source;
entry.variables = wixVariables;
sources.add(entry);
Builder setWixVariables(Map<String, String> v) {
wixVariables.clear();
wixVariables.putAll(v);
return this;
}
WixPipeline addLightOptions(String ... v) {
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(msi);
case Wix4 -> buildMsiWix4(msi);
case Wix3 -> buildMsiWix3(transientMsi);
case Wix4 -> buildMsiWix4(transientMsi);
default -> throw new IllegalArgumentException();
}
IOUtils.copyFile(workDir.resolve(transientMsi), msi);
}
private void addWixVariblesToCommandLine(
@ -141,7 +206,7 @@ public class WixPipeline {
"build",
"-nologo",
"-pdbtype", "none",
"-intermediatefolder", wixObjDir.toAbsolutePath().toString(),
"-intermediatefolder", wixObjDir.toString(),
"-ext", "WixToolset.Util.wixext",
"-arch", WixFragmentBuilder.is64Bit() ? "x64" : "x86"
));
@ -151,7 +216,7 @@ public class WixPipeline {
addWixVariblesToCommandLine(mergedSrcWixVars, cmdline);
cmdline.addAll(sources.stream().map(wixSource -> {
return wixSource.source.toAbsolutePath().toString();
return wixSource.path.toString();
}).toList());
cmdline.addAll(List.of("-out", msi.toString()));
@ -182,15 +247,15 @@ public class WixPipeline {
private Path compileWix3(WixSource wixSource) throws IOException {
Path wixObj = wixObjDir.toAbsolutePath().resolve(PathUtils.replaceSuffix(
IOUtils.getFileName(wixSource.source), ".wixobj"));
wixSource.path.getFileName(), ".wixobj"));
List<String> cmdline = new ArrayList<>(List.of(
toolset.getToolPath(WixTool.Candle3).toString(),
"-nologo",
wixSource.source.toAbsolutePath().toString(),
wixSource.path.toString(),
"-ext", "WixUtilExtension",
"-arch", WixFragmentBuilder.is64Bit() ? "x64" : "x86",
"-out", wixObj.toAbsolutePath().toString()
"-out", wixObj.toString()
));
addWixVariblesToCommandLine(wixSource.variables, cmdline);
@ -201,19 +266,19 @@ public class WixPipeline {
}
private void execute(List<String> cmdline) throws IOException {
Executor.of(new ProcessBuilder(cmdline).directory(workDir.toFile())).
executeExpectSuccess();
Executor.of(new ProcessBuilder(cmdline).directory(workDir.toFile())).executeExpectSuccess();
}
private static final class WixSource {
Path source;
Map<String, String> variables;
private record WixSource(Path path, Map<String, String> variables) {
WixSource overridePath(Path path) {
return new WixSource(path, variables);
}
}
private WixToolset toolset;
private Map<String, String> wixVariables;
private List<String> lightOptions;
private Path wixObjDir;
private Path workDir;
private List<WixSource> sources;
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;
}

View File

@ -97,7 +97,7 @@ final class WixUiFragmentBuilder extends WixFragmentBuilder {
}
@Override
void configureWixPipeline(WixPipeline wixPipeline) {
void configureWixPipeline(WixPipeline.Builder wixPipeline) {
super.configureWixPipeline(wixPipeline);
if (withShortcutPromptDlg || withInstallDirChooserDlg || withLicenseDlg) {
@ -518,7 +518,7 @@ final class WixUiFragmentBuilder extends WixFragmentBuilder {
wxsFileName), wxsFileName);
}
void addToWixPipeline(WixPipeline wixPipeline) {
void addToWixPipeline(WixPipeline.Builder wixPipeline) {
wixPipeline.addSource(getConfigRoot().toAbsolutePath().resolve(
wxsFileName), wixVariables.getValues());
}

View File

@ -56,6 +56,7 @@ error.lock-resource=Failed to lock: {0}
error.unlock-resource=Failed to unlock: {0}
error.read-wix-l10n-file=Failed to parse {0} file
error.extract-culture-from-wix-l10n-file=Failed to read value of culture from {0} file
error.short-path-conv-fail=Failed to get short version of "{0}" path
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.potential.windows.defender.issue=Warning: Windows Defender may prevent jpackage from functioning. If there is an issue, it can be addressed by either disabling realtime monitoring, or adding an exclusion for the directory "{0}".

View File

@ -56,6 +56,7 @@ error.lock-resource=Sperren nicht erfolgreich: {0}
error.unlock-resource=Aufheben der Sperre nicht erfolgreich: {0}
error.read-wix-l10n-file=Datei {0} konnte nicht geparst werden
error.extract-culture-from-wix-l10n-file=Kulturwert konnte nicht aus Datei {0} gelesen werden
error.short-path-conv-fail=Failed to get short version of "{0}" path
message.icon-not-ico=Das angegebene Symbol "{0}" ist keine ICO-Datei und wird nicht verwendet. Stattdessen wird das Standardsymbol verwendet.
message.potential.windows.defender.issue=Warnung: Windows Defender verhindert eventuell die korrekte Ausführung von jpackage. Wenn ein Problem auftritt, deaktivieren Sie das Echtzeitmonitoring, oder fügen Sie einen Ausschluss für das Verzeichnis "{0}" hinzu.

View File

@ -56,6 +56,7 @@ error.lock-resource=ロックに失敗しました: {0}
error.unlock-resource=ロック解除に失敗しました: {0}
error.read-wix-l10n-file={0}ファイルの解析に失敗しました
error.extract-culture-from-wix-l10n-file={0}ファイルからのカルチャの値の読取りに失敗しました
error.short-path-conv-fail=Failed to get short version of "{0}" path
message.icon-not-ico=指定したアイコン"{0}"はICOファイルではなく、使用されません。デフォルト・アイコンがその位置に使用されます。
message.potential.windows.defender.issue=警告: Windows Defenderが原因でjpackageが機能しないことがあります。問題が発生した場合は、リアルタイム・モニタリングを無効にするか、ディレクトリ"{0}"の除外を追加することにより、問題に対処できます。

View File

@ -56,6 +56,7 @@ error.lock-resource=无法锁定:{0}
error.unlock-resource=无法解锁:{0}
error.read-wix-l10n-file=无法解析 {0} 文件
error.extract-culture-from-wix-l10n-file=无法从 {0} 文件读取文化值
error.short-path-conv-fail=Failed to get short version of "{0}" path
message.icon-not-ico=指定的图标 "{0}" 不是 ICO 文件, 不会使用。将使用默认图标代替。
message.potential.windows.defender.issue=警告Windows Defender 可能会阻止 jpackage 正常工作。如果存在问题,可以通过禁用实时监视或者为目录 "{0}" 添加排除项来解决。

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2020, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -668,4 +668,23 @@ tstring stripExeSuffix(const tstring& path) {
return path.substr(0, pos);
}
tstring toShortPath(const tstring& path) {
const DWORD len = GetShortPathName(path.c_str(), nullptr, 0);
if (0 == len) {
JP_THROW(SysError(tstrings::any() << "GetShortPathName("
<< path << ") failed", GetShortPathName));
}
std::vector<TCHAR> buf;
buf.resize(len);
const DWORD copied = GetShortPathName(path.c_str(), buf.data(),
static_cast<DWORD>(buf.size()));
if (copied != buf.size() - 1) {
JP_THROW(SysError(tstrings::any() << "GetShortPathName("
<< path << ") failed", GetShortPathName));
}
return tstring(buf.data(), buf.size() - 1);
}
} // namespace FileUtils

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2020, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -315,6 +315,8 @@ namespace FileUtils {
std::ofstream tmp;
tstring dstPath;
};
tstring toShortPath(const tstring& path);
} // FileUtils
#endif // WINFILEUTILS_H

View File

@ -25,6 +25,8 @@
#include "ResourceEditor.h"
#include "ErrorHandling.h"
#include "FileUtils.h"
#include "WinFileUtils.h"
#include "IconSwap.h"
#include "VersionInfo.h"
#include "JniUtils.h"
@ -162,4 +164,25 @@ extern "C" {
return 1;
}
/*
* Class: jdk_jpackage_internal_ShortPathUtils
* Method: getShortPath
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL
Java_jdk_jpackage_internal_ShortPathUtils_getShortPath(
JNIEnv *pEnv, jclass c, jstring jLongPath) {
JP_TRY;
const std::wstring longPath = jni::toUnicodeString(pEnv, jLongPath);
std::wstring shortPath = FileUtils::toShortPath(longPath);
return jni::toJString(pEnv, shortPath);
JP_CATCH_ALL;
return NULL;
}
} // extern "C"

View File

@ -53,7 +53,7 @@ public final class Executor extends CommandArguments<Executor> {
public Executor() {
saveOutputType = new HashSet<>(Set.of(SaveOutputType.NONE));
removePath = false;
removePathEnvVar = false;
}
public Executor setExecutable(String v) {
@ -85,8 +85,8 @@ public final class Executor extends CommandArguments<Executor> {
return setExecutable(v.getPath());
}
public Executor setRemovePath(boolean value) {
removePath = value;
public Executor setRemovePathEnvVar(boolean value) {
removePathEnvVar = value;
return this;
}
@ -348,7 +348,7 @@ public final class Executor extends CommandArguments<Executor> {
builder.directory(directory.toFile());
sb.append(String.format("; in directory [%s]", directory));
}
if (removePath) {
if (removePathEnvVar) {
// run this with cleared Path in Environment
TKit.trace("Clearing PATH in environment");
builder.environment().remove("PATH");
@ -478,7 +478,7 @@ public final class Executor extends CommandArguments<Executor> {
private Path executable;
private Set<SaveOutputType> saveOutputType;
private Path directory;
private boolean removePath;
private boolean removePathEnvVar;
private String winTmpDir = null;
private static enum SaveOutputType {

View File

@ -354,12 +354,12 @@ public final class HelloApp {
if (TKit.isWindows()) {
// When running app launchers on Windows, clear users environment (JDK-8254920)
removePath(true);
removePathEnvVar(true);
}
}
public AppOutputVerifier removePath(boolean v) {
removePath = v;
public AppOutputVerifier removePathEnvVar(boolean v) {
removePathEnvVar = v;
return this;
}
@ -455,7 +455,7 @@ public final class HelloApp {
Path outputFile = TKit.workDir().resolve(OUTPUT_FILENAME);
ThrowingFunction.toFunction(Files::deleteIfExists).apply(outputFile);
final Path executablePath;
Path executablePath;
if (launcherPath.isAbsolute()) {
executablePath = launcherPath;
} else {
@ -463,18 +463,27 @@ public final class HelloApp {
executablePath = Path.of(".").resolve(launcherPath.normalize());
}
if (TKit.isWindows()) {
var absExecutablePath = executablePath.toAbsolutePath().normalize();
var shortPath = WindowsHelper.toShortPath(absExecutablePath);
if (shortPath.isPresent()) {
TKit.trace(String.format("Will run [%s] as [%s]", executablePath, shortPath.get()));
executablePath = shortPath.get();
}
}
final List<String> launcherArgs = List.of(args);
return new Executor()
.setDirectory(outputFile.getParent())
.saveOutput(saveOutput)
.dumpOutput()
.setRemovePath(removePath)
.setRemovePathEnvVar(removePathEnvVar)
.setExecutable(executablePath)
.addArguments(launcherArgs);
}
private boolean launcherNoExit;
private boolean removePath;
private boolean removePathEnvVar;
private boolean saveOutput;
private final Path launcherPath;
private Path outputFilePath;

View File

@ -23,6 +23,7 @@
package jdk.jpackage.test;
import java.io.IOException;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
@ -36,7 +37,9 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static jdk.jpackage.internal.util.function.ExceptionBox.rethrowUnchecked;
import jdk.jpackage.internal.util.function.ThrowingRunnable;
import static jdk.jpackage.internal.util.function.ThrowingSupplier.toSupplier;
import jdk.jpackage.test.PackageTest.PackageHandlers;
public class WindowsHelper {
@ -94,8 +97,9 @@ public class WindowsHelper {
static PackageHandlers createMsiPackageHandlers() {
BiConsumer<JPackageCommand, Boolean> installMsi = (cmd, install) -> {
cmd.verifyIsOfType(PackageType.WIN_MSI);
var msiPath = TransientMsi.create(cmd).path();
runMsiexecWithRetries(Executor.of("msiexec", "/qn", "/norestart",
install ? "/i" : "/x").addArgument(cmd.outputBundle().normalize()));
install ? "/i" : "/x").addArgument(msiPath));
};
PackageHandlers msi = new PackageHandlers();
@ -112,6 +116,8 @@ public class WindowsHelper {
TKit.removeRootFromAbsolutePath(
getInstallationRootDirectory(cmd)));
final Path msiPath = TransientMsi.create(cmd).path();
// Put msiexec in .bat file because can't pass value of TARGETDIR
// property containing spaces through ProcessBuilder properly.
// Set folder permissions to allow msiexec unpack msi bundle.
@ -121,7 +127,7 @@ public class WindowsHelper {
String.join(" ", List.of(
"msiexec",
"/a",
String.format("\"%s\"", cmd.outputBundle().normalize()),
String.format("\"%s\"", msiPath),
"/qn",
String.format("TARGETDIR=\"%s\"",
unpackDir.toAbsolutePath().normalize())))));
@ -155,6 +161,49 @@ public class WindowsHelper {
return msi;
}
record TransientMsi(Path path) {
static TransientMsi create(JPackageCommand cmd) {
var outputMsiPath = cmd.outputBundle().normalize();
if (isPathTooLong(outputMsiPath)) {
return toSupplier(() -> {
var transientMsiPath = TKit.createTempDirectory("msi-copy").resolve("a.msi").normalize();
TKit.trace(String.format("Copy [%s] to [%s]", outputMsiPath, transientMsiPath));
Files.copy(outputMsiPath, transientMsiPath);
return new TransientMsi(transientMsiPath);
}).get();
} else {
return new TransientMsi(outputMsiPath);
}
}
}
public enum WixType {
WIX3,
WIX4
}
public static WixType getWixTypeFromVerboseJPackageOutput(Executor.Result result) {
return result.getOutput().stream().map(str -> {
if (str.contains("[light.exe]")) {
return WixType.WIX3;
} else if (str.contains("[wix.exe]")) {
return WixType.WIX4;
} else {
return null;
}
}).filter(Objects::nonNull).reduce((a, b) -> {
throw new IllegalArgumentException("Invalid input: multiple invocations of WiX tools");
}).orElseThrow(() -> new IllegalArgumentException("Invalid input: no invocations of WiX tools"));
}
static Optional<Path> toShortPath(Path path) {
if (isPathTooLong(path)) {
return Optional.of(ShortPathUtils.toShortPath(path));
} else {
return Optional.empty();
}
}
static PackageHandlers createExePackageHandlers() {
BiConsumer<JPackageCommand, Boolean> installExe = (cmd, install) -> {
cmd.verifyIsOfType(PackageType.WIN_EXE);
@ -303,6 +352,10 @@ public class WindowsHelper {
return cmd.hasArgument("--win-per-user-install");
}
private static boolean isPathTooLong(Path path) {
return path.toString().length() > WIN_MAX_PATH;
}
private static class DesktopIntegrationVerifier {
DesktopIntegrationVerifier(JPackageCommand cmd, String launcherName) {
@ -525,6 +578,32 @@ public class WindowsHelper {
return value;
}
private static final class ShortPathUtils {
private ShortPathUtils() {
try {
var shortPathUtilsClass = Class.forName("jdk.jpackage.internal.ShortPathUtils");
getShortPathWrapper = shortPathUtilsClass.getDeclaredMethod(
"getShortPathWrapper", String.class);
// Note: this reflection call requires
// --add-opens jdk.jpackage/jdk.jpackage.internal=ALL-UNNAMED
getShortPathWrapper.setAccessible(true);
} catch (ClassNotFoundException | NoSuchMethodException
| SecurityException ex) {
throw rethrowUnchecked(ex);
}
}
static Path toShortPath(Path path) {
return Path.of(toSupplier(() -> (String) INSTANCE.getShortPathWrapper.invoke(
null, path.toString())).get());
}
private final Method getShortPathWrapper;
private static final ShortPathUtils INSTANCE = new ShortPathUtils();
}
static final Set<Path> CRITICAL_RUNTIME_FILES = Set.of(Path.of(
"bin\\server\\jvm.dll"));
@ -540,4 +619,6 @@ public class WindowsHelper {
private static final String USER_SHELL_FOLDERS_REGKEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders";
private static final Map<String, String> REGISTRY_VALUES = new HashMap<>();
private static final int WIN_MAX_PATH = 260;
}

View File

@ -35,6 +35,8 @@ import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import jdk.jpackage.test.Executor;
import static jdk.jpackage.test.WindowsHelper.WixType.WIX3;
import static jdk.jpackage.test.WindowsHelper.getWixTypeFromVerboseJPackageOutput;
/*
* @test
@ -109,13 +111,13 @@ public class WinL10nTest {
});
}
private static Stream<String> getBuildCommandLine(Executor.Result result) {
private static Stream<String> getWixCommandLine(Executor.Result result) {
return result.getOutput().stream().filter(createToolCommandLinePredicate("light").or(
createToolCommandLinePredicate("wix")));
}
private static boolean isWix3(Executor.Result result) {
return result.getOutput().stream().anyMatch(createToolCommandLinePredicate("light"));
return getWixTypeFromVerboseJPackageOutput(result) == WIX3;
}
private final static Predicate<String> createToolCommandLinePredicate(String wixToolName) {
@ -127,10 +129,10 @@ public class WinL10nTest {
};
}
private static List<TKit.TextStreamVerifier> createDefaultL10nFilesLocVerifiers(Path tempDir) {
private static List<TKit.TextStreamVerifier> createDefaultL10nFilesLocVerifiers(Path wixSrcDir) {
return Arrays.stream(DEFAULT_L10N_FILES).map(loc ->
TKit.assertTextStream("-loc " + tempDir.resolve(
String.format("config/MsiInstallerStrings_%s.wxl", loc)).normalize()))
TKit.assertTextStream("-loc " + wixSrcDir.resolve(
String.format("MsiInstallerStrings_%s.wxl", loc))))
.toList();
}
@ -183,16 +185,20 @@ public class WinL10nTest {
cmd.addArguments("--temp", tempDir);
})
.addBundleVerifier((cmd, result) -> {
final List<String> wixCmdline = getWixCommandLine(result).toList();
final var isWix3 = isWix3(result);
if (expectedCultures != null) {
String expected;
if (isWix3(result)) {
if (isWix3) {
expected = "-cultures:" + String.join(";", expectedCultures);
} else {
expected = Stream.of(expectedCultures).map(culture -> {
return String.join(" ", "-culture", culture);
}).collect(Collectors.joining(" "));
}
TKit.assertTextStream(expected).apply(getBuildCommandLine(result));
TKit.assertTextStream(expected).apply(wixCmdline.stream());
}
if (expectedErrorMessage != null) {
@ -201,25 +207,27 @@ public class WinL10nTest {
}
if (wxlFileInitializers != null) {
var wixSrcDir = Path.of(cmd.getArgumentValue("--temp")).resolve("config");
var wixSrcDir = Path.of(cmd.getArgumentValue("--temp")).resolve(
"config").normalize().toAbsolutePath();
if (allWxlFilesValid) {
for (var v : wxlFileInitializers) {
if (!v.name.startsWith("MsiInstallerStrings_")) {
v.createCmdOutputVerifier(wixSrcDir).apply(getBuildCommandLine(result));
v.createCmdOutputVerifier(wixSrcDir).apply(wixCmdline.stream());
}
}
var tempDir = Path.of(cmd.getArgumentValue("--temp")).toAbsolutePath();
for (var v : createDefaultL10nFilesLocVerifiers(tempDir)) {
v.apply(getBuildCommandLine(result));
for (var v : createDefaultL10nFilesLocVerifiers(wixSrcDir)) {
v.apply(wixCmdline.stream());
}
} else {
Stream.of(wxlFileInitializers)
.filter(Predicate.not(WixFileInitializer::isValid))
.forEach(v -> v.createCmdOutputVerifier(
wixSrcDir).apply(result.getOutput().stream()));
TKit.assertFalse(getBuildCommandLine(result).findAny().isPresent(),
"Check light.exe was not invoked");
TKit.assertTrue(wixCmdline.stream().findAny().isEmpty(),
String.format("Check %s.exe was not invoked",
isWix3 ? "light" : "wix"));
}
}
});
@ -276,10 +284,9 @@ public class WinL10nTest {
}
@Override
TKit.TextStreamVerifier createCmdOutputVerifier(Path root) {
TKit.TextStreamVerifier createCmdOutputVerifier(Path wixSrcDir) {
return TKit.assertTextStream(String.format(
"Failed to parse %s file",
root.resolve("b.wxl").toAbsolutePath()));
"Failed to parse %s file", wixSrcDir.resolve("b.wxl")));
}
};
}
@ -297,9 +304,8 @@ public class WinL10nTest {
+ "\" xmlns=\"http://schemas.microsoft.com/wix/2006/localization\" Codepage=\"1252\"/>"));
}
TKit.TextStreamVerifier createCmdOutputVerifier(Path root) {
return TKit.assertTextStream(
"-loc " + root.resolve(name).toAbsolutePath().normalize());
TKit.TextStreamVerifier createCmdOutputVerifier(Path wixSrcDir) {
return TKit.assertTextStream("-loc " + wixSrcDir.resolve(name));
}
boolean isValid() {

View File

@ -0,0 +1,87 @@
/*
* Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* 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.
*/
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import jdk.jpackage.test.Annotations.Parameters;
import jdk.jpackage.test.PackageTest;
import jdk.jpackage.test.PackageType;
import jdk.jpackage.test.Annotations.Test;
import jdk.jpackage.test.JPackageCommand;
import jdk.jpackage.test.RunnablePackageTest.Action;
import jdk.jpackage.test.TKit;
/*
/* @test
* @bug 8289771
* @summary jpackage with long paths on windows
* @library /test/jdk/tools/jpackage/helpers
* @key jpackagePlatformPackage
* @build jdk.jpackage.test.*
* @requires (os.family == "windows")
* @compile WinLongPathTest.java
* @run main/othervm/timeout=540 -Xmx512m jdk.jpackage.test.Main
* --jpt-space-subst=*
* --jpt-exclude=WinLongPathTest(false,*--temp)
* --jpt-run=WinLongPathTest
*/
public record WinLongPathTest(Boolean appImage, String optionName) {
@Parameters
public static List<Object[]> input() {
List<Object[]> data = new ArrayList<>();
for (var appImage : List.of(Boolean.TRUE, Boolean.FALSE)) {
for (var option : List.of("--dest", "--temp")) {
data.add(new Object[]{appImage, option});
}
}
return data;
}
@Test
public void test() throws IOException {
if (appImage) {
var cmd = JPackageCommand.helloAppImage();
setOptionLongPath(cmd, optionName);
cmd.executeAndAssertHelloAppImageCreated();
} else {
new PackageTest()
.forTypes(PackageType.WINDOWS)
.configureHelloApp()
.addInitializer(cmd -> setOptionLongPath(cmd, optionName))
.run(Action.CREATE_AND_UNPACK);
}
}
private static void setOptionLongPath(JPackageCommand cmd, String option) throws IOException {
var root = TKit.createTempDirectory("long-path");
// 261 characters in total, which alone is above the 260 threshold
var longPath = root.resolve(Path.of("a".repeat(80), "b".repeat(90), "c".repeat(91)));
Files.createDirectories(longPath);
cmd.setArgumentValue(option, longPath);
}
}