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 00000000..6f83ffc9 --- /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 00000000..fb9ae685 --- /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 再扩)。 diff --git a/.github/workflows/ci-linux-e2e.yml b/.github/workflows/ci-linux-e2e.yml index f60d9a1e..0cc37bac 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 f24d7f70..92bb3ec6 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 9d2a4886..6ff6a9a3 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/CHANGELOG.md b/CHANGELOG.md index c6d499cf..aff36192 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/docs/00-getting-started.md b/docs/00-getting-started.md index 5525a1ae..98b145b9 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 8fd2e06d..3d88d7e8 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 34490dd7..0cb77ab2 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 56f35957..5f011228 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/mcpp.toml b/mcpp.toml index 3435a0d1..ab063aad 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/config.cppm b/src/config.cppm index dda011ce..9236972d 100644 --- a/src/config.cppm +++ b/src/config.cppm @@ -36,8 +36,22 @@ 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 +// 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 +265,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 +377,9 @@ home = "" default = "mcpplibs" [index.repos."mcpplibs"] -url = "https://github.com/mcpp-community/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] @@ -375,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; @@ -396,25 +419,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 +460,35 @@ 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"; + // ③ 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; + }); + if (!duplicate) normalized.push_back(std::move(r)); + } + cfg.indexRepos = std::move(normalized); +} + std::expected load_or_init( bool quiet, BootstrapProgressCallback onBootstrapProgress, @@ -538,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. @@ -556,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); @@ -568,11 +610,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", "https://github.com/mcpp-community/mcpp-index.git"); + add_default("mcpplibs", kMcpplibsIndexUrl, kMcpplibsIndexArtifact); canonicalize_legacy_index_names(cfg); // 5. Seed registry/.xlings.json if missing; migrate legacy cached @@ -651,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()); @@ -663,8 +708,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)" : ""); } } @@ -677,17 +724,28 @@ 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); + // #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/fallback/config_migration.cppm b/src/fallback/config_migration.cppm index 555119e0..56e75872 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; @@ -62,6 +66,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 +83,25 @@ 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"); + + // 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/src/manifest/toml.cppm b/src/manifest/toml.cppm index 5bed64c1..77c5a661 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 27110100..4ee2245b 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/src/publish/pipeline.cppm b/src/publish/pipeline.cppm index b69ea491..aa639a6a 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/src/toolchain/fingerprint.cppm b/src/toolchain/fingerprint.cppm index ba3ef157..826fac1b 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; diff --git a/src/xlings.cppm b/src/xlings.cppm index 7c4ee5eb..e06fe7be 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"; } @@ -247,8 +250,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 +1122,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 +1130,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/e2e/151_index_url_artifact_migration.sh b/tests/e2e/151_index_url_artifact_migration.sh new file mode 100755 index 00000000..1ad8c745 --- /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" diff --git a/tests/unit/test_config.cpp b/tests/unit/test_config.cpp index 6e07cc26..f3d1f2d1 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,151 @@ 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); +} + +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 +} + +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); +} diff --git a/tests/unit/test_manifest.cpp b/tests/unit/test_manifest.cpp index ee855509..ed9b65fb 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()); +} diff --git a/tests/unit/test_pm_package_fetcher.cpp b/tests/unit/test_pm_package_fetcher.cpp index 856eadbc..d5421057 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); +} diff --git a/tests/unit/test_xlings.cpp b/tests/unit/test_xlings.cpp index ccbab5f3..01a54a1b 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); +}