@@ -49,7 +49,14 @@ struct HostFlagOptions {
4949 // needs_explicit_libcxx path owns; duplicating that for a
5050 // host compile produced undefined __cxa_* /
5151 // __gxx_personality_v0 (build_program.cppm, pre-existing).
52- enum class CfgBypass { Always, LinuxOnly };
52+ // Never — always trust the cfg. No caller selects it; it completes
53+ // the enum, and it is what makes the cfg-trusting branch of
54+ // `host_link_tokens` reachable from a test on a Linux
55+ // runner. `LinuxOnly` folds into `Always` there, so without
56+ // this value that branch could only be exercised on the two
57+ // hosts it was written for -- which is how it came to be
58+ // missing the runtime-directory tokens in the first place.
59+ enum class CfgBypass { Always, LinuxOnly, Never };
5360 CfgBypass cfgBypass = CfgBypass::Always;
5461
5562 // binutils `-B` so the driver finds as/ld. A GCC/libstdc++ payload
@@ -206,7 +213,8 @@ std::vector<std::string> host_compile_tokens(const Toolchain& tc,
206213
207214 const bool bypassCfg =
208215 dm.hasCfg && (opt.cfgBypass == HostFlagOptions::CfgBypass::Always
209- || mcpp::platform::is_linux);
216+ || (opt.cfgBypass == HostFlagOptions::CfgBypass::LinuxOnly
217+ && mcpp::platform::is_linux));
210218
211219 // Trusting the cfg means contributing no include paths, stdlib selection
212220 // or runtime choices — it already carries them. It does NOT mean
@@ -297,6 +305,20 @@ std::vector<std::string> bmi_reference_tokens(std::string_view usePrefix,
297305 std::string (p.substr (sp + 1 )) + bmi.string () };
298306}
299307
308+ // The toolchain's own runtime directories, on both exits of the function
309+ // below. `-L` is link-time and wanted everywhere; rpath is an ELF and Mach-O
310+ // concept. A PE target reaches here too, where the rpath flag is inert and
311+ // self-containment comes from the static link instead (#299).
312+ void append_runtime_lib_dirs (const Toolchain& tc, const HostFlagOptions& opt,
313+ const PathEscape& esc, std::vector<std::string>& out) {
314+ if (!opt.runtimeLibDirs ) return ;
315+ for (auto & d : tc.linkRuntimeDirs ) {
316+ out.push_back (" -L" + esc (d));
317+ if constexpr (mcpp::platform::supports_rpath)
318+ out.push_back (" -Wl,-rpath," + esc (d));
319+ }
320+ }
321+
300322std::vector<std::string> host_link_tokens (const Toolchain& tc,
301323 const HostFlagOptions& opt,
302324 const PathEscape& esc) {
@@ -308,7 +330,8 @@ std::vector<std::string> host_link_tokens(const Toolchain& tc,
308330
309331 const bool bypassCfg =
310332 dm.hasCfg && (opt.cfgBypass == HostFlagOptions::CfgBypass::Always
311- || mcpp::platform::is_linux);
333+ || (opt.cfgBypass == HostFlagOptions::CfgBypass::LinuxOnly
334+ && mcpp::platform::is_linux));
312335
313336 if (bypassCfg) {
314337 for (auto & t : dm.link_tokens (esc)) out.push_back (t);
@@ -336,6 +359,37 @@ std::vector<std::string> host_link_tokens(const Toolchain& tc,
336359 // lld ships with the very toolchain doing the compile, so it cannot
337360 // be diverted to a libc++ it was not built against.
338361 if constexpr (mcpp::platform::is_macos) out.push_back (" -fuse-ld=lld" );
362+ // AND THE RUNTIME DIRECTORIES, WHICH THIS PATH USED TO SKIP.
363+ //
364+ // Trusting clang's cfg decides WHICH runtimes are linked. It does not
365+ // decide WHERE they are found, and on macOS `-lc++` resolves through
366+ // the SDK to /usr/lib/libc++.tbd -- the system copy, whose version
367+ // floats with the host OS -- while the headers come from the payload.
368+ // The two disagree the moment the headers reference a symbol the
369+ // system library does not export yet:
370+ //
371+ // ld64.lld: error: undefined symbol:
372+ // std::__1::__is_posix_terminal(__sFILE*)
373+ // >>> referenced by std::__1::__print::__is_terminal(__sFILE*)
374+ //
375+ // measured on macos-14 compiling a build program that does nothing but
376+ // `import std`. `std::print` is not header-only: its FILE* and ostream
377+ // overloads call into the libc++ dylib, and those two support symbols
378+ // arrived in a version macOS 14 does not ship. macOS 15's copy has
379+ // them, which is why every macOS runner this project uses was green.
380+ //
381+ // The payload ships its own `lib/libc++.1.0.dylib` -- the toolchain's
382+ // own copy, matching its own headers -- and `linkRuntimeDirs` already
383+ // names that directory. Emitting `-L` and `-rpath` for it is what the
384+ // Linux path has always done here; it was skipped on macOS only
385+ // because this branch returned first.
386+ //
387+ // A BUILD PROGRAM IS THE RIGHT PLACE FOR AN ABSOLUTE RPATH. It is
388+ // never distributed: mcpp compiles it, runs it on this machine, and
389+ // caches it against this toolchain's identity. The equivalent question
390+ // for an ARTIFACT is decided by the distribution contract, and is not
391+ // this function's to answer.
392+ append_runtime_lib_dirs (tc, opt, esc, out);
339393 return out;
340394 }
341395
@@ -346,16 +400,7 @@ std::vector<std::string> host_link_tokens(const Toolchain& tc,
346400 out.push_back (" -B" + esc (ar.parent_path ()));
347401 }
348402
349- if (opt.runtimeLibDirs ) {
350- // -L is link-time and wanted everywhere; rpath is an ELF-only concept.
351- // A PE target reaches here too, where the flag is inert and
352- // self-containment comes from the static link instead (#299).
353- for (auto & d : tc.linkRuntimeDirs ) {
354- out.push_back (" -L" + esc (d));
355- if constexpr (mcpp::platform::supports_rpath)
356- out.push_back (" -Wl,-rpath," + esc (d));
357- }
358- }
403+ append_runtime_lib_dirs (tc, opt, esc, out);
359404
360405 return out;
361406}
0 commit comments