|
| 1 | +# `[xlings]`: one table, and `deps` retired |
| 2 | + |
| 3 | +Date: 2026-09-03. Status: design, awaiting review. Not implemented. |
| 4 | +Baseline: `origin/main` at `4d99864` (2026.9.2.1). Every code reference below |
| 5 | +was read at that commit. |
| 6 | +Relates to: #531 (provisioning reads its result), #544 (per-platform values), |
| 7 | +and the packaging map that does not exist yet (section 7). |
| 8 | + |
| 9 | +## 0. Summary |
| 10 | + |
| 11 | +`[xlings] deps` and `[xlings.workspace]` state the same thing about a project — |
| 12 | +which package, at which version — and differ only in what mcpp then does with |
| 13 | +the statement. That difference is not a property of the declaration, and the |
| 14 | +proposal is to stop encoding it in the key: `[xlings.workspace]` becomes the |
| 15 | +one table, an entry means "this project uses this at this version, provision it |
| 16 | +if absent", and `deps` is retired through a deprecation path rather than a |
| 17 | +removal. |
| 18 | + |
| 19 | +Two things the change is not. It is not a translation layer: `[xlings]` mirrors |
| 20 | +xlings' own `.xlings.json` 1:1, and the merged semantics is the one xlings |
| 21 | +already has, so mcpp is following the schema rather than inventing one. And it |
| 22 | +does not weaken any existing behaviour: a declaration that cannot be satisfied |
| 23 | +stays a hard build error, which is the property #531 was filed to obtain. |
| 24 | + |
| 25 | +## 1. What the two keys are today |
| 26 | + |
| 27 | +Facts, read at `4d99864`. |
| 28 | + |
| 29 | +| | `deps` | `[xlings.workspace]` | |
| 30 | +|---|---|---| |
| 31 | +| Shape | ordered array of package references | map, name to version | |
| 32 | +| Reference form | `name`, `name@version`, `ns:name@version` | key is a bare name | |
| 33 | +| Readers in mcpp | five | one | |
| 34 | +| Provisioning | `install_packages`, result read, hard error, stamped on success | none | |
| 35 | +| Reaches `build.mcpp` | `MCPP_XPKG_<NAME>_DIR` per installed payload | nothing | |
| 36 | +| Reaches the runner lookup | payload `bin/`, in declaration order | nothing | |
| 37 | + |
| 38 | +The five readers of `deps`: the parser (`modules/manifest/src/toml.cppm:1410`), |
| 39 | +the materialisation into `.mcpp/.xlings.json` |
| 40 | +(`src/build/prepare.cppm:3196`), the provisioning pass |
| 41 | +(`prepare.cppm:3420-3482`), `fillXpkgDirs` |
| 42 | +(`prepare.cppm:4549`), and `BuildContext::xlingsDepBinDirs` |
| 43 | +(`prepare.cppm:8707`, added in 2026.9.2.1). The one reader of `workspace` is |
| 44 | +the materialisation, `prepare.cppm:3197-3198`; mcpp never acts on it. |
| 45 | + |
| 46 | +**The general form is weaker than its own shorthand.** `docs/05-mcpp-toml.md` |
| 47 | +§2.13 states that `[toolchain]` is "the ergonomic shorthand for the compiler" |
| 48 | +and `[xlings.workspace]` is "the general form". The shorthand installs: |
| 49 | +`[toolchain]`'s spec reaches `resolve_xpkg_path(pkg.target(), |
| 50 | +/*autoInstall=*/…)` at `prepare.cppm:2212`, and the `build.mcpp` host resolve |
| 51 | +does the same at `:3067`. The general form installs nothing. A general form |
| 52 | +that cannot express what its shorthand does is not general. |
| 53 | + |
| 54 | +**Nothing compares the two when both name one package.** A manifest may write |
| 55 | +`deps = ["make@4.4"]` and `[xlings.workspace] make = "4.5"`. mcpp provisions |
| 56 | +4.4 and writes both statements into `.xlings.json`; no code path in mcpp reads |
| 57 | +the pair. This is the drift shape the repository has paid for repeatedly, and |
| 58 | +merging the keys removes it by construction rather than by adding a check. |
| 59 | + |
| 60 | +## 2. Why they are one thing |
| 61 | + |
| 62 | +A version constraint and an installation are the same statement seen at two |
| 63 | +moments. "This project uses cmake 3.28" is what the build environment must be; |
| 64 | +whether cmake is already present decides whether anything has to be fetched, |
| 65 | +and that is a fact about the machine, not about the project. xlings' own |
| 66 | +`workspace` carries that reading, which is why the mcpp side has one reader: |
| 67 | +there was nothing for mcpp to decide. |
| 68 | + |
| 69 | +Keeping two keys forces every author to answer a question the manifest should |
| 70 | +not ask — "do I want this installed, or only pinned?" — whose honest answer is |
| 71 | +always "installed if it is not there". The one case that looks like a |
| 72 | +counterexample, pinning a tool the project may never invoke, is not one: an |
| 73 | +entry naming a tool the project does not use is noise regardless of the key it |
| 74 | +is written under. |
| 75 | + |
| 76 | +## 3. The schema after the change |
| 77 | + |
| 78 | +```toml |
| 79 | +[xlings.workspace] |
| 80 | +cmake = "3.28" |
| 81 | +qemu-riscv = "9.2.4-1" |
| 82 | +"xim:picolibc-riscv" = "1.8.12" |
| 83 | +make = "*" |
| 84 | +gcc = { linux = "15.1.0" } |
| 85 | +llvm = { macos = "20", default = "22" } |
| 86 | +``` |
| 87 | + |
| 88 | +Three decisions the table needs, listed for review. |
| 89 | + |
| 90 | +**W1. `"*"` means "any version, and it must be present".** `deps` accepts an |
| 91 | +entry with no version (`deps = ["cmake"]`), and a map keyed by name has no way |
| 92 | +to say "no constraint" other than a value that means it. Without `"*"` every |
| 93 | +author who does not care about a version is forced to invent one, and a pinned |
| 94 | +version nobody chose is worse than no pin. |
| 95 | + |
| 96 | +**W2. A key may carry a namespace prefix.** `deps` accepts `xim:name`, and the |
| 97 | +namespace is load-bearing: `parse_xpkg_ref` splits it and `xpkg_payload` |
| 98 | +resolves against it. If a workspace key cannot hold a colon, the namespace has |
| 99 | +to move into the value, and the value position is already taken by the |
| 100 | +per-platform table form. Whether xlings' workspace keys accept a colon is |
| 101 | +question Q1 of section 11; the answer decides between the key form above and a |
| 102 | +`{ namespace = "xim", version = "1.8.12" }` value form, which would be a second |
| 103 | +table shape and is worse. |
| 104 | + |
| 105 | +**W3. The per-platform value form is unchanged.** It is already accepted on |
| 106 | +both keys (2026.9.2.1) and it survives the merge unmodified. |
| 107 | + |
| 108 | +## 4. What mcpp writes into `.xlings.json` |
| 109 | + |
| 110 | +This is the decision the rest depends on, and it is xlings' to make. |
| 111 | + |
| 112 | +**Option A: mcpp writes only `workspace`.** The materialisation stops emitting |
| 113 | +a `deps` array; every entry lands in the `workspace` object, and xlings |
| 114 | +provisions from it. This is the shape the proposal is written to, and it holds |
| 115 | +only if xlings provisions from `workspace`. If it does not, a project that |
| 116 | +migrated would build on a machine where the packages happen to be installed and |
| 117 | +fail on a clean one, which is the failure mode with the longest detection |
| 118 | +delay. |
| 119 | + |
| 120 | +**Option B: mcpp derives a `deps` array from `workspace` when writing.** mcpp |
| 121 | +keeps one table in `mcpp.toml` and emits both fields. This works whatever |
| 122 | +xlings does, and it is a translation layer of exactly the kind §2.13 refuses. |
| 123 | +It is acceptable only as a transitional step with a stated end. |
| 124 | + |
| 125 | +The recommendation is A, conditional on Q1 and Q2. B is the fallback and must |
| 126 | +be labelled transitional in the code rather than left to look permanent. |
| 127 | + |
| 128 | +## 5. What provisioning means after the merge |
| 129 | + |
| 130 | +The provisioning pass keeps its current contract, with the input widened from |
| 131 | +`deps` to the merged table: |
| 132 | + |
| 133 | +- The result is read, not assumed. `xlings::call` is in the value state |
| 134 | + whenever the child ran, and the capability's status is inside `CallResult`; |
| 135 | + the check stays `!called || childRc != 0` (`prepare.cppm:3448`). |
| 136 | +- A failure is a hard build error naming the manual command, as today. |
| 137 | +- The stamp is written only on success, and it is keyed on the hash of the |
| 138 | + declared set, so the merged table changes the hash and every project |
| 139 | + re-provisions once after upgrading. That is correct: the declared set is a |
| 140 | + different set. |
| 141 | +- `MCPP_OFFLINE` and `MCPP_NO_AUTO_INSTALL` gate the install action and not the |
| 142 | + whole block, as today. |
| 143 | + |
| 144 | +**The one behaviour change is for `workspace` entries that exist now.** They |
| 145 | +start being provisioned. The population is small and the direction is toward |
| 146 | +the documented claim rather than away from it: an entry that was a pin becomes |
| 147 | +a pin that is also honoured. Section 9 makes it a criterion rather than an |
| 148 | +assumption. |
| 149 | + |
| 150 | +## 6. Migration, and what "retired" means |
| 151 | + |
| 152 | +`deps` is not deleted. Three phases, each with a criterion. |
| 153 | + |
| 154 | +**Phase 1 — the merged reader.** `workspace` gains provisioning, the payload |
| 155 | +directory hand-off (`MCPP_XPKG_*_DIR`) and the runner lookup path. `deps` keeps |
| 156 | +working exactly as it does and is documented as deprecated. A manifest that |
| 157 | +names one package in both, with different versions, is a hard error naming both |
| 158 | +lines: the drift of section 1 becomes unrepresentable at the moment the second |
| 159 | +reader appears rather than later. |
| 160 | + |
| 161 | +**Phase 2 — the warning.** A manifest using `deps` builds and prints one |
| 162 | +advisory naming the `[xlings.workspace]` line to write instead. The advisory is |
| 163 | +per package, so the message is the edit. |
| 164 | + |
| 165 | +**Phase 3 — refusal, never silence.** `deps` stops being honoured and becomes a |
| 166 | +hard error that names the replacement. It must not become an unknown key: |
| 167 | +`[xlings]` has no unknown-key sweep (verified — no `kKnownXlings` list exists |
| 168 | +in `toml.cppm`), so a removed key would be read by nobody and reported by |
| 169 | +nobody, which is the shape #531 exists to prevent. Phase 3 is gated on an index |
| 170 | +sweep showing no published manifest still uses `deps`, and on an mcpp floor in |
| 171 | +the packages that migrate. |
| 172 | + |
| 173 | +**The ecosystem denominator is small.** Measured across the local `mcpplibs` |
| 174 | +checkouts: three manifests declare `[xlings] deps` |
| 175 | +(`aarch64-virt-rt`, `riscv-virt-rt`, `std-freestanding`), one entry each, all |
| 176 | +of the form `xim:<name>@<version>`. mcpp's own `mcpp.toml` declares no |
| 177 | +`[xlings]` section. The index has to be swept before Phase 3; the local |
| 178 | +denominator is not the ecosystem. |
| 179 | + |
| 180 | +## 7. The packaging map, and a loss the current implementation has |
| 181 | + |
| 182 | +The reason a `deps`-shaped list exists in the first place is that a published |
| 183 | +package's install-time edge lives in the descriptor, as |
| 184 | +`xpm.<platform>.deps`. That mapping does not exist in mcpp: |
| 185 | +`src/publish/xpkg_emit.cppm` mentions neither `xlings` nor `deps`, and nothing |
| 186 | +in `src/pack` or `src/publish` emits a platform `deps` table. A package that |
| 187 | +declares `[xlings] deps` today gets a descriptor without it, and the edge is |
| 188 | +written by hand — which is why `riscv-virt-rt` carries a thirty-line comment |
| 189 | +about the release where the hand-written edge was removed and the C library |
| 190 | +stopped being installed. |
| 191 | + |
| 192 | +**The per-platform resolution shipped in 2026.9.2.1 is lossy for this path.** |
| 193 | +`XlingsConfig::deps` and `::workspace` hold values already resolved for the |
| 194 | +running host (`modules/manifest/src/types.cppm`, and `resolve_host_value` in |
| 195 | +`toml.cppm`), and the unresolved entries are discarded. An emitter needs all |
| 196 | +platforms at once: `xpm.linux.deps` and `xpm.windows.deps` are two tables, and |
| 197 | +a manifest loaded on Linux can no longer produce the second. Packing on macOS |
| 198 | +would emit a descriptor missing the Linux edge, and nothing would say so. |
| 199 | + |
| 200 | +The fix is additive and belongs with this work because the merged table |
| 201 | +inherits the same loss: |
| 202 | + |
| 203 | +```cpp |
| 204 | +struct XlingsConfig { |
| 205 | + std::map<std::string, std::string> workspace; // resolved for THIS host |
| 206 | + // The declaration as written, per platform, for consumers that are not |
| 207 | + // this host: the descriptor emitter needs every platform's entries at |
| 208 | + // once. The build path never reads this. |
| 209 | + std::map<std::string, std::map<std::string, std::string>> workspaceByPlatform; |
| 210 | +}; |
| 211 | +``` |
| 212 | +
|
| 213 | +`resolve_host_value` already knows which platform each key belongs to; keeping |
| 214 | +a second copy costs one insertion. The criterion is section 9's C4: packing on |
| 215 | +one host emits every platform's edge, and it fails today because the emitter |
| 216 | +does not exist. |
| 217 | +
|
| 218 | +## 8. Axes |
| 219 | +
|
| 220 | +**Structure.** One declaration site for "what this project's environment |
| 221 | +contains", one reader set, one provisioning pass. The count of things that can |
| 222 | +disagree about a package's version drops from two to zero. |
| 223 | +
|
| 224 | +**Compatibility.** Phase 1 adds no manifest key and removes none, so a manifest |
| 225 | +written for it loads on an older mcpp; there, a `workspace` entry is a pin that |
| 226 | +installs nothing, which is what it means today. The reverse direction — |
| 227 | +an older manifest on a newer mcpp — is unchanged through Phase 2. |
| 228 | +
|
| 229 | +**Upgrading.** The provisioning stamp is keyed on the declared set, so the |
| 230 | +first build after the merge re-provisions once per project and then behaves as |
| 231 | +before. No cache is invalidated and no output path changes. |
| 232 | +
|
| 233 | +**Consistency.** The general form gains what its shorthand already does. The |
| 234 | +`[toolchain]`/`[xlings.workspace]` relationship stated in §2.13 becomes true |
| 235 | +rather than aspirational. |
| 236 | +
|
| 237 | +**Cross-platform.** The per-platform value form is unchanged; section 7 makes |
| 238 | +it survive to the one consumer that needs the unresolved form. |
| 239 | +
|
| 240 | +**What a person sees.** One table instead of two, and one question fewer to |
| 241 | +answer when writing it. Every failure keeps naming the package and the manual |
| 242 | +command. |
| 243 | +
|
| 244 | +## 9. Test criteria |
| 245 | +
|
| 246 | +Each must be observed failing before the corresponding change. |
| 247 | +
|
| 248 | +| # | Criterion | Note | |
| 249 | +|---|---|---| |
| 250 | +| C1 | A `[xlings.workspace]` entry for a package that is not installed provisions it, and its payload directory reaches `build.mcpp` as `MCPP_XPKG_<NAME>_DIR` | Assert on the value the program read, not on a log line | |
| 251 | +| C2 | A `workspace` entry that cannot be provisioned fails the build with the manual command in the message | The existing `deps` diagnostic, reached from the new input | |
| 252 | +| C3 | One package named in both `deps` and `workspace` with different versions is a hard error naming both lines | Must be seen to fail on a manifest that today builds and silently provisions the `deps` version | |
| 253 | +| C4 | Packing a project whose table has per-platform entries emits `xpm.<platform>.deps` for every platform, from any host | Fails today because the emitter does not exist; the assertion is on the emitted descriptor, not on the manifest | |
| 254 | +| C5 | A `workspace` entry with `"*"` provisions the package and pins nothing | Both halves; a test that only checks the install cannot tell a wildcard from a version | |
| 255 | +| C6 | A migrated `riscv-virt-rt` resolves its emulator on a clean machine | The ecosystem case, run in a sandbox, because "installed already" is the state that hides this | |
| 256 | +| C7 | The second build of an unchanged project provisions nothing and prints nothing | The stamp; and the whole-project fast path never reaches this code, so the test must touch a source first | |
| 257 | +
|
| 258 | +C3 and C4 are the two that fail on the current engine. C7 is stated because |
| 259 | +this repository has read that measurement wrongly twice. |
| 260 | +
|
| 261 | +## 10. Implementation surface |
| 262 | +
|
| 263 | +- `modules/manifest/src/toml.cppm`: `workspace` gains the reference forms |
| 264 | + `deps` accepts (namespace prefix, `"*"`); `workspaceByPlatform` retained; |
| 265 | + the `deps`/`workspace` conflict check; the Phase 2 advisory. |
| 266 | +- `modules/manifest/src/types.cppm`: `XlingsConfig` fields and their comments. |
| 267 | +- `src/build/prepare.cppm`: the provisioning pass, `fillXpkgDirs` and |
| 268 | + `xlingsDepBinDirs` read the merged table; the materialisation emits what |
| 269 | + section 4 decides. |
| 270 | +- `src/xlings/xlings.cppm`: `ProjectEnv` and `seed_xlings_json`, per section 4. |
| 271 | +- `src/publish/xpkg_emit.cppm`: the `xpm.<platform>.deps` map (new). |
| 272 | +- `docs/05-mcpp-toml.md` §2.13, `docs/13-baremetal.md`, `docs/17`, and each |
| 273 | + `docs/zh/` twin, which CI enforces. |
| 274 | +- `tests/unit/test_manifest.cpp`, a new e2e for C1/C2/C3, and the packing |
| 275 | + criterion C4. |
| 276 | +- Ecosystem: `aarch64-virt-rt`, `riscv-virt-rt`, `std-freestanding` migrate |
| 277 | + after Phase 1 ships, each with an mcpp floor. |
| 278 | +
|
| 279 | +## 11. Open questions |
| 280 | +
|
| 281 | +1. **Does xlings provision from `workspace`?** Section 4 depends on it. If it |
| 282 | + does not, the answer decides between adding it there and Option B here. |
| 283 | +2. **Do xlings' workspace keys accept a namespace prefix (`xim:name`)?** W2 |
| 284 | + depends on it. A workspace key becomes a shim name in xvm, which is the |
| 285 | + reason to doubt it. |
| 286 | +3. **Is `"*"` already spelled something else in xlings?** W1 should take the |
| 287 | + existing spelling rather than introduce one. |
| 288 | +4. **Does a workspace entry bind the package or one program?** The |
| 289 | + `qemu-riscv` descriptor adds an umbrella node for the package name beside |
| 290 | + the two program nodes, so both are addressable; whether binding the package |
| 291 | + determines its programs' versions is the property the merged table relies on |
| 292 | + when a package ships several programs. |
| 293 | +5. **Phase 3's floor.** Which mcpp version the migrating packages declare, and |
| 294 | + whether the index sweep is a release gate or a one-off. |
0 commit comments