fix: track what a primary module interface imports
Some checks failed
CI / build-test-release (pull_request) Failing after 6m5s
Some checks failed
CI / build-test-release (pull_request) Failing after 6m5s
A primary module interface unit — `export module Widget;`, no partitions — recorded nothing about what it imported. GetInterfacesAndImplementations registered the Module and then erased the file from the scan list, so the import pass only ever saw partitions, and Module had no vectors to hold an edge anyway. Two consequences, both reported as issue #26: Module::Check consulted only its own .cppm and its partitions. A data member added to an imported module left Widget.pcm, Widget.o and every consumer object untouched while the imported library rebuilt and both binaries relinked — one executable holding two class layouts, no diagnostic, and a crash somewhere unrelated. Wiping build/ was the only cure, so `crafter-build test` could not be trusted straight after an interface edit. Module::Compile waited on nothing. Two modules in one Configuration compile on concurrent threads, so a primary interface importing a sibling was a coin flip between working and "module 'Base' not found". Partitions never had either problem — they carry the same three vectors and Check/Compile honour them — which is why the gap only surfaced on a module whose interface is one flat unit. Module now carries moduleDependencies, externalModuleDependencies and pendingImports with the same meanings as on ModulePartition; primary units stay in the scan list so their imports land there; Check sees through them; Compile orders itself behind a local sibling; and ResolvePendingImports sweeps them so an edge survives dependencies being wired up afterwards. Build() now Checks every interface before spawning any compile thread — the `compiled` flag a waiter blocks on is raised either by a Compile that runs or by the Check that decides none is needed, so a Check still pending while another module's thread waits would have hung the build. Resolves #26 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
e8f7bb12a8
commit
f38521298b
12 changed files with 437 additions and 22 deletions
|
|
@ -39,6 +39,10 @@ namespace Crafter {
|
|||
}
|
||||
needsRecompiling = false;
|
||||
compiled.store(true);
|
||||
// Nothing waits on `compiled` until the compile threads start,
|
||||
// which is after every Check has run — but notify anyway so the
|
||||
// flag is never left set without a wake-up behind it.
|
||||
compiled.notify_all();
|
||||
return false;
|
||||
} else {
|
||||
needsRecompiling = true;
|
||||
|
|
@ -100,24 +104,55 @@ namespace Crafter {
|
|||
std::string pcmPath = std::format("{}.pcm", (pcmDir/path.filename()).generic_string());
|
||||
std::string cppmPath = std::format("{}.cppm", path.generic_string());
|
||||
if(fs::exists(pcmPath) && std::max(fs::last_write_time(cppmPath), sourceFloor) < fs::last_write_time(pcmPath)) {
|
||||
fs::file_time_type pcmTime = fs::last_write_time(pcmPath);
|
||||
// Every partition gets Check()ed even once one is known stale:
|
||||
// Compile() drives partition rebuilds off their own
|
||||
// needsRecompiling flags, so short-circuiting here would leave
|
||||
// the later ones unevaluated and silently unbuilt.
|
||||
bool depCheck = false;
|
||||
for(std::unique_ptr<ModulePartition>& partition : partitions) {
|
||||
if(partition->Check(pcmDir, sourceFloor)) {
|
||||
depCheck = true;
|
||||
}
|
||||
}
|
||||
// Modules this interface unit imports directly. Local ones share
|
||||
// our pcmDir and are resolved recursively; external ones are
|
||||
// compared by PCM mtime, their owning Configuration having
|
||||
// already finished building by the time we run.
|
||||
for(Module* dependency : moduleDependencies) {
|
||||
if(dependency->Check(pcmDir, sourceFloor)) {
|
||||
depCheck = true;
|
||||
}
|
||||
}
|
||||
if(!depCheck) {
|
||||
for(const auto& [externalMod, externalPcmPath] : externalModuleDependencies) {
|
||||
std::error_code ec;
|
||||
fs::file_time_type t = fs::last_write_time(externalPcmPath, ec);
|
||||
if (!ec && t >= pcmTime) {
|
||||
depCheck = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(depCheck) {
|
||||
needsRecompiling = true;
|
||||
return true;
|
||||
} else {
|
||||
needsRecompiling = false;
|
||||
compiled.store(true);
|
||||
compiled.notify_all();
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// Already known stale, but the dependencies still need
|
||||
// evaluating: an unchecked local module would never get built,
|
||||
// and Compile() below waits on its `compiled` flag.
|
||||
for(std::unique_ptr<ModulePartition>& partition : partitions) {
|
||||
partition->Check(pcmDir, sourceFloor);
|
||||
}
|
||||
for(Module* dependency : moduleDependencies) {
|
||||
dependency->Check(pcmDir, sourceFloor);
|
||||
}
|
||||
needsRecompiling = true;
|
||||
return true;
|
||||
}
|
||||
|
|
@ -127,6 +162,17 @@ namespace Crafter {
|
|||
}
|
||||
|
||||
void Module::Compile(const std::string_view clang, const fs::path& pcmDir, const fs::path& buildDir, std::atomic<bool>& buildCancelled, std::string& buildError) {
|
||||
// A sibling module in the same Configuration that this interface unit
|
||||
// imports has to have its PCM on disk before we precompile against it.
|
||||
// Safe to block here: Build() Checks every interface before it spawns
|
||||
// any compile thread, so a dependency that needs no rebuild already has
|
||||
// `compiled` set and this returns immediately.
|
||||
for(Module* dependency : moduleDependencies) {
|
||||
if(!dependency->compiled.load()) {
|
||||
dependency->compiled.wait(false);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::thread> threads;
|
||||
threads.reserve(partitions.size());
|
||||
for(std::unique_ptr<ModulePartition>& part : partitions) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue