Skip to content

Commit 92d219a

Browse files
committed
design: D8 withdrawn — the merge already does it, and the defect is the 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.
1 parent c0cc681 commit 92d219a

1 file changed

Lines changed: 75 additions & 102 deletions

File tree

.agents/docs/2026-09-03-xlings-workspace-as-the-one-table.md

Lines changed: 75 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -750,38 +750,42 @@ both name one package. §13 strengthened rather than weakened them: xlings makes
750750
the same split in the same direction, which is why the merge belongs in the
751751
manifest and not in the file.
752752

753-
## 16. D8: the question is which environment mcpp provisions into
753+
## 16. D8 withdrawn: the merge already does it, once the scope is right
754754

755-
The requirement is one sentence — a declared tool at a declared version is
756-
installed if absent and active afterwards. It looked like a flag. It is not.
755+
The requirement — a declared tool at a declared version is installed if absent
756+
and active afterwards — needs no flag, no explicit switch and no new rule. It
757+
needs mcpp to ask in the right environment. Everything else is already built.
757758

758-
### 16.1 xlings already does the whole job, correctly
759+
### 16.1 Both halves are native
759760

760-
Installing an already-present payload does not reinstall it. The installer
761-
checks whether the payload exists and is registered to this package, sets
762-
`payloadInstalled` and skips the install hook
763-
(`src/core/xim/installer.cpp:2740-2775`), then proceeds to the mapping and
764-
activation steps. So "map when the payload is there, install when it is not" is
765-
the existing behaviour, not something to build.
761+
**Mapping rather than reinstalling.** The installer checks whether the payload
762+
exists and is registered to this package, sets `payloadInstalled` and skips the
763+
install hook (`src/core/xim/installer.cpp:2740-2775`). An already-present
764+
payload is mapped; only a missing one is fetched.
766765

767-
Activation is equally native. `activate_requested_targets` switches whenever
768-
**nothing is active for that name**:
766+
**The declaration outranking the machine.** `merge_workspace_into_` assigns
767+
rather than inserts (`src/core/config.cpp`), so in `merged_workspace` the later
768+
layer wins:
769769

770-
```cpp
771-
auto active = xvm::get_active_version(Config::effective_workspace(), match.name);
772-
if ((active.empty() || useAfterInstall) && has_version(db, match.name, match.version))
773-
cmd_use(match.name, match.version, stream);
774-
else if (!active.empty() && active != match.version)
775-
// declining to switch is a decision, and it used to be a silent one
776-
```
770+
| Mode | Layers, in merge order | What wins |
771+
|---|---|---|
772+
| Anonymous | global, project manifest, project SubOS | the project's declaration beats the machine's |
773+
| Named | project manifest, project SubOS | the machine's layer is not present at all |
774+
775+
So a project that declares `gcc = "15.1.0"` resolves `gcc` to 15.1.0 even on a
776+
machine whose global workspace is on 16.1.0 — because mcpp materialises that
777+
declaration into the project file's `workspace` object (§4), which is a layer
778+
that beats global. The developer chooses the strength by choosing the mode:
779+
name a SubOS for an isolated space, or leave it anonymous and get the machine's
780+
environment with the project's own entries laid over it.
777781

778-
Both halves of the requirement are therefore already implemented. What decides
779-
whether mcpp gets them is the environment it asks in.
782+
Nothing has to force anything. `activate_requested_targets` does not fire in
783+
this situation, and correctly: the merged view already answers with the
784+
declared version, so there is nothing to switch.
780785

781-
### 16.2 Corrected: mcpp provisions into the registry home, not into the project
786+
### 16.2 What is actually wrong today: mcpp provisions in the wrong scope
782787

783-
An earlier draft of this section claimed activation lands in the project's own
784-
layer. It does not. The provisioning pass calls
788+
The provisioning pass calls
785789

