Skip to content

Commit 9ba86b4

Browse files
committed
design: D8 worked through — activation must be obtained, not requested
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.
1 parent d088490 commit 9ba86b4

1 file changed

Lines changed: 109 additions & 0 deletions

File tree

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

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -749,3 +749,112 @@ its documented shorthand installs, and nothing still compares the two keys when
749749
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.
752+
753+
## 16. D8 in full: how mcpp makes a declared version the active one
754+
755+
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.
758+
759+
### 16.1 xlings already auto-activates, and states when it will not
760+
761+
`activate_requested_targets` in the installer:
762+
763+
```cpp
764+
auto active = xvm::get_active_version(Config::effective_workspace(), match.name);
765+
if ((active.empty() || useAfterInstall) && has_version(db, match.name, match.version))
766+
cmd_use(match.name, match.version, stream);
767+
else if (!active.empty() && active != match.version)
768+
// declining to switch is a decision, and it used to be a silent one
769+
```
770+
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.
775+
776+
### 16.2 Activation lands inside the project, not on the machine
777+
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:
781+
782+
```cpp
783+
// config.cpp:348-353
784+
if (!projectSubosName_.empty()) return projectDir_ / ".xlings" / "subos" / projectSubosName_;
785+
if (projectSubosMode_ == Anonymous) return projectDir_ / ".xlings" / "subos" / "_";
786+
```
787+
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
848+
849+
| # | Criterion |
850+
|---|---|
851+
| 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 |
853+
| A3 | Two checkouts declaring different versions of one tool each get their own, in one session |
854+
| 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 |
856+
| A6 | The second build of an unchanged project performs no install and no switch |
857+
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.

0 commit comments

Comments
 (0)