Summary
libclangCodeGen.a contains TWO members named X86.cpp.o (and ARM/RISCV/…): one from
clang/lib/CodeGen/Targets/X86.cpp (defines createX86TargetCodeGenInfo, demanded EARLY) and one from
clang/lib/CodeGen/TargetBuiltins/X86.cpp (defines EmitX86BuiltinExpr, demanded LATE — only by
EmitTargetBuiltinExpr). The member-granular pool-pull binds the Targets/ member and never pulls the
TargetBuiltins/ twin, so all 21 EmitBuiltinExpr (X86/ARM/AArch64/RISCV/PPC/Hexagon/NVPTX/AMDGPU/
SPIRV/DirectX/SystemZ/BPF/WebAssembly + Cpu init/supports) go UNDEFINED → phantom env imports at the
final link. Silent wrong output — no error, just missing symbols.
Evidence
The LTO module (merged.bc) had exactly 34 undefined = these 21 emitters + 13 legit host imports. The 21
are U in merged.bc even though libclangCodeGen.a DEFINES them (verified: EmitX86BuiltinExpr is T in
the archive). ar x X86.cpp.o also extracts the WRONG twin — the same duplicate-member-name hazard.
Workaround (ours)
llvm-link the 11 CodeGen/TargetBuiltins/*.cpp.o FROM THE BUILD TREE (not via ar x) into the module →
all 21 defined; then coroutinize+link as usual.
Suggested fix
Pool-pull should resolve members by (member, defining-symbol), not by member NAME, so a late-demanded symbol pulls the correct twin even when two members share a name. Alternatively, the LLVM CMake build should give TargetBuiltins objects distinct archive names.
Proposed fix targets brintos/hwjs-cc only — no distro submodule pins moved. All distros share one
lineage, so pin bumps are coordinated on merge, not per-engineer.