Yesterday during our rust workshop we ran into this very niche issue compiling rust on a windows machine.
The issue is related to libgcc. the below wasa the fix summary from claude.
Details
What libgcc_eh.a is
libgcc is GCC's runtime support library — a bundle of low-level helpers the compiler emits calls to for things like 64-bit arithmetic on 32-bit systems, software floating point, etc. Most C/C++ programs need it linked in.
libgcc_eh is the exception handling part of that runtime, split out into its own library. The _eh stands for "exception handling." It contains the unwinder code that runs when a C++ throw propagates up the stack, or when Rust does a panic! that needs to unwind.
Historically GCC ships both as separate libraries when built as shared libraries, so you can dynamically link against just the EH bits if that's all you need. But when GCC is built with --disable-shared (static-only), the EH code gets folded directly into libgcc.a and the separate libgcc_eh.a doesn't get produced. There's nothing to put in it — it's already inside libgcc.a.
Why this matters for Rtools
Rtools is built --disable-shared because R packages need to be statically self-contained on Windows. So Rtools' libgcc.a has the EH code baked in, and there's no separate libgcc_eh.a.
For C and C++ R packages, this is fine — R's build system knows to just link -lgcc and not ask for -lgcc_eh separately.
Why Rust breaks
Rust's Windows-GNU target was designed against a "normal" mingw-w64 toolchain (like MSYS2's), which does ship a separate libgcc_eh.a. So when rustc generates linker commands, it hardcodes -lgcc_eh in the list. It doesn't know or care that Rtools merged everything into libgcc.
When the linker hits -lgcc_eh, it searches for libgcc_eh.a, doesn't find one, and bails out:
ld.exe: cannot find -lgcc_eh: No such file or directory
What the fix does
By copying libgcc.a to libgcc_eh.a, you're handing the linker exactly what it's asking for. Since the EH code is already inside libgcc.a, the duplicate file has all the symbols the linker needs. When -lgcc_eh is processed, the linker finds the file, scans it for the needed symbols, and is happy. The fact that it's a byte-for-byte copy of libgcc.a means no symbols are missing.
It's slightly wasteful (some symbols get pulled in twice during linking, but the linker dedupes), but it's harmless and it works.
Why this is everyone's problem, not just yours
This has been an open issue in the extendr/rextendr/Rtools world for ~5 years. There's a long-standing request to have Rtools just include the symlink/copy automatically (the "libgcc_eh hack" — it's literally a one-line patch in the Rtools build script). The mingw32 build of Rtools has it; the mingw64 build doesn't. Anyone trying to build Rust-based R packages on Windows hits this exact wall.
So the TL;DR:
Rust's linker asks for a library file (libgcc_eh.a) that doesn't exist in Rtools because Rtools builds GCC in a mode where that file's contents get merged into another library (libgcc.a). The fix is to make a copy of libgcc.a named libgcc_eh.a so the linker finds what it's looking for. The two files are identical; you're just giving the linker the filename it expects.
Glad it's working! That was a four-layer onion of Windows build pain.
On Fri, Jun 26, 2026 at 3:47 PM Alysson Farris <[redacted]> wrote:
> devtools::document()
ℹ Updating cascadia documentation
ℹ Loading cascadia
ℹ Re-compiling cascadia (debug build)
── R CMD INSTALL ─────────────────────────────────────────────────────────────────────────────────────
─ installing *source* package 'cascadia' ... (486ms)
** this is package 'cascadia' version '0.0.0.9000'
** using staged installation
Using cargo 1.96.0 (30a34c682 2026-05-25)
Using rustc 1.96.0 (ac68faa20 2026-05-25)
Creating DEBUG build.
Writing `src/Makevars.win`.
`tools/config.R` has finished.
** libs
using C compiler: 'gcc.exe (GCC) 14.3.0'
gcc -I"C:/PROGRA~1/R/R-45~1.3/include" -DNDEBUG -I"C:/rtools45/x86_64-w64-mingw32.static.posix/include" -O2 -Wall -std=gnu2x -gdwarf-2 -mfpmath=sse -msse2 -mstackrealign -UNDEBUG -Wall -pedantic -g -O0 -fdiagnostics-color=always -c entrypoint.c -o entrypoint.o
mkdir -p ./rust/target/libgcc_mock
touch ./rust/target/libgcc_mock/libgcc_eh.a
if [ -d ./vendor ]; then \
echo "=== Using offline vendor directory ==="; \
mkdir -p /c/Users/alyss/cascadia/src/.cargo && \
cp rust/vendor-config.toml /c/Users/alyss/cascadia/src/.cargo/config.toml; \
elif [ -f ./rust/vendor.tar.xz ]; then \
echo "=== Using offline vendor tarball ==="; \
tar xf rust/vendor.tar.xz && \
mkdir -p /c/Users/alyss/cascadia/src/.cargo && \
cp rust/vendor-config.toml /c/Users/alyss/cascadia/src/.cargo/config.toml; \
fi
# Build the project using Cargo with additional flags
export CARGO_HOME=/c/Users/alyss/cascadia/src/.cargo && \
export LIBRARY_PATH=";/c/Users/alyss/cascadia/src/./rust/target/libgcc_mock" && \
RUSTFLAGS=" --print=native-static-libs" cargo build --target=x86_64-pc-windows-gnu --lib --manifest-path=rust/Cargo.toml --target-dir=./rust/target
note: link against the following native artifacts when linking against this static library. The order and any duplication can be significant on some platforms
note: native-static-libs: -lR -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.09s
# Generate wrappers
export CARGO_HOME=/c/Users/alyss/cascadia/src/.cargo && \
export PATH="/x86_64-w64-mingw32.static.posix/bin:/usr/bin:/c/PROGRA~1/R/R-45~1.3/bin/x64:/x86_64-w64-mingw32.static.posix/bin:/usr/bin:/x86_64-w64-mingw32.static.posix/bin:/usr/bin:/c/Users/alyss/.positron/extensions/posit.air-vscode-0.26.0-win32-x64/bundled/bin:/c/Users/alyss/AppData/Local/Programs/Positron/resources/app/quarto/bin:/c/Users/alyss/AppData/Local/Programs/Positron/resources/app/quarto/bin:/c/WINDOWS/system32:/c/WINDOWS:/c/WINDOWS/System32/Wbem:/c/WINDOWS/System32/WindowsPowerShell/v1.0:/c/WINDOWS/System32/OpenSSH:/c/Program Files/PuTTY:/c/Program Files (x86)/PharosSystems/Core:/c/Users/alyss/.cargo/bin:/c/Users/alyss/AppData/Local/Programs/Python/Python39/Scripts:/c/Users/alyss/AppData/Local/Programs/Python/Python39:/c/Users/alyss/AppData/Local/Microsoft/WindowsApps:/c/Users/alyss/AppData/Roaming/TinyTeX/bin/windows:/c/Users/alyss/AppData/Local/Programs/Positron/bin:/c/Users/alyss/AppData/Local/Programs/Microsoft VS Code/bin:/:/c/Users/alyss/.positron/extensions/ms-python.debugpy-2026.6.0-win32-x64/bundled/scripts/noConfigScripts:/c/Users/alyss/.positron/extensions/ms-python.debugpy-2026.6.0-win32-x64/bundled/scripts/noConfigScripts/:/c/Users/alyss/OneDrive/Documents/.cargo/bin" && \
cargo run --bin document --target x86_64-pc-windows-gnu --manifest-path=./rust/Cargo.toml --target-dir ./rust/target
Compiling cascadia v0.1.0 (C:\Users\alyss\cascadia\src\rust)
error: linking with `x86_64-w64-mingw32-gcc` failed: exit code: 1
|
= note: "x86_64-w64-mingw32-gcc" "-fno-use-linker-plugin" "-Wl,--dynamicbase" "-Wl,--disable-auto-image-base" "-m64" "-Wl,--high-entropy-va" "C:\\Users\\alyss\\cascadia\\src\\./rust/target\\x86_64-pc-windows-gnu\\debug\\deps\\rustcQRq5UU\\symbols.o" "<18 object files omitted>" "-Wl,-Bstatic" "C:\\Users\\alyss\\cascadia\\src\\rust\\target\\x86_64-pc-windows-gnu\\debug\\deps/{libcascadia-86923fca78494bb1,libextendr_api-b01aaf2adf064b97,libonce_cell-930cf3712b58e90c,libextendr_ffi-df408659d551ff2b}.rlib" "<sysroot>\\lib\\rustlib\\x86_64-pc-windows-gnu\\lib/{libstd-*,libpanic_unwind-*,libobject-*,libmemchr-*,libaddr2line-*,libgimli-*,libcfg_if-*,libwindows_link-*,librustc_demangle-*,libstd_detect-*,libhashbrown-*,librustc_std_workspace_alloc-*,libminiz_oxide-*,libadler2-*,libunwind-*,liblibc-*,librustc_std_workspace_core-*,liballoc-*,libcore-*,libcompiler_builtins-*}.rlib" "-Wl,-Bdynamic" "-lR" "-lkernel32" "-lkernel32" "-lkernel32" "-lntdll" "-luserenv" "-lws2_32" "-ldbghelp" "-lgcc_eh" "-l:libpthread.a" "-lmsvcrt" "-lmingwex" "-lmingw32" "-lgcc" "-lmsvcrt" "-lmingwex" "-luser32" "-lkernel32" "-Wl,--nxcompat" "-L" "C:\\Program Files\\R\\R-4.5.3\\bin\\x64" "-o" "C:\\Users\\alyss\\cascadia\\src\\./rust/target\\x86_64-pc-windows-gnu\\debug\\deps\\document-f4f85132dc1121bd.exe" "-Wl,--gc-sections" "-nodefaultlibs"
= note: some arguments are omitted. use `--verbose` to show all linker arguments
= note: C:\rtools45\x86_64-w64-mingw32.static.posix\bin/ld.exe: cannot find -lgcc_eh: No such file or directory
collect2.exe: error: ld returned 1 exit status
error: could not compile `cascadia` (bin "document") due to 1 previous error
make: *** [Makevars.win:24: rust/target/x86_64-pc-windows-gnu/debug/libcascadia.a] Error 101
ERROR: compilation failed for package 'cascadia'
─ removing 'C:/Users/alyss/AppData/Local/Temp/RtmpAd62ef/devtools_install_266c53d264f7/cascadia'
Error:
! ! System command 'Rcmd.exe' failed
On Fri, Jun 26, 2026 at 3:41 PM Amelia V <redacted> wrote:
cd C:\rtools45\x86_64-w64-mingw32.static.posix\bin
copy gcc.exe x86_64-w64-mingw32-gcc.exe
copy g++.exe x86_64-w64-mingw32-g++.exe
copy ar.exe x86_64-w64-mingw32-ar.exe
copy ld.exe x86_64-w64-mingw32-ld.exe
copy ranlib.exe x86_64-w64-mingw32-ranlib.exe
copy windres.exe x86_64-w64-mingw32-windres.exe
Yesterday during our rust workshop we ran into this very niche issue compiling rust on a windows machine.
The issue is related to libgcc. the below wasa the fix summary from claude.
Details
What libgcc_eh.a is libgcc is GCC's runtime support library — a bundle of low-level helpers the compiler emits calls to for things like 64-bit arithmetic on 32-bit systems, software floating point, etc. Most C/C++ programs need it linked in.libgcc_eh is the exception handling part of that runtime, split out into its own library. The _eh stands for "exception handling." It contains the unwinder code that runs when a C++ throw propagates up the stack, or when Rust does a panic! that needs to unwind.
Historically GCC ships both as separate libraries when built as shared libraries, so you can dynamically link against just the EH bits if that's all you need. But when GCC is built with --disable-shared (static-only), the EH code gets folded directly into libgcc.a and the separate libgcc_eh.a doesn't get produced. There's nothing to put in it — it's already inside libgcc.a.
Why this matters for Rtools
Rtools is built --disable-shared because R packages need to be statically self-contained on Windows. So Rtools' libgcc.a has the EH code baked in, and there's no separate libgcc_eh.a.
For C and C++ R packages, this is fine — R's build system knows to just link -lgcc and not ask for -lgcc_eh separately.
Why Rust breaks
Rust's Windows-GNU target was designed against a "normal" mingw-w64 toolchain (like MSYS2's), which does ship a separate libgcc_eh.a. So when rustc generates linker commands, it hardcodes -lgcc_eh in the list. It doesn't know or care that Rtools merged everything into libgcc.
When the linker hits -lgcc_eh, it searches for libgcc_eh.a, doesn't find one, and bails out:
ld.exe: cannot find -lgcc_eh: No such file or directory
What the fix does
By copying libgcc.a to libgcc_eh.a, you're handing the linker exactly what it's asking for. Since the EH code is already inside libgcc.a, the duplicate file has all the symbols the linker needs. When -lgcc_eh is processed, the linker finds the file, scans it for the needed symbols, and is happy. The fact that it's a byte-for-byte copy of libgcc.a means no symbols are missing.
It's slightly wasteful (some symbols get pulled in twice during linking, but the linker dedupes), but it's harmless and it works.
Why this is everyone's problem, not just yours
This has been an open issue in the extendr/rextendr/Rtools world for ~5 years. There's a long-standing request to have Rtools just include the symlink/copy automatically (the "libgcc_eh hack" — it's literally a one-line patch in the Rtools build script). The mingw32 build of Rtools has it; the mingw64 build doesn't. Anyone trying to build Rust-based R packages on Windows hits this exact wall.
So the TL;DR:
Rust's linker asks for a library file (libgcc_eh.a) that doesn't exist in Rtools because Rtools builds GCC in a mode where that file's contents get merged into another library (libgcc.a). The fix is to make a copy of libgcc.a named libgcc_eh.a so the linker finds what it's looking for. The two files are identical; you're just giving the linker the filename it expects.
Glad it's working! That was a four-layer onion of Windows build pain.