Emacs threw this at me after a macOS upgrade:
ld: library not found for -lemutls_w
libgccjit.so: error: error invoking gcc driver
Internal native compiler error: failed to compile ...
I stared at it. I didn’t know what emutls_w was. I didn’t know what libgccjit was doing in my text editor. I definitely didn’t know why the linker was complaining about a library I’d never heard of.
Three hours later, I understood the entire compilation toolchain, fixed the bug, hit a second bug caused by macOS GUI apps not inheriting shell environment variables, fixed that too, and came out the other side with a mental model I wish someone had just written down.
So here it is.
What native compilation actually is
Emacs has always been an Elisp interpreter. Your config, your packages, every M-x command — it’s all Elisp, interpreted at runtime. This is fine. It’s also slow, in the way that interpreted languages are always slow compared to compiled ones.
Emacs 28 (released 2022) added native compilation — also called gccemacs or the native-comp branch before it merged. The idea: instead of interpreting Elisp at runtime, compile it to native machine code ahead of time using libgccjit, the GCC JIT compilation library.
The pipeline looks like this:
Elisp source (.el)
→ byte-compile to bytecode (.elc)
→ libgccjit compiles bytecode to native code (.eln)
→ stored in ~/.emacs.d/eln-cache/
The .eln files are shared libraries — actual compiled native code for your architecture. Emacs loads them instead of interpreting the .elc bytecode. The result is 2-5x faster Elisp execution. You notice it in things like Org-mode rendering, completion frameworks, and anything that does heavy Elisp processing.
The catch: libgccjit is a GCC library. It needs GCC. Not “a C compiler” — specifically GCC, because libgccjit is part of the GCC project and links against GCC internals.
On macOS, this is where things get interesting.
The macOS compiler situation
macOS has a fun property: /usr/bin/gcc is not GCC.
$ which gcc
/usr/bin/gcc
$ gcc --version
Apple clang version 16.0.0 (clang-1600.0.26.6)
Apple ships Clang and symlinks it to gcc. This has been the case since… Xcode 5? A decade. Every macOS developer has been tripped by this at least once. The system “gcc” is Apple Clang wearing a fake mustache.
This matters because libgccjit doesn’t work with Clang. It’s a GCC component that generates code through GCC’s internal pipeline. When it tries to invoke gcc to finish the compilation, it expects to find actual GCC — not Clang pretending to be GCC.
So you end up with this call chain:
Emacs → libgccjit → "gcc" → Apple Clang → ld → error: -lemutls_w not found
-lemutls_w is a GCC-specific threading emulation library. Clang doesn’t have it, doesn’t need it, doesn’t ship it. But libgccjit asked for it because it thinks it’s talking to GCC.
The error message makes perfect sense once you understand the call chain. Before you understand the call chain, it looks like your text editor is trying to link against a threading library and nobody knows why.
Fix 1: Give Emacs real GCC
Install the actual GCC and libgccjit from Homebrew:
brew install gcc libgccjit
Verify it’s there:
$ which gcc-15
/opt/homebrew/bin/gcc-15
$ gcc-15 --version
gcc-15 (Homebrew GCC 15.1.0) 15.1.0
Now tell Emacs (and libgccjit) to use this compiler instead of the system impostor:
export CC=/opt/homebrew/bin/gcc-15
Add that to ~/.zshrc so it persists.
One more thing — clear the native compilation cache. The old .eln files were compiled (or failed to compile) with the wrong toolchain. They need to go:
rm -rf ~/.emacs.d/eln-cache
Restart Emacs from the terminal. The -lemutls_w error should be gone.
Mine was. But it was replaced with a new error: -lSystem not found.
Fix 2: SDK visibility
GCC needs to find macOS system libraries — things like libSystem, which is the fundamental C runtime on macOS. These live inside the macOS SDK, and GCC needs to be told where the SDK is.
Check where your SDK lives:
$ xcrun --show-sdk-path
/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk
If that looks right (or if you’re not sure), the fix is:
export SDKROOT=$(xcrun --show-sdk-path)
Add to ~/.zshrc. Restart Emacs from the terminal.
Native compilation should now work. You’ll see Emacs quietly compiling .eln files in the background (check *Async-native-compile-log* buffer if you’re curious).
If xcrun --show-sdk-path gives you an error or a weird path, you might need to reinstall the Command Line Tools:
sudo rm -rf /Library/Developer/CommandLineTools
xcode-select --install
Then set SDKROOT again.
At this point I thought I was done. Terminal Emacs worked perfectly. Opened Emacs from Finder — same error as before.
Fix 3: The GUI environment variable trap
This is a classic macOS gotcha that bites people across all kinds of tools, not just Emacs.
macOS GUI applications do not inherit your shell environment variables.
When you launch Emacs from the terminal, it inherits your shell environment — CC, SDKROOT, PATH, everything in ~/.zshrc. When you launch Emacs from Spotlight, Finder, or the Dock, it gets a minimal environment set by launchd. Your ~/.zshrc never runs. CC is unset. SDKROOT is unset. libgccjit falls back to /usr/bin/gcc (Apple Clang), and we’re back to square one.
The fix is launchctl setenv, which sets environment variables at the launchd level — the process that launches GUI apps:
launchctl setenv CC /opt/homebrew/bin/gcc-15
launchctl setenv SDKROOT $(xcrun --show-sdk-path)
These take effect for any GUI app launched after you run them. No restart required, but you do need to quit and relaunch Emacs.
The downside: launchctl setenv doesn’t persist across reboots. You’ll need to either:
- Run these commands at login (via a LaunchAgent plist or a login item script)
- Use a tool like
exec-path-from-shellin Emacs to pull in your shell environment at startup - Just launch Emacs from the terminal (honestly, this is what I do)
The full fix, summarized
# 1. Install real GCC
brew install gcc libgccjit
# 2. Set compiler (add to ~/.zshrc)
export CC=/opt/homebrew/bin/gcc-15
# 3. Set SDK path (add to ~/.zshrc)
export SDKROOT=$(xcrun --show-sdk-path)
# 4. Clear stale compilation cache
rm -rf ~/.emacs.d/eln-cache
# 5. For GUI launch (run after login, or via LaunchAgent)
launchctl setenv CC /opt/homebrew/bin/gcc-15
launchctl setenv SDKROOT $(xcrun --show-sdk-path)
What I actually learned
The specific bug doesn’t matter that much. Here’s what generalizes:
Know the call chain. The error said ld: library not found for -lemutls_w. That’s a linker error. But the linker was invoked by gcc, which was invoked by libgccjit, which was invoked by Emacs. Fixing the linker error means fixing the thing four layers up that called the wrong gcc. Error messages tell you where the chain broke. They don’t tell you where the chain went wrong.
macOS /usr/bin is a lie. /usr/bin/gcc is Clang. /usr/bin/python3 is… complicated. Apple shims these paths for compatibility, and they mostly work, until you need the actual tool they claim to be. When debugging toolchain issues on macOS, always check what’s really behind the path.
GUI vs terminal is a real environment split. This isn’t an Emacs quirk — it’s a macOS architecture decision. Any tool that depends on environment variables will behave differently when launched from Spotlight vs Terminal. I’ve seen this bite people with PATH for Homebrew binaries, JAVA_HOME, NVM_DIR, and now CC and SDKROOT. If something works in your terminal but not in a GUI app, check the environment first.
Clear caches when toolchains change. The .eln-cache was full of files compiled (or half-compiled) with the wrong compiler. Emacs doesn’t automatically invalidate these when CC changes. After any toolchain change — compiler version, SDK path, Homebrew upgrade — nuke the cache and let it rebuild.
The whole debugging session took about three hours, most of which was spent being intimidated by unfamiliar terminology. Once I understood the call chain — Emacs calls libgccjit, libgccjit calls gcc, gcc calls ld — the fix was straightforward. The hard part wasn’t the fix. The hard part was building the mental model that made the fix obvious.