Skip to content

Commit f9c344e

Browse files
committed
feat(index): per-repo artifact source plumbing — IndexRepo/SeedRepo/toml/template (#269)
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.
1 parent 8e5efa5 commit f9c344e

4 files changed

Lines changed: 129 additions & 19 deletions

File tree

‎src/config.cppm‎

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ inline constexpr std::string_view kXlingsPinnedVersion = mcpp::xlings::pinned::k
3636
struct IndexRepo {
3737
std::string name;
3838
std::string url;
39+
std::string artifact; // optional artifact source base (xlings >= 0.4.68, #269)
40+
std::string source; // optional "auto" | "artifact" | "git" ("" = xlings default auto)
3941
};
4042

4143
// Canonical mcpplibs index coordinates. The index repository moved from the
@@ -375,7 +377,9 @@ home = ""
375377
default = "mcpplibs"
376378
377379
[index.repos."mcpplibs"]
378-
url = "https://github.com/mcpplibs/mcpp-index.git"
380+
url = "https://github.com/mcpplibs/mcpp-index.git"
381+
artifact = "https://github.com/xlings-res/mcpp-index"
382+
# source = "auto" # default: artifact first, git fallback; set "git" to force git
379383
# xlings auto-adds xim / awesome / scode / d2x as defaults.
380384
381385
[cache]
@@ -393,10 +397,11 @@ bool write_default_xlings_json(const std::filesystem::path& path,
393397
const std::vector<IndexRepo>& repos,
394398
std::string_view mirror_override = {})
395399
{
396-
// Delegate to xlings module. Convert IndexRepo vec to pair span.
397-
std::vector<std::pair<std::string,std::string>> pairs;
400+
// Delegate to xlings module. Convert IndexRepo vec to SeedRepo span.
401+
std::vector<mcpp::xlings::SeedRepo> pairs;
398402
pairs.reserve(repos.size());
399-
for (auto& r : repos) pairs.emplace_back(r.name, r.url);
403+
for (auto& r : repos)
404+
pairs.push_back({ r.name, r.url, r.artifact, r.source });
400405
// seed_xlings_json writes to env.home / ".xlings.json", so we
401406
// construct a temporary Env with home = path.parent_path().
402407
mcpp::xlings::Env env;
@@ -469,6 +474,12 @@ void canonicalize_legacy_index_names(GlobalConfig& cfg) {
469474
// repo that happens to be named "mcpp-index" untouched.
470475
if (r.name == "mcpp-index" && r.url == kMcpplibsIndexUrl)
471476
r.name = "mcpplibs";
477+
// ③ In-memory artifact default for the official index (#269): heals
478+
// configs written by pre-artifact templates on every load without
479+
// text surgery on user-editable config.toml. `source = "git"` is the
480+
// explicit opt-out; a user-set artifact base always wins.
481+
if (r.url == kMcpplibsIndexUrl && r.artifact.empty() && r.source != "git")
482+
r.artifact = std::string(kMcpplibsIndexArtifact);
472483
bool duplicate = std::any_of(normalized.begin(), normalized.end(),
473484
[&](const IndexRepo& existing) {
474485
return existing.name == r.name && existing.url == r.url;
@@ -562,7 +573,12 @@ std::expected<GlobalConfig, ConfigError> load_or_init(
562573
auto& tt = val.as_table();
563574
auto it = tt.find("url");
564575
if (it == tt.end() || !it->second.is_string()) continue;
565-
cfg.indexRepos.push_back({ name, it->second.as_string() });
576+
IndexRepo r{ name, it->second.as_string() };
577+
if (auto a = tt.find("artifact"); a != tt.end() && a->second.is_string())
578+
r.artifact = a->second.as_string();
579+
if (auto s = tt.find("source"); s != tt.end() && s->second.is_string())
580+
r.source = s->second.as_string();
581+
cfg.indexRepos.push_back(std::move(r));
566582
}
567583
}
568584
// [indices] — new-schema custom index repositories.
@@ -592,11 +608,13 @@ std::expected<GlobalConfig, ConfigError> load_or_init(
592608
// them ourselves can cause cross-index name conflicts during
593609
// dependency resolution (e.g. linux-headers existing in both
594610
// scode and xim). See docs/21 §VII.
595-
auto add_default = [&](std::string_view name, std::string_view url) {
611+
auto add_default = [&](std::string_view name, std::string_view url,
612+
std::string_view artifact = {}) {
596613
for (auto& r : cfg.indexRepos) if (r.name == name) return;
597-
cfg.indexRepos.push_back({ std::string(name), std::string(url) });
614+
cfg.indexRepos.push_back({ std::string(name), std::string(url),
615+
std::string(artifact), std::string() });
598616
};
599-
add_default("mcpplibs", kMcpplibsIndexUrl);
617+
add_default("mcpplibs", kMcpplibsIndexUrl, kMcpplibsIndexArtifact);
600618
canonicalize_legacy_index_names(cfg);
601619

602620
// 5. Seed registry/.xlings.json if missing; migrate legacy cached
@@ -687,8 +705,10 @@ void print_env(const GlobalConfig& cfg) {
687705
std::println("Index repos:");
688706
for (auto& r : cfg.indexRepos) {
689707
bool isDefault = (r.name == cfg.defaultIndex);
690-
std::println(" {} {}{}",
691-
r.name, r.url, isDefault ? " (default)" : "");
708+
std::println(" {} {}{}{}",
709+
r.name, r.url,
710+
r.artifact.empty() ? "" : " [artifact]",
711+
isDefault ? " (default)" : "");
692712
}
693713
}
694714

@@ -701,17 +721,17 @@ bool ensure_project_index_dir(
701721
// Collect custom non-builtin indices that need xlings project-scope data.
702722
// Local path indices are also seeded so xlings can create its own
703723
// project-local repo link and install packages from that index.
704-
std::vector<std::pair<std::string,std::string>> customRepos;
724+
std::vector<mcpp::xlings::SeedRepo> customRepos;
705725
for (auto& [name, spec] : indices) {
706726
if (spec.is_builtin()) continue;
707727
if (spec.is_local()) {
708728
auto source = resolve_project_index_path(projectDir, spec);
709729
std::error_code ec;
710730
std::filesystem::remove(source / ".xlings-index-cache.json", ec);
711-
customRepos.emplace_back(name, source.generic_string());
731+
customRepos.push_back({ name, source.generic_string(), "", "" });
712732
continue;
713733
}
714-
customRepos.emplace_back(name, spec.url);
734+
customRepos.push_back({ name, spec.url, "", "" });
715735
}
716736

717737
// NOTE: the official global `xim` index is deliberately NOT injected into

‎src/xlings.cppm‎

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -247,8 +247,19 @@ struct ProjectEnv {
247247
}
248248
};
249249

250+
// One index_repos entry for seed_xlings_json. `artifact`/`source` are the
251+
// xlings >= 0.4.68 per-repo artifact-sync fields (#269); empty means "do not
252+
// emit the key", which keeps pre-artifact output byte-identical and matches
253+
// xlings' "undeclared = plain git" semantics. Older xlings ignores both keys.
254+
struct SeedRepo {
255+
std::string name;
256+
std::string url;
257+
std::string artifact; // artifact source base, e.g. https://github.com/xlings-res/mcpp-index
258+
std::string source; // "auto" | "artifact" | "git"
259+
};
260+
250261
void seed_xlings_json(const Env& env,
251-
std::span<const std::pair<std::string,std::string>> repos,
262+
std::span<const SeedRepo> repos,
252263
std::string_view mirror = "auto",
253264
const ProjectEnv& penv = {});
254265

@@ -1108,18 +1119,24 @@ int install_direct(const Env& env, std::string_view target, bool quiet) {
11081119
// ─── Sandbox lifecycle ──────────────────────────────────────────────
11091120

11101121
void seed_xlings_json(const Env& env,
1111-
std::span<const std::pair<std::string,std::string>> repos,
1122+
std::span<const SeedRepo> repos,
11121123
std::string_view mirror,
11131124
const ProjectEnv& penv)
11141125
{
11151126
auto path = env.home / ".xlings.json";
11161127
std::string json = "{\n";
11171128
json += " \"index_repos\": [\n";
11181129
for (std::size_t i = 0; i < repos.size(); ++i) {
1119-
json += std::format(" {{ \"name\": \"{}\", \"url\": \"{}\" }}{}\n",
1120-
json_escape(repos[i].first),
1121-
json_escape(repos[i].second),
1122-
i + 1 == repos.size() ? "" : ",");
1130+
json += std::format(" {{ \"name\": \"{}\", \"url\": \"{}\"",
1131+
json_escape(repos[i].name),
1132+
json_escape(repos[i].url));
1133+
if (!repos[i].artifact.empty())
1134+
json += std::format(", \"artifact\": \"{}\"",
1135+
json_escape(repos[i].artifact));
1136+
if (!repos[i].source.empty())
1137+
json += std::format(", \"source\": \"{}\"",
1138+
json_escape(repos[i].source));
1139+
json += std::format(" }}{}\n", i + 1 == repos.size() ? "" : ",");
11231140
}
11241141
json += " ],\n";
11251142
// [xlings] build environment (L-1): materialize deps/workspace/subos/envs

‎tests/unit/test_config.cpp‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,3 +223,30 @@ TEST(ConfigIndexMigration, MigrateConfigTomlRewritesOrgUrlIdempotently) {
223223
EXPECT_EQ(read_all(p), text);
224224
std::filesystem::remove_all(dir);
225225
}
226+
227+
TEST(ConfigIndexMigration, CanonicalizeFillsDefaultArtifactForOfficialRepo) {
228+
mcpp::config::GlobalConfig cfg;
229+
cfg.indexRepos.push_back({ "mcpplibs",
230+
std::string(mcpp::config::kMcpplibsIndexUrlLegacy) });
231+
mcpp::config::canonicalize_legacy_index_names(cfg);
232+
ASSERT_EQ(cfg.indexRepos.size(), 1u);
233+
EXPECT_EQ(cfg.indexRepos[0].artifact, mcpp::config::kMcpplibsIndexArtifact);
234+
}
235+
236+
TEST(ConfigIndexMigration, CanonicalizeRespectsExplicitGitSourceAndCustomArtifact) {
237+
mcpp::config::GlobalConfig cfg;
238+
mcpp::config::IndexRepo git{ "mcpplibs",
239+
std::string(mcpp::config::kMcpplibsIndexUrl) };
240+
git.source = "git";
241+
cfg.indexRepos.push_back(git);
242+
mcpp::config::IndexRepo custom{ "mirror",
243+
std::string(mcpp::config::kMcpplibsIndexUrl) };
244+
custom.artifact = "https://example.com/my-mirror";
245+
cfg.indexRepos.push_back(custom);
246+
cfg.indexRepos.push_back({ "other", "https://example.com/other-index.git" });
247+
mcpp::config::canonicalize_legacy_index_names(cfg);
248+
ASSERT_EQ(cfg.indexRepos.size(), 3u);
249+
EXPECT_TRUE(cfg.indexRepos[0].artifact.empty()); // explicit git opt-out
250+
EXPECT_EQ(cfg.indexRepos[1].artifact, "https://example.com/my-mirror");
251+
EXPECT_TRUE(cfg.indexRepos[2].artifact.empty()); // foreign repo untouched
252+
}

‎tests/unit/test_xlings.cpp‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,3 +408,49 @@ TEST(XlingsHomeTool, FindsPayloadUnderNonXimPrefix) {
408408

409409
std::filesystem::remove_all(tmp);
410410
}
411+
412+
// ─── seed_xlings_json — per-repo artifact/source emission (#269) ──────
413+
414+
namespace {
415+
416+
std::string read_seeded(const std::filesystem::path& home) {
417+
std::stringstream ss;
418+
ss << std::ifstream(home / ".xlings.json").rdbuf();
419+
return ss.str();
420+
}
421+
422+
} // namespace
423+
424+
TEST(XlingsSeed, PlainRepoEmissionIsByteStable) {
425+
// Zero-diff gate: repos without artifact/source must serialize exactly
426+
// as before the SeedRepo extension.
427+
auto dir = make_tempdir("mcpp-seed-plain");
428+
mcpp::xlings::Env env;
429+
env.home = dir;
430+
std::vector<mcpp::xlings::SeedRepo> repos{
431+
{ "mcpplibs", "https://x.git", "", "" } };
432+
mcpp::xlings::seed_xlings_json(env, repos);
433+
auto text = read_seeded(dir);
434+
EXPECT_NE(text.find(
435+
" { \"name\": \"mcpplibs\", \"url\": \"https://x.git\" }\n"),
436+
std::string::npos) << text;
437+
EXPECT_EQ(text.find("artifact"), std::string::npos);
438+
EXPECT_EQ(text.find("source"), std::string::npos);
439+
std::filesystem::remove_all(dir);
440+
}
441+
442+
TEST(XlingsSeed, ArtifactAndSourceEmittedWhenSet) {
443+
auto dir = make_tempdir("mcpp-seed-art");
444+
mcpp::xlings::Env env;
445+
env.home = dir;
446+
std::vector<mcpp::xlings::SeedRepo> repos{
447+
{ "mcpplibs", "https://x.git",
448+
"https://github.com/xlings-res/mcpp-index", "auto" } };
449+
mcpp::xlings::seed_xlings_json(env, repos);
450+
auto text = read_seeded(dir);
451+
EXPECT_NE(text.find(
452+
"\"url\": \"https://x.git\", \"artifact\": "
453+
"\"https://github.com/xlings-res/mcpp-index\", \"source\": \"auto\""),
454+
std::string::npos) << text;
455+
std::filesystem::remove_all(dir);
456+
}

0 commit comments

Comments
 (0)