786790
```cpp
787791
mcpp::xlings::call(mcpp::config::make_xlings_env(**cfg2), "install_packages", …)
@@ -790,89 +794,58 @@ mcpp::xlings::call(mcpp::config::make_xlings_env(**cfg2), "install_packages",
790794
(`prepare.cppm:3421`), and `make_xlings_env` carries no `projectDir`
791795
(`src/config.cpp:129`), so no `XLINGS_PROJECT_DIR` reaches the child and the
792796
cwd walk finds nothing — mcpp writes `<project>/.mcpp/.xlings.json`, one level
793-
below the directory a walk from the project root inspects. The child therefore
794-
runs in **global** scope against mcpp's registry home, where
795-
`Config::workspace_mut()` is the global workspace and `effective_workspace()`
796-
is the global workspace.
797-
798-
Three consequences follow, and they explain measurements this repository
799-
already has:
800-
801-
1. `active` is read from a workspace shared by **every mcpp project on the
802-
machine**, so a version another project activated is enough to make this
803-
project's declaration lose.
804-
2. Forcing the switch there would let two checkouts flip each other. The
805-
decline is protective in this scope, not obstructive.
806-
3. The install's view and the shim's view disagree. The install activates in
807-
the registry's global workspace; a program invoked from the project
808-
directory resolves through the project's own layers. That is exactly the
797+
below the directory a walk from the project root inspects. The child runs in
798+
**global** scope against mcpp's registry home.
799+
800+
That single fact produces every symptom in this section:
801+
802+
1. The install's records land in the registry's global workspace, while a
803+
program invoked from the project resolves through the project's layers. The
804+
two views disagree, which is the
809805
`qemu-aarch64-static is not installed in this subos (_)` measured during the
810-
2026.9.2.1 verification, and the reason mcpp had to add a payload-directory
811-
lookup for the runner at all (§13.2 of the runner design).
812-
813-
### 16.3 The two designs
814-
815-
**Design P — provision in the project's environment.** mcpp calls with
816-
`make_project_xlings_env(cfg, root)`, so `XLINGS_PROJECT_DIR=<project>/.mcpp`
817-
and the child runs in project scope. `Config::workspace_mut()` is then the
818-
project's own SubOS workspace at `<project>/.mcpp/.xlings/subos/{name|_}`
819-
(`config.cpp:348-353`), and `effective_workspace()` includes the project's
820-
layers.
821-
822-
Under P, xlings' native behaviour is what is wanted and no flag is needed —
823-
with one condition. In **Anonymous** mode the merge still includes the global
824-
layer (§13.3), so a globally active version still shadows the declaration. In
825-
**Named** mode the global layer is dropped, `active` is empty for a name the
826-
project has not pinned, and the install activates exactly what was declared.
827-
So P is "provision in the project scope, and the project's environment is its
828-
own SubOS".
829-
830-
**Design G — keep provisioning globally, and force.** Pass
831-
`useAfterInstall: true`. It makes the declaration win, and it makes two
832-
checkouts fight over one shared workspace. It also cannot be verified: a forced
833-
switch that fails is a `log::warn` inside a call that exits zero
834-
(`installer.cpp`), so mcpp would hold a request whose outcome it cannot read.
835-
836-
**P is the recommendation**, and it is the one that matches what `[xlings]`
837-
claims to be — a project-level isolated environment. G's forcing is not needed
838-
once the scope is right, and G's shared workspace is the thing the isolation
839-
exists to avoid.
840-
841-
### 16.4 What P costs, and what has to be decided with it
842-
843-
P is not free, and the cost is the same fact §13.3 records: a Named SubOS does
844-
not inherit the machine's global pins. A project that declares one gets an
845-
environment holding what it declared and what was installed into it, and
846-
nothing else. That is isolation working as specified, and it is a change for
847-
every project that today relies on the machine's tools being visible.
848-
849-
So P carries one decision that is not mcpp's alone:
850-
851-
**Does an mcpp project's environment inherit the machine's, or not?**
852-
853-
- Inheriting (Anonymous) keeps every existing project building and leaves the
854-
declaration able to lose to a globally active version.
855-
- Not inheriting (Named) makes the declaration authoritative and requires the
856-
project to declare everything it uses.
857-
858-
A middle position exists and should be considered rather than assumed away:
859-
provision in project scope while staying Anonymous, and accept that a declared
860-
version can still lose to a globally active one — then report that case rather
861-
than silently accepting it. The installer already emits the sentence; mcpp
862-
would have to read it, which returns to the verification problem G has.
863-
864-
### 16.5 What to verify, whichever is chosen
806+
2026.9.2.1 verification — and the reason the runner needed a
807+
payload-directory lookup at all.
808+
2. `active` is read from a workspace shared by every mcpp project on the
809+
machine, so the auto-activation decides against a state no project owns.
810+
3. Any attempt to force the switch would write into that shared workspace,
811+
letting two checkouts flip each other.
812+
813+
**The fix is the scope, not a flag**: provision with
814+
`make_project_xlings_env(cfg, root)`. The child then runs in project scope, the
815+
installed set and activation records land in the project's own SubOS
816+
(`config.cpp:348-353` puts it under `<project>/.mcpp/.xlings/subos/…`), and the
817+
view the install writes is the view the shim reads.
818+
819+
**D8 is withdrawn.** `useAfterInstall` is not passed, and no `use_version` call
820+
is added. The provisioning pass keeps its shape and changes one argument.
821+
822+
### 16.3 Two consequences to write down rather than discover
823+
824+
**The project SubOS layer outranks the project manifest.** It is merged last,
825+
so an explicit `xlings use` performed inside the project beats what
826+
`mcpp.toml` declares, until the manifest is re-materialised. That is defensible
827+
— an action a person took beats a file — but it is not obvious, and it belongs
828+
in the documentation next to the table above.
829+
830+
**A declared version that is not installed fails at the shim, with the right
831+
words.** Resolution finds the pin, `match_version` finds no such installed
832+
version, and the diagnostic is already
833+
`"{}@{} is the version this project asks for"` (`src/core/xvm/shim.cpp:495`).
834+
Provisioning is what prevents it; the diagnostic is what happens if
835+
provisioning was skipped, and it names the project rather than the machine.
836+
837+
### 16.4 What to verify
865838
866839
| # | Criterion |
867840
|---|---|
868-
| A1 | A project declaring a version different from the machine's active one builds and runs against the declared version |
841+
| A1 | A project declaring a version different from the machine's active one builds and runs against the declared version, in Anonymous mode |
869842
| A2 | The machine's global workspace file is unchanged after that build |
870843
| A3 | Two checkouts declaring different versions of one tool each get their own, in one session |
871844
| A4 | An entry with `""` on a machine where another version is active leaves that version active |
872-
| A5 | A declared version that could not be made active ends the build with a message naming tool, version and layer — never a warning inside a successful build |
873-
| A6 | The second build of an unchanged project performs no install and no switch |
874-
| A7 | A program the project did not declare still resolves, or fails with a message naming the environment it was looked up in |
845+
| A5 | A Named SubOS project resolves only what it declared plus what its environment holds, and a tool it did not declare fails naming that environment |
846+
| A6 | The second build of an unchanged project performs no install |
847+
| A7 | A declared version that was never installed fails at the shim with the "version this project asks for" wording, not with a bare "not found" |
875848
876-
A2 and A3 are the ones that fail under G today. A7 is the one that changes
877-
meaning between the two branches of §16.4, and it is where the decision becomes
878-
visible to a user.
849+
A1, A2 and A3 fail today for one reason, the scope of `make_xlings_env`, and
850+
pass together once it changes. A5 is the Named branch of the table in §16.1 and
851+
is what makes the developer's choice visible.

0 commit comments

Comments
 (0)