Skip to content

Commit 75799cd

Browse files
compnerdetcwilde
authored andcommitted
[lit][clang] Avoid realpath on Windows due to MAX_PATH limitations
Running lit tests on Windows can fail because its use of `os.path.realpath` expands substitute drives, which are used to keep paths short and avoid hitting MAX_PATH limitations. Changes lit logic to: Use `os.path.abspath` on Windows, where `MAX_PATH` is a concern that we can work around using substitute drives, which `os.path.realpath` would resolve. Use `os.path.realpath` on Unix, where the current directory always has symlinks resolved, so it is impossible to preserve symlinks in the presence of relative paths, and so we must make sure that all code paths use real paths. Also updates clang's `FileManager::getCanonicalName` and `ExtractAPI` code to avoid resolving substitute drives (i.e. resolving to a path under a different root). How tested: built with `-DLLVM_ENABLE_PROJECTS=clang` and built `check-all` on both Windows Differential Revision: https://reviews.llvm.org/D154130 Reviewed By: @benlangmuir Patch by Tristan Labelle <[email protected]>!
1 parent 8b7a515 commit 75799cd

File tree

16 files changed

+150
-92
lines changed

16 files changed

+150
-92
lines changed

clang/include/clang/Basic/FileManager.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,13 @@ class FileManager : public RefCountedBase<FileManager> {
339339
/// required, which is (almost) never.
340340
StringRef getCanonicalName(const FileEntry *File);
341341

342+
private:
343+
/// Retrieve the canonical name for a given file or directory.
344+
///
345+
/// The first param is a key in the CanonicalNames array.
346+
StringRef getCanonicalName(const void *Entry, StringRef Name);
347+
348+
public:
342349
void PrintStats() const;
343350
};
344351

clang/lib/Basic/FileManager.cpp

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -651,33 +651,48 @@ void FileManager::GetUniqueIDMapping(
651651
}
652652

653653
StringRef FileManager::getCanonicalName(DirectoryEntryRef Dir) {
654-
auto Known = CanonicalNames.find(Dir);
655-
if (Known != CanonicalNames.end())
656-
return Known->second;
657-
658-
StringRef CanonicalName(Dir.getName());
659-
660-
SmallString<4096> CanonicalNameBuf;
661-
if (!FS->getRealPath(Dir.getName(), CanonicalNameBuf))
662-
CanonicalName = CanonicalNameBuf.str().copy(CanonicalNameStorage);
663-
664-
CanonicalNames.insert({Dir, CanonicalName});
665-
return CanonicalName;
654+
return getCanonicalName(Dir, Dir.getName());
666655
}
667656

668657
StringRef FileManager::getCanonicalName(const FileEntry *File) {
669-
llvm::DenseMap<const void *, llvm::StringRef>::iterator Known
670-
= CanonicalNames.find(File);
658+
return getCanonicalName(File, File->getName());
659+
}
660+
661+
StringRef FileManager::getCanonicalName(const void *Entry, StringRef Name) {
662+
llvm::DenseMap<const void *, llvm::StringRef>::iterator Known =
663+
CanonicalNames.find(Entry);
671664
if (Known != CanonicalNames.end())
672665
return Known->second;
673666

674-
StringRef CanonicalName(File->getName());
675-
676-
SmallString<4096> CanonicalNameBuf;
677-
if (!FS->getRealPath(File->getName(), CanonicalNameBuf))
678-
CanonicalName = CanonicalNameBuf.str().copy(CanonicalNameStorage);
667+
// Name comes from FileEntry/DirectoryEntry::getName(), so it is safe to
668+
// store it in the DenseMap below.
669+
StringRef CanonicalName(Name);
670+
671+
SmallString<256> AbsPathBuf;
672+
SmallString<256> RealPathBuf;
673+
if (!FS->getRealPath(Name, RealPathBuf)) {
674+
if (is_style_windows(llvm::sys::path::Style::native)) {
675+
// For Windows paths, only use the real path if it doesn't resolve
676+
// a substitute drive, as those are used to avoid MAX_PATH issues.
677+
AbsPathBuf = Name;
678+
if (!FS->makeAbsolute(AbsPathBuf)) {
679+
if (llvm::sys::path::root_name(RealPathBuf) ==
680+
llvm::sys::path::root_name(AbsPathBuf)) {
681+
CanonicalName = RealPathBuf.str().copy(CanonicalNameStorage);
682+
} else {
683+
// Fallback to using the absolute path.
684+
// Simplifying /../ is semantically valid on Windows even in the
685+
// presence of symbolic links.
686+
llvm::sys::path::remove_dots(AbsPathBuf, /*remove_dot_dot=*/true);
687+
CanonicalName = AbsPathBuf.str().copy(CanonicalNameStorage);
688+
}
689+
}
690+
} else {
691+
CanonicalName = RealPathBuf.str().copy(CanonicalNameStorage);
692+
}
693+
}
679694

680-
CanonicalNames.insert({File, CanonicalName});
695+
CanonicalNames.insert({Entry, CanonicalName});
681696
return CanonicalName;
682697
}
683698

