Skip to content

lean_exe link fails with undefined libstdc++ symbols from libleanffi.a (lean_lib works fine) #196

Description

@ide-developer

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":

  1. 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.
  2. "-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.
  3. "-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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions