From 4ea39c43e1338a16cf165f234dd88b8ecb53dcd1 Mon Sep 17 00:00:00 2001 From: Junji Watanabe Date: Tue, 3 Mar 2026 14:45:06 +0900 Subject: [PATCH] [LIT] Use forward slashes in substitutions when LLVM_WINDOWS_PREFER_FORWARD_SLASH is set (#179865) When building with `-DLLVM_WINDOWS_PREFER_FORWARD_SLASH=ON`, tools like lld output paths with forward slashes on Windows. However, lit's default substitutions (`%t`, `%p`) typically use backslashes on Windows, causing FileCheck failures in tests that strictly match path separators. This patch propagates the `LLVM_WINDOWS_PREFER_FORWARD_SLASH` build flag to llvm-lit via `builtin_parameters`. It also updates lit's TestRunner to respect the 'use_normalized_slashes' parameter. When enabled, lit normalizes paths in substitutions to use forward slashes, ensuring that test expectations align with the tool output. With this fix, the number of failed tests with `-DLLVM_WINDOWS_PREFER_FORWARD_SLASH=ON` changes as follow: - The total number of failed tests: 303 -> 168 - Break down: - `Builtins-i386-windows` tests: 99 -> 0 - `Clang` tests: 28 -> 5 - `Clang Tools` tests: 7 -> 1 - `LLVM` tests: 6 -> 4 - `lld` tests: 2 -> 0 - Other failed tests: - `glang`: 1 - `Clang-Unit`: 5 - `Clangd`: 3 - `Clangd Unit Tests`: 150 - `LLVM-Unit`: 1 --- llvm/utils/lit/lit/LitConfig.py | 2 + llvm/utils/lit/lit/TestRunner.py | 49 ++++++++++++++++++++----- llvm/utils/lit/lit/llvm/config.py | 8 ++++ llvm/utils/lit/tests/lit.cfg | 13 +++++-- llvm/utils/lit/tests/shtest-readfile.py | 2 +- llvm/utils/llvm-lit/llvm-lit.in | 5 +++ 6 files changed, 66 insertions(+), 13 deletions(-) diff --git a/llvm/utils/lit/lit/LitConfig.py b/llvm/utils/lit/lit/LitConfig.py index 71dad85bbadd..3b0dfaa981d4 100644 --- a/llvm/utils/lit/lit/LitConfig.py +++ b/llvm/utils/lit/lit/LitConfig.py @@ -216,6 +216,8 @@ class LitConfig(object): # Step out of _write_message, and then out of wrapper. f = f.f_back.f_back file = os.path.abspath(inspect.getsourcefile(f)) + if lit.util.pythonize_bool(self.params.get("use_normalized_slashes")): + file = file.replace("\\", "/") line = inspect.getlineno(f) sys.stderr.write( "%s: %s:%d: %s: %s\n" % (self.progname, file, line, kind, message) diff --git a/llvm/utils/lit/lit/TestRunner.py b/llvm/utils/lit/lit/TestRunner.py index 0d72592ad23e..9e7d64a5ea8c 100644 --- a/llvm/utils/lit/lit/TestRunner.py +++ b/llvm/utils/lit/lit/TestRunner.py @@ -89,12 +89,13 @@ class ShellEnvironment(object): we maintain a dir stack for pushd/popd. """ - def __init__(self, cwd, env, umask=-1, ulimit=None): + def __init__(self, cwd, env, umask=-1, ulimit=None, normalize_slashes=False): self.cwd = cwd self.env = dict(env) self.umask = umask self.dirStack = [] self.ulimit = ulimit if ulimit else {} + self.normalize_slashes = normalize_slashes def change_dir(self, newdir): if os.path.isabs(newdir): @@ -723,7 +724,7 @@ def processRedirects(cmd, stdin_source, cmd_shenv, opened_files): return std_fds -def _expandLateSubstitutions(cmd, arguments, cwd): +def _expandLateSubstitutions(cmd, arguments, cwd, normalize_slashes=False): for i, arg in enumerate(arguments): if not isinstance(arg, str): continue @@ -732,6 +733,8 @@ def _expandLateSubstitutions(cmd, arguments, cwd): filePath = match.group(1) if not os.path.isabs(filePath): filePath = os.path.join(cwd, filePath) + if normalize_slashes: + filePath = filePath.replace("\\", "/") try: with open(filePath) as fileHandle: return fileHandle.read() @@ -815,7 +818,9 @@ def _executeShCmd(cmd, shenv, results, timeoutHelper): not_crash = False # Expand all late substitutions. - args = _expandLateSubstitutions(j, args, cmd_shenv.cwd) + args = _expandLateSubstitutions( + j, args, cmd_shenv.cwd, cmd_shenv.normalize_slashes + ) while True: if args[0] == "env": @@ -826,7 +831,12 @@ def _executeShCmd(cmd, shenv, results, timeoutHelper): # env FOO=1 llc < %s | env BAR=2 llvm-mc | FileCheck %s # env FOO=1 %{another_env_plus_cmd} | FileCheck %s if cmd_shenv is shenv: - cmd_shenv = ShellEnvironment(shenv.cwd, shenv.env, shenv.umask) + cmd_shenv = ShellEnvironment( + shenv.cwd, + shenv.env, + shenv.umask, + normalize_slashes=shenv.normalize_slashes, + ) args = updateEnv(cmd_shenv, args) if not args: # Return the environment variables if no argument is provided. @@ -1225,7 +1235,10 @@ def executeScriptInternal( results = [] timeoutInfo = None - shenv = ShellEnvironment(cwd, test.config.environment) + normalize_slashes = litConfig.params.get("use_normalized_slashes", False) + shenv = ShellEnvironment( + cwd, test.config.environment, normalize_slashes=normalize_slashes + ) shenv.env["LIT_CURRENT_TESTCASE"] = test.getFullName() exitCode, timeoutInfo = executeShCmd( @@ -1524,11 +1537,25 @@ def getDefaultSubstitutions(test, tmpDir, tmpBase, normalize_slashes=False): substitutions.append(("%{s:basename}", sourceBaseName)) substitutions.append(("%{t:stem}", tmpBaseName)) + fs_sep = os.path.sep + if normalize_slashes: + fs_sep = "/" + substitutions.extend( [ - ("%{fs-src-root}", pathlib.Path(sourcedir).anchor), - ("%{fs-tmp-root}", pathlib.Path(tmpBase).anchor), - ("%{fs-sep}", os.path.sep), + ( + "%{fs-src-root}", + pathlib.Path(sourcedir).anchor.replace("\\", "/") + if normalize_slashes + else pathlib.Path(sourcedir).anchor, + ), + ( + "%{fs-tmp-root}", + pathlib.Path(tmpBase).anchor.replace("\\", "/") + if normalize_slashes + else pathlib.Path(tmpBase).anchor, + ), + ("%{fs-sep}", fs_sep), ] ) @@ -2465,7 +2492,11 @@ def executeShTest( tmpDir, tmpBase = getTempPaths(test) substitutions = list(extra_substitutions) substitutions += getDefaultSubstitutions( - test, tmpDir, tmpBase, normalize_slashes=useExternalSh + test, + tmpDir, + tmpBase, + normalize_slashes=useExternalSh + or litConfig.params.get("use_normalized_slashes", False), ) conditions = {feature: True for feature in test.config.available_features} script = applySubstitutions( diff --git a/llvm/utils/lit/lit/llvm/config.py b/llvm/utils/lit/lit/llvm/config.py index 8b794c697889..de70e80e6017 100644 --- a/llvm/utils/lit/lit/llvm/config.py +++ b/llvm/utils/lit/lit/llvm/config.py @@ -57,6 +57,10 @@ class LLVMConfig(object): self.lit_config.diagnostic_level_enabled("note") and lit_path_displayed is False ): + if lit.util.pythonize_bool( + self.lit_config.params.get("use_normalized_slashes") + ): + path = path.replace("\\", "/") self.lit_config.note("using lit tools: {}".format(path)) lit_path_displayed = True @@ -557,6 +561,10 @@ class LLVMConfig(object): if tool: tool = os.path.normpath(tool) + if lit.util.pythonize_bool( + self.lit_config.params.get("use_normalized_slashes") + ): + tool = tool.replace("\\", "/") if not quiet: self.lit_config.note("using {}: {}".format(name, tool)) return tool diff --git a/llvm/utils/lit/tests/lit.cfg b/llvm/utils/lit/tests/lit.cfg index 922fbd6c9c87..8fa625a16240 100644 --- a/llvm/utils/lit/tests/lit.cfg +++ b/llvm/utils/lit/tests/lit.cfg @@ -64,14 +64,21 @@ for attribute in ("llvm_tools_dir", "lit_tools_dir"): # within %{inputs}'s test suites. Thus, %{lit} clears environment variables # that can affect FileCheck's output. It also includes "--order=lexical -j1" # to ensure predictable test order, as it is often required for FileCheck -# matches. +# matches. It also specifies "-D use_normalized_slashes={0,1}" to be aligned +# with the slashes in the substitutions (e.g, %t, %p) for Windows since it +# switches between forward slashes and backslashes based on +# LLVM_WINDOWS_PREFER_FORWARD_SLASH build config. config.substitutions.append(("%{inputs}", "Inputs")) config.substitutions.append(("%{lit}", "%{lit-no-order-opt} --order=lexical")) +use_normalized_slashes = "0" +if lit.util.pythonize_bool(lit_config.params.get("use_normalized_slashes")): + use_normalized_slashes = "1" config.substitutions.append( ( "%{lit-no-order-opt}", - "{env} %{{python}} {lit} -j1".format( - env="env -u FILECHECK_OPTS", lit=os.path.join(lit_path, "lit.py") + "{env} %{{python}} {lit} -j1 -D use_normalized_slashes={use_normalized_slashes}".format( + env="env -u FILECHECK_OPTS", lit=os.path.join(lit_path, "lit.py"), + use_normalized_slashes=use_normalized_slashes, ), ) ) diff --git a/llvm/utils/lit/tests/shtest-readfile.py b/llvm/utils/lit/tests/shtest-readfile.py index ca57db82e661..57b94c57a9ba 100644 --- a/llvm/utils/lit/tests/shtest-readfile.py +++ b/llvm/utils/lit/tests/shtest-readfile.py @@ -3,7 +3,7 @@ # TODO(boomanaiden154): This sometimes fails, possibly due to buffers not being flushed. # ALLOW_RETRIES: 2 -# RUN: env LIT_USE_INTERNAL_SHELL=1 not %{lit} -v %{inputs}/shtest-readfile | FileCheck -match-full-lines -DTEMP_PATH=%S%{fs-sep}Inputs%{fs-sep}shtest-readfile%{fs-sep}Output %s +# RUN: env LIT_USE_INTERNAL_SHELL=1 not %{lit} -v %{inputs}/shtest-readfile | FileCheck -match-full-lines -DTEMP_PATH=%/S/Inputs/shtest-readfile/Output %s # CHECK: -- Testing: 5 tests{{.*}} diff --git a/llvm/utils/llvm-lit/llvm-lit.in b/llvm/utils/llvm-lit/llvm-lit.in index 0b6683b6b623..62d42e062e04 100755 --- a/llvm/utils/llvm-lit/llvm-lit.in +++ b/llvm/utils/llvm-lit/llvm-lit.in @@ -25,6 +25,11 @@ builtin_parameters['config_map'] = config_map llvm_source_root = path(r'@LLVM_SOURCE_DIR@') sys.path.insert(0, os.path.join(llvm_source_root, 'utils', 'lit')) +import lit.util + +if lit.util.pythonize_bool('@LLVM_WINDOWS_PREFER_FORWARD_SLASH@'): + builtin_parameters['use_normalized_slashes'] = True + if __name__=='__main__': from lit.main import main main(builtin_parameters)