xlings: [xlings.workspace] is the one table, deps superseded, envs removed, and the install-time edge published (2026.9.3.1) - #548
Conversation
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.
…nged against Section 4 is decided rather than open: the correspondence is one to one in name and in meaning, so the merged table materialises as the file's own workspace object and no deps array is derived. The decision carries a consequence for W2 — mcpp appends the target's C library to that channel itself, as a namespaced reference with a version, so a key that cannot hold a namespace would leave mcpp unable to express its own entry. Section 12 records the section as a whole: field correspondence, the three places mcpp is not a pure mirror, the ownership rule and where the file is written, subos in full, and the two environment channels that are easy to confuse.
Section 13 answers section 4 and Q1 from the implementation rather than from inference, and corrects two claims this document and docs/05 both made. xlings does not provision from workspace: deps is the only key any install path reads, so the merge belongs in mcpp.toml and the file keeps both fields — one statement materialised into the two places xlings reads its two halves. envs has no reader on either side. Every consumer in the xlings source is either xvm's per-program VData::envs or a SubOS's subos_info provider sections, and neither is the flat object mcpp writes; mcpp's own run environment comes from the runtime binding. The key is documented as having an effect it does not have, and the section proposes deciding its fate before this proposal ships. Also recorded: a named subos drops the global workspace layer entirely, which is a property of subos that nothing in mcpp states today.
…rprises The previous wording named the layer a declared subos drops and not the two rows where inheritance is what an author would expect. A project that pins a few tools and declares no subos starts from the global workspace, which is the common shape; a named subos inherits its own environment's workspace instead, because carrying the host's pins into a different installed set would name versions that are not there; and a fork copies its base's map once at creation. Also records that nothing falls back silently: with no layer naming a tool the shim diagnoses rather than choosing, which is the line the 2026.9.2.1 sandbox verification measured.
…ges the proposal Section 14 assigns the inheritance rule to docs/05 §2.13 and docs/17 with their Chinese twins, as a table plus the reason the middle row is not an omission, and gives it a criterion so the paragraph is not the only record. Section 15 is the second read against the code. Its first finding changes the proposal: a project-file workspace entry is merged verbatim and the shim looks up a program's own name, so naming a package root pins a node nothing invokes while its programs resolve from the layer underneath. Group expansion happens in cmd_use, which a manifest never goes through. Three ways out are stated; expanding at read time is recommended and is xlings' to accept. Two smaller corrections: 'retired' has always meant the manifest key and never the file field, and section 12.6's claim to touch neither subos nor envs is withdrawn.
15.1 claimed a manifest workspace entry cannot pin a package's programs. Both its premises hold and the conclusion does not: the expansion happens in the action mcpp triggers, not in the merge. Installing calls cmd_use, cmd_use resolves the release and writes one entry per member, and with a project config loaded those land in the project's own SubOS layer. What the read did find is one flag. Activation after install happens only when nothing is active yet, so on a machine already holding another version the declared one is installed and not run. The capability already takes useAfterInstall; D8 sends it. W1 and W2 are rewritten against the file format rather than inferred: stored values are always concrete, latest and a version prefix are the input spellings, and every key in three real subos files is a bare target name with no namespace. envs is decided — retired, with the documentation correction shippable on its own.
… it, and the migration collapsed to one release Three corrections, all from data rather than inference. A workspace key is an xvm target of any kind. The default SubOS holds binutils beside ar, as and ld at one version, and gcc beside g++ and cc at another: package roots, programs and file assets share one namespace, and a root and its programs agree because cmd_use wrote them as one release. Calling the keys program names was wrong, and writing a package in a manifest is a legitimate entry whose programs receive the same version. The namespace rides the value, not the key: parse_ns_version splits the first colon of a version string and a real entry reads xim:2026.8.30.2. So the form is picolibc-riscv = "xim:1.8.12", which needs no rule mcpp has to invent. The migration is one release. Three manifests declare the key and mcpp declares none, so the deprecation window buys nothing; what does not bend is that deps is refused with a message rather than dropped in silence.
…ith its reason W2 asserted where the namespace goes; it now measures. 1635 version-database targets and 546 workspace entries on the development host, and not one key contains a colon. Two reasons ns:name cannot be the key, and neither is convention. A workspace key is looked up by the name a program is invoked as, and nothing is invoked as xim:picolibc-riscv, so the key would be read by nobody. And the namespace qualifies where a version came from rather than what the tool is: mcpp itself holds eight versions under one target, some scoped xim: and some not, which moving the namespace onto the key would split into two tools with one name. A colon in a key is therefore a hard error naming the correct form, not a second accepted spelling. The section 3 example is corrected in the same pass: the invented "*" becomes latest.
… file Two artifacts share the name and this document measured one for the other. The authored file maps a name to a version string or a platform object; d2mcpp/.xlings.json is the published example, and it settles two of the three open items. Version unconstrained is the empty string. "code": "" is already in use, the resolver returns it unchanged, and an empty value reads downstream as claiming no version — which is what deps = ["cmake"] means today. No * and no latest is introduced. The namespace in the key position is undefined rather than forbidden: nothing derives an install address from a workspace entry, because nothing installs from workspace at all, so the question becomes real only when mcpp does. Both candidate forms are stated with what each costs, and the item is marked as a request rather than a finding. The native platform keys are recorded as linux, windows, macosx and default.
… file's own W2 was a request; it is now a decision. An author may put the namespace on the version or on the key, because the two are interconvertible and neither is more natural than the other — one matches how deps spells a package, the other matches what the file already contains. Four rules keep it one fact: mcpp materialises the file's own form only, the install address is assembled from whichever half carried the namespace, stating it twice and differently is a hard error, and the key form needs quotes. The quoting is measured rather than asserted. TOML bare keys are [A-Za-z0-9_-] and mcpp's lexer matches; on the 2026.9.2.1 binary the unquoted key fails at the colon with a parser message that mentions nothing about namespaces, so the documented example carries the quotes. The previous rule making a colon in a key an error is withdrawn.
Three facts shape it. xlings auto-activates only when nothing is active for that name in the merged view, so a version active globally makes a project's declaration lose. Activation writes into the project's own SubOS layer, which config.cpp places under the project directory, so forcing it disturbs no other checkout and not the machine. And the two value spellings ask for different things: a version wants activation, an empty value wants presence. The deciding fact is the fourth. When a forced install fails to switch, the installer logs a warning and the call still exits zero, so useAfterInstall gives mcpp a request whose outcome it cannot read — #531 from the other side. use_version returns cmd_use's exit code instead. Hence the shape: one unforced install batch, then one use_version per versioned entry with its exit code checked, nothing extra for unconstrained entries, and the existing stamp covering both. Six criteria, one of which must be seen failing first.
…he right one Correction first: provisioning does not write into the project's layer. It calls with make_xlings_env, which carries no projectDir, so the child runs in global scope against mcpp's registry home. The earlier claim that forcing was safely contained is withdrawn, and three measured facts follow from the real scope, including why the runner needed a payload lookup at all. xlings already implements both halves of the requirement: an existing payload is mapped rather than reinstalled, and install activates whenever nothing is active for that name. Whether mcpp receives that behaviour depends on the environment it asks in, not on a flag. Design P provisions in project scope, where the project's own SubOS is the layer written; with a named SubOS the global layer is not merged, so the declaration is what activates and no forcing is needed. Design G keeps the global scope and forces, which lets two checkouts fight and cannot be verified. P is recommended, and it carries one question that is not mcpp's alone: whether a project environment inherits the machine's. Both branches are stated with what they cost.
…he provisioning scope merge_workspace_into_ assigns rather than inserts, so the later layer wins: in Anonymous mode the project's declaration beats the machine's, and in Named mode the machine's layer is not merged at all. A project declaring a version therefore resolves to it without anything forcing a switch, and the developer picks the strength by naming a SubOS or not. What is wrong today is one argument. Provisioning calls with make_xlings_env, which carries no projectDir, so the child runs in global scope: the install's records land in the registry's shared workspace while programs invoked from the project resolve through the project's layers. That disagreement is the 'not installed in this subos' line the 2026.9.2.1 verification measured and the reason the runner needed a payload lookup. So the change is make_project_xlings_env, and useAfterInstall is not passed and no use_version call is added. Two consequences are written down rather than left to be discovered: the project SubOS layer outranks the manifest, and an uninstalled declared version already fails with wording that names the project.
…hecked The document had accumulated seven contradictions across the review rounds: the summary described a deprecation path the migration section had replaced with a single release, the open-questions section still named a decision a later section had withdrawn, and the axes referred to phases that no longer exist. Rewritten rather than patched, 851 lines to 493, with section 15 keeping every abandoned position and why it was abandoned. The framing is the one the review arrived at: [xlings] is mcpp's manifest surface for xlings' local project mechanism, and each decision is an answer to what that mechanism already does. Added a precondition the scope fix depends on and that nothing else would have caught: xlings refuses to treat a directory holding a bare subos/ as a project, and a project's SubOS lives one level deeper, so .mcpp qualifies today — measured — but a future bare subos/ under it would make mcpp's own project file invisible everywhere at once. C13 asserts it on an effect, because a skip degrades to today's behaviour and would look like success.
…tation plan Step 1 of the migration refused deps everywhere. That counts the wrong denominator: the three manifests that declare the key are consumed, and a consumer pins an exact version, so a refusal reaching a dependency's manifest makes riscv-virt-rt@0.6.0 unbuildable for everyone who pinned it while no republished version reaches them. The refusal applies to the root manifest, where the author can act on it; a dependency's is honoured and reported once. Section 16 splits the work into eight tasks with their dependency order, and states what each review axis demands of the implementation rather than of the design.
…e edge published `[xlings]` is mcpp's surface for xlings' local project mechanism, and this brings the manifest into line with what that mechanism has. `[xlings.workspace]` becomes the one table. An entry names a package and the version this project uses it at, and produces two projections: an install address for provisioning and a resolution pin for the materialised `.xlings.json`. The namespace may be written on the key or on the version — `"xim:picolibc-riscv" = "1.8.12"` and `picolibc-riscv = "xim:1.8.12"` are one entry — because both spellings already exist in the ecosystem and neither is more natural; writing it on both halves with different values is refused, as is naming one package twice. `""` means present with the version unconstrained, which is the spelling an authored project file already uses. Platform keys are xlings' own, with `macos` kept as an alias. `deps` is superseded and still honoured, reported once with the line to write instead. It is not refused: a refusal would reach a dependency's manifest that a consumer pinning that package cannot edit. One package in both tables with two versions is a hard error, because the two are provisioned in order while the last wins the pin — install one, resolve the other. `envs` is removed. It was materialised into the file and read by nothing: the two `envs` structures xlings has belong to a program's own shim record and to a SubOS's provider sections, and mcpp's run environment comes from the runtime binding. The documentation described an effect that did not occur, which is why this is an error rather than a warning. `mcpp emit xpkg` writes `xpm.<platform>.deps`. Nothing emitted it before, so a package declaring an environment had its install-time edge written by hand — which is how riscv-virt-rt 0.3.0 shipped without the C library its own target row named. The declaration is kept unresolved per platform for it, since a descriptor has a block per platform and the host resolution has discarded two. Documented: the three-row inheritance rule, which has always held and which mcpp stated nowhere — no `subos` means the machine's environment with the project's entries over it, a named one means isolation, and an `xlings use` inside the project outranks both. Verified: e2e 331 asserts on the addresses the provisioning pass names with auto-install off, so every criterion runs on every shard with no network; it fails at its first assertion on 2026.9.2.1. Unit: test_manifest +8. Full unit suite 97 passed; e2e 06, 93, 130, 131, 132, 134 re-run.
The deps compatibility path derived (namespace, target, version) from an address twice — once to compare against a workspace pin and once to phrase the advisory — which is the shape where two copies of one rule drift. parse_address is the inverse of XlingsEntry::address, and both sites read it. The comparison also widened. It tested versions alone, so 'other:foo@1.0' beside 'foo = "xim:1.0"' passed as agreement: two packages sharing a name at one version. Comparing the pin covers both halves.
It rides schemaWarnings, which prepare prints for the root manifest and escalates under --strict. A dependency's are attached and not printed, which is a pre-existing gap: surfacing them would also surface every unrelated warning those manifests carry, and that is a change of its own. So the package's author is told the moment they build it, and a consumer of an unmigrated package is not — stated here rather than left to be discovered.
…ression Republishing the three packages with the new table before their consumers move would stop provisioning on every older engine, silently: an older mcpp parses [xlings.workspace] and provisions nothing from it. That is worse than the advisory it would silence. This cycle verifies the three packages on the new engine instead; the republish waits for a floor that makes the older engine refuse rather than degrade.
The chapter taught [xlings] deps as the way a board package declares its emulator. The declaration is the same statement; only the table changed, and the older spelling still works and says so.
…nd deps emits no edge by design The 2.13 example put `subos = "dev"` after `[xlings.workspace]`, which in TOML makes it `xlings.workspace.subos` — a key nothing reads. Two blocks, each under the table it belongs to. The emitter gains the reason it skips a package still on `deps`: that key is host-resolved at load, so the per-platform declaration a descriptor needs is already gone, and writing the host's answer into all three blocks would be the machine-dependent descriptor the function exists to avoid.
…only macOS could fail Three CI-only failures, all from this change and each in a fixture rather than in the engine. e2e 88 and 205 declared [xlings.envs], which is now refused. 205 drops it; 88 asserted the key was materialized and now asserts it is not, and gains two criteria the merge introduced: the same statement in both tables collapses to one entry, and the superseded key is reported with the line to write. The manifest parser gained that collapse. deps and workspace agreeing about one package is not an error and is not two entries — appending it twice would ask xlings to install one package twice. Manifest.XlingsWorkspaceAcceptsPerPlatformValues compared host_platform_key() against "macos", and this change made that key xlings' own spelling, "macosx". The assertion was written in terms of the function precisely so it would run on every host, and it was the one line in the file that could only fail on a macOS runner. It did.
It declared its emulator with [xlings] deps, which now reports itself. The address the new form assembles is byte-identical — measured with auto-install off, which prints what the pass was going to ask for: 'xlings install xim:qemu-riscv@9.2.4-1' either way.
Four items, three visible only once the code ran: a nested std::map in an exported module truncating the BMI and reporting it in an unrelated file; the removed key's first casualties being this repository's own fixtures; one assertion that could only fail on macOS doing so; and the same statement in both tables collapsing rather than installing one package twice.
Two names for one platform: mcpp's triple vocabulary says macos, an xpkg descriptor and xlings' project file say macosx. The manifest parser had a second copy of that fold and a second copy of the host key, both hand-rolled beside a rule modules/platform already had. mcpp.platform.axis gains xpkg_platform_key_for, which is now the only place that knows, and TargetPlatform::for_os is written in terms of it rather than repeating the table. The manifest reads it for both directions — canonicalising a written key and folding a requested platform — and takes the host from xpkg_platform instead of its own #if. Both spellings are therefore accepted wherever a platform is named, and a new unit test asserts that the two write the same declaration on every host by resolving explicitly rather than against the one it runs on.
W3 said mcpp accepts the alias; it did not say where the fold lives. The implementation lifted it into mcpp::platform::xpkg_platform_key_for, which TargetPlatform::for_os and the manifest parser both call, and takes the host key from xpkg_platform rather than a second #if — an earlier draft hand-rolled both, which would have been a third copy of a two-line table.
Self-review, and what implementation changedAll 36 checks green. Five findings, four of them in this branch's own work. A nested The design's second change was withdrawn against a measurement already in the code. It moved provisioning to project scope; the call site records that this was tried and that project scope puts payloads in The removed key's first casualties were this repository's own fixtures. e2e 88 and 205 declared One assertion could only fail on macOS, and did.
Deliberately not doneThe three ecosystem packages are not migrated in this cycle. An older mcpp parses Verificatione2e Unit |
[xlings]is mcpp's surface for xlings' local project mechanism — the project.xlings.jsonthat gives a directory its own environment. This brings the manifest into line with what that mechanism has.Design record, with every measurement:
.agents/docs/2026-09-03-xlings-workspace-as-the-one-table.md.What changes
[xlings.workspace]becomes the one table. An entry names a package and the version this project uses it at, and produces two projections: an install address ([<ns>:]<target>[@<version>]) for provisioning, and a resolution pin ([<ns>:]<version>) for the materialised file'sworkspaceobject — the file's two fields, which xlings reads in two different places.The namespace may be written on either half;
"xim:picolibc-riscv" = "1.8.12"andpicolibc-riscv = "xim:1.8.12"are one entry. Writing it on the key needs quotes (a TOML bare key cannot hold a colon, measured: the unquoted form fails at the colon with a parser message that mentions nothing about namespaces). Writing it on both halves with different values is refused, as is naming one package twice.""means present with the version unconstrained — the spelling an authored project file already uses. Platform keys are xlings' ownlinux/macosx/windows/default, withmacoskept as an alias.Why the split could not be defended.
docs/05§2.13 calls[toolchain]the shorthand and[xlings.workspace]the general form; the shorthand installs and the general form installed nothing. And nothing compared the two when both named one package.depsis superseded, still honoured, reported once with the line to write instead. It is not refused: a refusal would reach a dependency's manifest, which a consumer that pinned an exact version of that package cannot edit. One package in both tables with two versions is a hard error — they are provisioned in order while the last wins the pin, so accepting it installs one and resolves the other.envsis removed. It was materialised into.xlings.jsonand read by nothing: the twoenvsstructures xlings has belong to a program's own shim record and to a SubOS's provider sections, and mcpp's own run environment comes from the runtime binding. The documentation described an effect that did not occur, which is why this is an error rather than a warning. No manifest in the ecosystem uses it.mcpp emit xpkgwritesxpm.<platform>.deps. Nothing emitted it before, so a package declaring an environment had its install-time edge written into the descriptor by hand — which is howriscv-virt-rt0.3.0 shipped without the C library its own target row named. The declaration is kept unresolved per platform (workspaceByPlatform) because a descriptor has a block per platform while the host resolution has already discarded two.Documented: the three-row inheritance rule, which has always held and which mcpp stated nowhere.
[xlings.workspace], nosubos[xlings.workspace]andsubos = "<name>"An
xlings useinside the project outranks both, being the layer merged last.Withdrawn during implementation
The design's second change moved provisioning to project scope. The call site records that this was tried and measured: project scope puts payloads in
<proj>/.mcpp/.xlings/subos/_/usr/includewhile--sysrootnames the registry SubOS, so a declared dependency's headers were not found. The scope stays global; §3 of the design carries the withdrawal and what it leaves unsolved.Verification
e2e
331_xlings_workspace_one_table.shneeds no network on any shard: every entry reaches the provisioning pass, and withMCPP_NO_AUTO_INSTALL=1that pass refuses and prints the addresses it was going to ask for, so the refusal is the assertion. It covers address assembly, both namespace positions, the unquoted-key error,"", thedepsadvisory and its suggested line, the two-table conflict, theenvsrefusal, and that a project declaring none of it hears nothing. It fails at its first assertion on 2026.9.2.1.Unit:
test_manifest+8. Full unit suite 97 passed. e2e 06, 93, 130, 131, 132, 134 re-run on this branch.Version 2026.9.3.1; the bootstrap pin stays at 2026.9.2.1 until the release is indexed.