clang/lib/ExtractAPI/ExtractAPIConsumer.cpp

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -187,9 +187,7 @@ struct LocationFileChecker {
187187
if (ExternalFileEntries.count(File))
188188
return false;
189189

190-
StringRef FileName = File->tryGetRealPathName().empty()
191-
? File->getName()
192-
: File->tryGetRealPathName();
190+
StringRef FileName = SM.getFileManager().getCanonicalName(File);
193191

194192
// Try to reduce the include name the same way we tried to include it.
195193
bool IsQuoted = false;

clang/test/Lexer/case-insensitive-include-win.c

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,15 @@
22
// This file should only include code that really needs a Windows host OS to
33
// run.
44

5+
// Note: We must use the real path here, because the logic to detect case
6+
// mismatches must resolve the real path to figure out the original casing.
7+
// If we use %t and we are on a substitute drive S: mapping to C:\subst,
8+
// then we will compare "S:\test.dir\FOO.h" to "C:\subst\test.dir\foo.h"
9+
// and avoid emitting the diagnostic because the structure is different.
10+
511
// REQUIRES: system-windows
612
// RUN: mkdir -p %t.dir
713
// RUN: touch %t.dir/foo.h
8-
// RUN: not %clang_cl /FI\\?\%t.dir\FOO.h /WX -fsyntax-only %s 2>&1 | FileCheck %s
14+
// RUN: not %clang_cl /FI \\?\%{t:real}.dir\FOO.h /WX -fsyntax-only %s 2>&1 | FileCheck %s
915

1016
// CHECK: non-portable path to file '"\\?\

llvm/cmake/modules/AddLLVM.cmake

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1711,10 +1711,15 @@ endfunction()
17111711
# use it and can't be in a lit module. Use with make_paths_relative().
17121712
string(CONCAT LLVM_LIT_PATH_FUNCTION
17131713
"# Allow generated file to be relocatable.\n"
1714-
"from pathlib import Path\n"
1714+
"import os\n"
1715+
"import platform\n"
17151716
"def path(p):\n"
17161717
" if not p: return ''\n"
1717-
" return str((Path(__file__).parent / p).resolve())\n"
1718+
" # Follows lit.util.abs_path_preserve_drive, which cannot be imported here.\n"
1719+
" if platform.system() == 'Windows':\n"
1720+
" return os.path.abspath(os.path.join(os.path.dirname(__file__), p))\n"
1721+
" else:\n"
1722+
" return os.path.realpath(os.path.join(os.path.dirname(__file__), p))\n"
17181723
)
17191724

17201725
# This function provides an automatic way to 'configure'-like generate a file

llvm/docs/CommandGuide/lit.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,16 @@ TestRunner.py:
535535
%/p %p but ``\`` is replaced by ``/``
536536
%/t %t but ``\`` is replaced by ``/``
537537
%/T %T but ``\`` is replaced by ``/``
538+
%{s:real} %s after expanding all symbolic links and substitute drives
539+
%{S:real} %S after expanding all symbolic links and substitute drives
540+
%{p:real} %p after expanding all symbolic links and substitute drives
541+
%{t:real} %t after expanding all symbolic links and substitute drives
542+
%{T:real} %T after expanding all symbolic links and substitute drives
543+
%{/s:real} %/s after expanding all symbolic links and substitute drives
544+
%{/S:real} %/S after expanding all symbolic links and substitute drives
545+
%{/p:real} %/p after expanding all symbolic links and substitute drives
546+
%{/t:real} %/t after expanding all symbolic links and substitute drives
547+
%{/T:real} %/T after expanding all symbolic links and substitute drives
538548
%{/s:regex_replacement} %/s but escaped for use in the replacement of a ``s@@@`` command in sed
539549
%{/S:regex_replacement} %/S but escaped for use in the replacement of a ``s@@@`` command in sed
540550
%{/p:regex_replacement} %/p but escaped for use in the replacement of a ``s@@@`` command in sed

llvm/docs/TestingGuide.rst

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -671,7 +671,7 @@ RUN lines:
671671
``${fs-sep}``
672672
Expands to the file system separator, i.e. ``/`` or ``\`` on Windows.
673673

674-
``%/s, %/S, %/t, %/T:``
674+
``%/s, %/S, %/t, %/T``
675675

676676
Act like the corresponding substitution above but replace any ``\``
677677
character with a ``/``. This is useful to normalize path separators.
@@ -680,7 +680,17 @@ RUN lines:
680680

681681
Example: ``%/s: C:/Desktop Files/foo_test.s.tmp``
682682

683-
``%:s, %:S, %:t, %:T:``
683+
``%{s:real}, %{S:real}, %{t:real}, %{T:real}``
684+
``%{/s:real}, %{/S:real}, %{/t:real}, %{/T:real}``
685+
686+
Act like the corresponding substitution, including with ``/``, but use
687+
the real path by expanding all symbolic links and substitute drives.
688+
689+
Example: ``%s: S:\foo_test.s.tmp``
690+
691+
Example: ``%{/s:real}: C:/SDrive/foo_test.s.tmp``
692+
693+
``%:s, %:S, %:t, %:T``
684694

685695
Act like the corresponding substitution above but remove colons at
686696
the beginning of Windows paths. This is useful to allow concatenation

llvm/utils/lit/lit/TestRunner.py

Lines changed: 40 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ def change_dir(self, newdir):
7474
if os.path.isabs(newdir):
7575
self.cwd = newdir
7676
else:
77-
self.cwd = os.path.realpath(os.path.join(self.cwd, newdir))
77+
self.cwd = lit.util.abs_path_preserve_drive(os.path.join(self.cwd, newdir))
7878

7979

8080
class TimeoutHelper(object):
@@ -427,7 +427,7 @@ def executeBuiltinMkdir(cmd, cmd_shenv):
427427
dir = to_unicode(dir) if kIsWindows else to_bytes(dir)
428428
cwd = to_unicode(cwd) if kIsWindows else to_bytes(cwd)
429429
if not os.path.isabs(dir):
430-
dir = os.path.realpath(os.path.join(cwd, dir))
430+
dir = lit.util.abs_path_preserve_drive(os.path.join(cwd, dir))
431431
if parent:
432432
lit.util.mkdir_p(dir)
433433
else:
@@ -473,7 +473,7 @@ def on_rm_error(func, path, exc_info):
473473
path = to_unicode(path) if kIsWindows else to_bytes(path)
474474
cwd = to_unicode(cwd) if kIsWindows else to_bytes(cwd)
475475
if not os.path.isabs(path):
476-
path = os.path.realpath(os.path.join(cwd, path))
476+
path = lit.util.abs_path_preserve_drive(os.path.join(cwd, path))
477477
if force and not os.path.exists(path):
478478
continue
479479
try:
@@ -1228,18 +1228,10 @@ def getDefaultSubstitutions(test, tmpDir, tmpBase, normalize_slashes=False):
12281228
substitutions.extend(test.config.substitutions)
12291229
tmpName = tmpBase + ".tmp"
12301230
baseName = os.path.basename(tmpBase)
1231-
substitutions.extend(
1232-
[
1233-
("%s", sourcepath),
1234-
("%S", sourcedir),
1235-
("%p", sourcedir),
1236-
("%{pathsep}", os.pathsep),
1237-
("%t", tmpName),
1238-
("%basename_t", baseName),
1239-
("%T", tmpDir),
1240-
]
1241-
)
12421231

1232+
substitutions.append(("%{pathsep}", os.pathsep))
1233+
substitutions.append(("%basename_t", baseName))
1234+
12431235
substitutions.extend(
12441236
[
12451237
("%{fs-src-root}", pathlib.Path(sourcedir).anchor),
@@ -1248,49 +1240,47 @@ def getDefaultSubstitutions(test, tmpDir, tmpBase, normalize_slashes=False):
12481240
]
12491241
)
12501242

1251-
# "%/[STpst]" should be normalized.
1252-
substitutions.extend(
1253-
[
1254-
("%/s", sourcepath.replace("\\", "/")),
1255-
("%/S", sourcedir.replace("\\", "/")),
1256-
("%/p", sourcedir.replace("\\", "/")),
1257-
("%/t", tmpBase.replace("\\", "/") + ".tmp"),
1258-
("%/T", tmpDir.replace("\\", "/")),
1259-
("%/et", tmpName.replace("\\", "\\\\\\\\\\\\\\\\")),
1260-
]
1261-
)
1243+
substitutions.append(("%/et", tmpName.replace("\\", "\\\\\\\\\\\\\\\\")))
12621244

1263-
# "%{/[STpst]:regex_replacement}" should be normalized like "%/[STpst]" but we're
1264-
# also in a regex replacement context of a s@@@ regex.
12651245
def regex_escape(s):
12661246
s = s.replace("@", r"\@")
12671247
s = s.replace("&", r"\&")
12681248
return s
12691249

1270-
substitutions.extend(
1271-
[
1272-
("%{/s:regex_replacement}", regex_escape(sourcepath.replace("\\", "/"))),
1273-
("%{/S:regex_replacement}", regex_escape(sourcedir.replace("\\", "/"))),
1274-
("%{/p:regex_replacement}", regex_escape(sourcedir.replace("\\", "/"))),
1275-
(
1276-
"%{/t:regex_replacement}",
1277-
regex_escape(tmpBase.replace("\\", "/")) + ".tmp",
1278-
),
1279-
("%{/T:regex_replacement}", regex_escape(tmpDir.replace("\\", "/"))),
1280-
]
1281-
)
1250+
path_substitutions = [
1251+
("s", sourcepath), ("S", sourcedir), ("p", sourcedir),
1252+
("t", tmpName), ("T", tmpDir)
1253+
]
1254+
for path_substitution in path_substitutions:
1255+
letter = path_substitution[0]
1256+
path = path_substitution[1]
1257+
1258+
# Original path variant
1259+
substitutions.append(("%" + letter, path))
1260+
1261+
# Normalized path separator variant
1262+
substitutions.append(("%/" + letter, path.replace("\\", "/")))
1263+
1264+
# realpath variants
1265+
# Windows paths with substitute drives are not expanded by default
1266+
# as they are used to avoid MAX_PATH issues, but sometimes we do
1267+
# need the fully expanded path.
1268+
real_path = os.path.realpath(path)
1269+
substitutions.append(("%{" + letter + ":real}", real_path))
1270+
substitutions.append(("%{/" + letter + ":real}",
1271+
real_path.replace("\\", "/")))
1272+
1273+
# "%{/[STpst]:regex_replacement}" should be normalized like
1274+
# "%/[STpst]" but we're also in a regex replacement context
1275+
# of a s@@@ regex.
1276+
substitutions.append(
1277+
("%{/" + letter + ":regex_replacement}",
1278+
regex_escape(path.replace("\\", "/"))))
1279+
1280+
# "%:[STpst]" are normalized paths without colons and without
1281+
# a leading slash.
1282+
substitutions.append(("%:" + letter, colonNormalizePath(path)))
12821283

1283-
# "%:[STpst]" are normalized paths without colons and without a leading
1284-
# slash.
1285-
substitutions.extend(
1286-
[
1287-
("%:s", colonNormalizePath(sourcepath)),
1288-
("%:S", colonNormalizePath(sourcedir)),
1289-
("%:p", colonNormalizePath(sourcedir)),
1290-
("%:t", colonNormalizePath(tmpBase + ".tmp")),
1291-
("%:T", colonNormalizePath(tmpDir)),
1292-
]
1293-
)
12941284
return substitutions
12951285

12961286

llvm/utils/lit/lit/builtin_commands/diff.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ def main(argv):
281281
try:
282282
for file in args:
283283
if file != "-" and not os.path.isabs(file):
284-
file = os.path.realpath(os.path.join(os.getcwd(), file))
284+
file = util.abs_path_preserve_drive(file)
285285

286286
if flags.recursive_diff:
287287
if file == "-":

llvm/utils/lit/lit/discovery.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import sys
88

99
from lit.TestingConfig import TestingConfig
10-
from lit import LitConfig, Test
10+
from lit import LitConfig, Test, util
1111

1212

1313
def chooseConfigFileFromDir(dir, config_names):
@@ -56,8 +56,8 @@ def search1(path):
5656
# configuration to load instead.
5757
config_map = litConfig.params.get("config_map")
5858
if config_map:
59-
cfgpath = os.path.realpath(cfgpath)
60-
target = config_map.get(os.path.normcase(cfgpath))
59+
cfgpath = util.abs_path_preserve_drive(cfgpath)
60+
target = config_map.get(cfgpath)
6161
if target:
6262
cfgpath = target
6363

@@ -67,13 +67,13 @@ def search1(path):
6767

6868
cfg = TestingConfig.fromdefaults(litConfig)
6969
cfg.load_from_path(cfgpath, litConfig)
70-
source_root = os.path.realpath(cfg.test_source_root or path)
71-
exec_root = os.path.realpath(cfg.test_exec_root or path)
70+
source_root = util.abs_path_preserve_drive(cfg.test_source_root or path)
71+
exec_root = util.abs_path_preserve_drive(cfg.test_exec_root or path)
7272
return Test.TestSuite(cfg.name, source_root, exec_root, cfg), ()
7373

7474
def search(path):
7575
# Check for an already instantiated test suite.
76-
real_path = os.path.realpath(path)
76+
real_path = util.abs_path_preserve_drive(path)
7777
res = cache.get(real_path)
7878
if res is None:
7979
cache[real_path] = res = search1(path)

llvm/utils/lit/lit/util.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,23 @@ def usable_core_count():
127127

128128
return n
129129

130+
def abs_path_preserve_drive(path):
131+
"""Return the absolute path without resolving drive mappings on Windows.
132+
133+
"""
134+
if platform.system() == "Windows":
135+
# Windows has limitations on path length (MAX_PATH) that
136+
# can be worked around using substitute drives, which map
137+
# a drive letter to a longer path on another drive.
138+
# Since Python 3.8, os.path.realpath resolves sustitute drives,
139+
# so we should not use it. In Python 3.7, os.path.realpath
140+
# was implemented as os.path.abspath.
141+
return os.path.normpath(os.path.abspath(path))
142+
else:
143+
# On UNIX, the current directory always has symbolic links resolved,
144+
# so any program accepting relative paths cannot preserve symbolic
145+
# links in paths and we should always use os.path.realpath.
146+
return os.path.normpath(os.path.realpath(path))
130147

131148
def mkdir(path):
132149
try:

llvm/utils/lit/tests/Inputs/config-map-discovery/driver.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@
22
import os
33
import sys
44

5-
main_config = sys.argv[1]
6-
main_config = os.path.realpath(main_config)
7-
main_config = os.path.normcase(main_config)
5+
main_config = lit.util.abs_path_preserve_drive(sys.argv[1])
86

97
config_map = {main_config: sys.argv[2]}
108
builtin_parameters = {"config_map": config_map}

llvm/utils/lit/tests/Inputs/config-map-discovery/lit.alt.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@ config.suffixes = ['.txt']
55
config.test_format = lit.formats.ShTest()
66

77
import os
8-
config.test_exec_root = os.path.realpath(os.path.dirname(__file__))
8+
config.test_exec_root = os.path.dirname(lit.util.abs_path_preserve_drive(__file__))
99
config.test_source_root = os.path.join(config.test_exec_root, "tests")

0 commit comments

Comments
 (0)