Skip to content

Commit c927106

Browse files
committed
design: [xlings] converges on one table, and deps is retired
deps and workspace state the same thing about a project and differ only in what mcpp does with the statement. The document proposes that workspace become the one table, records the two facts that make the split indefensible — the general form installs nothing while its own documented shorthand installs, and nothing compares the two when both name one package — and carries the deprecation path, the packaging map that does not exist, and the loss the per-platform resolution introduced for it. Design only. Five questions in section 11 are xlings' to answer and are stated rather than assumed.
1 parent 4d99864 commit c927106

1 file changed

Lines changed: 294 additions & 0 deletions

File tree

Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
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

Comments
 (0)