Skip to content

Commit dc8f26b

Browse files
authored
feat: [indices] TOML parsing, IndexSpec data structure, and project-level isolation (#39)
Implement the first phase of custom package index support: - Fill in IndexSpec struct in src/pm/index_spec.cppm (name, url, rev, tag, branch, path) with is_local/is_pinned/is_builtin helpers - Parse [indices] section in mcpp.toml (short string form, inline table with url/rev/tag/branch/path, and local path form) - Parse [indices] section in config.toml (global config) - Add indices field to Manifest and GlobalConfig structs - Add projectDir field to xlings::Env; build_command_prefix conditionally sets XLINGS_PROJECT_DIR for project-level isolation - Add ensure_project_index_dir() to create .mcpp/.xlings.json with custom index entries when non-builtin indices are configured - Add Fetcher::read_xpkg_lua_from_path() for local path index support - Update cmd_index_list to display project-level indices from mcpp.toml - Update prepare_build to set up .mcpp/ directory for custom indices - Add E2E test (42_custom_local_index.sh) verifying parsing + display
1 parent 91d5336 commit dc8f26b

7 files changed

Lines changed: 267 additions & 12 deletions

File tree

src/cli.cppm

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import mcpp.xlings;
3434
import mcpp.fetcher;
3535
import mcpp.pm.resolver; // PR-R4: extracted from cli.cppm
3636
import mcpp.pm.commands; // PR-R5: cmd_add / cmd_remove / cmd_update live here now
37+
import mcpp.pm.index_spec; // IndexSpec for [indices] support
3738
import mcpp.pm.mangle; // Level 1 multi-version fallback (cross-major coexistence)
3839
import mcpp.pm.compat; // 0.0.6: namespace field + dotted-name compat shims
3940
import mcpp.pm.dep_spec;
@@ -1206,6 +1207,17 @@ prepare_build(bool print_fingerprint,
12061207
}
12071208
}
12081209
}
1210+
1211+
// Set up project-level .mcpp/ directory for custom indices.
1212+
// This creates .mcpp/.xlings.json with non-builtin, non-local index
1213+
// entries so xlings can clone them into the project-scoped data dir.
1214+
if (!m->indices.empty()) {
1215+
auto cfg2 = get_cfg();
1216+
if (cfg2) {
1217+
mcpp::config::ensure_project_index_dir(**cfg2, *root, m->indices);
1218+
}
1219+
}
1220+
12091221
std::vector<mcpp::modgraph::PackageRoot> packages;
12101222
packages.push_back({*root, *m});
12111223

@@ -2495,10 +2507,31 @@ int cmd_index_list(const mcpplibs::cmdline::ParsedArgs& /*parsed*/) {
24952507
std::println(" {:<15} {}{}",
24962508
r.name, r.url, isDefault ? " (default)" : "");
24972509
}
2498-
return 0;
2510+
} else {
2511+
for (auto& r : *repos) {
2512+
std::println(" {:<15} {}", r.name, r.url);
2513+
}
24992514
}
2500-
for (auto& r : *repos) {
2501-
std::println(" {:<15} {}", r.name, r.url);
2515+
2516+
// Show project-level custom indices from mcpp.toml [indices].
2517+
auto root = find_manifest_root(std::filesystem::current_path());
2518+
if (root) {
2519+
auto m = mcpp::manifest::load(*root / "mcpp.toml");
2520+
if (m && !m->indices.empty()) {
2521+
std::println("");
2522+
std::println("Project indices (mcpp.toml):");
2523+
for (auto& [name, spec] : m->indices) {
2524+
if (spec.is_local()) {
2525+
std::println(" {:<15} {} (local path)", name, spec.path.string());
2526+
} else {
2527+
std::string suffix;
2528+
if (spec.is_builtin()) suffix = " (pin)";
2529+
else if (!spec.tag.empty()) suffix = std::format(" (tag: {})", spec.tag);
2530+
else if (!spec.rev.empty()) suffix = std::format(" (rev: {})", spec.rev.substr(0, 8));
2531+
std::println(" {:<15} {}{}", name, spec.url, suffix);
2532+
}
2533+
}
2534+
}
25022535
}
25032536
return 0;
25042537
}

src/config.cppm

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export module mcpp.config;
2121

2222
import std;
2323
import mcpp.libs.toml;
24+
import mcpp.pm.index_spec;
2425
import mcpp.xlings;
2526

