@@ -11,8 +11,8 @@ import std;
1111import mcpp.config;
1212import mcpp.libs.json;
1313import mcpp.platform;
14- import mcpp.xlings.runtime_selection;
15- import mcpp.xlings.subos_info;
14+ import mcpp.platform. xlings.runtime_selection;
15+ import mcpp.platform. xlings.subos_info;
1616
1717export namespace mcpp ::platform::runtime {
1818
@@ -28,7 +28,17 @@ struct RuntimeBinding {
2828 std::optional<std::filesystem::path> loader;
2929 std::optional<std::string> libc;
3030 std::optional<std::string> hostLibc;
31+ // IMMUTABLE payload directories (`<store>/xim-x-glibc/2.39/lib64`).
3132 std::vector<std::filesystem::path> libraryDirs;
33+ // The SubOS symlink farm (`<subos>/lib`) — a union view of everything
34+ // installed into this environment, rewritten on every re-resolution.
35+ //
36+ // Deliberately a SECOND field rather than more entries in `libraryDirs`:
37+ // merging them discards the immutability distinction, and that
38+ // distinction is the whole of `mcpp.platform.runtime_search`'s ordering
39+ // rule. A payload directory and a farm directory are not interchangeable
40+ // even when they currently resolve to the same file.
41+ std::vector<std::filesystem::path> searchDirs;
3242 std::vector<mcpp::xlings::subos::EnvDecl> environment;
3343 std::vector<std::string> providerBindings;
3444 std::vector<std::string> capabilities;
@@ -37,6 +47,32 @@ struct RuntimeBinding {
3747 std::string provenance;
3848 std::filesystem::path subosDir;
3949 mcpp::xlings::runtime::RuntimeSelection selection;
50+
51+ // Did the SubOS describe itself (does it carry a `subos_info` block)?
52+ //
53+ // FALSE IS NOT AN ERROR. A SubOS that says nothing leaves some facts
54+ // unknown — rules A/B become inconclusive, declared environment is
55+ // unavailable — and leaves everything else working. Treating absence as a
56+ // failure is what stopped every `mcpp build` and `mcpp test` on Windows
57+ // (openxlings/xlings#543), on a machine where the missing facts describe
58+ // concepts (ELF, PT_INTERP, a private libc) that do not exist there.
59+ //
60+ // A CONTRADICTION still fails: naming a SubOS that is not present cannot
61+ // be satisfied, so it is reported rather than degraded.
62+ bool declared = false ;
63+
64+ // Why something degraded. Non-empty ⇒ the caller MUST surface it. Never
65+ // an error: "it did not happen" and "it succeeded" producing identical
66+ // output is the property that made mcpp#352 expensive.
67+ std::string note;
68+
69+ // Does this artifact run under a PRIVATE loader?
70+ //
71+ // The predicate the closure resolver needs: when PT_INTERP points into a
72+ // payload, the HOST loader's built-in default directories are not part of
73+ // the search path, and modelling them is how a binary that cannot start
74+ // was reported as valid.
75+ bool hermetic () const { return loader.has_value (); }
4076};
4177
4278namespace detail {
@@ -80,6 +116,14 @@ std::string canonical_contract(const RuntimeBinding& binding) {
80116 append_field (out, binding.hostLibc .value_or (" " ));
81117 for (auto const & p : binding.libraryDirs )
82118 append_field (out, p.generic_string ());
119+ // The farm participates in the hash because it participates in the
120+ // artifact: it lands in DT_RPATH, so a build made against one farm is not
121+ // interchangeable with a build made against another. `declared` is in for
122+ // the same reason — a SubOS that gains self-description changes what the
123+ // build knows, and the fast path must not reuse the older answer.
124+ append_field (out, binding.declared ? " declared" : " undeclared" );
125+ for (auto const & p : binding.searchDirs )
126+ append_field (out, p.generic_string ());
83127 for (auto const & provider : binding.providerBindings )
84128 append_field (out, provider);
85129 for (auto const & d : binding.environment ) {
@@ -213,22 +257,51 @@ resolve_runtime_binding(
213257 selection.subosName , out.subosDir .string ()));
214258 }
215259
260+ // CONTRADICTION vs ABSENCE. The check above is a contradiction: the user
261+ // named a SubOS that is not there, and no amount of degrading makes that
262+ // request satisfiable. Everything below is absence — some facts are
263+ // unavailable, the rest of the build is unaffected — so it degrades.
264+ //
265+ // The distinction is not academic. Collapsing it is what made every
266+ // `mcpp build` and `mcpp test` on Windows fail with a message about GL
267+ // drivers (openxlings/xlings#543), and it is the same shape as the index
268+ // floor incident: DATA THAT IS MISSING OR NEWER MUST NOT INVALIDATE THE
269+ // PROGRAM THAT READS IT.
270+ auto note = [&](std::string message) {
271+ if (!out.note .empty ()) out.note += " \n " ;
272+ out.note += std::move (message);
273+ };
274+
216275 auto info = mcpp::xlings::subos::read (out.subosDir );
276+ out.declared = info.present ;
217277 if (!info.present ) {
218- return std::unexpected (std::format (
219- " selected SubOS '{}' cannot provide a RuntimeBinding: {}" ,
220- selection.subosName , info.note ));
221- }
222- if (info.schema != mcpp::xlings::subos::kSupportedSchema ) {
223- return std::unexpected (std::format (
224- " selected SubOS '{}' uses runtime contract schema {}, but this "
225- " mcpp requires schema {}; update xlings/mcpp before building" ,
278+ note (std::format (
279+ " SubOS '{}' does not describe itself: {}\n "
280+ " Runtime facts (identity, loader, declared environment) are "
281+ " unavailable: runtime rules report `inconclusive` rather than a "
282+ " verdict, and a program launched from here gets no environment this "
283+ " SubOS declares.\n "
284+ " Where the C runtime comes from a payload, there is now no "
285+ " declared runtime to bind to — mcpp declines to guess a version, so "
286+ " the link falls back to the host and the hermeticity check will say "
287+ " so. `xlings self update` writes the block." ,
288+ selection.subosName ,
289+ info.note .empty () ? " no `subos_info` block" : info.note ));
290+ } else if (info.schema > mcpp::xlings::subos::kSupportedSchema ) {
291+ // Mirrors `subos_info::read`, which already reads a HIGHER schema and
292+ // says so. A consumer stricter than its own reader is a time bomb:
293+ // the day xlings writes schema 2, an equality check stops every build
294+ // on every platform.
295+ note (std::format (
296+ " SubOS '{}' declares runtime contract schema {}, newer than the {} "
297+ " this mcpp understands; using the fields it knows" ,
226298 selection.subosName , info.schema ,
227299 mcpp::xlings::subos::kSupportedSchema ));
228300 }
229- if (info.runtime .empty ()) {
230- return std::unexpected (std::format (
231- " selected SubOS '{}' has no runtime identity in subos_info.runtime" ,
301+ if (info.present && info.runtime .empty ()) {
302+ note (std::format (
303+ " SubOS '{}' has no runtime identity in subos_info.runtime; runtime "
304+ " rules cannot be evaluated for artifacts built here" ,
232305 selection.subosName ));
233306 }
234307
@@ -247,11 +320,27 @@ resolve_runtime_binding(
247320 out.libc = info.runtime ;
248321 if (!info.hostGlibc .empty ()) out.hostLibc = info.hostGlibc ;
249322
250- // Resolve the selected SubOS VIEW to its immutable payload. The view
251- // already embodies RuntimeSelection, so following these exact links is
252- // not payload discovery and cannot choose another installed version.
323+ // ONE traversal, TWO answers.
324+ //
325+ // searchDirs the view directory itself — the farm, where every
326+ // library this environment installed is reachable by
327+ // SONAME (`-lGL` already resolves here, because
328+ // `--sysroot=<subos>` makes it the linker's default).
329+ // libraryDirs the immutable payload the view's libc RESOLVES to.
330+ //
331+ // Deriving both here rather than in two places is the point: the
332+ // layout knowledge (`lib64` before `lib`) exists exactly once.
333+ //
334+ // The view already embodies RuntimeSelection, so following these exact
335+ // links is not payload discovery and cannot choose another installed
336+ // version.
253337 std::vector<std::filesystem::path> candidates{
254338 out.subosDir / " lib64" , out.subosDir / " lib" };
339+ for (auto const & candidate : candidates) {
340+ std::error_code fec;
341+ if (std::filesystem::is_directory (candidate, fec))
342+ out.searchDirs .push_back (candidate.lexically_normal ());
343+ }
255344 for (auto const & candidate : candidates) {
256345 std::error_code lec;
257346 auto libc = candidate / " libc.so.6" ;
@@ -322,6 +411,11 @@ std::string serialize_runtime_binding(const RuntimeBinding& binding) {
322411 j[" library_dirs" ] = nlohmann::json::array ();
323412 for (auto const & path : binding.libraryDirs )
324413 j[" library_dirs" ].push_back (path.generic_string ());
414+ j[" declared" ] = binding.declared ;
415+ j[" note" ] = binding.note ;
416+ j[" search_dirs" ] = nlohmann::json::array ();
417+ for (auto const & path : binding.searchDirs )
418+ j[" search_dirs" ].push_back (path.generic_string ());
325419 j[" environment" ] = nlohmann::json::array ();
326420 for (auto const & decl : binding.environment )
327421 j[" environment" ].push_back ({
@@ -391,6 +485,11 @@ deserialize_runtime_binding(std::string_view encoded) {
391485 if (auto it = j.find (" library_dirs" ); it != j.end () && it->is_array ())
392486 for (auto const & v : *it) if (v.is_string ())
393487 out.libraryDirs .emplace_back (v.get <std::string>());
488+ out.declared = j.value (" declared" , false );
489+ out.note = j.value (" note" , " " );
490+ if (auto it = j.find (" search_dirs" ); it != j.end () && it->is_array ())
491+ for (auto const & v : *it) if (v.is_string ())
492+ out.searchDirs .emplace_back (v.get <std::string>());
394493 if (auto it = j.find (" environment" ); it != j.end () && it->is_array ()) {
395494 for (auto const & v : *it) {
396495 if (!v.is_object ()) continue ;
@@ -456,8 +555,15 @@ deserialize_runtime_binding(std::string_view encoded) {
456555 : mcpp::xlings::runtime::RuntimeSelection::Source::DefaultPolicy;
457556 out.selection .subosName = s.value (" name" , " default" );
458557 out.selection .ownerRoot = s.value (" owner_root" , " " );
459- if (out.schema == 0 || out.runtimeId .empty ()
460- || out.contractHash .empty () || out.subosDir .empty ())
558+ // Completeness is conditional on `declared`. An UNDECLARED binding
559+ // legitimately has schema 0 and no runtime identity — that is what
560+ // "the SubOS said nothing" looks like — so demanding those fields
561+ // would make every cached degraded binding undecodable and send the
562+ // build back down the slow path forever. The hash still has to match,
563+ // which is what actually proves the record was not tampered with.
564+ if (out.contractHash .empty () || out.subosDir .empty ())
565+ return std::unexpected (" cached RuntimeBinding is incomplete" );
566+ if (out.declared && (out.schema == 0 || out.runtimeId .empty ()))
461567 return std::unexpected (" cached RuntimeBinding is incomplete" );
462568 if (detail::hash_contract (detail::canonical_contract (out))
463569 != out.contractHash )
0 commit comments