fix: re-resolve module imports before checking staleness
Adding a data member to a class in a module interface did not rebuild every object compiled against the old layout. The build succeeded with no error or warning and the resulting binary mixed both layouts, surfacing later as a SIGSEGV in a destructor. GetInterfacesAndImplementations scans a TU's `import X;` statements when the source is declared. An import that matches neither a module in the Configuration nor one reachable through `dependencies` was dropped on the floor, leaving that TU with no staleness edge to the interface it consumes. `dependencies` is frequently assigned *after* the scan — AddTest does exactly that, resolving tests/<name>/main.cpp and only then returning a builder whose .Dependencies() supplies the library — so consumers of a dependency's modules routinely carried no edge at all. A layout change then rebuilt the library, relinked the consumer, and kept the consumer's object as it was. Unresolved names are now remembered on the partition/implementation as pendingImports, and Configuration::ResolvePendingImports retries them against the dependency DAG as it stands. Build() calls it immediately before comparing mtimes, which closes the window for every caller rather than only the ones that declare in the right order; TestBuilder::Dependencies also calls it so the Configuration is coherent for anyone inspecting it before the build. Resolves #27
This commit is contained in:
parent
8892154b28
commit
13697cd026
11 changed files with 341 additions and 7 deletions
|
|
@ -25,9 +25,14 @@ namespace fs = std::filesystem;
|
|||
using namespace Crafter;
|
||||
|
||||
|
||||
void Configuration::GetInterfacesAndImplementations(std::span<fs::path> interfaces, std::span<fs::path> implementations) {
|
||||
auto resolveImport = [this](const std::string& importName, std::vector<Module*>& localDeps, std::vector<std::pair<Module*, fs::path>>& externalDeps) -> bool {
|
||||
for(const std::unique_ptr<Module>& interface : this->interfaces) {
|
||||
namespace {
|
||||
// Map one `import X;` name onto either a module this Configuration owns or
|
||||
// one exported by a Configuration reachable through its dependency DAG,
|
||||
// appending the matching staleness edge. False means nothing in reach
|
||||
// provides X — either it's supplied from outside the graph (`std`) or the
|
||||
// dependency isn't wired up *yet*, which is why callers remember the name.
|
||||
bool ResolveImportName(Configuration& cfg, const std::string& importName, std::vector<Module*>& localDeps, std::vector<std::pair<Module*, fs::path>>& externalDeps) {
|
||||
for(const std::unique_ptr<Module>& interface : cfg.interfaces) {
|
||||
if(interface->name == importName) {
|
||||
localDeps.push_back(interface.get());
|
||||
return true;
|
||||
|
|
@ -50,10 +55,38 @@ void Configuration::GetInterfacesAndImplementations(std::span<fs::path> interfac
|
|||
return false;
|
||||
};
|
||||
|
||||
for(Configuration* depCfg : this->dependencies) {
|
||||
for(Configuration* depCfg : cfg.dependencies) {
|
||||
if (walk(depCfg)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void Configuration::ResolvePendingImports() {
|
||||
// Discard the local-module hits: a name recorded as pending already failed
|
||||
// to match this Configuration's own interfaces, and nothing adds interfaces
|
||||
// between the scan and here. Only the external edge can newly appear.
|
||||
std::vector<Module*> ignored;
|
||||
auto sweep = [this, &ignored](std::vector<std::string>& pending, std::vector<std::pair<Module*, fs::path>>& externalDeps) {
|
||||
std::erase_if(pending, [&](const std::string& name) {
|
||||
return ResolveImportName(*this, name, ignored, externalDeps);
|
||||
});
|
||||
};
|
||||
for(const std::unique_ptr<Module>& interface : interfaces) {
|
||||
for(const std::unique_ptr<ModulePartition>& partition : interface->partitions) {
|
||||
sweep(partition->pendingImports, partition->externalModuleDependencies);
|
||||
}
|
||||
}
|
||||
for(Implementation& implementation : implementations) {
|
||||
sweep(implementation.pendingImports, implementation.externalModuleDependencies);
|
||||
}
|
||||
}
|
||||
|
||||
void Configuration::GetInterfacesAndImplementations(std::span<fs::path> interfaces, std::span<fs::path> implementations) {
|
||||
auto resolveImport = [this](const std::string& importName, std::vector<Module*>& localDeps, std::vector<std::pair<Module*, fs::path>>& externalDeps, std::vector<std::string>& pending) {
|
||||
if (!ResolveImportName(*this, importName, localDeps, externalDeps)) {
|
||||
pending.push_back(importName);
|
||||
}
|
||||
};
|
||||
|
||||
std::vector<std::tuple<fs::path, std::string, ModulePartition*, Module*>> tempModulePaths = std::vector<std::tuple<fs::path, std::string, ModulePartition*, Module*>>(interfaces.size());
|
||||
|
|
@ -128,7 +161,7 @@ void Configuration::GetInterfacesAndImplementations(std::span<fs::path> interfac
|
|||
std::sregex_iterator modCurrent(fileContent.begin(), fileContent.end(), modulePattern);
|
||||
while (modCurrent != lastMatch) {
|
||||
std::smatch match = *modCurrent;
|
||||
resolveImport(match[1].str(), partition->moduleDependencies, partition->externalModuleDependencies);
|
||||
resolveImport(match[1].str(), partition->moduleDependencies, partition->externalModuleDependencies, partition->pendingImports);
|
||||
++modCurrent;
|
||||
}
|
||||
}
|
||||
|
|
@ -173,7 +206,7 @@ void Configuration::GetInterfacesAndImplementations(std::span<fs::path> interfac
|
|||
while (modCurrent != lastMatch) {
|
||||
std::smatch match2 = *modCurrent;
|
||||
if (match2[1] != match[1]) {
|
||||
resolveImport(match2[1].str(), implementation.moduleDependencies, implementation.externalModuleDependencies);
|
||||
resolveImport(match2[1].str(), implementation.moduleDependencies, implementation.externalModuleDependencies, implementation.pendingImports);
|
||||
}
|
||||
++modCurrent;
|
||||
}
|
||||
|
|
@ -188,7 +221,7 @@ void Configuration::GetInterfacesAndImplementations(std::span<fs::path> interfac
|
|||
std::sregex_iterator lastMatch;
|
||||
while (currentMatch != lastMatch) {
|
||||
std::smatch match2 = *currentMatch;
|
||||
resolveImport(match2[1].str(), implementation.moduleDependencies, implementation.externalModuleDependencies);
|
||||
resolveImport(match2[1].str(), implementation.moduleDependencies, implementation.externalModuleDependencies, implementation.pendingImports);
|
||||
++currentMatch;
|
||||
}
|
||||
}
|
||||
|
|
@ -224,6 +257,18 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
|||
}
|
||||
}
|
||||
|
||||
// Sources were scanned when they were declared, which for most callers is
|
||||
// before `dependencies` exists — AddTest resolves tests/<name>/main.cpp and
|
||||
// only then hands back a builder whose .Dependencies() supplies the library.
|
||||
// Any `import <DepModule>;` in such a TU resolved to nothing and so carried
|
||||
// no staleness edge, which meant a member added to a dependency's interface
|
||||
// rebuilt the library, relinked the consumer, and silently kept the
|
||||
// consumer's object compiled against the *old* class layout (issue #27).
|
||||
// Re-resolving here — the last point before mtimes are compared, with the
|
||||
// DAG fully wired — closes that window for every caller rather than for the
|
||||
// ones that remember to declare in the right order.
|
||||
config.ResolvePendingImports();
|
||||
|
||||
// Auto-detect the WASI sysroot before any compile step runs so BuildStdPcm
|
||||
// and the main compile command see the same value. Linux-only — Windows
|
||||
// users supply cfg.sysroot pointing at their wasi-sdk install. Covers all
|
||||
|
|
|
|||
|
|
@ -483,6 +483,14 @@ TestBuilder& TestBuilder::Args(std::vector<std::string> a) { Ref().args = std::
|
|||
TestBuilder& TestBuilder::Requires(std::string r) { Ref().requires_.push_back(std::move(r)); return *this; }
|
||||
TestBuilder& TestBuilder::Dependencies(std::vector<Configuration*> d) {
|
||||
Ref().config.dependencies = std::move(d);
|
||||
// AddTest already scanned tests/<name>/main.cpp, at which point this test
|
||||
// had no dependencies, so every `import <DepModule>;` in it came back
|
||||
// unresolved. Place them now that the libraries are known — without this
|
||||
// the test's object carries no staleness edge to the interfaces it consumes
|
||||
// and survives a layout change to them (issue #27). Build() re-runs the
|
||||
// same sweep as a backstop; doing it here keeps the Configuration coherent
|
||||
// for anyone inspecting it before the build.
|
||||
Ref().config.ResolvePendingImports();
|
||||
return *this;
|
||||
}
|
||||
TestBuilder& TestBuilder::LinkFlag(std::string f) { Ref().config.linkFlags.push_back(std::move(f)); return *this; }
|
||||
|
|
|
|||
Loading…
Reference in a new issue