2627
export namespace mcpp::config {
@@ -51,6 +52,10 @@ struct GlobalConfig {
5152
std::string defaultIndex; // "mcpplibs"
5253
std::vector<IndexRepo> indexRepos;
5354

55+
// From config.toml [indices] — custom index repositories (new schema).
56+
// Merged: project mcpp.toml > global config.toml > built-in default.
57+
std::map<std::string, mcpp::pm::IndexSpec> indices;
58+
5459
// From config.toml [cache]
5560
std::int64_t searchTtlSeconds = 3600;
5661

@@ -75,6 +80,21 @@ mcpp::xlings::Env make_xlings_env(const GlobalConfig& cfg) {
7580
return { cfg.xlingsBinary, cfg.xlingsHome() };
7681
}
7782

83+
// Create an xlings::Env that targets the project-level .mcpp/ directory.
84+
// Used when custom (non-builtin) indices are configured in mcpp.toml.
85+
mcpp::xlings::Env make_project_xlings_env(const GlobalConfig& cfg,
86+
const std::filesystem::path& projectDir) {
87+
return { cfg.xlingsBinary, cfg.xlingsHome(), projectDir / ".mcpp" };
88+
}
89+
90+
// Ensure the project-level .mcpp/ directory exists and contains a
91+
// .xlings.json seeded with the custom (non-builtin, non-local) index
92+
// entries. Returns true if a .mcpp/ directory was created/updated.
93+
bool ensure_project_index_dir(
94+
const GlobalConfig& cfg,
95+
const std::filesystem::path& projectDir,
96+
const std::map<std::string, mcpp::pm::IndexSpec>& indices);
97+
7898
struct ConfigError { std::string message; };
7999

80100
// Load (or create) the global config. Idempotent. Performs:
@@ -442,6 +462,27 @@ std::expected<GlobalConfig, ConfigError> load_or_init(
442462
cfg.indexRepos.push_back({ name, it->second.as_string() });
443463
}
444464
}
465+
// [indices] — new-schema custom index repositories.
466+
// Accepts the same short/long/path forms as mcpp.toml [indices].
467+
if (auto* indices_t = doc->get_table("indices")) {
468+
for (auto& [k, v] : *indices_t) {
469+
mcpp::pm::IndexSpec spec;
470+
spec.name = k;
471+
if (v.is_string()) {
472+
spec.url = v.as_string();
473+
} else if (v.is_table()) {
474+
auto& sub = v.as_table();
475+
if (auto it = sub.find("url"); it != sub.end() && it->second.is_string()) spec.url = it->second.as_string();
476+
if (auto it = sub.find("rev"); it != sub.end() && it->second.is_string()) spec.rev = it->second.as_string();
477+
if (auto it = sub.find("tag"); it != sub.end() && it->second.is_string()) spec.tag = it->second.as_string();
478+
if (auto it = sub.find("branch"); it != sub.end() && it->second.is_string()) spec.branch = it->second.as_string();
479+
if (auto it = sub.find("path"); it != sub.end() && it->second.is_string()) spec.path = it->second.as_string();
480+
}
481+
if (!spec.url.empty() || !spec.path.empty())
482+
cfg.indices[k] = std::move(spec);
483+
}
484+
}
485+
445486
// Defaults: only mcpplibs. xlings auto-adds its own standard
446487
// defaults (xim / awesome / scode / d2x) because globalIndexRepos_
447488
// is non-empty (per xlings/src/core/config.cppm). Explicitly listing
@@ -520,6 +561,32 @@ void print_env(const GlobalConfig& cfg) {
520561
}
521562
}
522563

564+
bool ensure_project_index_dir(
565+
const GlobalConfig& cfg,
566+
const std::filesystem::path& projectDir,
567+
const std::map<std::string, mcpp::pm::IndexSpec>& indices)
568+
{
569+
// Collect custom (non-builtin, non-local) indices that need xlings cloning.
570+
std::vector<std::pair<std::string,std::string>> customRepos;
571+
for (auto& [name, spec] : indices) {
572+
if (spec.is_builtin()) continue;
573+
if (spec.is_local()) continue; // local path, mcpp reads directly
574+
customRepos.emplace_back(name, spec.url);
575+
}
576+
577+
if (customRepos.empty()) return false; // nothing to do
578+
579+
auto dotMcpp = projectDir / ".mcpp";
580+
std::error_code ec;
581+
std::filesystem::create_directories(dotMcpp, ec);
582+
583+
// Seed .xlings.json with the custom index entries.
584+
mcpp::xlings::Env env;
585+
env.home = dotMcpp;
586+
mcpp::xlings::seed_xlings_json(env, customRepos);
587+
return true;
588+
}
589+
523590
// M5.5: persist [toolchain].default into config.toml without disturbing
524591
// other fields. Naive: read text, replace/insert one line.
525592
std::expected<void, ConfigError>

