Skip to content

Compare LLVM_ROOT case-insensitively in the sanity check on Windows - #27624

Closed
tovabar wants to merge 1 commit into
emscripten-core:mainfrom
tovabar:sanity-normcase-llvm-root
Closed

Compare LLVM_ROOT case-insensitively in the sanity check on Windows#27624
tovabar wants to merge 1 commit into
emscripten-core:mainfrom
tovabar:sanity-normcase-llvm-root

Conversation

@tovabar

@tovabar tovabar commented Aug 30, 2026

Copy link
Copy Markdown

Problem

generate_sanity() embeds config.LLVM_ROOT verbatim, and check_sanity() compares that string against cache/sanity.txt, calling cache.erase() on any difference:

def generate_sanity():
  return f'{utils.EMSCRIPTEN_VERSION}|{config.LLVM_ROOT}\n'

LLVM_ROOT is derived from the path the compiler was invoked through, so on Windows the very same directory can arrive as C:/llvm/bin from one caller and c:\llvm\bin from 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 as compilerPath, and the extension then executes it to query built-in defines and includes. CMake and ninja invoke the same em++ 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:

shared:INFO: old sanity: 6.0.3|c:/users/user/.../upstream/bin
shared:INFO: new sanity: 6.0.3|C:/Users/User/.../upstream/bin
shared:INFO: (Emscripten: config changed, clearing cache)
shared:INFO: (Emscripten: Running sanity checks)
cache:INFO: generating system headers: sysroot_install.stamp...

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:

C:\path\to\emsdk\upstream\emscripten\emcc.exe -c hello.c -o hello.o
c:\path\to\emsdk\upstream\emscripten\emcc.exe -c hello.c -o hello.o

Fix

Run LLVM_ROOT through os.path.normcase() when building the sanity string. On Windows that folds both case and separator style (ntpath.normcase lowercases 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

  • This costs one cache clear on the release that includes it, as any change to the sanity string does.
  • I considered normalizing once in config.normalize_config_settings() instead. I kept it at the point of comparison so that nothing else in the toolchain observes a rewritten LLVM_ROOT — happy to move it if you would rather have the config normalized at load.
  • Related: Cache sanity file llvm as relative path from emscripten root #15053 asks for the sanity file to stop being sensitive to where the toolchain lives. This is the narrower case: same location, different spelling.

Testing

Added test_llvm_root_path_spelling to test/test_sanity.py. It establishes the sanity file, then re-runs emcc with EM_LLVM_ROOT respelled (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.normcase maps both spellings to one string; posixpath.normcase is the identity).

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
@sbc100

sbc100 commented Aug 30, 2026

Copy link
Copy Markdown
Collaborator

Out of curiosity do you know how/why you are settings EM_LLVM_ROOT in the environment? If you using emsdk this should never necessary as LLVM_ROOT should be found via emsdk/.emscripten config file. Is this something that is being done by vscode or some vscode plugin maybe? I wonder if we should also/instead consider fixing that.

Perhaps some plugin is trying to replicate the contends of the config file use EM_LLVM_ROOT and EM_BINARYEN_ROOT instead of just pointing directly at the config file itself using EM_CONFIG=... ?

@sbc100

sbc100 commented Aug 30, 2026

Copy link
Copy Markdown
Collaborator

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:

  1. LLVM depends on CFGDIR
  2. CFGDIR depend on EM_CONFIG
  3. EM_CONFIG by default is calculated relative to __file__ in the python code.
  4. __file__ for tools/utils.py is calculated based on sys.path[0] internally in python
  5. sys.path[0] is set based on the command line. (e.g. python c:/path/to/emcc.py will have a different sys.path[0] to python C:/PATH/TO/emcc.py)

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.

sbc100 added a commit to sbc100/emscripten that referenced this pull request Aug 30, 2026
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
@sbc100

sbc100 commented Aug 30, 2026

Copy link
Copy Markdown
Collaborator

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?

@tovabar

tovabar commented Aug 31, 2026

Copy link
Copy Markdown
Author

Your version is better — please go with #27626.

Normalizing __rootpath__ in utils.py is the right place: it fixes the source rather than the symptom, and it also makes the default CACHE path stable, which I had missed. Two differently-cased invocations resolving different cache directories would have been the same bug wearing a different hat.

Your test is better too. To answer your earlier question: we are not setting EM_LLVM_ROOT anywhere — I only reached for it in the test as a way to vary LLVM_ROOT without re-invoking the compiler through a differently-cased path. Driving it through EMCC.lower() / EMCC.upper() is the scenario that actually happens, and the normcase fix to check_working is a good catch.

@sbc100

sbc100 commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Ok, lets close this in favor of #27626.

@sbc100 sbc100 closed this Aug 31, 2026
sbc100 added a commit that referenced this pull request Aug 31, 2026
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants