From aec93617eec7b49fbd34ebf567c2c84ae77f0156 Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Sat, 31 Oct 2026 02:56:14 +0100 Subject: [PATCH] feat(external): forward a project's args to its sibling projects by default Every project.cpp with a sibling dependency opened with the same line, std::vector depArgs(args.begin(), args.end()); and handed depArgs to each GitProject and LocalProject. CrafterBuildProject receives a span of string_views and the specs wanted owned strings, and without the copy the sibling got none of the project's args: it was built for the host while the project cross-compiled, and in release while it built --debug. GitProject then turned the copy straight back into string_views for LoadProject. Both specs now default their args to ProjectArgs(), the args LoadProject was given for the project file whose CrafterBuildProject is running. It reads the per-thread stack LoadProject already keeps for IsTopLevelProject and ProjectDependency, whose frames now record the args. A default member initializer rather than a new type or an optional: the member stays a std::vector, so every existing `.args = depArgs` keeps its exact meaning (each of the ~50 calls across the Catcrafts projects passes one), and `.args = {}` still forwards nothing. It is the shape of Configuration::target = HostTarget(). A project.cpp that drops its .args needs a crafter-build with this change: an older one compiles it and forwards nothing. ProjectArgs() is declared in :External, next to the defaults that call it. :Platform, where it is defined, cannot be imported there: its unexported `struct Configuration;` would make :External's exported one an ill-formed redeclaration, which clang rejects. The #pragma crafter dependency path now passes `.args = {}` explicitly. Host tooling is built for the host whatever the consumer targets and was loaded with no args; the default would otherwise hand it the consumer's --target= and project flags. Co-Authored-By: Claude Opus 5.5 --- implementations/Crafter.Build-Platform.cpp | 36 +++++++++++++++++----- interfaces/Crafter.Build-External.cppm | 25 ++++++++++++--- 2 files changed, 48 insertions(+), 13 deletions(-) diff --git a/implementations/Crafter.Build-Platform.cpp b/implementations/Crafter.Build-Platform.cpp index e4ead32..1d21c9c 100644 --- a/implementations/Crafter.Build-Platform.cpp +++ b/implementations/Crafter.Build-Platform.cpp @@ -413,6 +413,10 @@ namespace { struct ProjectFileFrame { fs::path project; + // What LoadProject was given, for ProjectArgs(). A view is enough: + // the caller's storage outlives LoadProject, and the frame is popped + // before LoadProject returns. + std::span args; std::vector dependencies; }; @@ -426,7 +430,10 @@ namespace { // already on the stack is a cycle, which used to recurse forever. class ProjectFileGuard { public: - explicit ProjectFileGuard(const fs::path& absProject) { + // The args are recorded with the frame rather than just before + // CrafterBuildProject runs, so a spec built while dlopen runs the + // project file's static initialisers sees them too. + ProjectFileGuard(const fs::path& absProject, std::span args) { for (const ProjectFileFrame& frame : LoadingFrames) { if (frame.project != absProject) continue; std::string chain; @@ -434,7 +441,7 @@ namespace { throw std::runtime_error(std::format("circular project dependency: {}{}", chain, absProject.string())); } index_ = LoadingFrames.size(); - LoadingFrames.push_back({ absProject, {} }); + LoadingFrames.push_back({ absProject, args, {} }); } ~ProjectFileGuard() { LoadingFrames.pop_back(); } ProjectFileGuard(const ProjectFileGuard&) = delete; @@ -451,8 +458,9 @@ namespace { // same PCM cache LoadProject populates — the hostConfig LoadProject itself // compiles project.so with, so the dependency's std/Crafter.Build PCMs // land in the cache dir project.so is built against. Recursive, because - // the dependency's project.cpp forwards no arguments but still reads - // CRAFTER_BUILD_MARCH from the environment for its own deps. + // the dependency's project.cpp is loaded with no arguments and so passes + // none on, while its own deps still read CRAFTER_BUILD_MARCH from the + // environment. void ForceHostTooling(Configuration& cfg, const Configuration& host, std::unordered_set& seen) { if (!seen.insert(&cfg).second) return; cfg.target = host.target; @@ -495,10 +503,16 @@ namespace { std::set libs; for (const PragmaDependency& pragma : pragmas) { + // Host tooling is loaded with none of the consumer's args, so both + // specs pass `.args = {}` instead of defaulting to them. The tool + // is built for the host whatever the consumer targets: given the + // consumer's --target=, its project.cpp would pick sources and + // flags for the wrong triple before ForceHostTooling resets the + // fields, and the consumer's own flags would hash it apart from + // the build every other consumer of the tool shares. Configuration* cfg = nullptr; if (IsGitSource(pragma.source)) { - GitProjectSpec spec; - spec.source = { pragma.source, pragma.branch, pragma.commit }; + GitProjectSpec spec{ .source = { pragma.source, pragma.branch, pragma.commit }, .args = {} }; if (!pragma.project.empty()) spec.projectFile = pragma.project; cfg = GitProject(spec); } else { @@ -512,7 +526,7 @@ namespace { PragmaError(absProject, pragma.line, "'project' only applies to a directory source"); } if (!fs::exists(local)) PragmaError(absProject, pragma.line, std::format("no such project file: {}", local.string())); - cfg = LocalProject({ .projectFile = local }); + cfg = LocalProject({ .projectFile = local, .args = {} }); } if (std::ranges::contains(out.configs, cfg)) continue; @@ -580,6 +594,12 @@ bool Crafter::IsTopLevelProject() { return LoadingFrames.size() <= 1; } +std::vector Crafter::ProjectArgs() { + if (LoadingFrames.empty()) return {}; + const std::span args = LoadingFrames.back().args; + return std::vector(args.begin(), args.end()); +} + namespace { // The one reading of a wait status (pclose, system) for every helper // that runs a command: a signal death is reported the way a shell @@ -882,7 +902,7 @@ namespace { Configuration Crafter::LoadProject(const fs::path& projectFile, std::span args) { RequireToolchain(); fs::path absProject = fs::canonical(projectFile); - ProjectFileGuard guard(absProject); + ProjectFileGuard guard(absProject, args); fs::path buildDir = absProject.parent_path() / "build"; if (!fs::exists(buildDir)) { fs::create_directories(buildDir); diff --git a/interfaces/Crafter.Build-External.cppm b/interfaces/Crafter.Build-External.cppm index fe8e229..2ab2760 100644 --- a/interfaces/Crafter.Build-External.cppm +++ b/interfaces/Crafter.Build-External.cppm @@ -54,16 +54,31 @@ export namespace Crafter { std::vector ExternalIncludeFlags(const ExternalDependency& dep, std::string_view target); ExternalBuildResult BuildExternal(const ExternalDependency& dep, std::string_view target, std::atomic& cancelled); + // The args LoadProject was given for the project file currently running + // its CrafterBuildProject: what that function receives, standard flags + // included (cfg.projectArgs is only the part ApplyStandardArgs left + // over). Empty when no project file is running, and on any thread but + // the one loading it, because it reads the stack LoadProject keeps per + // thread, as IsTopLevelProject does. The specs below default their args + // to it. Defined beside LoadProject, but declared here so those defaults + // can see it: importing :Platform into this partition would make the + // exported `struct Configuration;` above a redeclaration of :Platform's + // unexported one, which clang rejects. + std::vector ProjectArgs(); + // Specification for a sibling crafter-build project to fetch and depend on. // GitSource picks the revision: leave branch + commit empty for the // remote's default branch HEAD (sloppy but convenient), set branch to // track a branch tip, or set commit to pin to an immutable SHA. args are - // forwarded verbatim to the dep's CrafterBuildProject — typically you - // pass through the parent's args so target/march/debug propagate. + // what the dep's CrafterBuildProject receives. They default to this + // project's own, so --target=, --march=, --debug and flags of its own + // such as --local reach a whole chain of sibling projects without being + // passed. Set them to forward something else: a copy without one of this + // project's flags, one with an extra --target=, or {} for nothing. struct GitProjectSpec { GitSource source; fs::path projectFile = "project.cpp"; - std::vector args; + std::vector args = ProjectArgs(); }; // Clones the spec's git URL into the per-project external cache (keyed by @@ -77,8 +92,8 @@ export namespace Crafter { // example folder depending on its parent project, or a workspace where // multiple projects live side-by-side without going through git). struct LocalProjectSpec { - fs::path projectFile; // relative to cwd or absolute - std::vector args; // forwarded to dep's CrafterBuildProject + fs::path projectFile; // relative to cwd or absolute + std::vector args = ProjectArgs(); // as GitProjectSpec::args }; // Same as GitProject but for a local on-disk project — no fetch, no