diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/ExecutableRebrander.java b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/ExecutableRebrander.java index a297f507da8..166675b5893 100644 --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/ExecutableRebrander.java +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/ExecutableRebrander.java @@ -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 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)); diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/ShortPathUtils.java b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/ShortPathUtils.java new file mode 100644 index 00000000000..21dc2a1c3ff --- /dev/null +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/ShortPathUtils.java @@ -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 toShortPath(String path) { + Objects.requireNonNull(path); + return toShortPath(Path.of(path)).map(Path::toString); + } + + static Optional 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"); + } +} diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinMsiBundler.java b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinMsiBundler.java index f6b0fb4be20..f724e08fc89 100644 --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinMsiBundler.java +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinMsiBundler.java @@ -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 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); } } diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixFragmentBuilder.java b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixFragmentBuilder.java index f0a5840eb6f..48a1d04f8dc 100644 --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixFragmentBuilder.java +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixFragmentBuilder.java @@ -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)); diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixPipeline.java b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixPipeline.java index 5b626c8a565..d7c1b54a48c 100644 --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixPipeline.java +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixPipeline.java @@ -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 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 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 v) { + wixVariables.clear(); + wixVariables.putAll(v); + return this; + } + + Builder addSource(Path source, Map wixVariables) { + sources.add(new WixSource(source, wixVariables)); + return this; + } + + Builder addLightOptions(String ... v) { + lightOptions.addAll(List.of(v)); + return this; + } + + private List mapLightOptions(UnaryOperator normalizePath) { + var pathOptions = Set.of("-b", "-loc"); + List 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 wixVariables = new HashMap<>(); + private final List lightOptions = new ArrayList<>(); + private final List sources = new ArrayList<>(); } - WixPipeline setToolset(WixToolset v) { - toolset = v; - return this; + static Builder build() { + return new Builder(); } - WixPipeline setWixVariables(Map v) { - wixVariables = v; - return this; - } - - WixPipeline setWixObjDir(Path v) { - wixObjDir = v; - return this; - } - - WixPipeline setWorkDir(Path v) { - workDir = v; - return this; - } - - WixPipeline addSource(Path source, Map wixVariables) { - WixSource entry = new WixSource(); - entry.source = source; - entry.variables = wixVariables; - sources.add(entry); - return this; - } - - WixPipeline addLightOptions(String ... v) { - lightOptions.addAll(List.of(v)); - return this; + private WixPipeline(WixToolset toolset, Path workDir, Path wixObjDir, + Map wixVariables, List lightOptions, + List 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 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 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 variables; + private record WixSource(Path path, Map variables) { + WixSource overridePath(Path path) { + return new WixSource(path, variables); + } } - private WixToolset toolset; - private Map wixVariables; - private List lightOptions; - private Path wixObjDir; - private Path workDir; - private List sources; + private final WixToolset toolset; + private final Map wixVariables; + private final List lightOptions; + private final Path wixObjDir; + private final Path workDir; + private final List sources; } diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixUiFragmentBuilder.java b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixUiFragmentBuilder.java index 5eb23fc58c4..25db6e6ab0b 100644 --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixUiFragmentBuilder.java +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixUiFragmentBuilder.java @@ -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()); } diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/WinResources.properties b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/WinResources.properties index 9e7504364d3..584342397a6 100644 --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/WinResources.properties +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/WinResources.properties @@ -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}". diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/WinResources_de.properties b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/WinResources_de.properties index a7212d9640a..dce8ca6176d 100644 --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/WinResources_de.properties +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/WinResources_de.properties @@ -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. diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/WinResources_ja.properties b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/WinResources_ja.properties index 352aab7a493..47e5b585869 100644 --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/WinResources_ja.properties +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/WinResources_ja.properties @@ -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}"の除外を追加することにより、問題に対処できます。 diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/WinResources_zh_CN.properties b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/WinResources_zh_CN.properties index a8d4a4471d6..abd3d13a667 100644 --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/WinResources_zh_CN.properties +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/WinResources_zh_CN.properties @@ -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}" 添加排除项来解决。 diff --git a/src/jdk.jpackage/windows/native/common/WinFileUtils.cpp b/src/jdk.jpackage/windows/native/common/WinFileUtils.cpp index 993aed8bb13..f801009ed94 100644 --- a/src/jdk.jpackage/windows/native/common/WinFileUtils.cpp +++ b/src/jdk.jpackage/windows/native/common/WinFileUtils.cpp @@ -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 buf; + buf.resize(len); + const DWORD copied = GetShortPathName(path.c_str(), buf.data(), + static_cast(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 diff --git a/src/jdk.jpackage/windows/native/common/WinFileUtils.h b/src/jdk.jpackage/windows/native/common/WinFileUtils.h index 2dfb32fa088..ba07efec78e 100644 --- a/src/jdk.jpackage/windows/native/common/WinFileUtils.h +++ b/src/jdk.jpackage/windows/native/common/WinFileUtils.h @@ -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 diff --git a/src/jdk.jpackage/windows/native/libjpackage/jpackage.cpp b/src/jdk.jpackage/windows/native/libjpackage/jpackage.cpp index 6317842787e..66fa92e3563 100644 --- a/src/jdk.jpackage/windows/native/libjpackage/jpackage.cpp +++ b/src/jdk.jpackage/windows/native/libjpackage/jpackage.cpp @@ -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" \ No newline at end of file diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/Executor.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/Executor.java index f182f4f7f7d..00f6ab5263c 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/Executor.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/Executor.java @@ -53,7 +53,7 @@ public final class Executor extends CommandArguments { 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 { 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 { 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 { private Path executable; private Set saveOutputType; private Path directory; - private boolean removePath; + private boolean removePathEnvVar; private String winTmpDir = null; private static enum SaveOutputType { diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/HelloApp.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/HelloApp.java index bc35912bcbb..0c7476e863d 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/HelloApp.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/HelloApp.java @@ -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 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; diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java index 42ea9e3e9a7..4fb937864aa 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java @@ -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 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 toShortPath(Path path) { + if (isPathTooLong(path)) { + return Optional.of(ShortPathUtils.toShortPath(path)); + } else { + return Optional.empty(); + } + } + static PackageHandlers createExePackageHandlers() { BiConsumer 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 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 REGISTRY_VALUES = new HashMap<>(); + + private static final int WIN_MAX_PATH = 260; } diff --git a/test/jdk/tools/jpackage/windows/WinL10nTest.java b/test/jdk/tools/jpackage/windows/WinL10nTest.java index 814b6401f47..dee1e42267d 100644 --- a/test/jdk/tools/jpackage/windows/WinL10nTest.java +++ b/test/jdk/tools/jpackage/windows/WinL10nTest.java @@ -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 getBuildCommandLine(Executor.Result result) { + private static Stream 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 createToolCommandLinePredicate(String wixToolName) { @@ -127,10 +129,10 @@ public class WinL10nTest { }; } - private static List createDefaultL10nFilesLocVerifiers(Path tempDir) { + private static List 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 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() { diff --git a/test/jdk/tools/jpackage/windows/WinLongPathTest.java b/test/jdk/tools/jpackage/windows/WinLongPathTest.java new file mode 100644 index 00000000000..e9e5ef8ce03 --- /dev/null +++ b/test/jdk/tools/jpackage/windows/WinLongPathTest.java @@ -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 input() { + List 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); + } +}