From 060ab4380fd18b1798244a8efe61a90082e2c7b9 Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Sat, 31 Oct 2026 05:09:51 +0100 Subject: [PATCH] fix: give a project file its args only for the CrafterBuildProject call ProjectArgs() read args that LoadProject recorded when it pushed the frame, so they were there for everything that ran on that frame, not only for CrafterBuildProject. Two things went wrong with that. The pragma pre-pass runs on the frame too, so both of its specs needed a hand-written `.args = {}` to keep the consumer's args from reaching host tooling, and anything else built there would have needed one as well. And dlopen runs a project file's static initialisers only on the first load of that file: a later load gets the same mapping back. A spec made at namespace scope therefore kept the first load's args for every later load of the file, a silently wrong variant. The frame now gets the args just before CrafterBuildProject is called. The pre-pass and the static initialisers see none, on every load, so the two opt-outs go. The dependencies the pre-pass resolved move the other way, to just before dlopen: they are the same on every load, and ProjectDependency() called from a static initialiser used to find none and terminate the process. Co-Authored-By: Claude Opus 5.5 --- implementations/Crafter.Build-Platform.cpp | 44 ++++++++++++---------- interfaces/Crafter.Build-External.cppm | 25 +++++++----- 2 files changed, 40 insertions(+), 29 deletions(-) diff --git a/implementations/Crafter.Build-Platform.cpp b/implementations/Crafter.Build-Platform.cpp index 1d21c9c..f93e9f2 100644 --- a/implementations/Crafter.Build-Platform.cpp +++ b/implementations/Crafter.Build-Platform.cpp @@ -413,9 +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. + // What LoadProject was given, for ProjectArgs(), and only once + // CrafterBuildProject is about to be called (see LoadProject). A view + // is enough: the caller's storage outlives LoadProject, and the frame + // is popped before LoadProject returns. std::span args; std::vector dependencies; }; @@ -430,10 +431,7 @@ namespace { // already on the stack is a cycle, which used to recurse forever. class ProjectFileGuard { public: - // 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) { + explicit ProjectFileGuard(const fs::path& absProject) { for (const ProjectFileFrame& frame : LoadingFrames) { if (frame.project != absProject) continue; std::string chain; @@ -441,7 +439,7 @@ namespace { throw std::runtime_error(std::format("circular project dependency: {}{}", chain, absProject.string())); } index_ = LoadingFrames.size(); - LoadingFrames.push_back({ absProject, args, {} }); + LoadingFrames.push_back({ absProject, {}, {} }); } ~ProjectFileGuard() { LoadingFrames.pop_back(); } ProjectFileGuard(const ProjectFileGuard&) = delete; @@ -503,16 +501,15 @@ 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. + // Neither spec sets args, and the tool still gets none of the + // consumer's: this pre-pass runs before LoadProject hands the + // frame its args, so ProjectArgs() is empty. Given the consumer's + // --target=, the tool's project.cpp would pick sources and flags + // for the wrong triple, and its flags would split the tool's + // variant from the one every other consumer shares. Configuration* cfg = nullptr; if (IsGitSource(pragma.source)) { - GitProjectSpec spec{ .source = { pragma.source, pragma.branch, pragma.commit }, .args = {} }; + GitProjectSpec spec{ .source = { pragma.source, pragma.branch, pragma.commit } }; if (!pragma.project.empty()) spec.projectFile = pragma.project; cfg = GitProject(spec); } else { @@ -526,7 +523,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, .args = {} }); + cfg = LocalProject({ .projectFile = local }); } if (std::ranges::contains(out.configs, cfg)) continue; @@ -902,7 +899,7 @@ namespace { Configuration Crafter::LoadProject(const fs::path& projectFile, std::span args) { RequireToolchain(); fs::path absProject = fs::canonical(projectFile); - ProjectFileGuard guard(absProject, args); + ProjectFileGuard guard(absProject); fs::path buildDir = absProject.parent_path() / "build"; if (!fs::exists(buildDir)) { fs::create_directories(buildDir); @@ -974,6 +971,9 @@ Configuration Crafter::LoadProject(const fs::path& projectFile, std::span ProjectArgs(); // Specification for a sibling crafter-build project to fetch and depend on.