@@ -100,6 +100,57 @@ struct LoadContext {
100100 bool insideWorkspace = false ;
101101};
102102
103+ // ─── `[xlings]` values per host platform (#544, D7) ─────────────────────────
104+ //
105+ // xlings' own `.xlings.json` lets a `workspace` value be an object keyed by
106+ // platform — `{ "linux": "15.1.0", "default": "22" }` — and resolves it
107+ // against the host it runs on: the host's key wins, `default` is the
108+ // fallback, and no match at all means the entry is absent on that host.
109+ // `[xlings]` claims to mirror that file 1:1, but its parser accepted only
110+ // strings and dropped a table value in silence. `deps` has no xlings-side
111+ // reader (mcpp reads the list and calls `install_packages` itself), so its
112+ // per-platform form is mcpp's to define, and it takes the same one: an entry
113+ // is a string or a `{ <platform> = "<package>" }` table.
114+ //
115+ // Resolved HERE, at load, against this host. `[xlings]` describes the
116+ // environment of the machine mcpp runs on, so the host is the right axis, and
117+ // resolving once keeps every downstream reader — the provisioning pass, its
118+ // stamp, the build-program hand-off — on the flat list it already reads.
119+ //
120+ // The keys are mcpp's OS names, `linux` / `macos` / `windows`, plus `default`;
121+ // `macosx` is accepted as xlings' own spelling of `macos`. An unknown key is a
122+ // hard error, not a dropped entry: a typo'd platform that silently declared
123+ // nothing is the shape #531 was filed for.
124+ inline std::string_view host_platform_key () {
125+ if constexpr (mcpp::platform::is_windows) return " windows" ;
126+ else if constexpr (mcpp::platform::is_macos) return " macos" ;
127+ else return " linux" ;
128+ }
129+
130+ inline std::expected<std::optional<std::string>, std::string>
131+ resolve_host_value (const mcpp::libs::toml::Value& v, std::string_view host) {
132+ if (v.is_string ()) return std::optional<std::string>{v.as_string ()};
133+ if (!v.is_table ())
134+ return std::unexpected (std::string (
135+ " expected a string or a { <platform> = \" ...\" } table" ));
136+ static constexpr std::string_view kKnown [] = {
137+ " linux" , " macos" , " macosx" , " windows" , " default" ,
138+ };
139+ std::optional<std::string> chosen, fallback;
140+ for (auto & [k, val] : v.as_table ()) {
141+ if (std::ranges::find (kKnown , k) == std::ranges::end (kKnown ))
142+ return std::unexpected (std::format (
143+ " unknown platform key '{}'; expected one of linux, macos, windows, default" , k));
144+ if (!val.is_string ())
145+ return std::unexpected (std::format (" platform key '{}' must be a string" , k));
146+ const std::string_view canon = (k == " macosx" ) ? " macos" : std::string_view (k);
147+ if (canon == host) chosen = val.as_string ();
148+ else if (canon == " default" ) fallback = val.as_string ();
149+ }
150+ if (chosen) return chosen;
151+ return fallback; // may be nullopt: not declared on this host
152+ }
153+
103154std::expected<Manifest, ManifestError> parse_string (std::string_view content,
104155 const std::filesystem::path& origin = " mcpp.toml" ,
105156 LoadContext ctx = {});
@@ -270,13 +321,19 @@ std::expected<Manifest, ManifestError> parse_string(std::string_view content,
270321 " target.*.build.flags" , // #258 — middle segment is the cfg predicate
271322 " runtime.requirements" ,
272323 " runtime.artifacts" ,
324+ // #544: `deps = [{ linux = "..." }]` — every entry a per-platform
325+ // table — is the same Value shape as `[[xlings.deps]]`, and the guard
326+ // cannot tell the inline form from the doubled-bracket typo. The
327+ // reader below type-checks every entry, so nothing is silently
328+ // dropped on this path either way.
329+ " xlings.deps" ,
273330 };
274331 if (auto badPath = find_disallowed_array_of_tables (doc->root (), " " , kAllowedArraysOfTables )) {
275332 return std::unexpected (error (origin, std::format (
276333 " [[{}]] (array-of-tables) is not allowed for section '{}'; "
277334 " array-of-tables syntax is only supported for [[build.flags]], "
278- " [[features.<name>.flags]], [[runtime.requirements]], and "
279- " [[runtime.artifacts]]" ,
335+ " [[features.<name>.flags]], [[runtime.requirements]], "
336+ " [[runtime.artifacts]], and [xlings] deps entries " ,
280337 *badPath, *badPath)));
281338 }
282339
@@ -1347,15 +1404,30 @@ std::expected<Manifest, ManifestError> parse_string(std::string_view content,
13471404 m.buildConfig .dependencyLinkage = *v;
13481405 }
13491406
1350- // [xlings] — build environment (L-1). Subsections mirror .xlings.json 1:1.
1351- if (auto v = doc->get_string_array (" xlings.deps" )) m.xlings .deps = *v;
1407+ // [xlings] — build environment (L-1). Subsections mirror .xlings.json 1:1,
1408+ // including its per-platform value form, resolved here for this host
1409+ // (see resolve_host_value above).
1410+ if (auto * arr = doc->get (" xlings.deps" ); arr && arr->is_array ()) {
1411+ std::size_t i = 0 ;
1412+ for (auto & el : arr->as_array ()) {
1413+ auto r = resolve_host_value (el, host_platform_key ());
1414+ if (!r) return std::unexpected (error (origin,
1415+ std::format (" [xlings] deps[{}]: {}" , i, r.error ())));
1416+ if (*r) m.xlings .deps .push_back (**r);
1417+ ++i;
1418+ }
1419+ }
13521420 if (doc->get (" xlings.subos" )) {
13531421 m.xlings .subosDeclared = true ;
13541422 if (auto v = doc->get_string (" xlings.subos" )) m.xlings .subos = *v;
13551423 }
13561424 if (auto * wt = doc->get_table (" xlings.workspace" ))
1357- for (auto & [k, val] : *wt)
1358- if (val.is_string ()) m.xlings .workspace [k] = val.as_string ();
1425+ for (auto & [k, val] : *wt) {
1426+ auto r = resolve_host_value (val, host_platform_key ());
1427+ if (!r) return std::unexpected (error (origin,
1428+ std::format (" [xlings.workspace] {}: {}" , k, r.error ())));
1429+ if (*r) m.xlings .workspace [k] = **r;
1430+ }
13591431 if (auto * et = doc->get_table (" xlings.envs" ))
13601432 for (auto & [k, val] : *et)
13611433 if (val.is_string ()) m.xlings .envs [k] = val.as_string ();
@@ -1913,12 +1985,12 @@ std::expected<Manifest, ManifestError> parse_string(std::string_view content,
19131985 }
19141986 }
19151987
1916- // Unsupported SCALAR keys are REPORTED, not dropped.
1988+ // Unsupported scalar and array keys are REPORTED, not dropped.
19171989 // `[targets.<name>]` has done this since #249; this table did not,
19181990 // so a key that looks plausible — `cxx_runtime_tests` was the real
19191991 // one — was accepted in silence and had no effect (#418).
19201992 //
1921- // ⚠️ SCALARS ONLY , AND THAT IS THE POINT. The sub-TABLES here are the
1993+ // ⚠️ NO SUB-TABLES , AND THAT IS THE POINT. The sub-TABLES here are the
19221994 // conditional channel (`[target.<pred>.build]`, `.dependencies`,
19231995 // `.dev-dependencies`, `.build-dependencies`, `.feature-deps`) and
19241996 // TOML presents each as a key of this table. A hand-written list of
@@ -1930,32 +2002,33 @@ std::expected<Manifest, ManifestError> parse_string(std::string_view content,
19302002 // warned about `[target.'cfg(unix)'.dependencies]`, a documented
19312003 // feature with its own e2e.
19322004 //
1933- // Restricting the check to scalars removes the coupling entirely:
2005+ // Restricting the check to non-tables removes the coupling entirely:
19342006 // new conditional sections need no change here, and the reported
1935- // case — a scalar that does nothing — is still caught.
2007+ // case — a key that does nothing — is still caught.
2008+ //
2009+ // Scalars AND arrays (#544). The sweep used to skip arrays, which
2010+ // kept it from reporting `runner` as unsupported while honouring
2011+ // it — but it also let `runnerX = [...]` pass in silence, and its
2012+ // list of supported keys omitted the one array this table reads.
2013+ // Two lists, one per type, so an array typo is reported and a
2014+ // correctly spelled array is not; the message prints both in one
2015+ // alphabetical line, because a reader of the warning should not
2016+ // have to know a key's type to find it there.
19362017 static constexpr std::string_view kKnownTargetScalars [] = {
19372018 " cxx_runtime" , " linkage" , " sysroot" , " toolchain" ,
19382019 };
2020+ static constexpr std::string_view kKnownTargetArrays [] = { " runner" };
19392021 for (auto & [key, value] : body) {
19402022 if (value.is_table ()) continue ; // the conditional channel
1941- // ...and arrays, which this sweep was never about. It checks
1942- // SCALARS ("a scalar that does nothing"), and `runner` is an
1943- // array read a few lines above — reaching here it was reported
1944- // as "unsupported key 'runner' (ignored)" while in fact being
1945- // honoured, which is worse than either being true.
1946- if (value.is_array ()) continue ;
1947- bool known = false ;
1948- for (auto k : kKnownTargetScalars ) if (key == k) { known = true ; break ; }
1949- if (known) continue ;
1950- std::string supported;
1951- for (auto k : kKnownTargetScalars ) {
1952- if (!supported.empty ()) supported += " , " ;
1953- supported += k;
1954- }
2023+ const std::span<const std::string_view> known = value.is_array ()
2024+ ? std::span<const std::string_view>(kKnownTargetArrays )
2025+ : std::span<const std::string_view>(kKnownTargetScalars );
2026+ if (std::ranges::find (known, key) != known.end ()) continue ;
19552027 m.schemaWarnings .push_back (std::format (
1956- " [target.{}] has unsupported key '{}' (ignored). Supported keys: {}. "
2028+ " [target.{}] has unsupported key '{}' (ignored). Supported keys: "
2029+ " cxx_runtime, linkage, runner, sysroot, toolchain. "
19572030 " Per-role contracts go in [build].cxx_runtime's table form." ,
1958- triple, key, supported ));
2031+ triple, key));
19592032 }
19602033 m.targetOverrides [canon_triple (triple)] = std::move (e);
19612034
0 commit comments