@@ -16,11 +16,13 @@ import mcpp.toolchain.cppfly;
1616import mcpp.toolchain.detect;
1717import mcpp.toolchain.dialect;
1818import mcpp.toolchain.fingerprint;
19+ import mcpp.toolchain.linkmodel;
1920import mcpp.toolchain.triple;
2021import mcpp.platform;
2122import mcpp.platform.runtime_binding;
2223import mcpp.platform.runtime_env_contract;
23- import mcpp.xlings.subos_info;
24+ import mcpp.platform.runtime_search;
25+ import mcpp.platform.xlings.subos_info;
2426
2527export namespace mcpp ::build {
2628
@@ -202,6 +204,22 @@ struct BuildPlan {
202204 std::vector<mcpp::manifest::RuntimeRequirement> runtimeRequirements;
203205 std::vector<mcpp::manifest::RuntimeArtifact> runtimeArtifacts;
204206 mcpp::manifest::LinkIntent linkIntent;
207+ // The complete run-time search closure of the artifacts this plan will
208+ // produce, in loader order and tagged with where each directory came from
209+ // (`mcpp.platform.runtime_search`).
210+ //
211+ // This is the RECORD — what resolution.json publishes and `mcpp why
212+ // runtime` explains. Emission is still owned by each origin's existing
213+ // producer (payloads by the toolchain link model, package dirs by
214+ // LinkIntent), with one exception: SubosFarm entries have no other
215+ // producer, so `flags.cppm` renders them from here, appended last.
216+ //
217+ // ⚠️ The farm deliberately does NOT enter `runtimeLibraryDirs`. That
218+ // vector becomes LD_LIBRARY_PATH for `mcpp run`, which is inherited by
219+ // every child process including host binaries — measured to kill
220+ // `xdg-open`/`notify-send` outright when a private libc is on it. The farm
221+ // is reachable PER OBJECT (DT_RPATH) and must stay that way.
222+ std::vector<mcpp::platform::search::Dir> runtimeSearch;
205223 // Windows runtime-DLL deployment. On PE (`supports_rpath` is false) a
206224 // directly-launched .exe cannot RUNPATH-locate a dependency's DLL, so each
207225 // *.dll found in a dependency's [runtime] library_dir is copied beside the
@@ -233,6 +251,13 @@ void merge_runtime_binding_contract(
233251 BuildPlan& plan,
234252 const mcpp::platform::runtime::RuntimeBinding& binding);
235253
254+ // The run-time search closure for this plan, in loader order and tagged with
255+ // provenance. Exported so a test can exercise the guards (cross target, non-ELF
256+ // format, undeclared SubOS) without linking a binary for each.
257+ std::vector<mcpp::platform::search::Dir> runtime_search_closure (
258+ const BuildPlan& plan,
259+ const mcpp::platform::runtime::RuntimeBinding& binding);
260+
236261// Is `p` inside one of `roots`, judged LEXICALLY?
237262//
238263// Lexical is the whole point (mcpp#344). std::filesystem::relative() runs
@@ -621,6 +646,76 @@ ResolvedRuntimeContract resolve_runtime_contract(
621646 return out;
622647}
623648
649+ // The run-time search closure of everything this plan will link, in the order
650+ // the loader will consult it, tagged with where each directory came from.
651+ //
652+ // ONE assembly, three producers. Payload directories come from the toolchain
653+ // link model, package directories from the resolved LinkIntent, and the SubOS
654+ // farm from the RuntimeBinding — and the farm is the addition that closes the
655+ // gap this whole change exists for: mcpp already passes `--sysroot=<subos>` on
656+ // the compile AND link lines, so `-lGL` resolves out of `<subos>/lib` with no
657+ // flags from the user, while the RUN-time path was derived from payload
658+ // directories alone. Link succeeded, the artifact could not start.
659+ //
660+ // FARM LAST, and it is the only invariant here. `<subos>/lib` is a symlink
661+ // view rewritten by every `xlings install`; payload directories are written
662+ // once and never touched. Payload-first keeps libc / libm / libstdc++ resolving
663+ // from the pinned payload and leaves the farm to supply only what nothing else
664+ // does. Farm-first would let a later install silently change which libc an
665+ // ALREADY LINKED artifact loads. `search::ordered` is what enforces it, and
666+ // e2e 219 asserts it on the produced ELF rather than on this code.
667+ std::vector<mcpp::platform::search::Dir> runtime_search_closure (
668+ const BuildPlan& plan,
669+ const mcpp::platform::runtime::RuntimeBinding& binding) {
670+ using mcpp::platform::search::Dir;
671+ using mcpp::platform::search::Origin;
672+
673+ // PAYLOAD DIRECTORIES COME FROM THE SAME FUNCTION THAT EMITS THEM.
674+ //
675+ // `resolve_link_model` is a pure function of the toolchain and is what
676+ // `flags.cppm` renders as `-L`/`-Wl,-rpath` for the C runtime; asking it
677+ // here is how the record and the artifact stay the same list. Deriving
678+ // them a second way is what made the first version of this record show a
679+ // one-entry closure while the artifact carried three — `linkRuntimeDirs`
680+ // is populated for CLANG ONLY, so on GCC it is simply empty and the
681+ // payloads arrive through the link model instead.
682+ //
683+ // Both are read, in the order `flags.cppm` concatenates them.
684+ std::vector<Dir> closure;
685+ for (auto const & dir : mcpp::toolchain::resolve_link_model (plan.toolchain ).libDirs )
686+ closure.push_back ({dir, Origin::Payload});
687+ for (auto const & dir : plan.toolchain .linkRuntimeDirs )
688+ closure.push_back ({dir, Origin::Payload});
689+ for (auto const & dir : plan.linkIntent .runtimeSearchDirs )
690+ closure.push_back ({dir, Origin::Package});
691+
692+ // TWO GUARDS, both about "will this artifact ever run here".
693+ //
694+ // format DT_RPATH exists on ELF only. Mach-O and PE get nothing rather
695+ // than a branch in every consumer — the same shape
696+ // `loader_contract` uses for the tag half of this contract.
697+ // host The farm belongs to THIS host's SubOS. A cross target
698+ // (aarch64-musl, mingw, wasm) would receive a path that is inert
699+ // at best and points at the wrong architecture's libraries at
700+ // worst.
701+ const auto triple = [&] {
702+ auto t = mcpp::toolchain::triple::parse (plan.toolchain .targetTriple );
703+ return t ? *t : mcpp::toolchain::triple::Triple{};
704+ }();
705+ const bool elfTarget = triple.empty ()
706+ ? bool (mcpp::platform::is_linux)
707+ : (triple.os != " macos" && triple.os != " windows" );
708+ const bool hostTarget = binding.platform == " linux"
709+ && (triple.empty ()
710+ || (triple.os == " linux"
711+ && (triple.arch .empty () || triple.arch == binding.arch )));
712+ if (elfTarget && hostTarget)
713+ for (auto const & dir : binding.searchDirs )
714+ closure.push_back ({dir, Origin::SubosFarm});
715+
716+ return mcpp::platform::search::ordered (std::move (closure));
717+ }
718+
624719void merge_runtime_binding_contract (
625720 BuildPlan& plan,
626721 const mcpp::platform::runtime::RuntimeBinding& binding) {
@@ -676,6 +771,8 @@ void merge_runtime_binding_contract(
676771 }))
677772 plan.runtimeArtifacts .push_back (std::move (value));
678773 }
774+
775+ plan.runtimeSearch = runtime_search_closure (plan, binding);
679776}
680777
681778// True if `src` defines a top-level `int main(` / `auto main(` entry point.
0 commit comments