From 9d1a046d2313c7a2b2eeff39cdb6586d687f26a5 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Wed, 22 Jul 2026 20:09:19 +0800 Subject: [PATCH 1/9] docs: index org migration + artifact adoption design & impl plan (#267 #269) --- .../docs/2026-07-22-issue267-269-impl-plan.md | 463 ++++++++++++++++++ ...index-artifact-and-org-migration-design.md | 316 ++++++++++++ 2 files changed, 779 insertions(+) create mode 100644 .agents/docs/2026-07-22-issue267-269-impl-plan.md create mode 100644 .agents/docs/2026-07-22-issue267-269-index-artifact-and-org-migration-design.md diff --git a/.agents/docs/2026-07-22-issue267-269-impl-plan.md b/.agents/docs/2026-07-22-issue267-269-impl-plan.md new file mode 100644 index 0000000..6f83ffc --- /dev/null +++ b/.agents/docs/2026-07-22-issue267-269-impl-plan.md @@ -0,0 +1,463 @@ +# Issue #267/#269 索引组织迁移 + artifact 采纳 — 实施计划(0.0.103) + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 按设计文档 `2026-07-22-issue267-269-index-artifact-and-org-migration-design.md` 落地 D1–D5,单 PR 携版本 0.0.103,合入后走发布闭环 + xlings 全生态验证。 + +**Architecture:** 新/旧 URL 与 artifact base 收敛为 `mcpp::config` 三常量;`canonicalize_legacy_index_names` 导出并承担"URL 归一→名字迁移→artifact 内存级填充→去重"单循环;文件层治愈只做最小文本手术(config.toml URL 替换、.xlings.json URL 替换 + artifact 注入,幂等闸=已含 res base);seed 管线由 `pair` 升级为 `SeedRepo{name,url,artifact,source}`,非空才发射,零 diff 门。 + +**Tech Stack:** C++23 modules(自托管 `mcpp build`/`mcpp test`)、gtest 单测(tests/unit)、bash e2e(tests/e2e)、gh CLI。 + +## Global Constraints + +- 版本:`mcpp.toml` version 与 `src/toolchain/fingerprint.cppm` MCPP_VERSION **同 commit 同步**为 `0.0.103`(不一致 release smoke 失败)。 +- 新 URL `https://github.com/mcpplibs/mcpp-index.git`;旧 URL `https://github.com/mcpp-community/mcpp-index.git`;artifact base `https://github.com/xlings-res/mcpp-index`。 +- 无 artifact 的仓 seed 输出**逐字节不变**(零 diff 门)。 +- `.xlings.json` 迁移**不整文件重生成**(保 subos/版本绑定),只做目标文本替换,双间距变体(mcpp pretty / xlings compact)。 +- `mcpp.fallback.config_migration` 是叶子模块,不 import mcpp.config —— URL 字面量在此重复是接受的(与现有 name 迁移同风格)。 +- 构建/测试命令:`mcpp build`;`MCPP_FRESH=$(realpath $(find target -type f -name mcpp -printf '%T@ %p\n' | sort -rn | head -1 | cut -d' ' -f2))`;`"$MCPP_FRESH" test`;e2e 单跑 `MCPP="$MCPP_FRESH" bash tests/e2e/.sh`。 +- 分支:`feat/index-artifact-0.0.103` off `origin/main`(a768f48);单 PR;合并 `gh pr merge --squash --admin`。 + +--- + +### Task 0: 分支与文档 + +- [ ] `git checkout -b feat/index-artifact-0.0.103 origin/main` +- [ ] `git add .agents/docs/2026-07-22-issue267-269-*.md && git commit -m "docs: index org migration + artifact adoption design & impl plan (#267 #269)"` + +### Task 1: D1 — 常量 + canonicalize 导出与 URL 迁移(TDD) + +**Files:** +- Modify: `src/config.cppm`(导出区加三常量与 `canonicalize_legacy_index_names` 声明;原匿名 ns 实现移出;`:360` 模板 URL、`:575` add_default URL) +- Modify: `src/fallback/config_migration.cppm`(两迁移函数加 URL 替换) +- Modify: `src/publish/pipeline.cppm:158`、`docs/00-getting-started.md:98`、`docs/04-build-from-source.md:96`、`docs/zh/00-getting-started.md:99`、`docs/zh/04-build-from-source.md:103` +- Test: `tests/unit/test_config.cpp` + +**Interfaces (produces):** +```cpp +// mcpp::config, exported +inline constexpr std::string_view kMcpplibsIndexUrl = "https://github.com/mcpplibs/mcpp-index.git"; +inline constexpr std::string_view kMcpplibsIndexUrlLegacy = "https://github.com/mcpp-community/mcpp-index.git"; +inline constexpr std::string_view kMcpplibsIndexArtifact = "https://github.com/xlings-res/mcpp-index"; +void canonicalize_legacy_index_names(GlobalConfig& cfg); // 导出(可测性) +``` + +- [ ] **Step 1: 失败单测**(`test_config.cpp` 末尾追加;`import mcpp.fallback.config_migration;` 加到文件头 import 区) + +```cpp +TEST(ConfigIndexMigration, CanonicalizeRewritesOrgAndNameAndFillsArtifact) { + mcpp::config::GlobalConfig cfg; + cfg.defaultIndex = "mcpp-index"; + cfg.indexRepos.push_back({ "mcpp-index", + std::string(mcpp::config::kMcpplibsIndexUrlLegacy) }); + mcpp::config::canonicalize_legacy_index_names(cfg); + ASSERT_EQ(cfg.indexRepos.size(), 1u); + EXPECT_EQ(cfg.defaultIndex, "mcpplibs"); + EXPECT_EQ(cfg.indexRepos[0].name, "mcpplibs"); + EXPECT_EQ(cfg.indexRepos[0].url, mcpp::config::kMcpplibsIndexUrl); + EXPECT_EQ(cfg.indexRepos[0].artifact, mcpp::config::kMcpplibsIndexArtifact); +} + +TEST(ConfigIndexMigration, CanonicalizeFoldsLegacyAndDefaultEntries) { + mcpp::config::GlobalConfig cfg; + cfg.indexRepos.push_back({ "mcpplibs", std::string(mcpp::config::kMcpplibsIndexUrlLegacy) }); + cfg.indexRepos.push_back({ "mcpplibs", std::string(mcpp::config::kMcpplibsIndexUrl) }); + mcpp::config::canonicalize_legacy_index_names(cfg); + EXPECT_EQ(cfg.indexRepos.size(), 1u); +} + +TEST(ConfigIndexMigration, CanonicalizeRespectsExplicitGitSourceAndForeignRepos) { + mcpp::config::GlobalConfig cfg; + mcpp::config::IndexRepo git{ "mcpplibs", std::string(mcpp::config::kMcpplibsIndexUrl) }; + git.source = "git"; + cfg.indexRepos.push_back(git); + cfg.indexRepos.push_back({ "other", "https://example.com/other-index.git" }); + mcpp::config::canonicalize_legacy_index_names(cfg); + EXPECT_TRUE(cfg.indexRepos[0].artifact.empty()); + EXPECT_TRUE(cfg.indexRepos[1].artifact.empty()); + EXPECT_EQ(cfg.indexRepos[1].url, "https://example.com/other-index.git"); +} + +TEST(ConfigIndexMigration, MigrateConfigTomlRewritesOrgUrlIdempotently) { + auto dir = make_tempdir("mcpp-migrate-toml"); + auto p = dir / "config.toml"; + { std::ofstream os(p); os << + "[index]\ndefault = \"mcpplibs\"\n\n[index.repos.\"mcpplibs\"]\n" + "url = \"https://github.com/mcpp-community/mcpp-index.git\"\n"; } + EXPECT_TRUE(mcpp::fallback::migrate_config_toml_index_names(p)); + std::stringstream ss; ss << std::ifstream(p).rdbuf(); + EXPECT_NE(ss.str().find("github.com/mcpplibs/mcpp-index.git"), std::string::npos); + EXPECT_EQ(ss.str().find("mcpp-community/mcpp-index"), std::string::npos); + EXPECT_FALSE(mcpp::fallback::migrate_config_toml_index_names(p)); // 幂等 + std::filesystem::remove_all(dir); +} +``` + +注:`IndexRepo` 的 `source`/`artifact` 字段 Task 2 才加 —— 本任务与 Task 2 的实现同 commit 落(测试先全写,分两步跑)。若想严格分步,`CanonicalizeRespectsExplicitGitSource` 测试留到 Task 2。 + +- [ ] **Step 2: 跑测确认编译失败**(常量/导出不存在):`mcpp build` 期望 FAIL +- [ ] **Step 3: 实现**——config.cppm 导出区(`struct IndexRepo` 后)加三常量;把 `canonicalize_legacy_index_names` 从匿名 ns 移到导出区,实现: + +```cpp +void canonicalize_legacy_index_names(GlobalConfig& cfg) { + if (cfg.defaultIndex == "mcpp-index") cfg.defaultIndex = "mcpplibs"; + std::vector normalized; + for (auto r : cfg.indexRepos) { + // ① 组织迁移(先于名字判断与去重:老配置匹配 + 新旧条目可折叠) + if (r.url == kMcpplibsIndexUrlLegacy) r.url = std::string(kMcpplibsIndexUrl); + // ② 名字迁移(URL 已归一,只认新 URL) + if (r.name == "mcpp-index" && r.url == kMcpplibsIndexUrl) r.name = "mcpplibs"; + // ③ 内存级 artifact 填充(官方仓、未显式退出、未自定义)——Task 2 落地字段后启用 + if (r.url == kMcpplibsIndexUrl && r.artifact.empty() && r.source != "git") + r.artifact = std::string(kMcpplibsIndexArtifact); + bool duplicate = std::any_of(normalized.begin(), normalized.end(), + [&](const IndexRepo& e) { return e.name == r.name && e.url == r.url; }); + if (!duplicate) normalized.push_back(std::move(r)); + } + cfg.indexRepos = std::move(normalized); +} +``` + +`config_migration.cppm` 两函数各加(在现有 replace 之后): +```cpp +replace_all(updated, "https://github.com/mcpp-community/mcpp-index.git", + "https://github.com/mcpplibs/mcpp-index.git"); +``` +模板 `:360`、`add_default` `:575` 改用 `kMcpplibsIndexUrl`(模板是字符串字面量,直接写新 URL);`publish/pipeline.cppm:158` Fork 行 → `https://github.com/mcpplibs/mcpp-index`;docs 4 处链接同改。 + +- [ ] **Step 4: `mcpp build && "$MCPP_FRESH" test`** 期望 PASS(新测试含 Task 2 字段的暂缓) +- [ ] **Step 5: Commit** `fix(index): migrate mcpplibs index URL to mcpplibs org (#267-A)` + +### Task 2: D2 — SeedRepo 管线 + IndexRepo/TOML/模板 artifact + +**Files:** +- Modify: `src/config.cppm`(IndexRepo 加字段:36;TOML 读 artifact/source:535-543;模板:359-361;add_default:571-575;write_default_xlings_json:374-396;print_env:663-668) +- Modify: `src/xlings.cppm`(SeedRepo struct + seed_xlings_json 签名/发射:250-253、1110-1150) +- Test: `tests/unit/test_xlings.cpp`、`tests/unit/test_config.cpp` + +**Interfaces (produces):** +```cpp +// mcpp::config +struct IndexRepo { std::string name, url, artifact, source; }; +// mcpp::xlings +struct SeedRepo { std::string name, url, artifact, source; }; +void seed_xlings_json(const Env&, std::span, + std::string_view mirror = "auto", const ProjectEnv& = {}); +``` + +- [ ] **Step 1: 失败单测**(`test_xlings.cpp`,`import mcpp.xlings;` 已有则复用其 temp-dir 惯例) + +```cpp +TEST(XlingsSeed, PlainRepoEmissionIsByteStable) { // 零 diff 门 + auto dir = make_tempdir("mcpp-seed-plain"); + mcpp::xlings::Env env; env.home = dir; + std::vector repos{ { "mcpplibs", "https://x.git", "", "" } }; + mcpp::xlings::seed_xlings_json(env, repos); + std::stringstream ss; ss << std::ifstream(dir / ".xlings.json").rdbuf(); + EXPECT_NE(ss.str().find( + " { \"name\": \"mcpplibs\", \"url\": \"https://x.git\" }\n"), std::string::npos); + EXPECT_EQ(ss.str().find("artifact"), std::string::npos); + std::filesystem::remove_all(dir); +} + +TEST(XlingsSeed, ArtifactAndSourceEmittedWhenSet) { + auto dir = make_tempdir("mcpp-seed-art"); + mcpp::xlings::Env env; env.home = dir; + std::vector repos{ + { "mcpplibs", "https://x.git", "https://github.com/xlings-res/mcpp-index", "auto" } }; + mcpp::xlings::seed_xlings_json(env, repos); + std::stringstream ss; ss << std::ifstream(dir / ".xlings.json").rdbuf(); + EXPECT_NE(ss.str().find("\"url\": \"https://x.git\", \"artifact\": " + "\"https://github.com/xlings-res/mcpp-index\", \"source\": \"auto\""), std::string::npos); + std::filesystem::remove_all(dir); +} +``` + +- [ ] **Step 2: 编译失败确认**(SeedRepo 不存在) +- [ ] **Step 3: 实现**——`xlings.cppm` 声明区加 `struct SeedRepo { std::string name, url, artifact, source; };`,签名改 `std::span`;发射循环: + +```cpp +for (std::size_t i = 0; i < repos.size(); ++i) { + json += std::format(" {{ \"name\": \"{}\", \"url\": \"{}\"", + json_escape(repos[i].name), json_escape(repos[i].url)); + if (!repos[i].artifact.empty()) + json += std::format(", \"artifact\": \"{}\"", json_escape(repos[i].artifact)); + if (!repos[i].source.empty()) + json += std::format(", \"source\": \"{}\"", json_escape(repos[i].source)); + json += std::format(" }}{}\n", i + 1 == repos.size() ? "" : ","); +} +``` + +`config.cppm`:IndexRepo 加 `artifact`/`source`;TOML 解析读两 key(可选 string);`write_default_xlings_json` pairs→`std::vector` 直传四字段;模板加: +```toml +[index.repos."mcpplibs"] +url = "https://github.com/mcpplibs/mcpp-index.git" +artifact = "https://github.com/xlings-res/mcpp-index" +# source = "auto" # default: artifact first, git fallback; set "git" to force git +``` +`add_default` lambda 加 artifact 形参并给 mcpplibs 传 `kMcpplibsIndexArtifact`;`print_env` 的 repo 行追加 ` [artifact]` 标记(`r.artifact` 非空时);`ensure_project_index_dir` 的 `customRepos` 改 `std::vector`(artifact 字段 Task 4 接入,先留空串)。canonicalize 的 ③ 分支此时编译生效。 + +- [ ] **Step 4: `mcpp build && "$MCPP_FRESH" test`** 全绿(含 Task 1 暂缓测试) +- [ ] **Step 5: Commit** `feat(index): per-repo artifact source plumbing — IndexRepo/SeedRepo/toml/template (#269)` + +### Task 3: D3 — 存量 .xlings.json artifact 注入(TDD) + +**Files:** +- Modify: `src/fallback/config_migration.cppm:69-81` +- Test: `tests/unit/test_config.cpp` + +- [ ] **Step 1: 失败单测** + +```cpp +namespace { +std::string read_all(const std::filesystem::path& p) { + std::stringstream ss; ss << std::ifstream(p).rdbuf(); return ss.str(); +} +} // namespace + +TEST(ConfigIndexMigration, XlingsJsonHealsPrettyLegacyEntry) { + auto dir = make_tempdir("mcpp-migrate-xj"); + auto p = dir / ".xlings.json"; + { std::ofstream os(p); os << "{\n \"index_repos\": [\n" + " { \"name\": \"mcpplibs\", \"url\": \"https://github.com/mcpp-community/mcpp-index.git\" }\n" + " ],\n \"subos\": \"default\",\n \"mirror\": \"auto\"\n}\n"; } + EXPECT_TRUE(mcpp::fallback::migrate_xlings_json_index_names(p)); + auto text = read_all(p); + EXPECT_NE(text.find("\"url\": \"https://github.com/mcpplibs/mcpp-index.git\", " + "\"artifact\": \"https://github.com/xlings-res/mcpp-index\""), std::string::npos); + EXPECT_NE(text.find("\"subos\": \"default\""), std::string::npos); // 无关 key 保留 + EXPECT_EQ(text.find("mcpp-community/mcpp-index"), std::string::npos); + EXPECT_FALSE(mcpp::fallback::migrate_xlings_json_index_names(p)); // 幂等 + std::filesystem::remove_all(dir); +} + +TEST(ConfigIndexMigration, XlingsJsonHealsCompactLegacyNameAndUrl) { + auto dir = make_tempdir("mcpp-migrate-xjc"); + auto p = dir / ".xlings.json"; + { std::ofstream os(p); os << "{\"index_repos\":[{\"name\":\"mcpp-index\"," + "\"url\":\"https://github.com/mcpp-community/mcpp-index.git\"}],\"mirror\":\"auto\"}"; } + EXPECT_TRUE(mcpp::fallback::migrate_xlings_json_index_names(p)); + auto text = read_all(p); + EXPECT_NE(text.find("\"name\":\"mcpplibs\""), std::string::npos); + EXPECT_NE(text.find("\"url\":\"https://github.com/mcpplibs/mcpp-index.git\", " + "\"artifact\": \"https://github.com/xlings-res/mcpp-index\""), std::string::npos); + EXPECT_FALSE(mcpp::fallback::migrate_xlings_json_index_names(p)); + std::filesystem::remove_all(dir); +} + +TEST(ConfigIndexMigration, XlingsJsonSkipsInjectionWhenArtifactPresent) { + auto dir = make_tempdir("mcpp-migrate-xja"); + auto p = dir / ".xlings.json"; + std::string body = "{\n \"index_repos\": [\n" + " { \"name\": \"mcpplibs\", \"url\": \"https://github.com/mcpplibs/mcpp-index.git\", " + "\"artifact\": \"https://github.com/xlings-res/mcpp-index\" }\n ]\n}\n"; + { std::ofstream os(p); os << body; } + EXPECT_FALSE(mcpp::fallback::migrate_xlings_json_index_names(p)); + EXPECT_EQ(read_all(p), body); + std::filesystem::remove_all(dir); +} +``` + +- [ ] **Step 2: 跑测确认 FAIL**(artifact 未注入) +- [ ] **Step 3: 实现**(接在 name/URL 替换后): + +```cpp +// Artifact injection (xlings 0.4.68 per-repo artifact source). Idempotency +// gate: the res base appearing anywhere means a previous run (or a fresh +// seed) already declared it — plain replace_all would re-inject on every run. +if (updated.find("xlings-res/mcpp-index") == std::string::npos) { + constexpr std::string_view art = + ", \"artifact\": \"https://github.com/xlings-res/mcpp-index\""; + for (std::string_view urlkv : { + "\"url\": \"https://github.com/mcpplibs/mcpp-index.git\"", + "\"url\":\"https://github.com/mcpplibs/mcpp-index.git\"" }) + replace_all(updated, urlkv, std::string(urlkv) + std::string(art)); +} +``` + +- [ ] **Step 4: `mcpp build && "$MCPP_FRESH" test`** PASS +- [ ] **Step 5: Commit** `feat(index): heal existing registry .xlings.json — org URL + artifact injection (#267-A #269)` + +### Task 4: D5 — 项目级 [indices] artifact/source + pin 交互 + +**Files:** +- Modify: `src/pm/index_spec.cppm:14-23`(加字段 + `artifact_applicable()`) +- Modify: `src/manifest/toml.cppm:1142-1150`([indices] 长格式读 artifact/source) +- Modify: `src/config.cppm:546-563`(config.toml [indices] 同步读)+ `ensure_project_index_dir:680-691`(SeedRepo 透传 + 不适用 warn) +- Test: `tests/unit/test_manifest.cpp` + +**Interfaces (produces):** +```cpp +// mcpp::pm::IndexSpec 新增 +std::string artifact; // optional artifact source base (xlings 0.4.68+) +std::string source; // optional "auto" | "artifact" | "git" +bool artifact_applicable() const { // pin/local forces git — artifact only tracks latest + return !artifact.empty() && rev.empty() && tag.empty() && branch.empty() && path.empty(); +} +``` + +- [ ] **Step 1: 失败单测**(`test_manifest.cpp`,仿现有 [indices] 解析测试写法;若无现成解析入口测试,则直接测 IndexSpec 语义 + toml 解析函数) + +```cpp +TEST(ManifestIndices, ArtifactAndSourceParsedAndPinSuppresses) { + auto m = parse_manifest_from_string(R"( +[package] +name = "p" +version = "0.1.0" +[indices] +fast = { url = "https://example.com/i.git", artifact = "https://example.com/res/i", source = "auto" } +pinned = { url = "https://example.com/i.git", artifact = "https://example.com/res/i", rev = "abc123" } +)"); // 按 test_manifest.cpp 现有 helper 名对齐 + auto& fast = m.indices.at("fast"); + EXPECT_EQ(fast.artifact, "https://example.com/res/i"); + EXPECT_EQ(fast.source, "auto"); + EXPECT_TRUE(fast.artifact_applicable()); + auto& pinned = m.indices.at("pinned"); + EXPECT_FALSE(pinned.artifact_applicable()); +} +``` + +- [ ] **Step 2: FAIL 确认** → **Step 3: 实现**:两处解析各加两行 `find("artifact")`/`find("source")`;`ensure_project_index_dir` 远程分支: + +```cpp +mcpp::xlings::SeedRepo r{ name, spec.url, "", "" }; +if (spec.artifact_applicable()) { r.artifact = spec.artifact; r.source = spec.source; } +else if (!spec.artifact.empty()) + std::println("[warn] index '{}': artifact source ignored (rev/tag/branch/path pins force git)", name); +customRepos.push_back(std::move(r)); +``` + +- [ ] **Step 4: 全测 PASS** → **Step 5: Commit** `feat(index): project-level [indices] artifact/source with pin-forces-git rule (#269)` + +### Task 5: read_seeded_index_repos 容错锁测 + +**Files:** +- Test: `tests/unit/test_pm_package_fetcher.cpp:237` 附近克隆现有 fixture + +- [ ] **Step 1: 新测**——entry 含 artifact/source 仍正确提取 name/url: + +```cpp +TEST(PackageFetcher, SeededIndexReposTolerateArtifactFields) { + auto project = make_tempdir("mcpp-pf-artifact"); // 对齐该文件现有 temp helper + auto xjson = project / ".xlings.json"; + { std::ofstream os(xjson); os << "{\n \"index_repos\": [\n" + " { \"name\": \"mcpplibs\", \"url\": \"https://github.com/mcpplibs/mcpp-index.git\", " + "\"artifact\": \"https://github.com/xlings-res/mcpp-index\", \"source\": \"auto\" }\n ]\n}\n"; } + auto repos = mcpp::pm::read_seeded_index_repos(xjson); + ASSERT_EQ(repos.size(), 1u); + EXPECT_EQ(repos[0].first, "mcpplibs"); + EXPECT_EQ(repos[0].second, "https://github.com/mcpplibs/mcpp-index.git"); + std::filesystem::remove_all(project); +} +``` +(返回类型按现有测试实际形态对齐 —— 以 `:244` 现有断言写法为准。) + +- [ ] **Step 2: 跑测**(预期直接 PASS —— 这是回归锁,不是新功能;若 FAIL 则修 `read_seeded_index_repos` 的对象切分) +- [ ] **Step 3: Commit** `test(pm): lock read_seeded_index_repos tolerance to artifact/source fields` + +### Task 6: D4 — xlings pin → 0.4.68 + 常量对齐 + +**Files:** +- Modify: `.github/workflows/release.yml`(`:99`、`:284`、`:341-345` 硬编码 0.4.67 全部)、`.github/workflows/cross-build-test.yml:105,218`、`.github/workflows/ci-linux-e2e.yml:126` +- Modify: `src/xlings.cppm:36`(`kXlingsVersion` "0.4.51"→"0.4.68") +- Modify: `src/config.cppm` `print_env`(加 `bundled xlings pin = {kXlingsPinnedVersion}` 行,给常量一个消费点) + +- [ ] **Step 1**: `grep -rn '0\.4\.67' .github/workflows/` 逐处替换为 0.4.68(含注释里的版本说明,注释补一句 0.4.68 = per-repo index artifact #377) +- [ ] **Step 2**: 常量 + print_env;`mcpp build && "$MCPP_FRESH" self env` 目测新行 +- [ ] **Step 3: Commit** `ci: bundled xlings pin 0.4.67 -> 0.4.68 (per-repo index artifact); align kXlingsVersion` + +### Task 7: e2e — 151_index_url_artifact_migration.sh + +**Files:** +- Create: `tests/e2e/151_index_url_artifact_migration.sh`(仿 `10_env_command.sh` 的隔离 MCPP_HOME 手法;需 `MCPP_VENDORED_XLINGS` 或系统 xlings,CI bootstrap 已具备) + +- [ ] **Step 1: 写脚本** + +```bash +#!/usr/bin/env bash +# #267/#269: fresh init seeds mcpplibs-org URL + artifact source; legacy +# config.toml/.xlings.json (old org URL, no artifact) are healed in place, +# idempotently, in both pretty (mcpp) and compact (xlings) JSON spacings. +set -e +TMP=$(mktemp -d); trap "rm -rf $TMP" EXIT +export MCPP_HOME="$TMP/mcpp-home" + +NEW_URL='https://github.com/mcpplibs/mcpp-index.git' +OLD_URL='https://github.com/mcpp-community/mcpp-index.git' +ART='https://github.com/xlings-res/mcpp-index' +CFG="$MCPP_HOME/config.toml"; XJ="$MCPP_HOME/registry/.xlings.json" + +# 1. Fresh init: new org URL + artifact in both files. +"$MCPP" self env > /dev/null +grep -q "url = \"$NEW_URL\"" "$CFG" || { echo "config.toml missing new url"; exit 1; } +grep -q "artifact = \"$ART\"" "$CFG" || { echo "config.toml missing artifact"; exit 1; } +grep -q "\"url\": \"$NEW_URL\", \"artifact\": \"$ART\"" "$XJ" || { echo "seed missing artifact"; exit 1; } +grep -q 'mcpp-community/mcpp-index' "$CFG" "$XJ" && { echo "old org leaked"; exit 1; } + +# 2. Legacy heal (pretty): old URL, no artifact. +cat > "$CFG" < "$XJ" < /dev/null +grep -q "url = \"$NEW_URL\"" "$CFG" || { echo "toml url not healed"; exit 1; } +grep -q "\"url\": \"$NEW_URL\", \"artifact\": \"$ART\"" "$XJ" || { echo "xj not healed"; exit 1; } +grep -q '"subos": "default"' "$XJ" || { echo "unrelated key lost"; exit 1; } +cp "$CFG" "$TMP/cfg1"; cp "$XJ" "$TMP/xj1" +"$MCPP" self env > /dev/null # idempotent second run +cmp -s "$CFG" "$TMP/cfg1" && cmp -s "$XJ" "$TMP/xj1" || { echo "not idempotent"; exit 1; } + +# 3. Legacy heal (compact, old name): xlings-writer spacing. +printf '{"index_repos":[{"name":"mcpp-index","url":"%s"}],"mirror":"auto"}' "$OLD_URL" > "$XJ" +"$MCPP" self env > /dev/null +grep -q '"name":"mcpplibs"' "$XJ" || { echo "compact name not healed"; exit 1; } +grep -q "xlings-res/mcpp-index" "$XJ" || { echo "compact artifact not injected"; exit 1; } +grep -q 'mcpp-community' "$XJ" && { echo "compact old org left"; exit 1; } + +echo "OK" +``` + +- [ ] **Step 2: 本地跑** `MCPP="$MCPP_FRESH" bash tests/e2e/151_index_url_artifact_migration.sh` → OK +- [ ] **Step 3: Commit** `test(e2e): 151 — index org URL heal + artifact seed/injection` + +### Task 8: 版本 0.0.103 + CHANGELOG + +- [ ] `mcpp.toml:3` version → `0.0.103`;`src/toolchain/fingerprint.cppm:21` → `0.0.103`(同 commit!) +- [ ] CHANGELOG.md 顶部加 0.0.103 条目(#267 URL 迁移 + 存量治愈、#269 artifact 采纳 + pin 0.4.68、[indices] artifact) +- [ ] Commit `release: 0.0.103 — index org migration + per-repo artifact adoption (#267 #269)` + +### Task 9: 验证 + PR + CI + 合入 + +- [ ] `mcpp build && "$MCPP_FRESH" test` 全绿;e2e 相关子集(10、151)绿;可选跑本机 e2e 基线抽样 +- [ ] `git push -u origin feat/index-artifact-0.0.103`;`gh pr create`(标题 `feat(index): mcpplibs org migration + xlings 0.4.68 per-repo artifact adoption — 0.0.103 (#267 #269)`,body 引两 issue + 设计文档) +- [ ] 等 CI 全绿(ci-linux、ci-linux-e2e 两 shard、cross-build-test、windows/macos e2e、fresh-install) +- [ ] `gh pr merge --squash --admin`(不加 `--delete-branch` 以防叠栈坑;单 PR 无叠栈,可加) + +### Task 10: 发布 + xlings 全生态验证(runbook = release-publish-pipeline 记忆 + .agents/skills/mcpp-release) + +- [ ] `gh workflow run release.yml --ref main`(从 mcpp.toml 派生 v0.0.103;publish-ecosystem 自动镜像 xlings-res/mcpp 双端 + 开 xim-pkgindex bump PR) +- [ ] 双端 GET 核验四平台资产(`curl -sL -o /dev/null -w '%{http_code} %{size_download}'`,GitHub + gitcode.com 直链主机,必须 200 + 真字节;HEAD 不可信) +- [ ] `gh pr merge --repo openxlings/xim-pkgindex --squash --admin`(publish-artifact.yml 自动发索引) +- [ ] 实机验证:`xlings update && xlings install mcpp@0.0.103 -y` → `mcpp --version` = 0.0.103 +- [ ] **artifact 路径专项**(本 PR 主功能):fresh MCPP_HOME + xlings ≥0.4.68 捆绑,`mcpp index update` → `registry/data/mcpplibs` 变 artifact 管理(`.git` 消失、`.xlings-index-version` 出现);`registry/.xlings.json` mcpplibs 条目带 artifact 字段 +- [ ] 收尾 pin:工作区 `.xlings.json` bootstrap pin → 0.0.103 直推 main(`ci: workspace mcpp bootstrap pin -> 0.0.103 (released + indexed)`);`ci-fresh-install.yml` 的 0.0.102 pin 同步 bump + +## Self-Review 结论 + +- 设计文档 D1–D5 全覆盖(D1→T1,D2→T2,D3→T3,D5→T4,兼容锁→T5,D4→T6,e2e→T7);P5(CN region 对象)按设计文档明确推迟,非本计划范围。 +- 类型一致性:`SeedRepo{name,url,artifact,source}` 贯穿 T2/T4;`IndexRepo` 同形;`artifact_applicable()` 只在 T4 定义与使用。 +- 已知风险:test_manifest.cpp 的解析 helper 名需现场对齐(计划内已标注);`ensure_project_index_dir` 的 `spec.is_builtin()` 分支(mcpplibs)不进 customRepos,不受 T4 影响。 diff --git a/.agents/docs/2026-07-22-issue267-269-index-artifact-and-org-migration-design.md b/.agents/docs/2026-07-22-issue267-269-index-artifact-and-org-migration-design.md new file mode 100644 index 0000000..fb9ae68 --- /dev/null +++ b/.agents/docs/2026-07-22-issue267-269-index-artifact-and-org-migration-design.md @@ -0,0 +1,316 @@ +# 索引组织迁移 + 采纳 xlings 0.4.68 per-repo artifact 来源 — 设计方案 + +**日期**: 2026-07-22 +**关联**: mcpp#267(索引 URL 指向已迁移旧组织 + 被判定自定义索引强制走 git)、 +mcpp#269(采纳 xlings 0.4.68 per-repo artifact)、 +openxlings/xlings#377(功能实现,PR #379)、#378(compact::git CA 探测)、#380(shim env 去重) +**上游设计文档**: xlings 仓 `.agents/docs/2026-07-22-issue377-custom-index-artifact-design.md` +**上游 schema**: xlings 仓 `docs/spec/xlings-json-schema.md`(0.4.68+ `artifact` / `source` 字段) + +--- + +## 一、问题陈述 + +两个 issue 收敛为同一条主线:**mcpplibs 索引的同步路径健壮化**,外加一个正确性前提(URL 组织迁移)。 + +### #267-A:索引 URL 仍指向已迁移的旧组织(正确性,可独立修) + +索引仓库已从 `mcpp-community/mcpp-index` 迁到 `mcpplibs/mcpp-index`(工具仓库 +`mcpp-community/mcpp` 未迁移)。mcpp 目前**完全靠 GitHub 仓库重定向**才能工作, +该依赖脆弱:旧名一旦被再次占用即静默指向错误内容;部分镜像/代理不跟随重定向。 + +### #267-B / #269:索引同步硬依赖 git → 采纳 artifact 路径 + +xlings 0.4.68 前,自定义 `index_repos`(mcpplibs 即是)恒走 git +(`repo.cppm` 准入三分支全要求 `isDefaultOfficial`),拿不到官方索引 artifact 路径的 +adaptive mirror reorder + stall watchdog。实测症状:同一次 `mcpp index update` 里 +主索引 artifact 成功、mcpplibs git 因宿主 CA 路径问题失败。 + +xlings 0.4.68(#377/#379)已落地:任何 `index_repos` 条目可声明 `artifact` 来源, +`source: auto`(默认)下 artifact 优先、git 自动回退。**发布端零改动**—— +`xlings-res/mcpp-index` 现有布局(仓根 `mcpp-index-pointers.json` + `v` release + +`mcpp-index-.tar.gz`)与 xlings 消费端已实测逐字节兼容(v2e23e20:sha256 一致、 +sole-entry key 兜底覆盖 `mcpp` ≠ `mcpplibs` 命名差异)。 + +mcpp 侧要做的只是:**声明 + 透传 + 治愈存量 + 升 pin**。 + +--- + +## 二、现状盘点(涉改位置) + +### 代码(旧 URL 硬编码 4 处) + +| 位置 | 作用 | +|---|---| +| `src/config.cppm:360` | `write_default_config_toml` 模板 `[index.repos."mcpplibs"] url` | +| `src/config.cppm:407` | `canonicalize_legacy_index_names` 名字迁移分支的旧 URL 联合条件 | +| `src/config.cppm:575` | `add_default("mcpplibs", <旧URL>)` 内存级默认 | +| `src/publish/pipeline.cppm:158` | `mcpp publish` 打印给用户的 Fork 地址 | + +### 数据流(artifact 字段要穿过的层) + +``` +config.toml [index.repos."mcpplibs"] ← 模板 + TOML 解析(config.cppm:535-543,现只读 url) + │ + ▼ +IndexRepo { name, url } ← src/config.cppm:36,需扩字段 + │ + ▼ +seed_xlings_json(span>) ← src/xlings.cppm:250/1110,pair 表达不了新字段 + │ + ▼ +registry/.xlings.json "index_repos" ← xlings 0.4.68 消费 artifact/source + │(仅在缺失时 seed;存量安装走 migrate_xlings_json_index_names,config.cppm:581-586) + ▼ +xlings sync(0.4.68:artifact 优先 + git 回退) +``` + +另有两个旁路消费者,改动时必须不破坏: + +- `src/pm/package_fetcher.cppm:252-300` — 手写解析 `.xlings.json` 的 `index_repos` + (按 key 容错读 name/url,多余字段应天然无害,需单测锁住); +- 项目级 `ensure_project_index_dir`(`src/config.cppm:671-714`)— 从 `IndexSpec` + 构造 `customRepos` pair 后 seed 项目 `.mcpp/.xlings.json`。 + +### 迁移钩子(现成的,扩展即可) + +| 钩子 | 位置 | 现状 | +|---|---|---| +| `migrate_config_toml_index_names` | `src/fallback/config_migration.cppm:55` | 文本替换 `mcpp-index`→`mcpplibs`(名字) | +| `migrate_xlings_json_index_names` | `src/fallback/config_migration.cppm:69` | 同上,针对 `.xlings.json` 的 `"name"` 字段 | +| `canonicalize_legacy_index_names` | `src/config.cppm:400` | 内存级名字迁移 + 去重 | + +### 版本 pin + +- CI:`release.yml:99/281-345`、`cross-build-test.yml:105/218`、`ci-linux-e2e.yml:126` + 全部 `0.4.67`; +- 代码常量:`src/xlings.cppm:36` `pinned::kXlingsVersion = "0.4.51"` —— **已陈旧**, + 仅被 `config.cppm:34` 的 `kXlingsPinnedVersion` re-export,且后者无任何消费点 + (捆绑 xlings 实际来自 release bundle / `MCPP_VENDORED_XLINGS` / 系统 PATH, + 见 `src/fallback/xlings_binary.cppm`)。本次一并对齐,消除"版本常量双源"旧债 + (0.0.93 批次教训)。 + +### 文档(非阻塞,顺带) + +`docs/00-getting-started.md:98`、`docs/04-build-from-source.md:96` 及 zh 对应文件 +仍链接 `mcpp-community/mcpp-index`。 + +--- + +## 三、设计 + +**核心理念:默认值与治愈逻辑都收敛在代码常量一处;文件层迁移只做最小文本手术。** +新 URL 与 artifact base 定义为单一常量对,所有消费点(模板、add_default、canonicalize、 +迁移、publish 提示)引用它,不再各写一份字面量(避免下次迁移再来一遍 4 处 grep)。 + +```cpp +// src/config.cppm(或独立常量区) +inline constexpr std::string_view kMcpplibsUrl = "https://github.com/mcpplibs/mcpp-index.git"; +inline constexpr std::string_view kMcpplibsUrlLegacy= "https://github.com/mcpp-community/mcpp-index.git"; +inline constexpr std::string_view kMcpplibsArtifact = "https://github.com/xlings-res/mcpp-index"; +``` + +### D1. URL 组织迁移(#267-A,可独立发) + +1. **4 处硬编码**全部改为新 URL(经上面常量)。 +2. **`canonicalize_legacy_index_names` 扩展**(`config.cppm:400`)——顺序陷阱是 + issue 里点名的坑:现有名字分支条件是 `name=="mcpp-index" && url==<旧URL>`, + 若先重写 URL 再进名字分支,老配置就匹配不上了。**先 URL 归一、名字分支只看 name** + (归一后 URL 恒为新值,url 条件不再有区分意义;原 url 条件本意是避免误伤同名 + 自定义仓,归一后改为接受新旧两个官方 URL 即等价): + + ```cpp + for (auto r : cfg.indexRepos) { + if (r.url == kMcpplibsUrlLegacy) r.url = kMcpplibsUrl; // ① 组织迁移 + if (r.name == "mcpp-index" && r.url == kMcpplibsUrl) r.name = "mcpplibs"; // ② 名字迁移 + // ③ 去重逻辑不变 —— URL 归一后,"老条目+新默认条目"并存会在此折叠成一条 + } + ``` + + 注意去重是 `name+url` 联合判等:URL 归一必须发生在去重**之前**,否则 + `mcpplibs@旧URL` 与 `mcpplibs@新URL` 会被当成两个仓保留。现在的单循环结构 + 天然满足(逐条归一后再查重),保持即可。 +3. **`migrate_config_toml_index_names` 扩展**(文本层,治愈磁盘上的 config.toml): + 增加 `replace_all(updated, 旧URL, 新URL)`。幂等:替换后源串不再出现。 +4. **`migrate_xlings_json_index_names` 扩展**(治愈 registry `.xlings.json` + ——存量安装只走 migrate 不走 seed,见 `config.cppm:582-586`):同样 + `replace_all` 旧 URL→新 URL(带/不带空格两种 JSON 间距变体,与现有 name + 替换的双变体写法一致)。 +5. `publish/pipeline.cppm:158` Fork 地址、docs 4 处链接改新 org。 + +### D2. IndexRepo/seed 管线支持 artifact(#269 主体) + +1. **`IndexRepo` 扩字段**(`config.cppm:36`): + + ```cpp + struct IndexRepo { + std::string name; + std::string url; + std::string artifact; // 可选:artifact 来源 base(空 = 未声明,纯 git) + std::string source; // 可选:"auto"|"artifact"|"git"(空 = 不发射,xlings 默认 auto) + }; + ``` + + 本轮 `artifact` 只支持 string 形态;xlings 侧的 region 对象 + `{"GLOBAL":..,"CN":..}` 留待 CN 镜像仓实际部署后再扩(见 §六)。 + +2. **TOML 解析**(`config.cppm:535-543`):`[index.repos.NAME]` 增读 `artifact`、 + `source` 两个可选 string key。旧 mcpp 二进制读新 config.toml 时因只查 `url` + 而自然忽略新 key,向后安全。 + +3. **`seed_xlings_json` 签名升级**(`src/xlings.cppm:250/1110`): + `span>` 表达不了新字段。在 `mcpp::xlings` 内新增轻量 + 结构(保持该模块不依赖 `mcpp.config`): + + ```cpp + struct SeedRepo { + std::string name, url, artifact, source; + }; + void seed_xlings_json(const Env&, std::span, ...); + ``` + + 发射逻辑:`artifact` 非空才写 `"artifact"` key;`source` 非空才写 `"source"` key + ——未声明的仓输出逐字节不变,与 xlings 侧"未声明 = 纯 git 旧行为"对齐。 + 两个调用方(`write_default_xlings_json`、`ensure_project_index_dir`)同步改为 + 构造 `SeedRepo`。旧签名直接删除(内部 API,无兼容负担)。 + +4. **默认配置模板**(`config.cppm:359-361`): + + ```toml + [index.repos."mcpplibs"] + url = "https://github.com/mcpplibs/mcpp-index.git" + artifact = "https://github.com/xlings-res/mcpp-index" + # source = "auto" # 默认:artifact 优先,git 回退;可改 "git" 强制走 git + ``` + +5. **`add_default` 对齐**(`config.cppm:571-575`):默认条目带 artifact。另在 + `canonicalize_legacy_index_names` 末尾补一条**内存级治愈**:凡 `url == kMcpplibsUrl` + 且 `artifact` 为空且用户未显式 `source = "git"` 的条目,填充默认 artifact。 + 这样即使用户的 config.toml 是旧模板(没有 artifact 行),每次运行也拿到 + artifact 声明——**config.toml 不做 artifact 注入的文本手术**(用户可编辑文件, + 注入易碎且没必要;代码级默认已覆盖)。用户想退出 artifact 路径的显式出口就是 + `source = "git"`。 + +6. **`print_env`**(`config.cppm:663-668`)顺带打印 artifact 标记,便于 doctor 场景确认。 + +### D3. 存量 registry `.xlings.json` 的 artifact 注入(#269 建议 2) + +registry `.xlings.json` 只在缺失时 seed,存量安装唯一的治愈通道是 +`migrate_xlings_json_index_names`。扩展它(改名为语义更准的 +`migrate_xlings_json_index_entries`,或保名加逻辑,倾向后者少动调用点): + +``` +若 文本含 "xlings-res/mcpp-index" → 已注入,跳过(幂等闸) +否则 对 "url": "<新URL>" 与 "url":"<新URL>" 两种间距变体: + replace_all(..., "\"url\": \"<新URL>\"", + "\"url\": \"<新URL>\", \"artifact\": \"\"") +``` + +顺序上依赖 D1-4 已先把旧 URL 归一为新 URL(同一次 migrate 调用内先 URL 后 artifact), +故只需匹配新 URL。幂等性由"已含 res base 即跳过"保证——这是必须的: +纯 `replace_all` 二次运行会重复注入,替换后的文本仍包含原匹配串。 + +文本手术的安全性论证:该文件由两个写者维护——mcpp 的 `seed_xlings_json` +(pretty,`"key": "value"`)与 xlings 自身的序列化器(compact 可能无空格), +双变体替换覆盖两者;URL 串带 `/` 不会出现在其它 value 里(项目级 `.xlings.json` +不经此迁移,只有 registry 的走这条路)。**不整文件重生成**——文件里有 xlings 的 +subos/版本绑定状态,重生成会丢(0.0.9x 批次已确立的原则)。 + +旧 xlings(<0.4.68)读到 `artifact`/`source` 字段安全忽略(上游实测), +所以注入不需要先确认捆绑 xlings 版本,与 D4 无顺序耦合。 + +### D4. 捆绑 xlings pin → 0.4.68 + +- `release.yml`(x86_64 两处 + aarch64 硬编码 `0.4.67` 两处)、 + `cross-build-test.yml` ×2、`ci-linux-e2e.yml` quick_install 参数,全部 → `0.4.68`; +- `src/xlings.cppm:36` `kXlingsVersion` `"0.4.51"` → `"0.4.68"`,并给 + `config.cppm:34` 的无消费 re-export 补个用途或删除(倾向:doctor 输出里打印 + "bundled xlings pinned: X",让常量有单一消费点,防再度失同步); +- 低于 0.4.68 的 xlings 忽略 artifact 字段、行为不变,所以 pin 升级只是"吃到收益" + 的开关,不是正确性前提——**D1–D3 可以先合,D4 跟随下一次 release 节奏**。 + +### D5.(可选,#269 建议 4)项目级 `[indices]` artifact + +`IndexSpec`(`src/pm/index_spec.cppm:14`)增加 `std::string artifact; std::string source;`, +mcpp.toml `[indices]` 长格式与 `config.toml [indices]`(`config.cppm:546-563`)同步增读, +`ensure_project_index_dir` 构造 `SeedRepo` 时透传。 + +**与 pin 的交互是这里唯一的设计点**:`rev`/`tag`/`branch` 锁定的索引,artifact 通道 +只跟 latest pointer,无法表达任意 rev。规则:**凡 `rev`/`tag`/`branch` 任一非空, +不发射 artifact 字段**(等效强制 git),并在解析时对"pin + artifact 并存"给一条 +warn,说明 artifact 被忽略。本地 `path` 索引同理天然不发射。 + +此项不阻塞主线,可作为同批次的独立 commit 或推迟。 + +--- + +## 四、兼容性矩阵 + +| 组合 | 行为 | +|---|---| +| 新 mcpp + xlings ≥0.4.68 | artifact 优先,git 回退(完整收益) | +| 新 mcpp + xlings <0.4.68 | artifact 字段被忽略,纯 git,行为同今天(仅吃不到收益,不出错) | +| 旧 mcpp + 新 config.toml | TOML 解析只读 url,忽略 artifact/source 行,正常 | +| 旧 mcpp + 已注入 artifact 的 .xlings.json | `package_fetcher` 手写 parser 按 key 读 name/url,多余字段无害(单测锁) | +| 用户显式 `source = "git"` | 透传后 xlings 强制 git,且 D2-5 的内存级 artifact 填充跳过 | +| 旧 URL 仅存于用户自定义条目(name ≠ mcpplibs/mcpp-index)| URL 归一按 url 匹配不看 name,同样治愈(它就是官方仓的别名条目) | + +风险点与对策: + +- **`package_fetcher.cppm` 手写 JSON parser**:entry 对象内新增 key 后,其 + 按-brace 切分/按-key 提取逻辑必须仍取对 name/url。加一条含 artifact 字段的 + fixture 单测。 +- **文本迁移的双写者间距**:双变体 `replace_all` + "已含 res base"幂等闸, + 且 unit test 覆盖 mcpp-pretty 与 xlings-compact 两种输入。 +- **GitHub 重定向窗口期**:D1 合入前旧 URL 一直可用(靠重定向),合入后新旧 URL + 都直连有效,无停机窗口。 + +--- + +## 五、测试计划 + +单测(`tests/unit`): + +1. `canonicalize_legacy_index_names`:旧 URL+旧名 → 新 URL+`mcpplibs`; + 旧 URL+新名;新 URL 无 artifact → 填默认 artifact;`source="git"` 不填; + 老条目与默认条目 URL 归一后折叠为一条(去重顺序回归)。 +2. `migrate_config_toml_index_names`:旧模板文本 → URL 替换,幂等(跑两遍字节相同)。 +3. `migrate_xlings_json_index_names`:pretty/compact 两种输入 × {旧URL, 新URL无artifact, + 已注入} 矩阵,幂等,subos/envs 等无关 key 逐字节保留。 +4. `seed_xlings_json`:带/不带 artifact/source 的发射形态;无 artifact 条目输出与 + 现行为逐字节一致(零 diff 门,沿用 CommandDialect 批次的做法)。 +5. `package_fetcher` index_repos parser:含 artifact 字段的 entry 仍正确提取 name/url。 + +e2e(`tests/e2e`): + +6. fresh init:生成的 config.toml 与 registry `.xlings.json` 含新 URL + artifact 字段。 +7. 存量升级模拟:预置旧 URL、无 artifact 的 config.toml + `.xlings.json`(pretty 与 + compact 各一),跑任一命令后两文件被治愈,再跑一遍无变化。 + +实机验证(release 前,沿用发布闭环流程):Ubuntu 宿主(可复现 `/etc/ssl/cert.pem` +缺失环境更佳)fresh install → `mcpp index update` → 确认 `data/mcpplibs` 迁为 +artifact 管理(`.git` 消失、`.xlings-index-version` 出现)且 git 不可用时同步仍成功 +——issue #269 已实测过该路径,e2e 复核即可。 + +--- + +## 六、落地顺序 + +| 阶段 | 内容 | 依赖 | +|---|---|---| +| P1 | D1 URL 迁移(4 处 + 三个迁移钩子 + docs) | 无,可立即发 | +| P2 | D2 管线扩展 + D3 存量注入(同一 PR,D3 依赖 D1 的 URL 归一先行于同一次 migrate) | P1 | +| P3 | D4 pin → 0.4.68(CI + 常量对齐) | 随下一次 release | +| P4 | D5 项目级 `[indices]` artifact(可选) | P2 | +| P5 | CN region 对象:gitcode.com/xlings-res/mcpp-index 镜像仓部署后(mirror_res.sh 基建已有),`kMcpplibsArtifact` 升级为 region 对象发射,`SeedRepo.artifact` 相应扩为 GLOBAL/CN 两值 | P2 + 镜像仓就绪 | + +P1+P2+P3 目标合入同一个版本(0.0.103),单 PR 分 commit 或两个小 PR 均可; +P5 留待 CN 镜像仓真实存在后再动,避免声明一个 404 的 base(xlings auto 模式下 +artifact 失败会回退 git,不至于坏,但会白付一次探测)。 + +## 七、非目标 + +- 不改 xlings 侧任何代码(0.4.68 已全部就绪); +- 不改 mcpp-index / xlings-res 发布端(实测逐字节兼容,零改动); +- 不做 config.toml 的 artifact 文本注入(代码级默认覆盖,见 D2-5); +- `artifact` 的 region 对象形态本轮不进 TOML schema(P5 再扩)。 From 8e5efa54764ce5d35fcfd28ca9e31219f045b0db Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Wed, 22 Jul 2026 20:14:53 +0800 Subject: [PATCH 2/9] fix(index): migrate mcpplibs index URL to mcpplibs org (#267-A) The index repo moved from mcpp-community to the mcpplibs org; the old URL only worked via GitHub's repo redirect. Centralize the canonical/legacy URLs as constants, rewrite the URL in canonicalize_legacy_index_names (URL first, so legacy name entries still match and legacy/default entries fold in dedup), heal persisted config.toml/.xlings.json via the existing text migrations, and update the publish fork hint + docs links. canonicalize_legacy_index_names is now exported so ordering stays tested. --- docs/00-getting-started.md | 2 +- docs/04-build-from-source.md | 2 +- docs/zh/00-getting-started.md | 2 +- docs/zh/04-build-from-source.md | 2 +- src/config.cppm | 66 ++++++++++++++++++++---------- src/fallback/config_migration.cppm | 6 +++ src/publish/pipeline.cppm | 2 +- tests/unit/test_config.cpp | 61 +++++++++++++++++++++++++++ 8 files changed, 117 insertions(+), 26 deletions(-) diff --git a/docs/00-getting-started.md b/docs/00-getting-started.md index 5525a1a..98b145b 100644 --- a/docs/00-getting-started.md +++ b/docs/00-getting-started.md @@ -95,7 +95,7 @@ Declare dependencies in `mcpp.toml`: ``` `mcpp build` automatically resolves SemVer constraints against the -[mcpp-index](https://github.com/mcpp-community/mcpp-index), fetches the source, +[mcpp-index](https://github.com/mcpplibs/mcpp-index), fetches the source, and adds it to the build graph. For a complete example, see `02-with-deps` in [01 — Examples](01-examples.md). diff --git a/docs/04-build-from-source.md b/docs/04-build-from-source.md index 8fd2e06..3d88d7e 100644 --- a/docs/04-build-from-source.md +++ b/docs/04-build-from-source.md @@ -93,5 +93,5 @@ mcpp is in early iteration and its interfaces may change. Before submitting a PR - [Community forum](https://forum.d2learn.org/category/20) - Chat group QQ: 1067245099 -- [mcpp-index](https://github.com/mcpp-community/mcpp-index) — the default package index +- [mcpp-index](https://github.com/mcpplibs/mcpp-index) — the default package index - [mcpplibs](https://github.com/mcpplibs) — the companion collection of modular C++ libraries diff --git a/docs/zh/00-getting-started.md b/docs/zh/00-getting-started.md index 34490dd..0cb77ab 100644 --- a/docs/zh/00-getting-started.md +++ b/docs/zh/00-getting-started.md @@ -96,7 +96,7 @@ mcpp test # 编译并运行 tests/**/*.cpp(gtest 风格) ``` `mcpp build` 将自动从 -[mcpp-index](https://github.com/mcpp-community/mcpp-index) 解析 SemVer +[mcpp-index](https://github.com/mcpplibs/mcpp-index) 解析 SemVer 约束、拉取源码并加入编译图。完整示例参见 [01 — 示例项目](01-examples.md) 中的 `02-with-deps`。 diff --git a/docs/zh/04-build-from-source.md b/docs/zh/04-build-from-source.md index 56f3595..5f01122 100644 --- a/docs/zh/04-build-from-source.md +++ b/docs/zh/04-build-from-source.md @@ -100,5 +100,5 @@ mcpp 处于早期迭代阶段,接口可能调整,提交 PR 前请注意: - [社区论坛](https://forum.d2learn.org/category/20) - 交流群 QQ: 1067245099 -- [mcpp-index](https://github.com/mcpp-community/mcpp-index) — 默认包索引 +- [mcpp-index](https://github.com/mcpplibs/mcpp-index) — 默认包索引 - [mcpplibs](https://github.com/mcpplibs) — 配套的模块化 C++ 库集合 diff --git a/src/config.cppm b/src/config.cppm index dda011c..a01c432 100644 --- a/src/config.cppm +++ b/src/config.cppm @@ -38,6 +38,18 @@ struct IndexRepo { std::string url; }; +// Canonical mcpplibs index coordinates. The index repository moved from the +// mcpp-community org to the mcpplibs org (#267) — the legacy URL only works +// through GitHub's repo redirect, which breaks silently if the old name is +// ever re-taken. The artifact base is the release mirror consumed by +// xlings >= 0.4.68 per-repo artifact sync (#269); older xlings ignores it. +inline constexpr std::string_view kMcpplibsIndexUrl = + "https://github.com/mcpplibs/mcpp-index.git"; +inline constexpr std::string_view kMcpplibsIndexUrlLegacy = + "https://github.com/mcpp-community/mcpp-index.git"; +inline constexpr std::string_view kMcpplibsIndexArtifact = + "https://github.com/xlings-res/mcpp-index"; + struct GlobalConfig { // Resolved paths std::filesystem::path mcppHome; // ~/.mcpp/ @@ -251,6 +263,12 @@ std::expected load_or_init( // Pretty-print resolved config for `mcpp env` command. void print_env(const GlobalConfig& cfg); +// Normalize legacy index naming in a loaded config (exported for tests): +// org migration mcpp-community/mcpp-index -> mcpplibs/mcpp-index, index name +// mcpp-index -> mcpplibs, then name+url dedup. Order matters: URL first, so +// old-config name entries still match and legacy/default entries fold. +void canonicalize_legacy_index_names(GlobalConfig& cfg); + // M5.5: persist [toolchain].default to config.toml. std::expected write_default_toolchain(const GlobalConfig& cfg, std::string_view spec); @@ -357,7 +375,7 @@ home = "" default = "mcpplibs" [index.repos."mcpplibs"] -url = "https://github.com/mcpp-community/mcpp-index.git" +url = "https://github.com/mcpplibs/mcpp-index.git" # xlings auto-adds xim / awesome / scode / d2x as defaults. [cache] @@ -396,25 +414,8 @@ bool write_default_xlings_json(const std::filesystem::path& path, } // Migration helpers delegated to mcpp.fallback.config_migration. - -void canonicalize_legacy_index_names(GlobalConfig& cfg) { - if (cfg.defaultIndex == "mcpp-index") - cfg.defaultIndex = "mcpplibs"; - - std::vector normalized; - for (auto r : cfg.indexRepos) { - if (r.name == "mcpp-index" - && r.url == "https://github.com/mcpp-community/mcpp-index.git") { - r.name = "mcpplibs"; - } - bool duplicate = std::any_of(normalized.begin(), normalized.end(), - [&](const IndexRepo& existing) { - return existing.name == r.name && existing.url == r.url; - }); - if (!duplicate) normalized.push_back(std::move(r)); - } - cfg.indexRepos = std::move(normalized); -} +// canonicalize_legacy_index_names is exported (declared above, defined after +// this helper namespace) so its ordering rules stay under unit test. // Xlings binary acquisition delegated to mcpp.fallback.xlings_binary. @@ -454,6 +455,29 @@ void ensure_sandbox_patchelf(const GlobalConfig& cfg, bool quiet, } // namespace +void canonicalize_legacy_index_names(GlobalConfig& cfg) { + if (cfg.defaultIndex == "mcpp-index") + cfg.defaultIndex = "mcpplibs"; + + std::vector normalized; + for (auto r : cfg.indexRepos) { + // ① Org migration first — later steps (name match, dedup) key on the + // canonical URL, so legacy entries fold instead of surviving as dupes. + if (r.url == kMcpplibsIndexUrlLegacy) + r.url = std::string(kMcpplibsIndexUrl); + // ② Legacy index name. The URL condition keeps a user's unrelated + // repo that happens to be named "mcpp-index" untouched. + if (r.name == "mcpp-index" && r.url == kMcpplibsIndexUrl) + r.name = "mcpplibs"; + bool duplicate = std::any_of(normalized.begin(), normalized.end(), + [&](const IndexRepo& existing) { + return existing.name == r.name && existing.url == r.url; + }); + if (!duplicate) normalized.push_back(std::move(r)); + } + cfg.indexRepos = std::move(normalized); +} + std::expected load_or_init( bool quiet, BootstrapProgressCallback onBootstrapProgress, @@ -572,7 +596,7 @@ std::expected load_or_init( for (auto& r : cfg.indexRepos) if (r.name == name) return; cfg.indexRepos.push_back({ std::string(name), std::string(url) }); }; - add_default("mcpplibs", "https://github.com/mcpp-community/mcpp-index.git"); + add_default("mcpplibs", kMcpplibsIndexUrl); canonicalize_legacy_index_names(cfg); // 5. Seed registry/.xlings.json if missing; migrate legacy cached diff --git a/src/fallback/config_migration.cppm b/src/fallback/config_migration.cppm index 555119e..7e082e9 100644 --- a/src/fallback/config_migration.cppm +++ b/src/fallback/config_migration.cppm @@ -62,6 +62,9 @@ bool migrate_config_toml_index_names(const std::filesystem::path& path) { replace_all(updated, "default = \"mcpp-index\"", "default = \"mcpplibs\""); replace_all(updated, "[index.repos.\"mcpp-index\"]", "[index.repos.\"mcpplibs\"]"); + // Org migration (#267): the index repo moved to the mcpplibs org. + replace_all(updated, "https://github.com/mcpp-community/mcpp-index.git", + "https://github.com/mcpplibs/mcpp-index.git"); return write_text_if_changed(path, original, updated); } @@ -76,6 +79,9 @@ bool migrate_xlings_json_index_names(const std::filesystem::path& path) { replace_all(updated, "\"name\": \"mcpp-index\"", "\"name\": \"mcpplibs\""); replace_all(updated, "\"name\":\"mcpp-index\"", "\"name\":\"mcpplibs\""); + // Org migration (#267): the index repo moved to the mcpplibs org. + replace_all(updated, "https://github.com/mcpp-community/mcpp-index.git", + "https://github.com/mcpplibs/mcpp-index.git"); return write_text_if_changed(path, original, updated); } diff --git a/src/publish/pipeline.cppm b/src/publish/pipeline.cppm index b69ea49..aa639a6 100644 --- a/src/publish/pipeline.cppm +++ b/src/publish/pipeline.cppm @@ -155,7 +155,7 @@ export int publish_package(bool dry_run, bool allow_dirty) { std::println(" Attach: {}", tarball.string()); std::println(""); std::println(" 3. Open a PR to mcpp-index:"); - std::println(" Fork: https://github.com/mcpp-community/mcpp-index"); + std::println(" Fork: https://github.com/mcpplibs/mcpp-index"); std::println(" Add: pkgs/{}/{}.lua", first, pkg.name); std::println(" (file content is in {})", xpkgPath.string()); std::println(""); diff --git a/tests/unit/test_config.cpp b/tests/unit/test_config.cpp index 6e07cc2..70207ef 100644 --- a/tests/unit/test_config.cpp +++ b/tests/unit/test_config.cpp @@ -5,6 +5,7 @@ import std; import mcpp.config; +import mcpp.fallback.config_migration; import mcpp.pm.index_spec; namespace { @@ -162,3 +163,63 @@ TEST(Config, ProjectLocalIndexStaleCacheIsRemoved) { std::filesystem::remove_all(project); } + +// ── #267-A/#269: index org migration + artifact adoption ──────────────────── + +namespace { + +std::string read_all(const std::filesystem::path& p) { + std::stringstream ss; + ss << std::ifstream(p).rdbuf(); + return ss.str(); +} + +} // namespace + +TEST(ConfigIndexMigration, CanonicalizeRewritesOrgAndName) { + mcpp::config::GlobalConfig cfg; + cfg.defaultIndex = "mcpp-index"; + cfg.indexRepos.push_back({ "mcpp-index", + std::string(mcpp::config::kMcpplibsIndexUrlLegacy) }); + mcpp::config::canonicalize_legacy_index_names(cfg); + ASSERT_EQ(cfg.indexRepos.size(), 1u); + EXPECT_EQ(cfg.defaultIndex, "mcpplibs"); + EXPECT_EQ(cfg.indexRepos[0].name, "mcpplibs"); + EXPECT_EQ(cfg.indexRepos[0].url, mcpp::config::kMcpplibsIndexUrl); +} + +TEST(ConfigIndexMigration, CanonicalizeFoldsLegacyAndDefaultEntries) { + mcpp::config::GlobalConfig cfg; + cfg.indexRepos.push_back({ "mcpplibs", + std::string(mcpp::config::kMcpplibsIndexUrlLegacy) }); + cfg.indexRepos.push_back({ "mcpplibs", + std::string(mcpp::config::kMcpplibsIndexUrl) }); + mcpp::config::canonicalize_legacy_index_names(cfg); + EXPECT_EQ(cfg.indexRepos.size(), 1u); +} + +TEST(ConfigIndexMigration, CanonicalizeLeavesForeignReposAlone) { + mcpp::config::GlobalConfig cfg; + cfg.indexRepos.push_back({ "other", "https://example.com/other-index.git" }); + mcpp::config::canonicalize_legacy_index_names(cfg); + ASSERT_EQ(cfg.indexRepos.size(), 1u); + EXPECT_EQ(cfg.indexRepos[0].name, "other"); + EXPECT_EQ(cfg.indexRepos[0].url, "https://example.com/other-index.git"); +} + +TEST(ConfigIndexMigration, MigrateConfigTomlRewritesOrgUrlIdempotently) { + auto dir = make_tempdir("mcpp-migrate-toml"); + auto p = dir / "config.toml"; + { + std::ofstream os(p); + os << "[index]\ndefault = \"mcpplibs\"\n\n[index.repos.\"mcpplibs\"]\n" + "url = \"https://github.com/mcpp-community/mcpp-index.git\"\n"; + } + EXPECT_TRUE(mcpp::fallback::migrate_config_toml_index_names(p)); + auto text = read_all(p); + EXPECT_NE(text.find("github.com/mcpplibs/mcpp-index.git"), std::string::npos); + EXPECT_EQ(text.find("mcpp-community/mcpp-index"), std::string::npos); + EXPECT_FALSE(mcpp::fallback::migrate_config_toml_index_names(p)); // idempotent + EXPECT_EQ(read_all(p), text); + std::filesystem::remove_all(dir); +} From f9c344ea7930173fb1c3d28336493fbe4ebc71ba Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Wed, 22 Jul 2026 20:18:10 +0800 Subject: [PATCH 3/9] =?UTF-8?q?feat(index):=20per-repo=20artifact=20source?= =?UTF-8?q?=20plumbing=20=E2=80=94=20IndexRepo/SeedRepo/toml/template=20(#?= =?UTF-8?q?269)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit IndexRepo gains optional artifact/source; [index.repos.*] parses both keys; the default template and add_default declare the xlings-res/mcpp-index artifact base; canonicalize fills it in memory for pre-artifact configs (source="git" is the opt-out, user-set bases win). seed_xlings_json now takes SeedRepo spans and emits artifact/source only when set — output for plain repos stays byte-identical (locked by test). Older xlings (<0.4.68) ignores both keys. --- src/config.cppm | 46 +++++++++++++++++++++++++++----------- src/xlings.cppm | 29 +++++++++++++++++++----- tests/unit/test_config.cpp | 27 ++++++++++++++++++++++ tests/unit/test_xlings.cpp | 46 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 129 insertions(+), 19 deletions(-) diff --git a/src/config.cppm b/src/config.cppm index a01c432..6797a9a 100644 --- a/src/config.cppm +++ b/src/config.cppm @@ -36,6 +36,8 @@ inline constexpr std::string_view kXlingsPinnedVersion = mcpp::xlings::pinned::k struct IndexRepo { std::string name; std::string url; + std::string artifact; // optional artifact source base (xlings >= 0.4.68, #269) + std::string source; // optional "auto" | "artifact" | "git" ("" = xlings default auto) }; // Canonical mcpplibs index coordinates. The index repository moved from the @@ -375,7 +377,9 @@ home = "" default = "mcpplibs" [index.repos."mcpplibs"] -url = "https://github.com/mcpplibs/mcpp-index.git" +url = "https://github.com/mcpplibs/mcpp-index.git" +artifact = "https://github.com/xlings-res/mcpp-index" +# source = "auto" # default: artifact first, git fallback; set "git" to force git # xlings auto-adds xim / awesome / scode / d2x as defaults. [cache] @@ -393,10 +397,11 @@ bool write_default_xlings_json(const std::filesystem::path& path, const std::vector& repos, std::string_view mirror_override = {}) { - // Delegate to xlings module. Convert IndexRepo vec to pair span. - std::vector> pairs; + // Delegate to xlings module. Convert IndexRepo vec to SeedRepo span. + std::vector pairs; pairs.reserve(repos.size()); - for (auto& r : repos) pairs.emplace_back(r.name, r.url); + for (auto& r : repos) + pairs.push_back({ r.name, r.url, r.artifact, r.source }); // seed_xlings_json writes to env.home / ".xlings.json", so we // construct a temporary Env with home = path.parent_path(). mcpp::xlings::Env env; @@ -469,6 +474,12 @@ void canonicalize_legacy_index_names(GlobalConfig& cfg) { // repo that happens to be named "mcpp-index" untouched. if (r.name == "mcpp-index" && r.url == kMcpplibsIndexUrl) r.name = "mcpplibs"; + // ③ In-memory artifact default for the official index (#269): heals + // configs written by pre-artifact templates on every load without + // text surgery on user-editable config.toml. `source = "git"` is the + // explicit opt-out; a user-set artifact base always wins. + if (r.url == kMcpplibsIndexUrl && r.artifact.empty() && r.source != "git") + r.artifact = std::string(kMcpplibsIndexArtifact); bool duplicate = std::any_of(normalized.begin(), normalized.end(), [&](const IndexRepo& existing) { return existing.name == r.name && existing.url == r.url; @@ -562,7 +573,12 @@ std::expected load_or_init( auto& tt = val.as_table(); auto it = tt.find("url"); if (it == tt.end() || !it->second.is_string()) continue; - cfg.indexRepos.push_back({ name, it->second.as_string() }); + IndexRepo r{ name, it->second.as_string() }; + if (auto a = tt.find("artifact"); a != tt.end() && a->second.is_string()) + r.artifact = a->second.as_string(); + if (auto s = tt.find("source"); s != tt.end() && s->second.is_string()) + r.source = s->second.as_string(); + cfg.indexRepos.push_back(std::move(r)); } } // [indices] — new-schema custom index repositories. @@ -592,11 +608,13 @@ std::expected load_or_init( // them ourselves can cause cross-index name conflicts during // dependency resolution (e.g. linux-headers existing in both // scode and xim). See docs/21 §VII. - auto add_default = [&](std::string_view name, std::string_view url) { + auto add_default = [&](std::string_view name, std::string_view url, + std::string_view artifact = {}) { for (auto& r : cfg.indexRepos) if (r.name == name) return; - cfg.indexRepos.push_back({ std::string(name), std::string(url) }); + cfg.indexRepos.push_back({ std::string(name), std::string(url), + std::string(artifact), std::string() }); }; - add_default("mcpplibs", kMcpplibsIndexUrl); + add_default("mcpplibs", kMcpplibsIndexUrl, kMcpplibsIndexArtifact); canonicalize_legacy_index_names(cfg); // 5. Seed registry/.xlings.json if missing; migrate legacy cached @@ -687,8 +705,10 @@ void print_env(const GlobalConfig& cfg) { std::println("Index repos:"); for (auto& r : cfg.indexRepos) { bool isDefault = (r.name == cfg.defaultIndex); - std::println(" {} {}{}", - r.name, r.url, isDefault ? " (default)" : ""); + std::println(" {} {}{}{}", + r.name, r.url, + r.artifact.empty() ? "" : " [artifact]", + isDefault ? " (default)" : ""); } } @@ -701,17 +721,17 @@ bool ensure_project_index_dir( // Collect custom non-builtin indices that need xlings project-scope data. // Local path indices are also seeded so xlings can create its own // project-local repo link and install packages from that index. - std::vector> customRepos; + std::vector customRepos; for (auto& [name, spec] : indices) { if (spec.is_builtin()) continue; if (spec.is_local()) { auto source = resolve_project_index_path(projectDir, spec); std::error_code ec; std::filesystem::remove(source / ".xlings-index-cache.json", ec); - customRepos.emplace_back(name, source.generic_string()); + customRepos.push_back({ name, source.generic_string(), "", "" }); continue; } - customRepos.emplace_back(name, spec.url); + customRepos.push_back({ name, spec.url, "", "" }); } // NOTE: the official global `xim` index is deliberately NOT injected into diff --git a/src/xlings.cppm b/src/xlings.cppm index 7c4ee5e..fc439f3 100644 --- a/src/xlings.cppm +++ b/src/xlings.cppm @@ -247,8 +247,19 @@ struct ProjectEnv { } }; +// One index_repos entry for seed_xlings_json. `artifact`/`source` are the +// xlings >= 0.4.68 per-repo artifact-sync fields (#269); empty means "do not +// emit the key", which keeps pre-artifact output byte-identical and matches +// xlings' "undeclared = plain git" semantics. Older xlings ignores both keys. +struct SeedRepo { + std::string name; + std::string url; + std::string artifact; // artifact source base, e.g. https://github.com/xlings-res/mcpp-index + std::string source; // "auto" | "artifact" | "git" +}; + void seed_xlings_json(const Env& env, - std::span> repos, + std::span repos, std::string_view mirror = "auto", const ProjectEnv& penv = {}); @@ -1108,7 +1119,7 @@ int install_direct(const Env& env, std::string_view target, bool quiet) { // ─── Sandbox lifecycle ────────────────────────────────────────────── void seed_xlings_json(const Env& env, - std::span> repos, + std::span repos, std::string_view mirror, const ProjectEnv& penv) { @@ -1116,10 +1127,16 @@ void seed_xlings_json(const Env& env, std::string json = "{\n"; json += " \"index_repos\": [\n"; for (std::size_t i = 0; i < repos.size(); ++i) { - json += std::format(" {{ \"name\": \"{}\", \"url\": \"{}\" }}{}\n", - json_escape(repos[i].first), - json_escape(repos[i].second), - i + 1 == repos.size() ? "" : ","); + json += std::format(" {{ \"name\": \"{}\", \"url\": \"{}\"", + json_escape(repos[i].name), + json_escape(repos[i].url)); + if (!repos[i].artifact.empty()) + json += std::format(", \"artifact\": \"{}\"", + json_escape(repos[i].artifact)); + if (!repos[i].source.empty()) + json += std::format(", \"source\": \"{}\"", + json_escape(repos[i].source)); + json += std::format(" }}{}\n", i + 1 == repos.size() ? "" : ","); } json += " ],\n"; // [xlings] build environment (L-1): materialize deps/workspace/subos/envs diff --git a/tests/unit/test_config.cpp b/tests/unit/test_config.cpp index 70207ef..ff77f13 100644 --- a/tests/unit/test_config.cpp +++ b/tests/unit/test_config.cpp @@ -223,3 +223,30 @@ TEST(ConfigIndexMigration, MigrateConfigTomlRewritesOrgUrlIdempotently) { EXPECT_EQ(read_all(p), text); std::filesystem::remove_all(dir); } + +TEST(ConfigIndexMigration, CanonicalizeFillsDefaultArtifactForOfficialRepo) { + mcpp::config::GlobalConfig cfg; + cfg.indexRepos.push_back({ "mcpplibs", + std::string(mcpp::config::kMcpplibsIndexUrlLegacy) }); + mcpp::config::canonicalize_legacy_index_names(cfg); + ASSERT_EQ(cfg.indexRepos.size(), 1u); + EXPECT_EQ(cfg.indexRepos[0].artifact, mcpp::config::kMcpplibsIndexArtifact); +} + +TEST(ConfigIndexMigration, CanonicalizeRespectsExplicitGitSourceAndCustomArtifact) { + mcpp::config::GlobalConfig cfg; + mcpp::config::IndexRepo git{ "mcpplibs", + std::string(mcpp::config::kMcpplibsIndexUrl) }; + git.source = "git"; + cfg.indexRepos.push_back(git); + mcpp::config::IndexRepo custom{ "mirror", + std::string(mcpp::config::kMcpplibsIndexUrl) }; + custom.artifact = "https://example.com/my-mirror"; + cfg.indexRepos.push_back(custom); + cfg.indexRepos.push_back({ "other", "https://example.com/other-index.git" }); + mcpp::config::canonicalize_legacy_index_names(cfg); + ASSERT_EQ(cfg.indexRepos.size(), 3u); + EXPECT_TRUE(cfg.indexRepos[0].artifact.empty()); // explicit git opt-out + EXPECT_EQ(cfg.indexRepos[1].artifact, "https://example.com/my-mirror"); + EXPECT_TRUE(cfg.indexRepos[2].artifact.empty()); // foreign repo untouched +} diff --git a/tests/unit/test_xlings.cpp b/tests/unit/test_xlings.cpp index ccbab5f..01a54a1 100644 --- a/tests/unit/test_xlings.cpp +++ b/tests/unit/test_xlings.cpp @@ -408,3 +408,49 @@ TEST(XlingsHomeTool, FindsPayloadUnderNonXimPrefix) { std::filesystem::remove_all(tmp); } + +// ─── seed_xlings_json — per-repo artifact/source emission (#269) ────── + +namespace { + +std::string read_seeded(const std::filesystem::path& home) { + std::stringstream ss; + ss << std::ifstream(home / ".xlings.json").rdbuf(); + return ss.str(); +} + +} // namespace + +TEST(XlingsSeed, PlainRepoEmissionIsByteStable) { + // Zero-diff gate: repos without artifact/source must serialize exactly + // as before the SeedRepo extension. + auto dir = make_tempdir("mcpp-seed-plain"); + mcpp::xlings::Env env; + env.home = dir; + std::vector repos{ + { "mcpplibs", "https://x.git", "", "" } }; + mcpp::xlings::seed_xlings_json(env, repos); + auto text = read_seeded(dir); + EXPECT_NE(text.find( + " { \"name\": \"mcpplibs\", \"url\": \"https://x.git\" }\n"), + std::string::npos) << text; + EXPECT_EQ(text.find("artifact"), std::string::npos); + EXPECT_EQ(text.find("source"), std::string::npos); + std::filesystem::remove_all(dir); +} + +TEST(XlingsSeed, ArtifactAndSourceEmittedWhenSet) { + auto dir = make_tempdir("mcpp-seed-art"); + mcpp::xlings::Env env; + env.home = dir; + std::vector repos{ + { "mcpplibs", "https://x.git", + "https://github.com/xlings-res/mcpp-index", "auto" } }; + mcpp::xlings::seed_xlings_json(env, repos); + auto text = read_seeded(dir); + EXPECT_NE(text.find( + "\"url\": \"https://x.git\", \"artifact\": " + "\"https://github.com/xlings-res/mcpp-index\", \"source\": \"auto\""), + std::string::npos) << text; + std::filesystem::remove_all(dir); +} From d7570b7b9ed9f9f7efe4986cd11694df911ef869 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Wed, 22 Jul 2026 20:19:59 +0800 Subject: [PATCH 4/9] =?UTF-8?q?feat(index):=20heal=20existing=20registry?= =?UTF-8?q?=20.xlings.json=20=E2=80=94=20org=20URL=20+=20artifact=20inject?= =?UTF-8?q?ion=20(#267-A=20#269)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Existing installs only run the text migration (seed is skipped when the file exists), so it now rewrites the org URL and injects the artifact declaration next to the canonical mcpplibs url. Idempotency gate: skip injection when the res base is already present (plain replace_all would re-inject every run). Covers both writer spacings (mcpp pretty / xlings compact); unrelated keys (subos, bindings) are preserved byte-for-byte. --- src/fallback/config_migration.cppm | 28 ++++++++++++-- tests/unit/test_config.cpp | 61 ++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 4 deletions(-) diff --git a/src/fallback/config_migration.cppm b/src/fallback/config_migration.cppm index 7e082e9..56e7587 100644 --- a/src/fallback/config_migration.cppm +++ b/src/fallback/config_migration.cppm @@ -1,8 +1,12 @@ -// mcpp.fallback.config_migration — legacy index name migration. +// mcpp.fallback.config_migration — legacy index config migration. // -// Older mcpp sandboxes used "mcpp-index" as the default index name. -// These helpers rename it to "mcpplibs" in config.toml and .xlings.json -// so xlings config/list output matches mcpp's default namespace. +// Older mcpp sandboxes used "mcpp-index" as the default index name and the +// pre-migration mcpp-community org URL. These helpers rename the index to +// "mcpplibs", rewrite the org URL (#267), and inject the xlings >= 0.4.68 +// per-repo artifact declaration (#269) into config.toml / .xlings.json. +// Deliberately text-based: .xlings.json carries xlings-owned state (subos, +// version bindings) that a regeneration would destroy. This is a LEAF +// module (no mcpp.config import), so the URL literals are repeated here. export module mcpp.fallback.config_migration; @@ -83,6 +87,22 @@ bool migrate_xlings_json_index_names(const std::filesystem::path& path) { replace_all(updated, "https://github.com/mcpp-community/mcpp-index.git", "https://github.com/mcpplibs/mcpp-index.git"); + // Artifact injection (#269, xlings >= 0.4.68 per-repo artifact source). + // Existing installs never re-seed .xlings.json, so this migration is + // their only channel to the artifact declaration; older xlings safely + // ignores the key. Idempotency gate: the res base appearing anywhere + // means a previous run (or a fresh seed) already declared it — a plain + // replace_all would re-inject on every run. Both spacing variants + // because the file has two writers (mcpp pretty / xlings compact). + if (updated.find("xlings-res/mcpp-index") == std::string::npos) { + constexpr std::string_view art = + ", \"artifact\": \"https://github.com/xlings-res/mcpp-index\""; + for (std::string_view urlkv : { + "\"url\": \"https://github.com/mcpplibs/mcpp-index.git\"", + "\"url\":\"https://github.com/mcpplibs/mcpp-index.git\"" }) + replace_all(updated, urlkv, std::string(urlkv) + std::string(art)); + } + return write_text_if_changed(path, original, updated); } diff --git a/tests/unit/test_config.cpp b/tests/unit/test_config.cpp index ff77f13..f3d1f2d 100644 --- a/tests/unit/test_config.cpp +++ b/tests/unit/test_config.cpp @@ -250,3 +250,64 @@ TEST(ConfigIndexMigration, CanonicalizeRespectsExplicitGitSourceAndCustomArtifac EXPECT_EQ(cfg.indexRepos[1].artifact, "https://example.com/my-mirror"); EXPECT_TRUE(cfg.indexRepos[2].artifact.empty()); // foreign repo untouched } + +TEST(ConfigIndexMigration, XlingsJsonHealsPrettyLegacyEntry) { + auto dir = make_tempdir("mcpp-migrate-xj"); + auto p = dir / ".xlings.json"; + { + std::ofstream os(p); + os << "{\n \"index_repos\": [\n" + " { \"name\": \"mcpplibs\", \"url\": " + "\"https://github.com/mcpp-community/mcpp-index.git\" }\n" + " ],\n \"subos\": \"default\",\n \"mirror\": \"auto\"\n}\n"; + } + EXPECT_TRUE(mcpp::fallback::migrate_xlings_json_index_names(p)); + auto text = read_all(p); + EXPECT_NE(text.find( + "\"url\": \"https://github.com/mcpplibs/mcpp-index.git\", " + "\"artifact\": \"https://github.com/xlings-res/mcpp-index\""), + std::string::npos) << text; + EXPECT_NE(text.find("\"subos\": \"default\""), std::string::npos); + EXPECT_EQ(text.find("mcpp-community/mcpp-index"), std::string::npos); + EXPECT_FALSE(mcpp::fallback::migrate_xlings_json_index_names(p)); // idempotent + EXPECT_EQ(read_all(p), text); + std::filesystem::remove_all(dir); +} + +TEST(ConfigIndexMigration, XlingsJsonHealsCompactLegacyNameAndUrl) { + auto dir = make_tempdir("mcpp-migrate-xjc"); + auto p = dir / ".xlings.json"; + { + std::ofstream os(p); + os << "{\"index_repos\":[{\"name\":\"mcpp-index\"," + "\"url\":\"https://github.com/mcpp-community/mcpp-index.git\"}]," + "\"mirror\":\"auto\"}"; + } + EXPECT_TRUE(mcpp::fallback::migrate_xlings_json_index_names(p)); + auto text = read_all(p); + EXPECT_NE(text.find("\"name\":\"mcpplibs\""), std::string::npos); + EXPECT_NE(text.find( + "\"url\":\"https://github.com/mcpplibs/mcpp-index.git\", " + "\"artifact\": \"https://github.com/xlings-res/mcpp-index\""), + std::string::npos) << text; + EXPECT_FALSE(mcpp::fallback::migrate_xlings_json_index_names(p)); + std::filesystem::remove_all(dir); +} + +TEST(ConfigIndexMigration, XlingsJsonSkipsInjectionWhenArtifactPresent) { + auto dir = make_tempdir("mcpp-migrate-xja"); + auto p = dir / ".xlings.json"; + std::string body = + "{\n \"index_repos\": [\n" + " { \"name\": \"mcpplibs\", \"url\": " + "\"https://github.com/mcpplibs/mcpp-index.git\", " + "\"artifact\": \"https://github.com/xlings-res/mcpp-index\" }\n" + " ]\n}\n"; + { + std::ofstream os(p); + os << body; + } + EXPECT_FALSE(mcpp::fallback::migrate_xlings_json_index_names(p)); + EXPECT_EQ(read_all(p), body); + std::filesystem::remove_all(dir); +} From 7a1020e4b1b697cf214e25c0a8a9da7b0e2d6432 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Wed, 22 Jul 2026 20:22:50 +0800 Subject: [PATCH 5/9] feat(index): project-level [indices] artifact/source with pin-forces-git rule (#269) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit IndexSpec gains artifact/source, parsed from both mcpp.toml and config.toml [indices] long forms; ensure_project_index_dir passes them through to the project seed. artifact_applicable() encodes the design rule: rev/tag/branch pins (and local path) force git because the artifact channel only tracks the latest pointer — a declared-but-inapplicable artifact warns instead of silently syncing the wrong revision. --- src/config.cppm | 15 ++++++++++++++- src/manifest/toml.cppm | 2 ++ src/pm/index_spec.cppm | 9 +++++++++ tests/unit/test_manifest.cpp | 27 +++++++++++++++++++++++++++ 4 files changed, 52 insertions(+), 1 deletion(-) diff --git a/src/config.cppm b/src/config.cppm index 6797a9a..60b5ab4 100644 --- a/src/config.cppm +++ b/src/config.cppm @@ -596,6 +596,8 @@ std::expected load_or_init( if (auto it = sub.find("tag"); it != sub.end() && it->second.is_string()) spec.tag = it->second.as_string(); if (auto it = sub.find("branch"); it != sub.end() && it->second.is_string()) spec.branch = it->second.as_string(); if (auto it = sub.find("path"); it != sub.end() && it->second.is_string()) spec.path = it->second.as_string(); + if (auto it = sub.find("artifact"); it != sub.end() && it->second.is_string()) spec.artifact = it->second.as_string(); + if (auto it = sub.find("source"); it != sub.end() && it->second.is_string()) spec.source = it->second.as_string(); } if (!spec.url.empty() || !spec.path.empty()) cfg.indices[k] = std::move(spec); @@ -731,7 +733,18 @@ bool ensure_project_index_dir( customRepos.push_back({ name, source.generic_string(), "", "" }); continue; } - customRepos.push_back({ name, spec.url, "", "" }); + // #269: pass a declared artifact source through to xlings unless a + // rev/tag/branch pin forces git (the artifact channel only tracks + // the latest published pointer). + mcpp::xlings::SeedRepo repo{ name, spec.url, "", "" }; + if (spec.artifact_applicable()) { + repo.artifact = spec.artifact; + repo.source = spec.source; + } else if (!spec.artifact.empty()) { + std::println("warning: [indices].{}: artifact source ignored " + "(rev/tag/branch/path pins force git)", name); + } + customRepos.push_back(std::move(repo)); } // NOTE: the official global `xim` index is deliberately NOT injected into diff --git a/src/manifest/toml.cppm b/src/manifest/toml.cppm index 5bed64c..77c5a66 100644 --- a/src/manifest/toml.cppm +++ b/src/manifest/toml.cppm @@ -1146,6 +1146,8 @@ std::expected parse_string(std::string_view content, if (auto it = sub.find("tag"); it != sub.end() && it->second.is_string()) spec.tag = it->second.as_string(); if (auto it = sub.find("branch"); it != sub.end() && it->second.is_string()) spec.branch = it->second.as_string(); if (auto it = sub.find("path"); it != sub.end() && it->second.is_string()) spec.path = it->second.as_string(); + if (auto it = sub.find("artifact"); it != sub.end() && it->second.is_string()) spec.artifact = it->second.as_string(); + if (auto it = sub.find("source"); it != sub.end() && it->second.is_string()) spec.source = it->second.as_string(); if (spec.url.empty() && spec.path.empty()) { return std::unexpected(error(origin, std::format( "[indices].{} must specify 'url' or 'path'", k))); diff --git a/src/pm/index_spec.cppm b/src/pm/index_spec.cppm index 2711010..4ee2245 100644 --- a/src/pm/index_spec.cppm +++ b/src/pm/index_spec.cppm @@ -18,9 +18,18 @@ struct IndexSpec { std::string tag; // git tag std::string branch; // git branch std::filesystem::path path; // local path (takes priority over url) + std::string artifact; // optional artifact source base (xlings >= 0.4.68, #269) + std::string source; // optional "auto" | "artifact" | "git" bool is_local() const { return !path.empty(); } bool is_pinned() const { return !rev.empty(); } + // The artifact channel only tracks the latest published pointer; any + // rev/tag/branch pin (and local path) therefore forces git and the + // artifact declaration is ignored (with a warning at seed time). + bool artifact_applicable() const { + return !artifact.empty() && rev.empty() && tag.empty() + && branch.empty() && path.empty(); + } // R6: `name == "mcpplibs"` alone used to mean "builtin" unconditionally, // which was correct while the only way to reach that name was the // literal `[indices] mcpplibs = { url = ..., rev = ... }` pin form (still diff --git a/tests/unit/test_manifest.cpp b/tests/unit/test_manifest.cpp index ee85550..ed9b65f 100644 --- a/tests/unit/test_manifest.cpp +++ b/tests/unit/test_manifest.cpp @@ -2601,3 +2601,30 @@ mcpp = { EXPECT_EQ(host->modules.sources, nativeTarget->modules.sources); EXPECT_EQ(host->buildConfig.cxxflags, nativeTarget->buildConfig.cxxflags); } + +TEST(Manifest, IndicesArtifactAndSourceParsedAndPinSuppresses) { + // #269: [indices] entries may declare an xlings >= 0.4.68 artifact + // source. rev/tag/branch pins force git (the artifact channel only + // tracks the latest pointer), which artifact_applicable() encodes. + constexpr auto src = R"( +[package] +name = "hello" +version = "0.1.0" +[language] +standard = "c++23" +[modules] +sources = ["src/**/*.cppm"] +[indices] +fast = { url = "https://example.com/i.git", artifact = "https://example.com/res/i", source = "auto" } +pinned = { url = "https://example.com/i.git", artifact = "https://example.com/res/i", rev = "abc123" } +)"; + auto m = mcpp::manifest::parse_string(src); + ASSERT_TRUE(m.has_value()) << m.error().format(); + auto& fast = m->indices.at("fast"); + EXPECT_EQ(fast.artifact, "https://example.com/res/i"); + EXPECT_EQ(fast.source, "auto"); + EXPECT_TRUE(fast.artifact_applicable()); + auto& pinned = m->indices.at("pinned"); + EXPECT_EQ(pinned.artifact, "https://example.com/res/i"); + EXPECT_FALSE(pinned.artifact_applicable()); +} From b591a00ed11ff8c23c6bf1b8265dcfd30cfed0d5 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Wed, 22 Jul 2026 20:24:04 +0800 Subject: [PATCH 6/9] test(pm): lock read_seeded_index_repos tolerance to artifact/source fields --- tests/unit/test_pm_package_fetcher.cpp | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/unit/test_pm_package_fetcher.cpp b/tests/unit/test_pm_package_fetcher.cpp index 856eadb..d542105 100644 --- a/tests/unit/test_pm_package_fetcher.cpp +++ b/tests/unit/test_pm_package_fetcher.cpp @@ -253,3 +253,25 @@ TEST(PmPackageFetcher, ReadSeededIndexReposParsesXlingsJson) { std::filesystem::remove_all(project); } + +TEST(PmPackageFetcher, ReadSeededIndexReposToleratesArtifactFields) { + // #269: entries seeded with per-repo artifact/source keys (xlings >= + // 0.4.68) must still yield name+url — regression lock for the hand- + // rolled index_repos reader. + auto project = make_tempdir("mcpp-269-artifact-fields"); + auto xjson = project / ".xlings.json"; + write_file(xjson, R"({ + "index_repos": [ + { "name": "mcpplibs", "url": "https://github.com/mcpplibs/mcpp-index.git", "artifact": "https://github.com/xlings-res/mcpp-index", "source": "auto" } + ], + "lang": "en" +})"); + + auto repos = mcpp::pm::read_seeded_index_repos(xjson); + ASSERT_EQ(repos.size(), 1u); + EXPECT_NE(repos[0].find("mcpplibs"), std::string::npos); + EXPECT_NE(repos[0].find("https://github.com/mcpplibs/mcpp-index.git"), + std::string::npos); + + std::filesystem::remove_all(project); +} From 92d09e44f829495aa815384c1ba7f682b4eb5af0 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Wed, 22 Jul 2026 20:27:11 +0800 Subject: [PATCH 7/9] ci: bundled xlings pin 0.4.67 -> 0.4.68 (per-repo index artifact); align kXlingsVersion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 0.4.68 (openxlings/xlings#377/#379/#380) lets custom index_repos entries declare an artifact source with git fallback — the feature #269 adopts. Older xlings ignores the new fields, so this bump is the benefit switch, not a correctness prerequisite. kXlingsVersion was stale at 0.4.51 with no consumer; align it and print it from `mcpp self env` so the constant can't silently drift again. --- .github/workflows/ci-linux-e2e.yml | 2 +- .github/workflows/cross-build-test.yml | 16 ++++++++-------- .github/workflows/release.yml | 18 +++++++++--------- src/config.cppm | 1 + src/xlings.cppm | 5 ++++- 5 files changed, 23 insertions(+), 19 deletions(-) diff --git a/.github/workflows/ci-linux-e2e.yml b/.github/workflows/ci-linux-e2e.yml index f60d9a1..0cc37ba 100644 --- a/.github/workflows/ci-linux-e2e.yml +++ b/.github/workflows/ci-linux-e2e.yml @@ -123,7 +123,7 @@ jobs: - name: Bootstrap xlings + released mcpp run: | - curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v0.4.67 + curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v0.4.68 export PATH="$HOME/.xlings/subos/current/bin:$PATH" xlings update xlings install mcpp -y -g diff --git a/.github/workflows/cross-build-test.yml b/.github/workflows/cross-build-test.yml index f24d7f7..92bb3ec 100644 --- a/.github/workflows/cross-build-test.yml +++ b/.github/workflows/cross-build-test.yml @@ -92,17 +92,17 @@ jobs: - name: Bootstrap mcpp via xlings env: XLINGS_NON_INTERACTIVE: '1' - # 0.4.67 (current) — kept in lock-step with the xlings the release - # is built/bundled against (release.yml). Bumped 0.4.62 -> 0.4.67 for - # mcpp#238: xlings 0.4.67 carries the multi-index_repo install fix - # (openxlings/xlings#374, commit cf9b60d5) so a workspace member with - # >=2 inherited index_repos installs uncached packages instead of - # failing silently. A past 0.4.61 "download 404 + # 0.4.68 (current) — kept in lock-step with the xlings the release + # is built/bundled against (release.yml). 0.4.67 carried the + # multi-index_repo install fix (openxlings/xlings#374); 0.4.68 adds + # per-repo index artifact sources (openxlings/xlings#377) so the + # mcpplibs index syncs via artifact with git as fallback (mcpp#269). + # A past 0.4.61 "download 404 # for mcpp@" was NOT a version bug — the xlings-res/mcpp GitHub # release assets were uploaded in a broken state (records present, # blobs missing → 404 on GET); re-uploaded clean. The stale-INDEX # half is handled by the marker-clear below. - XLINGS_VERSION: '0.4.67' + XLINGS_VERSION: '0.4.68' run: | tarball="xlings-${XLINGS_VERSION}-linux-x86_64.tar.gz" curl -fsSL -o "/tmp/${tarball}" \ @@ -215,7 +215,7 @@ jobs: - name: Bootstrap mcpp via xlings env: XLINGS_NON_INTERACTIVE: '1' - XLINGS_VERSION: '0.4.67' + XLINGS_VERSION: '0.4.68' run: | tarball="xlings-${XLINGS_VERSION}-linux-x86_64.tar.gz" curl -fsSL -o "/tmp/${tarball}" \ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9d2a488..6ff6a9a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -96,7 +96,7 @@ jobs: # Pin xlings to a known-good version. The upstream install # script always grabs `latest` (no version override), so we # download + self-install manually to avoid broken releases. - XLINGS_VERSION: '0.4.67' + XLINGS_VERSION: '0.4.68' run: | if [ ! -x "$HOME/.xlings/subos/default/bin/xlings" ]; then tarball="xlings-${XLINGS_VERSION}-linux-x86_64.tar.gz" @@ -278,10 +278,10 @@ jobs: echo "version=$VERSION" >> "$GITHUB_OUTPUT" echo "tag=v$VERSION" >> "$GITHUB_OUTPUT" - - name: Bootstrap mcpp via xlings (latest 0.4.67) + - name: Bootstrap mcpp via xlings (latest 0.4.68) env: XLINGS_NON_INTERACTIVE: '1' - XLINGS_VERSION: '0.4.67' + XLINGS_VERSION: '0.4.68' run: | tarball="xlings-${XLINGS_VERSION}-linux-x86_64.tar.gz" curl -fsSL -o "/tmp/${tarball}" \ @@ -336,13 +336,13 @@ jobs: exec "$(dirname "$0")/bin/mcpp" "$@" LAUNCHER chmod +x "$STAGING/$WRAPPER/mcpp" - # Bundle the aarch64 xlings (0.4.67) so install.sh consumers on aarch64 + # Bundle the aarch64 xlings (0.4.68) so install.sh consumers on aarch64 # get an aarch64 xlings, not the x86_64 bootstrap one. - XLA="xlings-0.4.67-linux-aarch64.tar.gz" + XLA="xlings-0.4.68-linux-aarch64.tar.gz" if curl -fsSL -o "/tmp/$XLA" \ - "https://github.com/openxlings/xlings/releases/download/v0.4.67/$XLA"; then + "https://github.com/openxlings/xlings/releases/download/v0.4.68/$XLA"; then tar -xzf "/tmp/$XLA" -C /tmp - XLBIN=$(find /tmp/xlings-0.4.67-linux-aarch64 -path '*/bin/xlings' -type f | head -1) + XLBIN=$(find /tmp/xlings-0.4.68-linux-aarch64 -path '*/bin/xlings' -type f | head -1) if [ -n "$XLBIN" ]; then mkdir -p "$STAGING/$WRAPPER/registry/bin" cp "$XLBIN" "$STAGING/$WRAPPER/registry/bin/xlings" @@ -417,7 +417,7 @@ jobs: - name: Bootstrap mcpp via xlings env: XLINGS_NON_INTERACTIVE: '1' - XLINGS_VERSION: '0.4.67' + XLINGS_VERSION: '0.4.68' run: | if [ ! -x "$HOME/.xlings/subos/default/bin/xlings" ]; then WORK=$(mktemp -d) @@ -599,7 +599,7 @@ jobs: shell: bash env: XLINGS_NON_INTERACTIVE: '1' - XLINGS_VERSION: '0.4.67' + XLINGS_VERSION: '0.4.68' run: | WORK=$(mktemp -d) zipfile="xlings-${XLINGS_VERSION}-windows-x86_64.zip" diff --git a/src/config.cppm b/src/config.cppm index 60b5ab4..9236972 100644 --- a/src/config.cppm +++ b/src/config.cppm @@ -695,6 +695,7 @@ std::expected load_or_init( void print_env(const GlobalConfig& cfg) { std::println("MCPP_HOME = {}", cfg.mcppHome.string()); std::println("xlings binary = {}", cfg.xlingsBinary.string()); + std::println("xlings pinned = {}", kXlingsPinnedVersion); std::println("xlings home = {}", cfg.xlingsHome().string()); std::println("config = {}", cfg.configFile.string()); std::println("BMI cache = {}", cfg.bmiCacheDir.string()); diff --git a/src/xlings.cppm b/src/xlings.cppm index fc439f3..e06fe7b 100644 --- a/src/xlings.cppm +++ b/src/xlings.cppm @@ -33,7 +33,10 @@ struct Env { namespace pinned { inline constexpr std::string_view kPatchelfVersion = "0.18.0"; inline constexpr std::string_view kNinjaVersion = "1.12.1"; - inline constexpr std::string_view kXlingsVersion = "0.4.51"; + // Keep in lock-step with the XLINGS_VERSION pins in release.yml / + // cross-build-test.yml / ci-linux-e2e.yml (the xlings actually bundled + // into releases). Printed by `mcpp self env`. + inline constexpr std::string_view kXlingsVersion = "0.4.68"; inline constexpr std::string_view kNasmVersion = "3.02"; } From e93e52651b4a90fe371d92dc7758ce305d09d296 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Wed, 22 Jul 2026 20:29:40 +0800 Subject: [PATCH 8/9] =?UTF-8?q?test(e2e):=20151=20=E2=80=94=20index=20org?= =?UTF-8?q?=20URL=20heal=20+=20artifact=20seed/injection=20(#267=20#269)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/e2e/151_index_url_artifact_migration.sh | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100755 tests/e2e/151_index_url_artifact_migration.sh diff --git a/tests/e2e/151_index_url_artifact_migration.sh b/tests/e2e/151_index_url_artifact_migration.sh new file mode 100755 index 0000000..1ad8c74 --- /dev/null +++ b/tests/e2e/151_index_url_artifact_migration.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash +# #267/#269: fresh init seeds the mcpplibs-org index URL + artifact source; +# legacy config.toml / .xlings.json (old org URL, no artifact) are healed in +# place, idempotently, in both pretty (mcpp writer) and compact (xlings +# writer) JSON spacings. Unrelated .xlings.json state must survive. +set -e + +TMP=$(mktemp -d) +trap "rm -rf $TMP" EXIT + +export MCPP_HOME="$TMP/mcpp-home" + +NEW_URL='https://github.com/mcpplibs/mcpp-index.git' +OLD_URL='https://github.com/mcpp-community/mcpp-index.git' +ART='https://github.com/xlings-res/mcpp-index' +CFG="$MCPP_HOME/config.toml" +XJ="$MCPP_HOME/registry/.xlings.json" + +# 1. Fresh init: new org URL + artifact declaration in both files. +# (xlings' own bootstrap may re-serialize .xlings.json with its writer, so +# assert on the key/value pairs, not on mcpp's seed line layout.) +"$MCPP" self env > /dev/null +grep -q "url = \"$NEW_URL\"" "$CFG" || { echo "config.toml missing new url"; exit 1; } +grep -q "artifact = \"$ART\"" "$CFG" || { echo "config.toml missing artifact"; exit 1; } +grep -q "\"url\": \"$NEW_URL\"" "$XJ" \ + || { echo "seeded .xlings.json missing new url"; cat "$XJ"; exit 1; } +grep -q "\"artifact\": \"$ART\"" "$XJ" \ + || { echo "seeded .xlings.json missing artifact"; cat "$XJ"; exit 1; } +if grep -q 'mcpp-community/mcpp-index' "$CFG" "$XJ"; then + echo "old org URL leaked into fresh seed"; exit 1 +fi + +# 2. Legacy heal (pretty spacing): old org URL, no artifact. +cat > "$CFG" < "$XJ" < /dev/null +grep -q "url = \"$NEW_URL\"" "$CFG" || { echo "config.toml url not healed"; cat "$CFG"; exit 1; } +grep -q "\"url\": \"$NEW_URL\", \"artifact\": \"$ART\"" "$XJ" \ + || { echo ".xlings.json not healed"; cat "$XJ"; exit 1; } +grep -q '"subos": "default"' "$XJ" || { echo "unrelated .xlings.json key lost"; exit 1; } +if grep -q 'mcpp-community/mcpp-index' "$CFG" "$XJ"; then + echo "old org URL survived heal"; exit 1 +fi + +# Idempotent: a second run must not touch either file again. +cp "$CFG" "$TMP/cfg1"; cp "$XJ" "$TMP/xj1" +"$MCPP" self env > /dev/null +cmp -s "$CFG" "$TMP/cfg1" || { echo "config.toml heal not idempotent"; exit 1; } +cmp -s "$XJ" "$TMP/xj1" || { echo ".xlings.json heal not idempotent"; exit 1; } + +# 3. Legacy heal (compact spacing, old index name): xlings-writer format. +printf '{"index_repos":[{"name":"mcpp-index","url":"%s"}],"mirror":"auto"}' "$OLD_URL" > "$XJ" +"$MCPP" self env > /dev/null +grep -q '"name":"mcpplibs"' "$XJ" || { echo "compact name not healed"; cat "$XJ"; exit 1; } +grep -q 'xlings-res/mcpp-index' "$XJ" || { echo "compact artifact not injected"; cat "$XJ"; exit 1; } +if grep -q 'mcpp-community' "$XJ"; then + echo "compact old org URL survived"; exit 1 +fi + +echo "OK" From 20b35831cd1e488f843f301561c4dc483d451890 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Wed, 22 Jul 2026 20:32:12 +0800 Subject: [PATCH 9/9] =?UTF-8?q?release:=200.0.103=20=E2=80=94=20index=20or?= =?UTF-8?q?g=20migration=20+=20per-repo=20artifact=20adoption=20(#267=20#2?= =?UTF-8?q?69)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 19 +++++++++++++++++++ mcpp.toml | 2 +- src/toolchain/fingerprint.cppm | 2 +- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6d499c..aff3619 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,25 @@ > 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。 > 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)。 +## [0.0.103] — 2026-07-22 + +> #267/#269:索引侧健壮化一批——mcpplibs 索引 URL 组织迁移(不再依赖 GitHub 重定向)+ 采纳 xlings 0.4.68 per-repo artifact 来源(索引同步不再硬依赖可用的 git)。设计见 `.agents/docs/2026-07-22-issue267-269-index-artifact-and-org-migration-design.md`。 + +### 修复 + +- **mcpplibs 索引 URL 仍指向已迁移的旧组织**(#267-A):索引仓已迁 `mcpplibs/mcpp-index`,mcpp 4 处硬编码旧 `mcpp-community` URL 全靠 GitHub 仓库重定向才能工作——旧名一旦被再次占用即静默指向错误内容。新/旧 URL 与 artifact base 收敛为 `mcpp::config` 三常量;`canonicalize_legacy_index_names` 导出并承担"URL 归一→名字迁移→artifact 填充→去重"单循环(**URL 归一先行**,老配置的名字迁移仍命中、新旧条目在去重处折叠);存量安装经既有文本迁移钩子治愈(config.toml 与 registry `.xlings.json` 的 URL 就地替换),publish Fork 提示与 docs 链接同步。 + +### 新增 + +- **采纳 xlings 0.4.68 per-repo artifact 来源**(#269,#267-B):默认 `mcpplibs` 条目(config.toml 模板 + 内存级默认)声明 `artifact = xlings-res/mcpp-index`,`source = "git"` 为显式退出口;seed 管线由 `(name,url)` pair 升级为 `SeedRepo{name,url,artifact,source}`,非空才发射——无 artifact 的仓输出逐字节不变(测试锁定)。存量 registry `.xlings.json` 唯一治愈通道是文本迁移:URL 归一后就地注入 artifact 字段,幂等闸=res base 已出现即跳过,双间距变体覆盖 mcpp/xlings 两个写者,`subos`/版本绑定等无关状态逐字节保留(**不整文件重生成**)。旧 xlings(<0.4.68)安全忽略新字段,发布端 `xlings-res/mcpp-index` 布局实测零改动。 +- **项目级 `[indices]` artifact/source**(#269 可选项):`IndexSpec` 增两字段,mcpp.toml 与 config.toml `[indices]` 长格式同步解析,经项目 seed 透传;`artifact_applicable()` 编码设计规则——**rev/tag/branch pin(及本地 path)强制 git**(artifact 通道只跟 latest pointer),声明了 artifact 但不适用时告警而非静默同步错误版本。 +- **捆绑 xlings pin 0.4.67 → 0.4.68**(release/cross-build/e2e 三 workflow):0.4.68 携 openxlings/xlings#377(自定义 index artifact)/#380(shim env 去重)。陈旧无消费的 `kXlingsVersion`(0.4.51)对齐至 0.4.68 并由 `mcpp self env` 打印,常量不再能静默漂移。 + +### 备注 + +- e2e 新增 151:fresh init 播种新 URL + artifact;老 org URL/无 artifact 的存量 config.toml 与 `.xlings.json`(pretty/compact 双间距)就地治愈且幂等,无关 `.xlings.json` 状态保留。 +- CN region 对象形态(`{"GLOBAL":..,"CN":..}`)待 gitcode 侧 `xlings-res/mcpp-index` 镜像仓真实部署后再扩(设计文档 P5),避免声明 404 base 白付探测。 + ## [0.0.102] — 2026-07-22 > 四条 issue 一个批次:#261 windows 命令行长度、#257 clang 依赖跟踪缺口、#258 条件段表达力、#254 host/target 双轴不自洽。贯穿主线是**同一决策不许两处推导**,以及本批次新立的第二条不变量**失败必须响,不许静默降级**。设计见 `.agents/docs/2026-07-22-v0.0.102-batch-254-261-design.md`,issue 裁决见 `.agents/docs/2026-07-22-issue-triage-254-261.md`。 diff --git a/mcpp.toml b/mcpp.toml index 3435a0d..ab063aa 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "0.0.102" +version = "0.0.103" description = "Modern C++ build & package management tool" license = "Apache-2.0" authors = ["mcpp-community"] diff --git a/src/toolchain/fingerprint.cppm b/src/toolchain/fingerprint.cppm index ba3ef15..826fac1 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.102"; +inline constexpr std::string_view MCPP_VERSION = "0.0.103"; struct FingerprintInputs { Toolchain toolchain;