Compare LLVM_ROOT case-insensitively in the sanity check on Windows - #27624
Compare LLVM_ROOT case-insensitively in the sanity check on Windows#27624tovabar wants to merge 1 commit into
Conversation
generate_sanity() embeds config.LLVM_ROOT verbatim and check_sanity() string-compares the result against cache/sanity.txt, erasing the entire cache when they differ. LLVM_ROOT is derived from the path the compiler was invoked through, so on Windows two spellings of one directory -- "C:/llvm/bin" and "c:\llvm\bin" -- yield two different sanity strings, and each run erases the other's cache. This triggers without doing anything unusual. VSCode's CMake Tools normalizes the compiler path to lowercase before handing it to the C/C++ extension, which runs it to query built-in defines, while CMake and ninja invoke the same em++ in its canonical case. The two alternate, so every editor restart wipes the cache, reinstalls the sysroot headers, and leaves the build system rebuilding every object in the project. Run LLVM_ROOT through os.path.normcase(), which folds both case and separator style on Windows and is the identity on POSIX. This costs one cache clear on upgrade, as any change to the sanity string does. Related: emscripten-core#15053
|
Out of curiosity do you know how/why you are settings Perhaps some plugin is trying to replicate the contends of the config file use |
|
OK I think I figured out what is going on here (with Gemini's help). This issue can occur even without EM_LLVM_ROOT set on windows. The issue is that in emsdk config file we have `LLVM = '$CFGDIR/upstream/bin' so we have the following sequence:
At least now I understand what is going on here, and I think a fix like this does indeed make sense.. Its just a matter of deciding where to do the normalization. |
On Windows, file systems are case-insensitive. When tools like VS Code CMake Tools invoke the compiler through an all-lowercased path (e.g. via its `platformNormalizePath` helper), the caller-specified casing affects the value of `__file__` in Python which then propagates all the way to `LLVM_ROOT` and the `sanity.txt` file (at least in the emsdk case): `__file__` -> `__rootpath__` -> `EM_CONFIG` -> `CFGDIR` -> `LLVM_ROOT` -> `sanity.txt`. Address this in two places: 1. In `tools/utils.py`, normalize `__file__` using `normcase` when initializing `__rootpath__`. This prevents caller-specified casing from polluting paths derived from the script location (default `EM_CONFIG`, `$CFGDIR`, and default `CACHE`). 2. In `tools/shared.py`, normalize `config.LLVM_ROOT` using `normcase` in `generate_sanity()`. This ensures that even if `LLVM_ROOT` was configured explicitly (e.g. via `EM_LLVM_ROOT` or an absolute path in `.emscripten`), the string written to `sanity.txt` is always canonical on Windows. See: emscripten-core#27624
|
I took the liberty of creating a modified version of this PR: #27626. Are you ok my version (happy to credit you), or do you want to update this one? |
|
Your version is better — please go with #27626. Normalizing Your test is better too. To answer your earlier question: we are not setting |
|
Ok, lets close this in favor of #27626. |
On Windows, file systems are case-insensitive. When tools like VS Code CMake Tools invoke the compiler through an all-lowercased path (e.g. via its `platformNormalizePath` helper), the caller-specified casing affects the value of `__file__` in Python which then propagates all the way to `LLVM_ROOT` and the `sanity.txt` file (at least in the emsdk case): `__file__` -> `__rootpath__` -> `EM_CONFIG` -> `CFGDIR` -> `LLVM_ROOT` -> `sanity.txt`. Address this in two places: 1. In `tools/utils.py`, normalize `__file__` using `normcase` when initializing `__rootpath__`. This prevents caller-specified casing from polluting paths derived from the script location (default `EM_CONFIG`, `$CFGDIR`, and default `CACHE`). 2. In `tools/shared.py`, normalize `config.LLVM_ROOT` using `normcase` in `generate_sanity()`. This ensures that even if `LLVM_ROOT` was configured explicitly (e.g. via `EM_LLVM_ROOT` or an absolute path in `.emscripten`), the string written to `sanity.txt` is always canonical on Windows. The proximate cause of the issue was that the VS Code CMake Tools extension explicitly lowercases compiler paths on Windows: https://github.com/microsoft/vscode-cmake-tools/blob/09dd68b54de06469bd1b81d84d6b8666aa2dce1d/src/util.ts#L113-L115 By forcing the command line to all-lowercase, the above chain of events kicked in whenever the IDE queried compiler settings. See: #27624
Problem
generate_sanity()embedsconfig.LLVM_ROOTverbatim, andcheck_sanity()compares that string againstcache/sanity.txt, callingcache.erase()on any difference:LLVM_ROOTis derived from the path the compiler was invoked through, so on Windows the very same directory can arrive asC:/llvm/binfrom one caller andc:\llvm\binfrom another. Windows resolves both to one directory, but they produce different sanity strings — so two callers that spell it differently erase each other's cache on every alternation.How it shows up in practice
This needs nothing unusual to trigger. VSCode's CMake Tools normalizes the compiler path with
util.platformNormalizePath(normCase: 'platform', i.e. lowercase on win32) before handing it to the C/C++ extension ascompilerPath, and the extension then executes it to query built-in defines and includes. CMake and ninja invoke the sameem++in its canonical case. The two alternate, so the cache is erased on each window load.Each wipe reinstalls the sysroot headers. Every object file depends on those headers, so the build system then considers the entire project out of date. On a large codebase that is a full rebuild — minutes of work — every time the editor restarts, with nothing in the source tree, the CMake cache, or the build directory having changed.
Straight from a CMake configure log, with the two strings side by side:
Minimal reproduction on Windows — compile anything twice, varying only the case of the path you invoke the compiler through, and watch the cache get erased both times:
Fix
Run
LLVM_ROOTthroughos.path.normcase()when building the sanity string. On Windows that folds both case and separator style (ntpath.normcaselowercases and maps/to\), so every spelling of one directory yields one sanity string. On POSIX it is the identity, where the spellings genuinely are distinct paths.Notes
config.normalize_config_settings()instead. I kept it at the point of comparison so that nothing else in the toolchain observes a rewrittenLLVM_ROOT— happy to move it if you would rather have the config normalized at load.Testing
Added
test_llvm_root_path_spellingtotest/test_sanity.py. It establishes the sanity file, then re-runsemccwithEM_LLVM_ROOTrespelled (uppercased, backslash-separated) and asserts that no sanity re-check occurs. It fails before this change and passes after. Marked@only_windows, since path case and separator style only alias there.One caveat in the interest of full disclosure: I was not able to run the suite locally — I do not have an LLVM 24 build to hand — so I am relying on CI for the Windows run. I verified the underlying property directly (
ntpath.normcasemaps both spellings to one string;posixpath.normcaseis the identity).