Skip to content

Commit c0cc681

Browse files
committed
design: D8 restated — the flag was the wrong question, the scope is the 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.
1 parent 9ba86b4 commit c0cc681

1 file changed

Lines changed: 99 additions & 81 deletions

File tree

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

Lines changed: 99 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -750,15 +750,22 @@ 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 in full: how mcpp makes a declared version the active one
753+
## 16. D8: the question is which environment mcpp provisions into
754754

755755
The requirement is one sentence — a declared tool at a declared version is
756-
installed if absent and active afterwards — and it needs three facts before it
757-
can be designed.
756+
installed if absent and active afterwards. It looked like a flag. It is not.
758757

759-
### 16.1 xlings already auto-activates, and states when it will not
758+
### 16.1 xlings already does the whole job, correctly
760759

761-
`activate_requested_targets` in the installer:
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.
766+
767+
Activation is equally native. `activate_requested_targets` switches whenever
768+
**nothing is active for that name**:
762769

763770
```cpp
764771
auto active = xvm::get_active_version(Config::effective_workspace(), match.name);
@@ -768,93 +775,104 @@ else if (!active.empty() && active != match.version)
768775
// declining to switch is a decision, and it used to be a silent one
769776
```
770777
771-
So installing activates **when nothing is active for that name**, and otherwise
772-
declines and says so. The declined case is the one that matters here: `active`
773-
is read from the **merged** view, so a version active in the machine's global
774-
layer is enough to make the project's own declaration lose.
778+
Both halves of the requirement are therefore already implemented. What decides
779+
whether mcpp gets them is the environment it asks in.
775780
776-
### 16.2 Activation lands inside the project, not on the machine
781+
### 16.2 Corrected: mcpp provisions into the registry home, not into the project
777782
778-
`cmd_use` writes to `Config::workspace_mut()`, which returns the project's SubOS
779-
workspace whenever a project config is loaded (`config.cpp:1163-1168`), and
780-
that directory is under the project:
783+
An earlier draft of this section claimed activation lands in the project's own
784+
layer. It does not. The provisioning pass calls
781785
782786
```cpp
783-
// config.cpp:348-353
784-
if (!projectSubosName_.empty()) return projectDir_ / ".xlings" / "subos" / projectSubosName_;
785-
if (projectSubosMode_ == Anonymous) return projectDir_ / ".xlings" / "subos" / "_";
787+
mcpp::xlings::call(mcpp::config::make_xlings_env(**cfg2), "install_packages", …)
786788
```
787789

788-
For mcpp the project dir is `<project>/.mcpp`, so the layer written is
789-
`<project>/.mcpp/.xlings/subos/{<name>|_}/.xlings.json`. Two checkouts that
790-
declare different versions of one tool cannot disturb each other, and the
791-
machine's global choice is not touched. That is what makes forcing activation
792-
safe to do without a further opt-in.
793-
794-
### 16.3 The rule falls out of the two value spellings
795-
796-
W1 gave the manifest two ways to write an entry, and they ask for different
797-
things:
798-
799-
| Entry | The project is asking for | Activation |
800-
|---|---|---|
801-
| `picolibc-riscv = "1.8.12"` | this version | force it |
802-
| `code = ""` | presence, version unconstrained | do not force; xlings' own rule applies |
803-
804-
Forcing on an unconstrained entry would change a version the project never
805-
named, on a machine where something was already active. The empty spelling is
806-
the author saying they do not care, and mcpp should not decide for them.
807-
808-
### 16.4 Requesting activation is not the same as obtaining it
809-
810-
This is the part that decides the shape. When `install_packages` forces the
811-
switch and the switch fails, the installer logs a **warning** and the call
812-
still exits zero:
813-
814-
```cpp
815-
if (useRet != 0)
816-
log::warn("failed to activate {}@{} in current subos", match.name, match.version);
817-
```
818-
819-
So `useAfterInstall: true` alone gives mcpp a request whose outcome it cannot
820-
read — the exact shape #531 was filed for, arrived at from the other side. The
821-
`use_version` capability, by contrast, returns `cmd_use`'s own exit code
822-
(`src/capabilities.cpp:208-220`), which mcpp can check.
823-
824-
### 16.5 The proposed shape
825-
826-
1. **One `install_packages` batch, unforced**, for every declared entry, as
827-
today. Its result is already read (`!called || childRc != 0`), and its
828-
failure keeps naming the manual command.
829-
2. **Then one `use_version` per entry that named a version**, exit code
830-
checked. A failure is a hard build error naming the tool, the version and
831-
the layer, because the manifest asked for something the environment did not
832-
give.
833-
3. **Nothing extra for `""` entries.** Presence was the whole request, and step
834-
1 satisfied it.
835-
4. The provisioning **stamp covers both steps**, so the common build performs
836-
neither. Its key is the hash of the declared set, so editing a version
837-
re-runs the pair once.
838-
839-
The cost is N calls for N versioned entries on the rare path. The alternative —
840-
a second forced `install_packages` batch — is one call, and it buys that by
841-
giving up the exit code, which is the thing being paid for.
842-
843-
`use_version` takes a plain name ("Use plain name (e.g. gcc not xim:gcc)"), so
844-
mcpp passes the target without its namespace and keeps the namespace for the
845-
install address only (§3 W2).
846-
847-
### 16.6 What to verify
790+
(`prepare.cppm:3421`), and `make_xlings_env` carries no `projectDir`
791+
(`src/config.cpp:129`), so no `XLINGS_PROJECT_DIR` reaches the child and the
792+
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
809+
`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
848865

849866
| # | Criterion |
850867
|---|---|
851868
| A1 | A project declaring a version different from the machine's active one builds and runs against the declared version |
852-
| A2 | The machine's global workspace is unchanged after that build, measured on the file |
869+
| A2 | The machine's global workspace file is unchanged after that build |
853870
| A3 | Two checkouts declaring different versions of one tool each get their own, in one session |
854871
| A4 | An entry with `""` on a machine where another version is active leaves that version active |
855-
| A5 | A `use_version` that fails ends the build with a message naming tool, version and layer — not a warning |
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 |
856873
| 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 |
857875

858-
A5 is the one that must be seen failing first: with `useAfterInstall` and
859-
without the explicit switch, the same situation produces a warning inside a
860-
successful build, which is what the design is choosing against.
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.

0 commit comments

Comments
 (0)