@@ -749,3 +749,112 @@ its documented shorthand installs, and nothing still compares the two keys when
749749both name one package. §13 strengthened rather than weakened them: xlings makes
750750the same split in the same direction, which is why the merge belongs in the
751751manifest 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