Skip to content

Commit ab1da5d

Browse files
feat(xlings): provision [xlings] deps on first build, at global scope (#531)
* feat(xlings): provision `[xlings] deps` on first build, at global scope `[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 `<proj>/.mcpp/.xlings/subos/_/usr/include` while `--sysroot` names `<MCPP_HOME>/registry/subos/default`: two SubOS views, payload in the one the compiler does not read, `#include <gbm.h>` 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 `<name>@<version>` and rejects a bare `mesa` (verified: "invalid xpkg target 'xim:mesa': expected `<name>@<version>`"), 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=<registry>/subos/default/usr/lib/gbm `#include <gbm.h>` 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 * 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. --------- Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com>
1 parent 7f14f2c commit ab1da5d

1 file changed

Lines changed: 117 additions & 0 deletions

File tree

src/build/prepare.cppm

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2994,6 +2994,123 @@ prepare_build(bool print_fingerprint,
29942994
**cfg2, runtimeSelection.ownerRoot, {}, penv);
29952995
}
29962996

2997+
// `[xlings] deps` are DECLARED above and, until now, nothing
2998+
// installed them (mcpp-index #281 §9).
2999+
//
3000+
// `ensure_project_index_dir` writes them into `.mcpp/.xlings.json`
3001+
// verbatim and stops there, so a manifest saying
3002+
// `deps = ["xim:mesa"]` produced a file naming mesa, no project
3003+
// SubOS, and `fatal error: gbm.h: No such file or directory`. The
3004+
// declaration looked accepted and did nothing — which is the worst
3005+
// shape a config key can have.
3006+
//
3007+
// This is the same "declare it and mcpp provisions it on first use"
3008+
// contract `[toolchain]` has had all along; that path is a few
3009+
// hundred lines up ("First run — no toolchain configured …
3010+
// installing … as default"). A build environment should not have
3011+
// two grades of declaration.
3012+
//
3013+
// ORDER IS LOAD-BEARING: this must run BEFORE the runtime binding
3014+
// resolves, because a named `[xlings] subos` that does not exist
3015+
// yet is a hard error ("selected SubOS '…' does not exist;
3016+
// create/bootstrap that environment"), and provisioning is what
3017+
// creates it. Placed here, next to the index sync below, both
3018+
// first-use provisioning steps sit in one place.
3019+
//
3020+
// `install_packages` rather than `fetcher.install`: the install
3021+
// DESTINATION is chosen by package scope (project vs global), and
3022+
// the project scope is what materializes the project SubOS. It also
3023+
// carries the live progress UI and captured child errors, matching
3024+
// the toolchain and custom-index paths.
3025+
// Only what the MANIFEST declared, deliberately not `penv.deps`.
3026+
//
3027+
// A cross-compilation target sysroot is APPENDED to that list a few
3028+
// lines up, and provisioning it here would change behaviour for
3029+
// projects that never asked for it: a name that does not resolve
3030+
// would turn a build that used to proceed into a hard failure. The
3031+
// contract being added is "what you declared gets installed", and
3032+
// the sysroot entry is mcpp's own inference rather than the
3033+
// author's declaration.
3034+
const auto& declaredDeps = runtimeOwnerManifest.xlings.deps;
3035+
if (materializeRootRuntime && !declaredDeps.empty()) {
3036+
const auto stamp = runtimeSelection.ownerRoot / ".mcpp"
3037+
/ ".xlings-deps.stamp";
3038+
// Idempotence by CONTENT, not by existence: editing the list
3039+
// has to re-provision, and an unchanged list must not pay for
3040+
// an xlings round-trip on every build.
3041+
auto join_deps = [&](std::string_view sep) {
3042+
std::string out;
3043+
for (auto const& d : declaredDeps) {
3044+
if (!out.empty()) out += sep;
3045+
out += d;
3046+
}
3047+
return out;
3048+
};
3049+
std::string want;
3050+
for (auto const& d : declaredDeps) { want += d; want += '\n'; }
3051+
std::string have;
3052+
if (std::ifstream in{stamp}; in)
3053+
have.assign(std::istreambuf_iterator<char>(in), {});
3054+
if (have != want) {
3055+
mcpp::ui::status("Provisioning",
3056+
std::format("[xlings] deps ({})",
3057+
join_deps(", ")));
3058+
// GLOBAL scope, and the scope is the whole point.
3059+
//
3060+
// The obvious alternative -- `install_packages` against
3061+
// `make_project_xlings_env` -- installs at PROJECT scope,
3062+
// and that measurably does not work: on a fresh MCPP_HOME
3063+
// the headers land in
3064+
// `<proj>/.mcpp/.xlings/subos/_/usr/include` while
3065+
// `--sysroot` names `<MCPP_HOME>/registry/subos/default`,
3066+
// so `#include <gbm.h>` still failed with the dependency
3067+
// installed and declared. Two SubOS views, and the payload
3068+
// in the one the compiler does not read.
3069+
//
3070+
// `make_xlings_env` is the GLOBAL env, so this lands in the
3071+
// registry whose SubOS *is* mcpp's sysroot -- the same
3072+
// place `[toolchain]` has always installed into. A project
3073+
// dependency and a toolchain dependency now agree on where
3074+
// they live, which is the only arrangement in which one
3075+
// `--sysroot` can see both.
3076+
//
3077+
// `install_packages` rather than `resolve_xpkg_path`: the
3078+
// latter requires `<name>@<version>` and rejects a bare
3079+
// `mesa`, while a manifest is entitled to name a package
3080+
// without pinning it. install_packages resolves the version
3081+
// itself and reports an ambiguous name with its candidates,
3082+
// which is the error the author can act on.
3083+
// Built with the JSON library rather than by formatting
3084+
// the strings in. `deps` is manifest input, so a name
3085+
// containing a quote or a backslash would otherwise emit
3086+
// malformed JSON and the failure would surface as an
3087+
// unrelated xlings parse error naming neither the manifest
3088+
// nor the key.
3089+
nlohmann::json args;
3090+
args["targets"] = declaredDeps;
3091+
args["yes"] = true;
3092+
3093+
mcpp::fetcher::InstallProgressHandler progress;
3094+
auto r = mcpp::xlings::call(
3095+
mcpp::config::make_xlings_env(**cfg2), "install_packages",
3096+
args.dump(), &progress);
3097+
if (!r) {
3098+
// Shaped like the toolchain failure: say what failed and
3099+
// hand back a command the user can run themselves. An
3100+
// ambiguous bare name ("mesa" matching two repos) lands
3101+
// here, and xlings' own message names the candidates.
3102+
return std::unexpected(std::format(
3103+
"provisioning [xlings] deps failed: {}\n"
3104+
" you can install them manually with:\n"
3105+
" xlings install {}",
3106+
r.error(), join_deps(" ")));
3107+
}
3108+
std::error_code sec;
3109+
std::filesystem::create_directories(stamp.parent_path(), sec);
3110+
if (std::ofstream out{stamp}; out) out << want;
3111+
}
3112+
}
3113+
29973114
// On first build, the project index data root may be empty because
29983115
// ensure_project_index_dir only writes .xlings.json but does not
29993116
// trigger clone/link creation. Local path indices are read directly;

0 commit comments

Comments
 (0)