Summary
Building a [[lean_exe]] target in a project that depends on LeanCopilot fails to link with dozens of undefined symbol errors for basic libstdc++ types (std::basic_ifstream, std::filesystem::path, std::__cxx11::basic_string, exception-handling primitives, etc.) — all originating from ct2.cpp inside LeanCopilot's own libleanffi.a. A [[lean_lib]] target that imports and actually calls LeanCopilot (suggest_tactics) in the exact same project builds and runs correctly — only the executable-target link fails. I've root-caused this precisely (see below) but was not able to find a working link-time fix despite several targeted attempts, which I'm reporting honestly rather than claiming a false resolution.
Environment
|
|
| OS |
Fedora Linux 39 (Server Edition), kernel 6.8.4-200.fc39.x86_64 |
| gcc |
13.2.1 (system) |
| clang/lld (Lean's bundled toolchain) |
clang version 22.1.4 / lld, from the Lean toolchain itself |
| Lean/Lake |
4.32.0-rc1 / Lake 5.0.0-src via elan (the toolchain LeanCopilot v4.31.0 pins) |
| LeanCopilot |
tag v4.31.0, commit 2458f7339df4d90643cdc6eb3fbbe968cd73ac78 |
(Also reproduces, same symptom, on the same machine's macOS-adjacent setup is irrelevant here — this is Linux-only, since macOS builds CTranslate2 against Accelerate rather than OpenBLAS/system libstdc++; not tested on macOS since the underlying ABI mismatch doesn't apply there.)
Minimal reproduction
lakefile.toml
name = "leancopilot_demo_pkg"
version = "0.1.0"
defaultTargets = ["leancopilot_demo_pkg"]
moreLinkArgs = ["-L./.lake/packages/LeanCopilot/.lake/build/lib", "-lctranslate2"]
precompileModules = true
[[lean_lib]]
name = "LeancopilotDemoPkg"
[[lean_exe]]
name = "leancopilot_demo_pkg"
root = "Main"
[[require]]
name = "LeanCopilot"
git = "https://github.com/lean-dojo/LeanCopilot.git"
rev = "v4.31.0"
LeancopilotDemoPkg/Basic.lean:
import LeanCopilot
example (a b : Nat) : a + b = b + a := by
suggest_tactics
Main.lean is the default lake new template (def main : IO Unit := IO.println "hello" — doesn't even reference LeanCopilot directly, it's just the default [[lean_exe]] entry point).
lake build LeancopilotDemoPkg # succeeds, suggest_tactics genuinely works (see below)
lake build leancopilot_demo_pkg:exe # fails, see error below
What works
$ lake build LeancopilotDemoPkg
...
info: LeancopilotDemoPkg/Basic.lean:4:2: Try these:
[apply] simp [Nat.add_comm]
Build completed successfully (710 jobs).
This is a genuine, fresh model-inference call (confirmed via Built LeancopilotDemoPkg.Basic (7-10s), not a cached replay) — LeanCopilot's ML pipeline is fully functional in the library target.
What fails
Full error output (click to expand)
✖ [714/714] Building leancopilot_demo_pkg:exe (920ms)
trace: .> /home/jdanek/.elan/toolchains/leanprover--lean4---v4.32.0-rc1/bin/clang -o .../leancopilot_demo_pkg @.../leancopilot_demo_pkg.rsp
info: stderr:
ld.lld: error: undefined symbol: std::__throw_logic_error(char const*)
>>> referenced by ct2.cpp
>>> ct2.o:(...) in archive .../LeanCopilot/.lake/build/lib/libleanffi.a
ld.lld: error: undefined symbol: std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>::_M_create(unsigned long&, unsigned long)
>>> referenced by ct2.cpp ... (21 more references)
ld.lld: error: undefined symbol: std::__throw_bad_array_new_length()
ld.lld: error: undefined symbol: std::__throw_length_error(char const*)
ld.lld: error: undefined symbol: std::__cxx11::basic_string<...>::reserve(unsigned long)
ld.lld: error: undefined symbol: std::__cxx11::basic_string<...>::_M_append(char const*, unsigned long)
ld.lld: error: undefined symbol: std::__cxx11::basic_string<...>::append(char const*)
ld.lld: error: undefined symbol: std::__cxx11::basic_string<...>::_M_dispose() (249 references)
ld.lld: error: undefined symbol: std::__atomic_futex_unsigned_base::_M_futex_wait_until(...)
ld.lld: error: undefined symbol: std::__throw_out_of_range(char const*)
ld.lld: error: undefined symbol: std::__exception_ptr::exception_ptr::_M_addref()
ld.lld: error: undefined symbol: std::rethrow_exception(std::__exception_ptr::exception_ptr)
ld.lld: error: undefined symbol: std::__throw_future_error(int)
ld.lld: error: undefined symbol: std::filesystem::__cxx11::path::_List::_List()
ld.lld: error: undefined symbol: std::filesystem::__cxx11::path::_M_split_cmpts()
ld.lld: error: undefined symbol: std::filesystem::status(std::filesystem::__cxx11::path const&)
ld.lld: error: undefined symbol: std::filesystem::__cxx11::path::_List::_Impl_deleter::operator()(...)
ld.lld: error: undefined symbol: std::basic_ifstream<char, std::char_traits<char>>::basic_ifstream(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>> const&, std::_Ios_Openmode)
ld.lld: error: undefined symbol: vtable for std::basic_ifstream<char, std::char_traits<char>>
ld.lld: error: undefined symbol: vtable for std::basic_filebuf<char, std::char_traits<char>>
ld.lld: error: too many errors emitted, stopping now (use --error-limit=0 to see all errors)
clang: error: linker command failed with exit code 1
error: external command '.../clang' exited with code 1
error: build failed
(Full untruncated log available if useful — trimmed here to representative errors; roughly 15 distinct symbol names, ~300 total references, all traced to ct2.cpp inside libleanffi.a.)
Root cause (confirmed via the actual link command)
The response file (.rsp) for the leancopilot_demo_pkg:exe link step shows Lean's own exe-link recipe:
...
".../LeanCopilot/.lake/build/lib/libleanffi.a"
"-L./.lake/packages/LeanCopilot/.lake/build/lib"
"-lctranslate2"
...
"-Wl,--start-group" "-lleancpp" "-lLean" "-Wl,--end-group"
"-lStd"
"-Wl,--start-group" "-lInit" "-lleanrt" "-Wl,--end-group"
"-Wl,-Bstatic" "-lc++" "-lc++abi" "-Wl,-Bdynamic"
"-lLake"
...
libstdc++ never appears anywhere in this link line — Lean statically links its own bundled libc++/libc++abi (LLVM's C++ runtime) for executables. But libleanffi.a's ct2.cpp (and everything it pulls in from CTranslate2) is compiled by the system g++, and every undefined symbol above uses the GCC-specific std::__cxx11 inline-namespace mangling — these symbols simply don't exist in libc++ at all; they're libstdc++-only.
This is presumably why the library target works: a .so/dynlib built for --load-dynlib loading resolves its libstdc++ dependency at runtime, transitively via libctranslate2.so.4/libopenblas.so.0 (which are themselves normal system-g++ binaries dynamically linked against libstdc++.so.6, already present in the process by the time Lean dlopens LeanCopilot's .so). The executable link is a from-scratch static/dynamic combination that never provides libstdc++ at all, so the moment libleanffi.a's object code is pulled into that link (regardless of whether Main.lean itself even calls anything LeanCopilot-related — it's pulled in transitively because the library depends on it), it's dead on arrival.
What I tried (honestly reporting a non-fix)
Since moreLinkArgs is user-configurable, I tried adding -lstdc++ there, expecting it to supply the missing symbols. This did not work cleanly, and I want to report the actual investigation rather than a false "add -lstdc++ and it's fixed":
- Plain
"-lstdc++" added to moreLinkArgs: same undefined-symbol list, unchanged. Turned out this Fedora system has only the runtime /usr/lib64/libstdc++.so.6 — the unversioned -lstdc++-resolvable dev symlink (libstdc++.so) exists only under GCC's own private directory, /usr/lib/gcc/x86_64-redhat-linux/13/libstdc++.so (confirmed via rpm -ql libstdc++-devel — the package is installed, it's just not on clang's default search path). Since Lean's clang invocation passes an explicit --sysroot pointing at the Lean toolchain directory (not /), it apparently doesn't auto-detect the host's real GCC installation directory the way an unwrapped system clang normally would.
"-l:libstdc++.so.6" (referencing the versioned runtime lib directly by exact name, bypassing the need for the dev symlink): failed outright with ld.lld: error: unable to find library -l:libstdc++.so.6 — confirming /usr/lib64 genuinely isn't on the search path for this sysroot-scoped invocation at all.
"-L/usr/lib/gcc/x86_64-redhat-linux/13" + "-lstdc++" (explicit -L to the real, confirmed-present dev symlink — file confirms it's a genuine symlink to a real ELF .so.6.0.32, not a linker script): the library is now presumably found (no more "unable to find library" error), and both LeancopilotDemoPkg.Basic:dynlib and LeancopilotDemoPkg:shared build fine with this flag present — but the final leancopilot_demo_pkg:exe link step still fails with the exact same, byte-for-byte identical list of undefined symbols, despite the .rsp file (re-verified after the build) genuinely containing "-L/usr/lib/gcc/x86_64-redhat-linux/13" "-lstdc++" right after libleanffi.a. I independently confirmed via nm -D that /usr/lib64/libstdc++.so.6.0.32 does export the missing symbols (e.g. _ZSt19__throw_logic_errorPKc@@GLIBCXX_3.4), so the symbols genuinely exist in a library that's ostensibly on the link line — I could not determine why the linker isn't using them for this specific target.
I don't have enough visibility into Lake's own moreLinkArgs application per-target (whether it's actually applied identically to :exe vs :shared targets' final link steps) or into lld's exact archive-vs-shared-library resolution order in this scenario to go further — flagging this precisely in case it's either a Lake bug (moreLinkArgs not fully honored for the exe target's own link recipe) or a subtler lld interaction, since I've run out of ideas I can verify from outside the build system's own internals.
Why this matters
Any downstream project that wants to ship a lean_exe (not just a library) using LeanCopilot on Linux hits this immediately and unconditionally — it's not input- or model-dependent, it fails at pure link time before any LeanCopilot code even runs. Happy to share the full untruncated build logs, .rsp files, or test further configurations if that helps narrow this down.
Summary
Building a
[[lean_exe]]target in a project that depends on LeanCopilot fails to link with dozens ofundefined symbolerrors for basic libstdc++ types (std::basic_ifstream,std::filesystem::path,std::__cxx11::basic_string, exception-handling primitives, etc.) — all originating fromct2.cppinside LeanCopilot's ownlibleanffi.a. A[[lean_lib]]target that imports and actually calls LeanCopilot (suggest_tactics) in the exact same project builds and runs correctly — only the executable-target link fails. I've root-caused this precisely (see below) but was not able to find a working link-time fix despite several targeted attempts, which I'm reporting honestly rather than claiming a false resolution.Environment
6.8.4-200.fc39.x86_64clang version 22.1.4/ lld, from the Lean toolchain itself4.32.0-rc1/ Lake5.0.0-srcvia elan (the toolchain LeanCopilotv4.31.0pins)v4.31.0, commit2458f7339df4d90643cdc6eb3fbbe968cd73ac78(Also reproduces, same symptom, on the same machine's macOS-adjacent setup is irrelevant here — this is Linux-only, since macOS builds CTranslate2 against Accelerate rather than OpenBLAS/system libstdc++; not tested on macOS since the underlying ABI mismatch doesn't apply there.)
Minimal reproduction
lakefile.toml
LeancopilotDemoPkg/Basic.lean:Main.leanis the defaultlake newtemplate (def main : IO Unit := IO.println "hello"— doesn't even reference LeanCopilot directly, it's just the default[[lean_exe]]entry point).What works
This is a genuine, fresh model-inference call (confirmed via
Built LeancopilotDemoPkg.Basic (7-10s), not a cached replay) — LeanCopilot's ML pipeline is fully functional in the library target.What fails
Full error output (click to expand)
(Full untruncated log available if useful — trimmed here to representative errors; roughly 15 distinct symbol names, ~300 total references, all traced to
ct2.cppinsidelibleanffi.a.)Root cause (confirmed via the actual link command)
The response file (
.rsp) for theleancopilot_demo_pkg:exelink step shows Lean's own exe-link recipe:libstdc++never appears anywhere in this link line — Lean statically links its own bundled libc++/libc++abi (LLVM's C++ runtime) for executables. Butlibleanffi.a'sct2.cpp(and everything it pulls in from CTranslate2) is compiled by the systemg++, and every undefined symbol above uses the GCC-specificstd::__cxx11inline-namespace mangling — these symbols simply don't exist in libc++ at all; they're libstdc++-only.This is presumably why the library target works: a
.so/dynlib built for--load-dynlibloading resolves its libstdc++ dependency at runtime, transitively vialibctranslate2.so.4/libopenblas.so.0(which are themselves normal system-g++ binaries dynamically linked againstlibstdc++.so.6, already present in the process by the time Leandlopens LeanCopilot's.so). The executable link is a from-scratch static/dynamic combination that never provides libstdc++ at all, so the momentlibleanffi.a's object code is pulled into that link (regardless of whetherMain.leanitself even calls anything LeanCopilot-related — it's pulled in transitively because the library depends on it), it's dead on arrival.What I tried (honestly reporting a non-fix)
Since
moreLinkArgsis user-configurable, I tried adding-lstdc++there, expecting it to supply the missing symbols. This did not work cleanly, and I want to report the actual investigation rather than a false "add-lstdc++and it's fixed":"-lstdc++"added tomoreLinkArgs: same undefined-symbol list, unchanged. Turned out this Fedora system has only the runtime/usr/lib64/libstdc++.so.6— the unversioned-lstdc++-resolvable dev symlink (libstdc++.so) exists only under GCC's own private directory,/usr/lib/gcc/x86_64-redhat-linux/13/libstdc++.so(confirmed viarpm -ql libstdc++-devel— the package is installed, it's just not on clang's default search path). Since Lean's clang invocation passes an explicit--sysrootpointing at the Lean toolchain directory (not/), it apparently doesn't auto-detect the host's real GCC installation directory the way an unwrapped system clang normally would."-l:libstdc++.so.6"(referencing the versioned runtime lib directly by exact name, bypassing the need for the dev symlink): failed outright withld.lld: error: unable to find library -l:libstdc++.so.6— confirming/usr/lib64genuinely isn't on the search path for this sysroot-scoped invocation at all."-L/usr/lib/gcc/x86_64-redhat-linux/13"+"-lstdc++"(explicit-Lto the real, confirmed-present dev symlink —fileconfirms it's a genuine symlink to a real ELF.so.6.0.32, not a linker script): the library is now presumably found (no more "unable to find library" error), and bothLeancopilotDemoPkg.Basic:dynlibandLeancopilotDemoPkg:sharedbuild fine with this flag present — but the finalleancopilot_demo_pkg:exelink step still fails with the exact same, byte-for-byte identical list of undefined symbols, despite the.rspfile (re-verified after the build) genuinely containing"-L/usr/lib/gcc/x86_64-redhat-linux/13" "-lstdc++"right afterlibleanffi.a. I independently confirmed vianm -Dthat/usr/lib64/libstdc++.so.6.0.32does export the missing symbols (e.g._ZSt19__throw_logic_errorPKc@@GLIBCXX_3.4), so the symbols genuinely exist in a library that's ostensibly on the link line — I could not determine why the linker isn't using them for this specific target.I don't have enough visibility into Lake's own
moreLinkArgsapplication per-target (whether it's actually applied identically to:exevs:sharedtargets' final link steps) or into lld's exact archive-vs-shared-library resolution order in this scenario to go further — flagging this precisely in case it's either a Lake bug (moreLinkArgs not fully honored for the exe target's own link recipe) or a subtler lld interaction, since I've run out of ideas I can verify from outside the build system's own internals.Why this matters
Any downstream project that wants to ship a
lean_exe(not just a library) using LeanCopilot on Linux hits this immediately and unconditionally — it's not input- or model-dependent, it fails at pure link time before any LeanCopilot code even runs. Happy to share the full untruncated build logs,.rspfiles, or test further configurations if that helps narrow this down.