From f8f71b2229f5282f7dd4c9b8e650941b7b0d4321 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Tue, 30 Jun 2026 02:42:05 +0800 Subject: [PATCH] feat(manifest): [xlings] build environment materialized into .xlings.json; v0.0.77 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit L-1 of the manifest-environment design: a project's `[xlings]` section declares its build environment (host build-tools, pinned tool versions, a per-project sandbox, env vars), provisioned through xlings (mcpp's substrate). The subsection names mirror xlings' own .xlings.json schema 1:1, so mcpp materializes them VERBATIM into /.mcpp/.xlings.json — no translation layer to drift: [xlings] deps=["make@4.4"] subos="dev" [xlings.workspace] clang="20.1.7" [xlings.envs] OPENBLAS_NUM_THREADS="1" Mechanism: - manifest.cppm: XlingsConfig struct + parse of [xlings] deps/subos + [xlings.workspace]/[xlings.envs] tables into Manifest::xlings. - xlings.cppm: new ProjectEnv (plain types, no manifest dep) + seed_xlings_json extended to emit deps/workspace/subos/envs (each only when non-empty). - config.cppm: ensure_project_index_dir takes a ProjectEnv and now triggers on custom [indices] OR a non-empty [xlings]; prepare.cppm fills ProjectEnv from Manifest::xlings. Closes the gap where mcpp only ever wrote index_repos into the project .xlings.json while xlings already modelled deps/workspace/subos/envs (all unused). [toolchain] stays the compiler shorthand; [xlings.workspace] is the general form. Test: tests/e2e/88_xlings_environment.sh (asserts the [xlings] section is materialized 1:1 into .mcpp/.xlings.json). Regression: index e2e 42/43/44, unit 27/0, self-host build at 0.0.77. Docs: design doc L-1 status + user docs 05-mcpp-toml.md §2.12 (+ zh). --- ...anifest-environment-and-platform-design.md | 9 +++- docs/05-mcpp-toml.md | 23 ++++++++ docs/zh/05-mcpp-toml.md | 21 ++++++++ mcpp.toml | 2 +- src/build/prepare.cppm | 16 ++++-- src/config.cppm | 10 ++-- src/manifest.cppm | 29 +++++++++++ src/toolchain/fingerprint.cppm | 2 +- src/xlings.cppm | 40 +++++++++++++- tests/e2e/88_xlings_environment.sh | 52 +++++++++++++++++++ 10 files changed, 189 insertions(+), 15 deletions(-) create mode 100755 tests/e2e/88_xlings_environment.sh diff --git a/.agents/docs/2026-06-29-manifest-environment-and-platform-design.md b/.agents/docs/2026-06-29-manifest-environment-and-platform-design.md index bf51158..dc17533 100644 --- a/.agents/docs/2026-06-29-manifest-environment-and-platform-design.md +++ b/.agents/docs/2026-06-29-manifest-environment-and-platform-design.md @@ -285,8 +285,13 @@ missing declared outputs as failure. `insert()` keeps an existing unconditional entry (no silent override). Test: `tests/e2e/86_target_cfg_dependencies.sh`. **Still TODO:** `lazy = true` (fetch only when a gated path requests it) + content-hash identity. -- **Phase 2 — L-1 environment.** Surface `[xlings]` (+ `[xlings.workspace]`/`.deps`/ - `.subos`/`.envs`, 1:1 with `.xlings.json`) → extend the project +- **✅ Phase 2 — L-1 environment (mcpp 0.0.77).** `[xlings]` (+ `[xlings.workspace]`/ + `deps`/`subos`/`[xlings.envs]`) parsed into `Manifest::xlings` (`manifest.cppm`) and + materialized **1:1** into `/.mcpp/.xlings.json` via an extended + `seed_xlings_json` (new `xlings::ProjectEnv`) called from `ensure_project_index_dir` + (now triggered by custom `[indices]` **or** a non-empty `[xlings]`). Test: + `tests/e2e/88_xlings_environment.sh`. The keys map verbatim onto what xlings already + consumes (no translation layer). *Superseded the rest of this bullet:* extend the project `.xlings.json` writer (`config.cppm:699-705`) to emit `deps`/`workspace`/`envs`/`subos`; fold `[toolchain]` into `workspace`; wire `[build-dependencies]`. - **Phase 3 — mcpp-index workspace** (companion doc) — first real consumer of Phase 1/1b. diff --git a/docs/05-mcpp-toml.md b/docs/05-mcpp-toml.md index 9ec0257..addcec8 100644 --- a/docs/05-mcpp-toml.md +++ b/docs/05-mcpp-toml.md @@ -463,6 +463,29 @@ The vocabulary is fixed by mcpp (which owns the target/triple system): `linux | macos | windows`; unknown values produce a warning, and an error under `--strict`. +### 2.12 `[xlings]` — Build Environment + +```toml +[xlings] +deps = ["make@4.4", "cmake@3.28", "python@3.13"] # host build-tools to provision +subos = "dev" # a named per-project sandbox + +[xlings.workspace] # pin tool versions (general form of [toolchain]) +clang = "20.1.7" + +[xlings.envs] # env vars applied to the tool environment +OPENBLAS_NUM_THREADS = "1" +``` + +Declares the project's **build environment**, provisioned through xlings (which mcpp +is built on). The subsection names mirror xlings' own `.xlings.json` schema **1:1**, so +mcpp materializes them verbatim into `/.mcpp/.xlings.json` (no translation +layer): `deps` (host build-tools), `[xlings.workspace]` (tool→version pins), +`subos` (a named sandbox), `[xlings.envs]` (env vars). Use it to declare host tools a +build needs (`make`/`cmake`/`protoc`/…), pin tool versions per project, or set +build-time env vars — without hand-editing `.xlings.json`. `[toolchain]` (§2.7) remains +the ergonomic shorthand for the compiler; `[xlings.workspace]` is the general form. + ## Appendix A. Schema Ownership Principle (admission criteria for new fields) > **Closed syntax, open vocabulary**: whoever owns the parsing semantics defines the keys; whoever owns the domain knowledge defines the values. diff --git a/docs/zh/05-mcpp-toml.md b/docs/zh/05-mcpp-toml.md index c1b03cc..38d0ff2 100644 --- a/docs/zh/05-mcpp-toml.md +++ b/docs/zh/05-mcpp-toml.md @@ -433,6 +433,27 @@ platforms = ["linux", "macos", "windows"] (它拥有 target/triple 体系):`linux | macos | windows`;未知值 warning, `--strict` 下报错。 +### 2.12 `[xlings]` — 构建环境 + +```toml +[xlings] +deps = ["make@4.4", "cmake@3.28", "python@3.13"] # 要供给的 host 构建工具 +subos = "dev" # 命名的项目级沙箱 + +[xlings.workspace] # 固定工具版本([toolchain] 的通用形式) +clang = "20.1.7" + +[xlings.envs] # 应用到工具环境的环境变量 +OPENBLAS_NUM_THREADS = "1" +``` + +声明项目的**构建环境**,经 xlings(mcpp 的底座)供给。子段名与 xlings 自身的 +`.xlings.json` schema **1:1** 对齐,因此 mcpp 把它们**原样**物化进 +`<项目>/.mcpp/.xlings.json`(无翻译层):`deps`(host 构建工具)、`[xlings.workspace]` +(工具→版本固定)、`subos`(命名沙箱)、`[xlings.envs]`(环境变量)。用它声明构建所需的 +host 工具(`make`/`cmake`/`protoc`…)、按项目固定工具版本、或设构建期环境变量——无需手改 +`.xlings.json`。`[toolchain]`(§2.7)仍是编译器的便捷简写;`[xlings.workspace]` 是其通用形式。 + ## 附录 A. Schema 所有权原则(新字段准入标准) > **语法封闭,词汇开放**:谁拥有解析语义谁定义键;谁拥有领域知识谁定义值。 diff --git a/mcpp.toml b/mcpp.toml index 2eb3143..4c97b4f 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "0.0.76" +version = "0.0.77" description = "Modern C++ build & package management tool" license = "Apache-2.0" authors = ["mcpp-community"] diff --git a/src/build/prepare.cppm b/src/build/prepare.cppm index 94a4f21..f07eea8 100644 --- a/src/build/prepare.cppm +++ b/src/build/prepare.cppm @@ -898,13 +898,19 @@ prepare_build(bool print_fingerprint, } } - // Set up project-level .mcpp/ directory for custom indices. - // This creates .mcpp/.xlings.json with custom non-builtin index - // entries so xlings can clone them into the project-scoped data dir. - if (!m->indices.empty()) { + // Set up project-level .mcpp/ directory for custom indices and/or the + // [xlings] build environment (L-1). This creates .mcpp/.xlings.json with + // custom non-builtin index entries (so xlings can clone them) plus the + // [xlings] deps/workspace/subos/envs materialized verbatim. + if (!m->indices.empty() || !m->xlings.empty()) { auto cfg2 = get_cfg(); if (cfg2) { - mcpp::config::ensure_project_index_dir(**cfg2, *root, m->indices); + mcpp::xlings::ProjectEnv penv; + penv.deps = m->xlings.deps; + penv.subos = m->xlings.subos; + for (auto const& [k, v] : m->xlings.workspace) penv.workspace.emplace_back(k, v); + for (auto const& [k, v] : m->xlings.envs) penv.envs.emplace_back(k, v); + mcpp::config::ensure_project_index_dir(**cfg2, *root, m->indices, penv); // On first build, the project index data root may be empty because // ensure_project_index_dir only writes .xlings.json but does not diff --git a/src/config.cppm b/src/config.cppm index 466e7c3..e08713c 100644 --- a/src/config.cppm +++ b/src/config.cppm @@ -210,7 +210,8 @@ void reset_registry(const GlobalConfig& cfg) { bool ensure_project_index_dir( const GlobalConfig& cfg, const std::filesystem::path& projectDir, - const std::map& indices); + const std::map& indices, + const mcpp::xlings::ProjectEnv& penv = {}); struct ConfigError { std::string message; }; @@ -661,7 +662,8 @@ void print_env(const GlobalConfig& cfg) { bool ensure_project_index_dir( const GlobalConfig& cfg, const std::filesystem::path& projectDir, - const std::map& indices) + const std::map& indices, + const mcpp::xlings::ProjectEnv& penv) { // Collect custom non-builtin indices that need xlings project-scope data. // Local path indices are also seeded so xlings can create its own @@ -694,7 +696,7 @@ bool ensure_project_index_dir( } } - if (customRepos.empty()) return false; // nothing to do + if (customRepos.empty() && penv.empty()) return false; // nothing to do auto dotMcpp = projectDir / ".mcpp"; std::filesystem::create_directories(dotMcpp, ec); @@ -702,7 +704,7 @@ bool ensure_project_index_dir( // Seed .xlings.json with the custom index entries. mcpp::xlings::Env env; env.home = dotMcpp; - mcpp::xlings::seed_xlings_json(env, customRepos); + mcpp::xlings::seed_xlings_json(env, customRepos, "auto", penv); auto exposeLocalIndex = [&](const std::string& name, const std::filesystem::path& source, diff --git a/src/manifest.cppm b/src/manifest.cppm index 2f8d501..5823796 100644 --- a/src/manifest.cppm +++ b/src/manifest.cppm @@ -172,6 +172,24 @@ struct RuntimeConfig { std::map providerOverrides; }; +// `[xlings]` — the project's build ENVIRONMENT (L-1). The subsection names mirror +// xlings' own `.xlings.json` schema 1:1, so mcpp materializes them verbatim into +// `/.mcpp/.xlings.json` (no translation layer): `deps` (host build-tools +// installed by xlings), `[xlings.workspace]` (tool→version pins, the general form +// of `[toolchain]`), `subos` (a named per-project sandbox), `[xlings.envs]` +// (env vars applied by xvm shims). See +// .agents/docs/2026-06-29-manifest-environment-and-platform-design.md (L-1). +struct XlingsConfig { + std::vector deps; // → .xlings.json "deps" + std::map workspace; // → "workspace" (tool → version) + std::string subos; // → "subos" (named project sandbox) + std::map envs; // → "envs" (env var → value) + + bool empty() const { + return deps.empty() && workspace.empty() && subos.empty() && envs.empty(); + } +}; + // `[target.]` — per-target overrides. // Picked up when caller passes --target to build/run/test. struct TargetEntry { @@ -283,6 +301,7 @@ struct Manifest { Toolchain toolchain; // optional; empty == fallback BuildConfig buildConfig; RuntimeConfig runtimeConfig; + XlingsConfig xlings; // [xlings] build environment (L-1) std::vector conditionalConfigs; // [target.'cfg(...)'.build], deferred std::map profiles; // [profile.] // [features] — feature name → implied features ("default" = default set). @@ -1128,6 +1147,16 @@ std::expected parse_string(std::string_view content, if (auto v = doc->get_string("build.c_standard")) m.buildConfig.cStandard = *v; if (auto v = doc->get_string("build.default-profile")) m.buildConfig.defaultProfile = *v; else if (auto v = doc->get_string("build.profile")) m.buildConfig.defaultProfile = *v; // accepted alias + + // [xlings] — build environment (L-1). Subsections mirror .xlings.json 1:1. + if (auto v = doc->get_string_array("xlings.deps")) m.xlings.deps = *v; + if (auto v = doc->get_string("xlings.subos")) m.xlings.subos = *v; + if (auto* wt = doc->get_table("xlings.workspace")) + for (auto& [k, val] : *wt) + if (val.is_string()) m.xlings.workspace[k] = val.as_string(); + if (auto* et = doc->get_table("xlings.envs")) + for (auto& [k, val] : *et) + if (val.is_string()) m.xlings.envs[k] = val.as_string(); if (auto v = doc->get_string("build.macos_deployment_target")) m.buildConfig.macosDeploymentTarget = *v; for (auto const& flag : m.buildConfig.cxxflags) { diff --git a/src/toolchain/fingerprint.cppm b/src/toolchain/fingerprint.cppm index ee8c1f7..075bf12 100644 --- a/src/toolchain/fingerprint.cppm +++ b/src/toolchain/fingerprint.cppm @@ -18,7 +18,7 @@ import mcpp.toolchain.detect; export namespace mcpp::toolchain { -inline constexpr std::string_view MCPP_VERSION = "0.0.76"; +inline constexpr std::string_view MCPP_VERSION = "0.0.77"; struct FingerprintInputs { Toolchain toolchain; diff --git a/src/xlings.cppm b/src/xlings.cppm index fa2e0c1..c1ea7c4 100644 --- a/src/xlings.cppm +++ b/src/xlings.cppm @@ -233,9 +233,23 @@ int install_direct(const Env& env, std::string_view target, bool quiet = false); // slow/unreachable gitcode. An explicit `mcpp self config --mirror CN|GLOBAL` // still writes that fixed value (config priority). Mirror selection is xlings' // responsibility; mcpp just declines to override it by default. +// The project's build-environment payload (L-1), materialized 1:1 into the +// `.xlings.json` keys xlings already understands. Plain types (no manifest +// dependency); the caller fills it from a manifest's [xlings] section. +struct ProjectEnv { + std::vector deps; // → "deps" + std::vector> workspace; // → "workspace" + std::string subos; // → "subos" + std::vector> envs; // → "envs" + bool empty() const { + return deps.empty() && workspace.empty() && subos.empty() && envs.empty(); + } +}; + void seed_xlings_json(const Env& env, std::span> repos, - std::string_view mirror = "auto"); + std::string_view mirror = "auto", + const ProjectEnv& penv = {}); // Persist the xlings mirror selection in .xlings.json via xlings itself. int config_show(const Env& env); @@ -1069,7 +1083,8 @@ int install_direct(const Env& env, std::string_view target, bool quiet) { void seed_xlings_json(const Env& env, std::span> repos, - std::string_view mirror) + std::string_view mirror, + const ProjectEnv& penv) { auto path = env.home / ".xlings.json"; std::string json = "{\n"; @@ -1081,6 +1096,27 @@ void seed_xlings_json(const Env& env, i + 1 == repos.size() ? "" : ","); } json += " ],\n"; + // [xlings] build environment (L-1): materialize deps/workspace/subos/envs + // verbatim into the keys xlings reads. Each is emitted only when non-empty. + auto emit_obj = [&](std::string_view key, + std::span> kv) { + json += std::format(" \"{}\": {{\n", key); + for (std::size_t i = 0; i < kv.size(); ++i) + json += std::format(" \"{}\": \"{}\"{}\n", + json_escape(kv[i].first), json_escape(kv[i].second), + i + 1 == kv.size() ? "" : ","); + json += " },\n"; + }; + if (!penv.deps.empty()) { + json += " \"deps\": ["; + for (std::size_t i = 0; i < penv.deps.size(); ++i) + json += std::format("{}\"{}\"", i ? ", " : "", json_escape(penv.deps[i])); + json += "],\n"; + } + if (!penv.workspace.empty()) emit_obj("workspace", penv.workspace); + if (!penv.subos.empty()) + json += std::format(" \"subos\": \"{}\",\n", json_escape(penv.subos)); + if (!penv.envs.empty()) emit_obj("envs", penv.envs); json += " \"lang\": \"en\",\n"; json += std::format(" \"mirror\": \"{}\"\n", json_escape(mirror)); json += "}\n"; diff --git a/tests/e2e/88_xlings_environment.sh b/tests/e2e/88_xlings_environment.sh new file mode 100755 index 0000000..38c9aed --- /dev/null +++ b/tests/e2e/88_xlings_environment.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# 88_xlings_environment.sh — L-1 build environment: a project's `[xlings]` section +# is materialized verbatim into /.mcpp/.xlings.json (the keys xlings already +# reads: deps / workspace / subos / envs), so a project can declare its host +# build-tools, per-tool env vars, pinned tool versions, and a named sandbox. +# See .agents/docs/2026-06-29-manifest-environment-and-platform-design.md (L-1). +# +# requires: gcc +set -e + +TMP=$(mktemp -d) +trap "rm -rf $TMP" EXIT +cd "$TMP" + +mkdir -p app/src +cat > app/mcpp.toml <<'EOF' +[package] +name = "app" +version = "0.1.0" + +[xlings] +# Host build-tools the project wants available (xlings "deps"). +deps = ["make@4.4", "cmake@3.28"] +# A named per-project sandbox. +subos = "dev" + +[xlings.workspace] +# Pin a tool version (the general form of [toolchain]). +ninja = "1.12.1" + +[xlings.envs] +# Env vars applied by xvm shims. +APP_BUILD_ENV = "1" +EOF +echo 'int main() { return 0; }' > app/src/main.cpp + +cd app +"$MCPP" build > b.log 2>&1 || { cat b.log; echo "FAIL: build errored"; exit 1; } + +# The project .xlings.json must carry the [xlings] section materialized 1:1. +J=.mcpp/.xlings.json +[ -f "$J" ] || { echo "FAIL: $J was not written"; ls -la .mcpp 2>/dev/null; exit 1; } +echo "--- $J ---"; cat "$J" +grep -q '"deps"' "$J" || { echo "FAIL: deps not materialized"; exit 1; } +grep -q 'make@4.4' "$J" || { echo "FAIL: deps entry missing"; exit 1; } +grep -q '"subos": "dev"' "$J" || { echo "FAIL: subos not materialized"; exit 1; } +grep -q '"workspace"' "$J" || { echo "FAIL: workspace not materialized"; exit 1; } +grep -q '"ninja": "1.12.1"' "$J" || { echo "FAIL: workspace pin missing"; exit 1; } +grep -q '"envs"' "$J" || { echo "FAIL: envs not materialized"; exit 1; } +grep -q '"APP_BUILD_ENV": "1"' "$J" || { echo "FAIL: env var missing"; exit 1; } + +echo "OK"