Skip to content

Commit 9e8ed16

Browse files
committed
fix(build): support xlings mcpp build dependencies
1 parent e8d792b commit 9e8ed16

13 files changed

Lines changed: 238 additions & 52 deletions

src/build/compile_commands.cppm

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,15 @@ std::vector<std::string> split_flags(std::string_view s) {
8181
return out;
8282
}
8383

84+
std::vector<std::string> local_include_args(const CompileUnit& cu) {
85+
std::vector<std::string> args;
86+
args.reserve(cu.localIncludeDirs.size());
87+
for (auto const& inc : cu.localIncludeDirs) {
88+
args.push_back("-I" + inc.string());
89+
}
90+
return args;
91+
}
92+
8493
} // namespace
8594

8695
std::string emit_compile_commands(const BuildPlan& plan, const CompileFlags& flags) {
@@ -96,6 +105,8 @@ std::string emit_compile_commands(const BuildPlan& plan, const CompileFlags& fla
96105
// Build arguments array.
97106
nlohmann::json args = nlohmann::json::array();
98107
args.push_back(compiler.string());
108+
for (auto& f : local_include_args(cu))
109+
args.push_back(std::move(f));
99110
for (auto& f : split_flags(flagStr))
100111
args.push_back(std::move(f));
101112
args.push_back("-c");

src/build/ninja_backend.cppm

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,27 @@ std::string escape_ninja_path(const std::filesystem::path& p) {
7272
return out;
7373
}
7474

75+
std::string escape_flag_path(const std::filesystem::path& p) {
76+
auto s = p.string();
77+
std::string out;
78+
out.reserve(s.size());
79+
for (char c : s) {
80+
if (c == ' ' || c == '$' || c == ':')
81+
out.push_back('$');
82+
out.push_back(c);
83+
}
84+
return out;
85+
}
86+
87+
std::string local_include_flags(const CompileUnit& cu) {
88+
std::string flags;
89+
for (auto const& inc : cu.localIncludeDirs) {
90+
flags += " -I";
91+
flags += escape_flag_path(inc);
92+
}
93+
return flags;
94+
}
95+
7596
void write_file(const std::filesystem::path& p, std::string_view content) {
7697
std::filesystem::create_directories(p.parent_path());
7798
std::ofstream os(p);
@@ -294,13 +315,13 @@ std::string emit_ninja_string(const BuildPlan& plan) {
294315
if constexpr (mcpp::platform::is_windows) {
295316
// Windows: skip BMI restat optimization (requires POSIX shell).
296317
append(std::format(" command = "
297-
"$cxx $cxxflags{} -c $in -o $out\n", module_output_flag));
318+
"$cxx $local_includes $cxxflags{} -c $in -o $out\n", module_output_flag));
298319
} else {
299320
append(std::format(" command = "
300321
"if [ -n \"$bmi_out\" ] && [ -f \"$bmi_out\" ]; then "
301322
"cp -p \"$bmi_out\" \"$bmi_out.bak\"; "
302323
"fi && "
303-
"$cxx $cxxflags{} -c $in -o $out && "
324+
"$cxx $local_includes $cxxflags{} -c $in -o $out && "
304325
"if [ -n \"$bmi_out\" ] && [ -f \"$bmi_out.bak\" ] && "
305326
"cmp -s \"$bmi_out\" \"$bmi_out.bak\"; then "
306327
"mv \"$bmi_out.bak\" \"$bmi_out\"; "
@@ -314,15 +335,15 @@ std::string emit_ninja_string(const BuildPlan& plan) {
314335
append("\n");
315336

316337
append("rule cxx_object\n");
317-
append(" command = $cxx $cxxflags -c $in -o $out\n");
338+
append(" command = $cxx $local_includes $cxxflags -c $in -o $out\n");
318339
append(" description = OBJ $out\n");
319340
if (dyndep)
320341
append(" restat = 1\n");
321342
append("\n");
322343

323344
if (need_c_rule) {
324345
append("rule c_object\n");
325-
append(" command = $cc $cflags -c $in -o $out\n");
346+
append(" command = $cc $local_includes $cflags -c $in -o $out\n");
326347
append(" description = CC $out\n");
327348
if (dyndep)
328349
append(" restat = 1\n");
@@ -348,7 +369,7 @@ std::string emit_ninja_string(const BuildPlan& plan) {
348369
append("rule cxx_scan\n");
349370
if (plan.scanDepsPath.empty()) {
350371
// GCC path: compiler-integrated P1689 scanning.
351-
append(" command = $cxx $cxxflags -fmodules "
372+
append(" command = $cxx $local_includes $cxxflags -fmodules "
352373
"-fdeps-format=p1689r5 "
353374
"-fdeps-file=$out -fdeps-target=$compile_target "
354375
"-M -MM -MF $out.dep -E $in -o $compile_target\n");
@@ -358,10 +379,10 @@ std::string emit_ninja_string(const BuildPlan& plan) {
358379
// Wrap in cmd /c for shell redirection (ninja on Windows uses
359380
// CreateProcess which doesn't interpret > as redirect).
360381
append(" command = cmd /c \"$scan_deps -format=p1689 -- "
361-
"$cxx $cxxflags -c $in -o $compile_target > $out\"\n");
382+
"$cxx $local_includes $cxxflags -c $in -o $compile_target > $out\"\n");
362383
} else {
363384
append(" command = $scan_deps -format=p1689 -- "
364-
"$cxx $cxxflags -c $in -o $compile_target > $out\n");
385+
"$cxx $local_includes $cxxflags -c $in -o $compile_target > $out\n");
365386
}
366387
}
367388
append(" description = SCAN $out\n\n");
@@ -438,6 +459,8 @@ std::string emit_ninja_string(const BuildPlan& plan) {
438459
append(std::format("build {} : cxx_scan {}\n", escape_ninja_path(ddi),
439460
escape_ninja_path(cu.source)));
440461
append(std::format(" compile_target = {}\n", escape_ninja_path(cu.object)));
462+
if (auto includes = local_include_flags(cu); !includes.empty())
463+
append(std::format(" local_includes ={}\n", includes));
441464
}
442465
append("\n");
443466

@@ -481,6 +504,8 @@ std::string emit_ninja_string(const BuildPlan& plan) {
481504
} else {
482505
out_line += "\n";
483506
}
507+
if (auto includes = local_include_flags(cu); !includes.empty())
508+
out_line += " local_includes =" + includes + "\n";
484509
append(std::move(out_line));
485510
}
486511
append("\n");
@@ -519,6 +544,8 @@ std::string emit_ninja_string(const BuildPlan& plan) {
519544
if (!implicit.empty())
520545
out_line += " |" + implicit;
521546
out_line += "\n";
547+
if (auto includes = local_include_flags(cu); !includes.empty())
548+
out_line += " local_includes =" + includes + "\n";
522549
// Clang needs $bmi_out to emit -fmodule-output=$bmi_out
523550
if (cu.providesModule) {
524551
out_line += " bmi_out = " + bmi_path(*cu.providesModule) + "\n";

src/build/plan.cppm

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export namespace mcpp::build {
1717
struct CompileUnit {
1818
std::filesystem::path source;
1919
std::filesystem::path object; // relative to plan.outputDir
20+
std::vector<std::filesystem::path> localIncludeDirs;
2021
std::optional<std::string> providesModule; // logical name, if .cppm export
2122
std::vector<std::string> imports; // logical names imported
2223
};
@@ -119,6 +120,7 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest,
119120
auto& u = graph.units[idx];
120121
CompileUnit cu;
121122
cu.source = u.path;
123+
cu.localIncludeDirs = u.localIncludeDirs;
122124
const auto fname = object_filename_for(u.path);
123125
if (basenameCount[fname] > 1) {
124126
// Use <sanitized-pkg>/<parent-dir-name> as prefix to handle

src/cli.cppm

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1375,19 +1375,19 @@ prepare_build(bool print_fingerprint,
13751375
if (cfg2) {
13761376
mcpp::config::ensure_project_index_dir(**cfg2, *root, m->indices);
13771377

1378-
// Gap 1: On first build, .mcpp/data/ may be empty because
1378+
// On first build, .mcpp/data/ may be empty because
13791379
// ensure_project_index_dir only writes .xlings.json but doesn't
1380-
// trigger the actual clone. Check if there are any non-local,
1381-
// non-builtin indices and whether .mcpp/data/ exists with content.
1382-
// If not, run xlings update to clone them before dependency resolution.
1383-
bool hasCustomGitIndices = false;
1380+
// trigger clone/link creation. Check whether there are any custom
1381+
// non-builtin indices and whether .mcpp/data/ has index content.
1382+
// If not, run xlings update before dependency resolution.
1383+
bool hasCustomIndices = false;
13841384
for (auto& [idxName, spec] : m->indices) {
1385-
if (!spec.is_local() && !spec.is_builtin()) {
1386-
hasCustomGitIndices = true;
1385+
if (!spec.is_builtin()) {
1386+
hasCustomIndices = true;
13871387
break;
13881388
}
13891389
}
1390-
if (hasCustomGitIndices) {
1390+
if (hasCustomIndices) {
13911391
auto dataDir = *root / ".mcpp" / "data";
13921392
bool needsClone = !std::filesystem::exists(dataDir);
13931393
if (!needsClone) {
@@ -1521,9 +1521,11 @@ prepare_build(bool print_fingerprint,
15211521
// lua found — fall through to normal install path resolution.
15221522
}
15231523

1524-
// For custom git indices, try project-level .mcpp/data/ first.
1524+
const bool useProjectEnv = idxSpec && !idxSpec->is_builtin();
1525+
1526+
// For custom indices, try project-level .mcpp/data/ first.
15251527
std::optional<std::filesystem::path> installed;
1526-
if (idxSpec && !idxSpec->is_local() && !idxSpec->is_builtin()) {
1528+
if (useProjectEnv) {
15271529
installed = mcpp::fetcher::Fetcher::install_path_from_project_data(
15281530
*root, ns, shortName, version);
15291531
}
@@ -1539,11 +1541,6 @@ prepare_build(bool print_fingerprint,
15391541
: std::format("{}.{}", ns, shortName);
15401542
mcpp::ui::info("Downloading", std::format("{} v{}", fqname, version));
15411543

1542-
// Gap 2: For custom git indices, install using the project-level
1543-
// xlings env so packages land in .mcpp/data/xpkgs/ and the custom
1544-
// index clone is visible to xlings during resolution.
1545-
bool useProjectEnv = idxSpec && !idxSpec->is_local() && !idxSpec->is_builtin();
1546-
15471544
auto install_one = [&](std::string target) -> std::expected<mcpp::xlings::CallResult, mcpp::pm::CallError> {
15481545
if (useProjectEnv) {
15491546
auto projEnv = mcpp::config::make_project_xlings_env(**cfg, *root);
@@ -1559,7 +1556,7 @@ prepare_build(bool print_fingerprint,
15591556
return fetcher.install(targets, &progress);
15601557
};
15611558
auto target = std::format("{}@{}", fqname, version);
1562-
// For custom git indices, use indexName:shortName@version format
1559+
// For custom indices, use indexName:shortName@version format
15631560
// so xlings knows which index to resolve from.
15641561
if (useProjectEnv) {
15651562
target = std::format("{}:{}@{}", ns, shortName, version);
@@ -1800,6 +1797,9 @@ prepare_build(bool print_fingerprint,
18001797
const auto& name = item.name;
18011798
auto& spec = item.spec;
18021799

1800+
mcpp::pm::compat::normalize_nested_namespace(
1801+
spec.namespace_, spec.shortName);
1802+
18031803
// Pin SemVer constraint before dedup/fetch.
18041804
if (auto r = resolveSemver(spec, name); !r) {
18051805
return std::unexpected(r.error());

src/config.cppm

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -613,11 +613,18 @@ bool ensure_project_index_dir(
613613
const std::filesystem::path& projectDir,
614614
const std::map<std::string, mcpp::pm::IndexSpec>& indices)
615615
{
616-
// Collect custom (non-builtin, non-local) indices that need xlings cloning.
616+
// Collect custom non-builtin indices that need xlings project-scope data.
617+
// Local path indices are also seeded so xlings can create its own
618+
// project-local repo link and install packages from that index.
617619
std::vector<std::pair<std::string,std::string>> customRepos;
618620
for (auto& [name, spec] : indices) {
619621
if (spec.is_builtin()) continue;
620-
if (spec.is_local()) continue; // local path, mcpp reads directly
622+
if (spec.is_local()) {
623+
auto source = spec.path;
624+
if (source.is_relative()) source = projectDir / source;
625+
customRepos.emplace_back(name, source.lexically_normal().string());
626+
continue;
627+
}
621628
customRepos.emplace_back(name, spec.url);
622629
}
623630

src/libs/toml.cppm

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,12 @@ inline std::expected<Value, ParseError> read_array(Lexer& L) {
224224
if (!v) return std::unexpected(v.error());
225225
out.push_back(std::move(*v));
226226
L.skip_whitespace_and_comments();
227-
if (L.peek() == ',') { L.advance(); continue; }
227+
if (L.peek() == ',') {
228+
L.advance();
229+
L.skip_whitespace_and_comments();
230+
if (L.peek() == ']') { L.advance(); break; }
231+
continue;
232+
}
228233
if (L.peek() == ']') { L.advance(); break; }
229234
return std::unexpected(ParseError{"expected ',' or ']' in array", L.position()});
230235
}

src/modgraph/graph.cppm

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ struct ModuleId {
1515
struct SourceUnit {
1616
std::filesystem::path path;
1717
std::string packageName;
18+
std::vector<std::filesystem::path> localIncludeDirs;
1819
std::optional<ModuleId> provides;
1920
std::vector<ModuleId> requires_;
2021
bool isModuleInterface = false; // .cppm with export module

src/modgraph/scanner.cppm

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,23 @@ std::expected<SourceUnit, ScanError> scan_file(const std::filesystem::path& file
314314

315315
namespace {
316316

317+
std::vector<std::filesystem::path>
318+
local_include_dirs_for(const std::filesystem::path& root,
319+
const mcpp::manifest::Manifest& manifest)
320+
{
321+
std::vector<std::filesystem::path> dirs;
322+
for (auto const& inc : manifest.buildConfig.includeDirs) {
323+
if (inc.is_absolute()) {
324+
dirs.push_back(inc);
325+
continue;
326+
}
327+
for (auto& d : expand_dir_glob(root, inc.generic_string())) {
328+
dirs.push_back(std::move(d));
329+
}
330+
}
331+
return dirs;
332+
}
333+
317334
// Phase 1: scan a single package, append units to result.graph.units;
318335
// errors go straight into result.errors. producerOf/edges are NOT built
319336
// here — the caller does that after all packages are scanned.
@@ -348,13 +365,15 @@ void scan_one_into(ScanResult& result,
348365
manifest.package.namespace_.empty()
349366
? manifest.package.name
350367
: manifest.package.namespace_ + "." + manifest.package.name;
368+
const auto localIncludeDirs = local_include_dirs_for(root, manifest);
351369

352370
for (auto const& f : all_files) {
353371
auto r = scan_file(f, qualifiedName);
354372
if (!r) {
355373
result.errors.push_back(r.error());
356374
continue;
357375
}
376+
r->localIncludeDirs = localIncludeDirs;
358377
result.graph.units.push_back(std::move(*r));
359378
}
360379
}
@@ -422,13 +441,15 @@ ScanResult scan_packages_p1689(const std::vector<PackageRoot>& packages,
422441
for (auto const& g : p.manifest.modules.sources) {
423442
for (auto& f : expand_glob(p.root, g)) all_files.insert(f);
424443
}
444+
const auto localIncludeDirs = local_include_dirs_for(p.root, p.manifest);
425445
for (auto const& f : all_files) {
426446
auto r = mcpp::modgraph::p1689::scan_file(
427447
f, p.manifest.package.name, tc, tmpDir);
428448
if (!r) {
429449
result.errors.push_back(ScanError{ f, 0, r.error() });
430450
continue;
431451
}
452+
r->localIncludeDirs = localIncludeDirs;
432453
result.graph.units.push_back(std::move(*r));
433454
}
434455
}

src/pm/compat.cppm

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,22 @@ inline std::string qualified_name(std::string_view ns,
8282
return std::format("{}.{}", ns, shortName);
8383
}
8484

85+
// Normalize legacy nested names after the first-dot split:
86+
// ns="mcpplibs", shortName="capi.lua" → ns="mcpplibs.capi", shortName="lua".
87+
//
88+
// This preserves the fully qualified name while making dependency de-dup use
89+
// the same structured key as TOML-native [dependencies."mcpplibs.capi"].
90+
inline void normalize_nested_namespace(std::string& ns, std::string& shortName)
91+
{
92+
if (ns.empty()) return;
93+
auto dot = shortName.rfind('.');
94+
if (dot == std::string::npos || dot + 1 >= shortName.size()) return;
95+
96+
ns += ".";
97+
ns += shortName.substr(0, dot);
98+
shortName = shortName.substr(dot + 1);
99+
}
100+
85101
// ─── Index directory naming ──────────────────────────────────────────
86102
//
87103
// Maps (indexName, namespace, shortName) → the xpkgs subdirectory name
@@ -201,6 +217,17 @@ inline std::vector<std::string> install_dir_candidates(std::string_view ns,
201217
candidates.push_back(std::format("{}-x-{}", indexName, shortName));
202218
}
203219

220+
// Legacy dotted dependency keys such as "mcpplibs.capi.lua" are parsed as
221+
// ns="mcpplibs", shortName="capi.lua". Some xlings indices store these as
222+
// nested namespaces: mcpplibs.capi-x-mcpplibs.capi.lua.
223+
if (!ns.empty()) {
224+
auto dot = shortName.rfind('.');
225+
if (dot != std::string_view::npos) {
226+
auto nestedNs = std::format("{}.{}", ns, shortName.substr(0, dot));
227+
candidates.push_back(std::format("{}-x-{}", nestedNs, fqname));
228+
}
229+
}
230+
204231
return candidates;
205232
}
206233

0 commit comments

Comments
 (0)