src/manifest.cppm

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export module mcpp.manifest;
55
import std;
66
import mcpp.libs.toml;
77
import mcpp.pm.dep_spec; // M5.x pm/ subsystem refactor: DependencySpec lives here
8+
import mcpp.pm.index_spec; // IndexSpec for [indices] section
89

910
export namespace mcpp::manifest {
1011

@@ -182,6 +183,9 @@ struct Manifest {
182183
// [workspace] — multi-package workspace.
183184
WorkspaceConfig workspace;
184185

186+
// [indices] — custom package index repositories (index-name → IndexSpec).
187+
std::map<std::string, mcpp::pm::IndexSpec> indices;
188+
185189
// M5.0: post-parse computed/inferred state
186190
bool usesModules = true; // refined by scanner
187191
bool usesImportStd = true; // refined by scanner
@@ -660,6 +664,41 @@ std::expected<Manifest, ManifestError> parse_string(std::string_view content,
660664
}
661665
}
662666

667+
// [indices] — custom package index repositories.
668+
//
669+
// Accepted forms:
670+
// acme = "git@gitlab.example.com:platform/mcpp-index.git" # short: value = url
671+
// acme-stable = { url = "git@...", tag = "v2.0" } # long: inline table
672+
// local-dev = { path = "/home/user/my-packages" } # local path
673+
// mcpplibs = { url = "https://...", rev = "abc123" } # pin built-in
674+
if (auto* indices_t = doc->get_table("indices")) {
675+
for (auto& [k, v] : *indices_t) {
676+
mcpp::pm::IndexSpec spec;
677+
spec.name = k;
678+
679+
if (v.is_string()) {
680+
// Short form: key = "url"
681+
spec.url = v.as_string();
682+
} else if (v.is_table()) {
683+
auto& sub = v.as_table();
684+
if (auto it = sub.find("url"); it != sub.end() && it->second.is_string()) spec.url = it->second.as_string();
685+
if (auto it = sub.find("rev"); it != sub.end() && it->second.is_string()) spec.rev = it->second.as_string();
686+
if (auto it = sub.find("tag"); it != sub.end() && it->second.is_string()) spec.tag = it->second.as_string();
687+
if (auto it = sub.find("branch"); it != sub.end() && it->second.is_string()) spec.branch = it->second.as_string();
688+
if (auto it = sub.find("path"); it != sub.end() && it->second.is_string()) spec.path = it->second.as_string();
689+
if (spec.url.empty() && spec.path.empty()) {
690+
return std::unexpected(error(origin, std::format(
691+
"[indices].{} must specify 'url' or 'path'", k)));
692+
}
693+
} else {
694+
return std::unexpected(error(origin, std::format(
695+
"[indices].{} must be a string (url) or inline table", k)));
696+
}
697+
698+
m.indices[k] = std::move(spec);
699+
}
700+
}
701+
663702
return m;
664703
}
665704

src/pm/index_spec.cppm

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,27 @@
11
// mcpp.pm.index_spec — package-index repository configuration.
22
//
3-
// Reserved for the upcoming `[indices]` parsing & IndexSpec data type;
4-
// see `.agents/docs/2026-05-08-package-index-config.md` for the full
5-
// design. The module placeholder is created early so the rest of the
6-
// pm/ subsystem can land its imports against a stable module path
7-
// while the implementation arrives.
3+
// `[indices]` in mcpp.toml and config.toml maps index names to their
4+
// location (git URL or local path) with optional version pinning.
5+
// See `.agents/docs/2026-05-16-indices-enhancement-design.md` for the
6+
// full design.
87

98
export module mcpp.pm.index_spec;
109

1110
import std;
1211

1312
export namespace mcpp::pm {
1413

15-
// Placeholder. The full `IndexSpec` (url / rev / tag / branch / path)
16-
// + `[indices]` TOML parsing lands in a dedicated PR per the
17-
// package-index-config design doc.
14+
struct IndexSpec {
15+
std::string name; // index name ([indices] key)
16+
std::string url; // git URL (short form fills this directly)
17+
std::string rev; // commit sha (strongest lock)
18+
std::string tag; // git tag
19+
std::string branch; // git branch
20+
std::filesystem::path path; // local path (takes priority over url)
21+
22+
bool is_local() const { return !path.empty(); }
23+
bool is_pinned() const { return !rev.empty(); }
24+
bool is_builtin() const { return name == "mcpplibs"; }
25+
};
1826

1927
} // namespace mcpp::pm

