Windows path handling: change representation for paths with spaces (#42754)

Some builds on Windows break when encountering paths with spaces. This
reencodes some paths in Windows 8.3 filename format (when on Windows):
this serves as an equivalent identifier for the file, but in a form that
does not have spaces.

8.3 filenames are also truncated in length, which could be helpful, but
that is not the primary intended purpose of using this format.

Overall

* nmake/msbuild packages do this generally for the install prefix
* curl/perl require additional modifications (as written now, each package
  may require calls to `windows_sfn` to work when the Spack
  root/install/staging prefixes contain spaces)

Some items for follow-up:

* Spack itself does not create paths with spaces "on top" of whatever
  the user configures or where it is placed (e.g. the Spack root, the
  staging directory, etc.), so it might be possible to edit some of these
  paths once and avoid a proliferation of individual `windows_sfn`
  calls in individual packages.
* This approach may result in the insertion of 8.3-style paths into
  build artifacts (on Windows), handling this may require additional
  bookkeeping (e.g. when relocating).
This commit is contained in:
John W. Parent 2024-02-23 16:30:11 -05:00 committed by GitHub
parent 3e713bb0fa
commit f51c9fc6c3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 57 additions and 4 deletions

View File

@ -1240,6 +1240,47 @@ def get_single_file(directory):
return fnames[0]
@system_path_filter
def windows_sfn(path: os.PathLike):
"""Returns 8.3 Filename (SFN) representation of
path
8.3 Filenames (SFN or short filename) is a file
naming convention used prior to Win95 that Windows
still (and will continue to) support. This convention
caps filenames at 8 characters, and most importantly
does not allow for spaces in addition to other specifications.
The scheme is generally the same as a normal Windows
file scheme, but all spaces are removed and the filename
is capped at 6 characters. The remaining characters are
replaced with ~N where N is the number file in a directory
that a given file represents i.e. Program Files and Program Files (x86)
would be PROGRA~1 and PROGRA~2 respectively.
Further, all file/directory names are all caps (although modern Windows
is case insensitive in practice).
Conversion is accomplished by fileapi.h GetShortPathNameW
Returns paths in 8.3 Filename form
Note: this method is a no-op on Linux
Args:
path: Path to be transformed into SFN (8.3 filename) format
"""
# This should not be run-able on linux/macos
if sys.platform != "win32":
return path
path = str(path)
import ctypes
k32 = ctypes.WinDLL("kernel32", use_last_error=True)
# stub Windows types TCHAR[LENGTH]
TCHAR_arr = ctypes.c_wchar * len(path)
ret_str = TCHAR_arr()
k32.GetShortPathNameW(path, ret_str, len(path))
return ret_str.value
@contextmanager
def temp_cwd():
tmp_dir = tempfile.mkdtemp()

View File

@ -69,7 +69,7 @@ class MSBuildBuilder(BaseBuilder):
@property
def build_directory(self):
"""Return the directory containing the MSBuild solution or vcxproj."""
return self.pkg.stage.source_path
return fs.windows_sfn(self.pkg.stage.source_path)
@property
def toolchain_version(self):

View File

@ -77,7 +77,11 @@ def ignore_quotes(self):
@property
def build_directory(self):
"""Return the directory containing the makefile."""
return self.pkg.stage.source_path if not self.makefile_root else self.makefile_root
return (
fs.windows_sfn(self.pkg.stage.source_path)
if not self.makefile_root
else fs.windows_sfn(self.makefile_root)
)
@property
def std_nmake_args(self):

View File

@ -8,6 +8,8 @@
import re
import sys
from llnl.util.filesystem import windows_sfn
from spack.build_systems.autotools import AutotoolsBuilder
from spack.build_systems.nmake import NMakeBuilder
from spack.package import *
@ -470,7 +472,8 @@ def nmake_args(self):
# The trailing path seperator is REQUIRED for cURL to install
# otherwise cURLs build system will interpret the path as a file
# and the install will fail with ambiguous errors
args.append("WITH_PREFIX=%s" % self.prefix + "\\")
inst_prefix = self.prefix + "\\"
args.append(f"WITH_PREFIX={windows_sfn(inst_prefix)}")
return args
def install(self, pkg, spec, prefix):
@ -485,6 +488,7 @@ def install(self, pkg, spec, prefix):
env["CC"] = ""
env["CXX"] = ""
winbuild_dir = os.path.join(self.stage.source_path, "winbuild")
winbuild_dir = windows_sfn(winbuild_dir)
with working_dir(winbuild_dir):
nmake("/f", "Makefile.vc", *self.nmake_args(), ignore_quotes=True)
with working_dir(os.path.join(self.stage.source_path, "builds")):

View File

@ -16,6 +16,7 @@
import sys
from contextlib import contextmanager
from llnl.util.filesystem import windows_sfn
from llnl.util.lang import match_predicate
from llnl.util.symlink import symlink
@ -287,7 +288,7 @@ def nmake_arguments(self):
args.append("CCTYPE=%s" % self.compiler.short_msvc_version)
else:
raise RuntimeError("Perl unsupported for non MSVC compilers on Windows")
args.append("INST_TOP=%s" % self.prefix.replace("/", "\\"))
args.append("INST_TOP=%s" % windows_sfn(self.prefix.replace("/", "\\")))
args.append("INST_ARCH=\\$(ARCHNAME)")
if self.spec.satisfies("~shared"):
args.append("ALL_STATIC=%s" % "define")
@ -368,6 +369,7 @@ def build(self, spec, prefix):
def build_test(self):
if sys.platform == "win32":
win32_dir = os.path.join(self.stage.source_path, "win32")
win32_dir = windows_sfn(win32_dir)
with working_dir(win32_dir):
nmake("test", ignore_quotes=True)
else:
@ -376,6 +378,7 @@ def build_test(self):
def install(self, spec, prefix):
if sys.platform == "win32":
win32_dir = os.path.join(self.stage.source_path, "win32")
win32_dir = windows_sfn(win32_dir)
with working_dir(win32_dir):
nmake("install", *self.nmake_arguments(), ignore_quotes=True)
else:
@ -409,6 +412,7 @@ def install_cpanm(self):
if sys.platform == "win32":
maker = nmake
cpan_dir = join_path(self.stage.source_path, cpan_dir)
cpan_dir = windows_sfn(cpan_dir)
if "+cpanm" in spec:
with working_dir(cpan_dir):
perl = spec["perl"].command