From 5d32ba8c50c08bf650e76923e6912c8e365d4b82 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Sun, 30 Aug 2026 01:26:47 +0800 Subject: [PATCH 1/2] feat(xlings): provision `[xlings] deps` on first build, at global scope MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `[xlings] deps` was DECLARED and never installed. `ensure_project_index_dir` wrote it into `.mcpp/.xlings.json` verbatim and stopped there, so a manifest saying `deps = ["xim:mesa"]` produced a file naming mesa, no payload anywhere, and `fatal error: gbm.h: No such file or directory`. The declaration looked accepted and did nothing, which is the worst shape a config key can have — `[toolchain]` has had "declare it and mcpp provisions it on first use" all along ("First run — no toolchain configured … installing … as default"), and a build environment should not have two grades of declaration. GLOBAL SCOPE, AND THE SCOPE IS THE WHOLE POINT. The obvious implementation — `install_packages` against `make_project_xlings_env` — installs at PROJECT scope, and measurably does not work. On a fresh MCPP_HOME the headers land in `/.mcpp/.xlings/subos/_/usr/include` while `--sysroot` names `/registry/subos/default`: two SubOS views, payload in the one the compiler does not read, `#include ` still failing with the dependency installed and declared. `make_xlings_env` is the global env, so the payload lands in the registry whose SubOS *is* the sysroot — the same place `[toolchain]` installs into. That single choice is what removes the need for any sysroot-layering machinery: a project dep and a toolchain dep now agree on where they live, so one `--sysroot` sees both. `install_packages` rather than `resolve_xpkg_path`: the latter requires `@` and rejects a bare `mesa` (verified: "invalid xpkg target 'xim:mesa': expected `@`"), while a manifest is entitled to name a package without pinning it. install_packages resolves the version itself and reports an ambiguous name with its candidates, which is an error the author can act on. ORDER IS LOAD-BEARING: provisioning runs BEFORE the runtime binding resolves, because a named `[xlings] subos` that does not exist yet is a hard error ("selected SubOS '…' does not exist; create/bootstrap that environment") and provisioning is what creates it. Placed next to the custom-index sync, both first-use steps sit in one place. Idempotent by CONTENT, not existence: a stamp records the dep list, so editing the list re-provisions and an unchanged list costs no xlings round-trip. Verified — a second `mcpp run` emits no Provisioning line. VERIFIED end to end on a FRESH MCPP_HOME, with a project that has no mcpp-index dependency at all: [xlings] deps = ["xim:mesa"] [build] ldflags = ["-lgbm"] Provisioning [xlings] deps (xim:mesa) Compiling nopkg v0.1.0 (.) Running `target/.../bin/nopkg` XR24 | GBM_BACKENDS_PATH=/subos/default/usr/lib/gbm `#include ` compiles, `-lgbm` links, and the SubOS env declaration reaches the process — the last of those needs openxlings/xim-pkgindex#713, which adds GBM_BACKENDS_PATH to the graphics discovery table. Design: mcpp-index .agents/docs/2026-08-30-gbm-cross-repo-closed-loop-plan.md --- src/build/prepare.cppm | 113 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/src/build/prepare.cppm b/src/build/prepare.cppm index a6cbc5e2..6881c968 100644 --- a/src/build/prepare.cppm +++ b/src/build/prepare.cppm @@ -2994,6 +2994,119 @@ prepare_build(bool print_fingerprint, **cfg2, runtimeSelection.ownerRoot, {}, penv); } + // `[xlings] deps` are DECLARED above and, until now, nothing + // installed them (mcpp-index #281 §9). + // + // `ensure_project_index_dir` writes them into `.mcpp/.xlings.json` + // verbatim and stops there, so a manifest saying + // `deps = ["xim:mesa"]` produced a file naming mesa, no project + // SubOS, and `fatal error: gbm.h: No such file or directory`. The + // declaration looked accepted and did nothing — which is the worst + // shape a config key can have. + // + // This is the same "declare it and mcpp provisions it on first use" + // contract `[toolchain]` has had all along; that path is a few + // hundred lines up ("First run — no toolchain configured … + // installing … as default"). A build environment should not have + // two grades of declaration. + // + // ORDER IS LOAD-BEARING: this must run BEFORE the runtime binding + // resolves, because a named `[xlings] subos` that does not exist + // yet is a hard error ("selected SubOS '…' does not exist; + // create/bootstrap that environment"), and provisioning is what + // creates it. Placed here, next to the index sync below, both + // first-use provisioning steps sit in one place. + // + // `install_packages` rather than `fetcher.install`: the install + // DESTINATION is chosen by package scope (project vs global), and + // the project scope is what materializes the project SubOS. It also + // carries the live progress UI and captured child errors, matching + // the toolchain and custom-index paths. + // Only what the MANIFEST declared, deliberately not `penv.deps`. + // + // A cross-compilation target sysroot is APPENDED to that list a few + // lines up, and provisioning it here would change behaviour for + // projects that never asked for it: a name that does not resolve + // would turn a build that used to proceed into a hard failure. The + // contract being added is "what you declared gets installed", and + // the sysroot entry is mcpp's own inference rather than the + // author's declaration. + const auto& declaredDeps = runtimeOwnerManifest.xlings.deps; + if (materializeRootRuntime && !declaredDeps.empty()) { + const auto stamp = runtimeSelection.ownerRoot / ".mcpp" + / ".xlings-deps.stamp"; + // Idempotence by CONTENT, not by existence: editing the list + // has to re-provision, and an unchanged list must not pay for + // an xlings round-trip on every build. + auto join_deps = [&](std::string_view sep) { + std::string out; + for (auto const& d : declaredDeps) { + if (!out.empty()) out += sep; + out += d; + } + return out; + }; + std::string want; + for (auto const& d : declaredDeps) { want += d; want += '\n'; } + std::string have; + if (std::ifstream in{stamp}; in) + have.assign(std::istreambuf_iterator(in), {}); + if (have != want) { + mcpp::ui::status("Provisioning", + std::format("[xlings] deps ({})", + join_deps(", "))); + // GLOBAL scope, and the scope is the whole point. + // + // The obvious alternative -- `install_packages` against + // `make_project_xlings_env` -- installs at PROJECT scope, + // and that measurably does not work: on a fresh MCPP_HOME + // the headers land in + // `/.mcpp/.xlings/subos/_/usr/include` while + // `--sysroot` names `/registry/subos/default`, + // so `#include ` still failed with the dependency + // installed and declared. Two SubOS views, and the payload + // in the one the compiler does not read. + // + // `make_xlings_env` is the GLOBAL env, so this lands in the + // registry whose SubOS *is* mcpp's sysroot -- the same + // place `[toolchain]` has always installed into. A project + // dependency and a toolchain dependency now agree on where + // they live, which is the only arrangement in which one + // `--sysroot` can see both. + // + // `install_packages` rather than `resolve_xpkg_path`: the + // latter requires `@` and rejects a bare + // `mesa`, while a manifest is entitled to name a package + // without pinning it. install_packages resolves the version + // itself and reports an ambiguous name with its candidates, + // which is the error the author can act on. + std::string targets; + for (auto const& d : declaredDeps) { + if (!targets.empty()) targets += ','; + targets += std::format("\"{}\"", d); + } + mcpp::fetcher::InstallProgressHandler progress; + auto r = mcpp::xlings::call( + mcpp::config::make_xlings_env(**cfg2), "install_packages", + std::format(R"({{"targets":[{}],"yes":true}})", targets), + &progress); + if (!r) { + // Shaped like the toolchain failure: say what failed and + // hand back a command the user can run themselves. An + // ambiguous bare name ("mesa" matching two repos) lands + // here, and xlings' own message names the candidates. + return std::unexpected(std::format( + "provisioning [xlings] deps failed: {}\n" + " you can install them manually with:\n" + " xlings install {}", + r.error(), join_deps(" "))); + } + std::error_code sec; + std::filesystem::create_directories(stamp.parent_path(), sec); + if (std::ofstream out{stamp}; out) out << want; + } + } + // On first build, the project index data root may be empty because // ensure_project_index_dir only writes .xlings.json but does not // trigger clone/link creation. Local path indices are read directly; From e537449a06bf101beba0648d3c526ce70baec25c Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Sun, 30 Aug 2026 02:47:08 +0800 Subject: [PATCH 2/2] fix(xlings): build the install_packages args with the JSON library `[xlings] deps` is manifest input and the arguments were assembled by formatting the strings into a JSON literal, so a dependency name containing a quote or a backslash would emit malformed JSON. The failure would then surface as an xlings parse error naming neither the manifest nor the key that caused it. nlohmann::json is already imported in this translation unit (mcpp.libs.json), so this is `args["targets"] = declaredDeps; args["yes"] = true; args.dump()` and the escaping stops being something a reader has to verify by eye. No behaviour change for well-formed names, which is every name in practice -- this is about the failure mode of the one that is not. --- src/build/prepare.cppm | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/build/prepare.cppm b/src/build/prepare.cppm index 6881c968..e55a1a6c 100644 --- a/src/build/prepare.cppm +++ b/src/build/prepare.cppm @@ -3080,16 +3080,20 @@ prepare_build(bool print_fingerprint, // without pinning it. install_packages resolves the version // itself and reports an ambiguous name with its candidates, // which is the error the author can act on. - std::string targets; - for (auto const& d : declaredDeps) { - if (!targets.empty()) targets += ','; - targets += std::format("\"{}\"", d); - } + // Built with the JSON library rather than by formatting + // the strings in. `deps` is manifest input, so a name + // containing a quote or a backslash would otherwise emit + // malformed JSON and the failure would surface as an + // unrelated xlings parse error naming neither the manifest + // nor the key. + nlohmann::json args; + args["targets"] = declaredDeps; + args["yes"] = true; + mcpp::fetcher::InstallProgressHandler progress; auto r = mcpp::xlings::call( mcpp::config::make_xlings_env(**cfg2), "install_packages", - std::format(R"({{"targets":[{}],"yes":true}})", targets), - &progress); + args.dump(), &progress); if (!r) { // Shaped like the toolchain failure: say what failed and // hand back a command the user can run themselves. An