Merge branch 'claude/issue-41': refuse a build that holds two builds of one module
All checks were successful
Flatpak / flatpak (push) Successful in 12m49s

PR #42, fixes #41. Build() now refuses a configuration whose transitive
dependencies hold two configurations exporting a module of the same
name, such as one project loaded along two paths with different arg
lists, before it builds anything, and names both paths. Until now both
builds ended up in the one program, which compiled cleanly and then
read garbage. Merged cleanly on top of claude/forward-project-args;
the suite, with claude/dependency-notes' fixtures, passes under the
new check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jorijn van der Graaf 2026-10-31 05:30:36 +01:00
commit eefd1df3eb
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB
11 changed files with 233 additions and 4 deletions

View file

@ -444,6 +444,42 @@ namespace {
}
return outcome;
}
// Two configurations among `config` and its transitive dependencies that
// export a module of the same name: the same project loaded along two
// paths with different args, say, or from a checkout on one path and a
// clone on the other. A program can hold one module of each name. Built
// anyway, both PCM dirs went onto the consumer's -fprebuilt-module-path,
// clang resolved the module by name to the first for every import, the
// ones inside the other build's consumers included, and nothing checked
// that it was the build they had been compiled against. The program then
// linked code compiled against two layouts of the same types, and ran
// (issue #41). Empty when the dependencies agree, otherwise the error,
// naming the path to each of the two builds.
std::string DuplicateModuleError(const Configuration& config) {
struct Owner {
const Configuration* cfg;
std::string chain;
};
const auto describe = [](const Owner& owner) {
return std::format("{} ({}, {}, project args {})", owner.chain, owner.cfg->path.string(), owner.cfg->VariantId(), owner.cfg->projectArgs);
};
std::unordered_map<std::string, Owner> owners;
std::unordered_set<const Configuration*> visited;
std::string error;
std::function<void(const Configuration&, const std::string&)> walk = [&](const Configuration& c, const std::string& chain) {
if (!error.empty() || !visited.insert(&c).second) return;
for (const std::unique_ptr<Module>& iface : c.interfaces) {
const auto [it, inserted] = owners.emplace(iface->name, Owner{ &c, chain });
if (inserted || it->second.cfg == &c) continue;
error = std::format("module {} is built twice for {}:\n {}\n {}\nA program can hold one build of each module: load {} with the same args, and from the same place, along every path.", iface->name, config.name, describe(it->second), describe(Owner{ &c, chain }), c.name);
return;
}
for (const Configuration* dep : c.dependencies) walk(*dep, std::format("{} -> {}", chain, dep->name));
};
walk(config, config.name);
return error;
}
}
BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, std::shared_future<BuildResult>>& depResults, std::mutex& depMutex) {
@ -452,6 +488,11 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
if (const Toolchain& tc = GetToolchain(); !tc.error.empty()) {
return {tc.error, false, {}};
}
// Before any directory is made or dependency built, so a refused graph
// leaves nothing behind.
if (std::string duplicate = DuplicateModuleError(config); !duplicate.empty()) {
return {std::move(duplicate), false, {}};
}
// Reset per-build cached state on every Module/ModulePartition so that
// successive Build() calls on the same Configuration re-evaluate mtimes
// (incremental-rebuild test scenarios).