src/pm/package_fetcher.cppm

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export module mcpp.pm.package_fetcher;
1515
import std;
1616
import mcpp.config;
1717
import mcpp.pm.compat;
18+
import mcpp.pm.index_spec;
1819
import mcpp.xlings;
1920
import mcpp.libs.toml; // re-used for tiny JSON-ish parsing? no — stick with manual
2021

@@ -103,6 +104,12 @@ public:
103104
std::optional<std::string>
104105
read_xpkg_lua(std::string_view ns, std::string_view shortName) const;
105106

107+
// Read the raw xpkg .lua file content from a local path index.
108+
// Used for [indices] entries with `path = "/some/dir"`.
109+
static std::optional<std::string>
110+
read_xpkg_lua_from_path(const std::filesystem::path& indexPath,
111+
std::string_view shortName);
112+
106113
// ─── Legacy overloads (COMPAT, remove in 1.0.0) ─────────────
107114
//
108115
// Accept a raw package name string and infer namespace from it.
@@ -407,6 +414,31 @@ Fetcher::read_xpkg_lua(std::string_view ns, std::string_view shortName) const
407414
return std::nullopt;
408415
}
409416

417+
// ─── read_xpkg_lua from local path index ────────────────────────────
418+
//
419+
// For [indices] entries with `path = "/some/dir"`, read the xpkg .lua
420+
// directly from the filesystem. The index layout follows the standard
421+
// mcpp-index convention: <path>/pkgs/<first-letter>/<name>.lua
422+
423+
std::optional<std::string>
424+
Fetcher::read_xpkg_lua_from_path(const std::filesystem::path& indexPath,
425+
std::string_view shortName)
426+
{
427+
if (shortName.empty()) return std::nullopt;
428+
429+
auto pkgsDir = indexPath / "pkgs";
430+
if (!std::filesystem::exists(pkgsDir)) return std::nullopt;
431+
432+
char first = static_cast<char>(std::tolower(
433+
static_cast<unsigned char>(shortName.front())));
434+
auto candidate = pkgsDir / std::string(1, first) / (std::string(shortName) + ".lua");
435+
if (!std::filesystem::exists(candidate)) return std::nullopt;
436+
437+
std::ifstream is(candidate);
438+
std::stringstream ss; ss << is.rdbuf();
439+
return ss.str();
440+
}
441+
410442
// ─── Legacy read_xpkg_lua (COMPAT, remove in 1.0.0) ─────────────────
411443
//
412444
// Infers namespace from the raw package_name string and delegates to the

src/xlings.cppm

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export namespace mcpp::xlings {
2323
struct Env {
2424
std::filesystem::path binary; // xlings binary path
2525
std::filesystem::path home; // XLINGS_HOME directory
26+
std::filesystem::path projectDir; // XLINGS_PROJECT_DIR (empty = global mode)
2627
};
2728

2829
// ─── Pinned version constants ───────────────────────────────────────
@@ -408,11 +409,23 @@ std::filesystem::path sandbox_init_marker(const Env& env) {
408409

409410
std::string build_command_prefix(const Env& env) {
410411
auto xvmBin = paths::sandbox_bin(env).string();
412+
if (env.projectDir.empty()) {
413+
// Global mode: unset XLINGS_PROJECT_DIR (existing behavior).
414+
return std::format(
415+
"cd {} && env -u XLINGS_PROJECT_DIR PATH={}:\"$PATH\" XLINGS_HOME={} {}",
416+
shq(env.home.string()),
417+
shq(xvmBin),
418+
shq(env.home.string()),
419+
shq(env.binary.string()));
420+
}
421+
// Project-level mode: set XLINGS_PROJECT_DIR so xlings uses
422+
// additive project repos alongside global repos.
411423
return std::format(
412-
"cd {} && env -u XLINGS_PROJECT_DIR PATH={}:\"$PATH\" XLINGS_HOME={} {}",
424+
"cd {} && env PATH={}:\"$PATH\" XLINGS_HOME={} XLINGS_PROJECT_DIR={} {}",
413425
shq(env.home.string()),
414426
shq(xvmBin),
415427
shq(env.home.string()),
428+
shq(env.projectDir.string()),
416429
shq(env.binary.string()));
417430
}
418431

0 commit comments

Comments
 (0)