From e420c9f95861de563ef4cd263527f4e0d5f90603 Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Sat, 31 Oct 2026 05:10:21 +0100 Subject: [PATCH] fix: keep host tooling out of the project cache the target uses ForceHostTooling rewrites a `#pragma crafter dependency` and everything it loads in place: host target, march native, release, -fPIC. Those are entries of the project cache like any other, keyed by project file and args alone. A project that loaded the same sibling as its tool, with the same args, got the rewritten entry back. That happens whenever the project gets no args, since host tooling gets none either: a consumer built for x86-64-v2 then imported a sibling's BMI compiled with -march=native, and clang refused it. Any arg on the command line split the keys and hid it. A thread-local count of pragma loads now marks the tool's load and every load inside it, its project file's siblings included, and GitProject and LocalProject put that mark in the key. The clone stays shared; only the Configuration is separate, as for two arg lists. A module-internal function rather than a spec field, because the siblings of a tool are loaded by the tool's own project.cpp, which cannot know it is host tooling. Co-Authored-By: Claude Opus 5.5 --- implementations/Crafter.Build-External.cpp | 7 +-- implementations/Crafter.Build-Platform.cpp | 58 ++++++++++++++++------ interfaces/Crafter.Build-Platform.cppm | 6 +++ 3 files changed, 52 insertions(+), 19 deletions(-) diff --git a/implementations/Crafter.Build-External.cpp b/implementations/Crafter.Build-External.cpp index 6e12988..ff0c766 100644 --- a/implementations/Crafter.Build-External.cpp +++ b/implementations/Crafter.Build-External.cpp @@ -360,11 +360,12 @@ Configuration* Crafter::GitProject(const GitProjectSpec& spec) { // source revision (forwarded args don't change what gets cloned, just // how the dep's project.cpp interprets them at runtime), while the // in-memory Configuration is keyed by the full spec so each (args) - // permutation gets its own Configuration object. + // permutation gets its own Configuration object, and host tooling one + // apart from the target's even for the same args. std::string srcKey = std::format("git|{}|{}|{}|{}", spec.source.url, spec.source.branch, spec.source.commit, spec.projectFile.string()); std::string srcHash = std::format("{:016x}", std::hash{}(srcKey)); - std::string fullKey = srcKey; + std::string fullKey = std::format("{}{}", LoadingHostTooling() ? "host|" : "", srcKey); for (const std::string& a : spec.args) { fullKey += '|'; fullKey += a; @@ -449,7 +450,7 @@ Configuration* Crafter::LocalProject(const LocalProjectSpec& spec) { } fs::path canonical = fs::canonical(projectPath); - std::string fullKey = std::format("local|{}", canonical.string()); + std::string fullKey = std::format("{}local|{}", LoadingHostTooling() ? "host|" : "", canonical.string()); for (const std::string& a : spec.args) { fullKey += '|'; fullKey += a; diff --git a/implementations/Crafter.Build-Platform.cpp b/implementations/Crafter.Build-Platform.cpp index f93e9f2..b727cbb 100644 --- a/implementations/Crafter.Build-Platform.cpp +++ b/implementations/Crafter.Build-Platform.cpp @@ -451,6 +451,21 @@ namespace { std::size_t index_ = 0; }; + // How many `#pragma crafter dependency` loads this thread is inside. The + // tool's project file runs within its load, and so does every sibling it + // loads in turn, which is what LoadingHostTooling() reports to GitProject + // and LocalProject. Per thread like LoadingFrames: a sibling loaded on a + // thread of the tool's own goes uncounted, as it gets no ProjectArgs(). + thread_local std::int32_t HostToolingLoads = 0; + + class HostToolingScope { + public: + HostToolingScope() { ++HostToolingLoads; } + ~HostToolingScope() { --HostToolingLoads; } + HostToolingScope(const HostToolingScope&) = delete; + HostToolingScope& operator=(const HostToolingScope&) = delete; + }; + // A pre-dependency is host tooling: project.so is loaded into the running // crafter-build, so the dependency is built for the host and against the // same PCM cache LoadProject populates — the hostConfig LoadProject itself @@ -506,24 +521,31 @@ namespace { // 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. + // variant from the one every other consumer shares. The scope + // keeps the tool and everything it loads in a project cache of + // their own: ForceHostTooling rewrites them in place, and a + // consumer loading the same sibling for itself got that host + // build back whenever the two cache keys matched. Configuration* cfg = nullptr; - if (IsGitSource(pragma.source)) { - GitProjectSpec spec{ .source = { pragma.source, pragma.branch, pragma.commit } }; - if (!pragma.project.empty()) spec.projectFile = pragma.project; - cfg = GitProject(spec); - } else { - if (!pragma.branch.empty() || !pragma.commit.empty()) PragmaError(absProject, pragma.line, "'branch' and 'commit' only apply to a git source"); - // Relative to the project file, not the cwd: LoadProject runs - // from wherever the consumer was invoked. - fs::path local = (absProject.parent_path() / pragma.source).lexically_normal(); - if (fs::is_directory(local)) { - local /= pragma.project.empty() ? fs::path("project.cpp") : fs::path(pragma.project); - } else if (!pragma.project.empty()) { - PragmaError(absProject, pragma.line, "'project' only applies to a directory source"); + { + HostToolingScope hostTooling; + if (IsGitSource(pragma.source)) { + GitProjectSpec spec{ .source = { pragma.source, pragma.branch, pragma.commit } }; + if (!pragma.project.empty()) spec.projectFile = pragma.project; + cfg = GitProject(spec); + } else { + if (!pragma.branch.empty() || !pragma.commit.empty()) PragmaError(absProject, pragma.line, "'branch' and 'commit' only apply to a git source"); + // Relative to the project file, not the cwd: LoadProject runs + // from wherever the consumer was invoked. + fs::path local = (absProject.parent_path() / pragma.source).lexically_normal(); + if (fs::is_directory(local)) { + local /= pragma.project.empty() ? fs::path("project.cpp") : fs::path(pragma.project); + } else if (!pragma.project.empty()) { + 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 }); } - if (!fs::exists(local)) PragmaError(absProject, pragma.line, std::format("no such project file: {}", local.string())); - cfg = LocalProject({ .projectFile = local }); } if (std::ranges::contains(out.configs, cfg)) continue; @@ -597,6 +619,10 @@ std::vector Crafter::ProjectArgs() { return std::vector(args.begin(), args.end()); } +bool Crafter::LoadingHostTooling() { + return HostToolingLoads > 0; +} + 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 diff --git a/interfaces/Crafter.Build-Platform.cppm b/interfaces/Crafter.Build-Platform.cppm index 2601275..eb9110c 100644 --- a/interfaces/Crafter.Build-Platform.cppm +++ b/interfaces/Crafter.Build-Platform.cppm @@ -75,6 +75,12 @@ namespace Crafter { // that library as a dependency, the test's GitProject loads crafter-build's // project file in turn and the two recurse until the cycle guard throws. export bool IsTopLevelProject(); + // True while this thread loads a `#pragma crafter dependency`: the tool's + // project file and every sibling it loads in turn. GitProject and + // LocalProject key the project cache on it, so host tooling, which + // ForceHostTooling rewrites in place, never shares a Configuration with a + // load for the target. + bool LoadingHostTooling(); // Resolves the directory holding distributed runtime assets (Crafter.Build // module sources, wasi-runtime/, etc). Honors CRAFTER_BUILD_HOME; otherwise // derives /share/crafter-build from the running executable's path.