2026-07-23 01:24:42 +02:00
|
|
|
// SPDX-License-Identifier: LGPL-3.0-only
|
|
|
|
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
2026-04-23 01:57:25 +02:00
|
|
|
|
|
|
|
|
module Crafter.Build:Interface_impl;
|
|
|
|
|
import std;
|
|
|
|
|
import :Interface;
|
2026-04-27 07:04:42 +02:00
|
|
|
import :Platform;
|
2026-04-23 01:57:25 +02:00
|
|
|
namespace fs = std::filesystem;
|
|
|
|
|
|
|
|
|
|
namespace Crafter {
|
|
|
|
|
ModulePartition::ModulePartition(std::string&& name, fs::path&& path) : name(std::move(name)), path(std::move(path)), compiled(false), checked(false) {}
|
|
|
|
|
|
2026-04-27 07:04:42 +02:00
|
|
|
bool ModulePartition::Check(const fs::path& pcmDir, fs::file_time_type sourceFloor) {
|
2026-04-23 01:57:25 +02:00
|
|
|
if(!checked) {
|
|
|
|
|
checked = true;
|
2026-07-23 01:24:42 +02:00
|
|
|
std::string pcmPath = std::format("{}.pcm", (pcmDir/path.filename()).generic_string());
|
|
|
|
|
std::string cppmPath = std::format("{}.cppm", path.generic_string());
|
2026-07-31 11:11:38 +00:00
|
|
|
// NewestPrerequisite covers the headers this partition #includes —
|
|
|
|
|
// they are inputs to the BMI just as much as the .cppm is, and the
|
|
|
|
|
// .cppm's own mtime says nothing about them. It only ever bounds
|
|
|
|
|
// *this* artifact, so it goes in the comparison below and not into
|
|
|
|
|
// the `sourceFloor` handed to the recursive Checks: each of those
|
|
|
|
|
// reads its own depfile.
|
|
|
|
|
if(fs::exists(pcmPath) && std::max({fs::last_write_time(cppmPath), sourceFloor, NewestPrerequisite(std::format("{}.d", pcmPath))}) < fs::last_write_time(pcmPath)) {
|
2026-04-27 07:04:42 +02:00
|
|
|
fs::file_time_type pcmTime = fs::last_write_time(pcmPath);
|
2026-04-23 01:57:25 +02:00
|
|
|
for(ModulePartition* dependency : partitionDependencies) {
|
2026-04-27 07:04:42 +02:00
|
|
|
if(dependency->Check(pcmDir, sourceFloor)) {
|
2026-04-23 01:57:25 +02:00
|
|
|
needsRecompiling = true;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for(Module* dependency : moduleDependencies) {
|
2026-04-27 07:04:42 +02:00
|
|
|
if(dependency->Check(pcmDir, sourceFloor)) {
|
|
|
|
|
needsRecompiling = true;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
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) {
|
2026-04-23 01:57:25 +02:00
|
|
|
needsRecompiling = true;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
needsRecompiling = false;
|
2026-04-27 07:04:42 +02:00
|
|
|
compiled.store(true);
|
fix: track what a primary module interface imports
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>
2026-07-30 18:19:04 +00:00
|
|
|
// 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();
|
2026-04-23 01:57:25 +02:00
|
|
|
return false;
|
|
|
|
|
} else {
|
|
|
|
|
needsRecompiling = true;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
return needsRecompiling;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-27 07:04:42 +02:00
|
|
|
void ModulePartition::Compile(const std::string_view clang, const fs::path& pcmDir, const fs::path& buildDir, std::atomic<bool>& buildCancelled, std::string& buildError) {
|
2026-04-23 01:57:25 +02:00
|
|
|
for(ModulePartition* dependency : partitionDependencies) {
|
|
|
|
|
if(!dependency->compiled.load()) {
|
2026-04-27 07:04:42 +02:00
|
|
|
dependency->compiled.wait(false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for(Module* dependency : moduleDependencies) {
|
|
|
|
|
if(!dependency->compiled.load()) {
|
|
|
|
|
dependency->compiled.wait(false);
|
2026-04-23 01:57:25 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (buildCancelled.load(std::memory_order_relaxed)) {
|
|
|
|
|
compiled.store(true);
|
|
|
|
|
compiled.notify_all();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-31 11:11:38 +00:00
|
|
|
// -MD records every header the preamble pulled in, next to the BMI as
|
|
|
|
|
// <name>.pcm.d, so the next Check can see an edit to one of them.
|
fix: decide a build step by its exit code, not by whether it printed
RunCommand merged stderr into stdout, dropped pclose's status and returned
the text; every compile, link and archive site then read "printed something"
as "failed". A warning is printing something, so a translation unit that
warned failed the build — but only on the run that actually recompiled it,
since warnings aren't re-emitted for an object that's already up to date.
The same unchanged source therefore passed or failed depending on the state
of the build tree: flaky-looking tests locally, and a cold CI checkout
surfacing every latent warning in a project at once as unrelated failures.
RunCommand is gone, replaced by RunBuildCommand: it goes through
RunCommandChecked, returns "" when the command exited 0 (so every caller's
`if (!result.empty())` error path is unchanged) and hands any warnings to
the new Progress::Diagnostic instead of to the error path. That also closes
the quiet half of the bug — a compiler killed by the OOM killer prints
nothing, so it used to read as success and leave the build carrying on with
a missing object; it now reports the signal that killed it.
Warnings are now shown rather than swallowed, which they weren't in either
direction before: invisible on an incremental build, fatal on a cold one.
Failing on them stays a project's choice, via -Werror in compileFlags.
2026-08-26 00:51:15 +00:00
|
|
|
std::string result = RunBuildCommand(std::format("{0} {1}.cppm --precompile -MD -MF {2}.pcm.d -o {2}.pcm", clang, path.string(), (pcmDir/path.filename()).string()));
|
2026-04-23 01:57:25 +02:00
|
|
|
|
|
|
|
|
if (!result.empty()) {
|
2026-04-27 07:04:42 +02:00
|
|
|
bool expected = false;
|
|
|
|
|
if (buildCancelled.compare_exchange_strong(expected, true)) {
|
2026-04-23 01:57:25 +02:00
|
|
|
buildError = std::move(result);
|
|
|
|
|
compiled.store(true);
|
|
|
|
|
compiled.notify_all();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
compiled.store(true);
|
|
|
|
|
compiled.notify_all();
|
|
|
|
|
|
fix: decide a build step by its exit code, not by whether it printed
RunCommand merged stderr into stdout, dropped pclose's status and returned
the text; every compile, link and archive site then read "printed something"
as "failed". A warning is printing something, so a translation unit that
warned failed the build — but only on the run that actually recompiled it,
since warnings aren't re-emitted for an object that's already up to date.
The same unchanged source therefore passed or failed depending on the state
of the build tree: flaky-looking tests locally, and a cold CI checkout
surfacing every latent warning in a project at once as unrelated failures.
RunCommand is gone, replaced by RunBuildCommand: it goes through
RunCommandChecked, returns "" when the command exited 0 (so every caller's
`if (!result.empty())` error path is unchanged) and hands any warnings to
the new Progress::Diagnostic instead of to the error path. That also closes
the quiet half of the bug — a compiler killed by the OOM killer prints
nothing, so it used to read as success and leave the build carrying on with
a missing object; it now reports the signal that killed it.
Warnings are now shown rather than swallowed, which they weren't in either
direction before: invisible on an incremental build, fatal on a cold one.
Failing on them stays a project's choice, via -Werror in compileFlags.
2026-08-26 00:51:15 +00:00
|
|
|
result = RunBuildCommand(std::format("{} -Wno-unused-command-line-argument {}.pcm -c -o {}.o", clang, (pcmDir/path.filename()).string(), (buildDir/path.filename()).string()));
|
2026-04-23 01:57:25 +02:00
|
|
|
|
|
|
|
|
if (!result.empty()) {
|
2026-04-27 07:04:42 +02:00
|
|
|
bool expected = false;
|
|
|
|
|
if (buildCancelled.compare_exchange_strong(expected, true)) {
|
2026-04-23 01:57:25 +02:00
|
|
|
buildError = std::move(result);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Module::Module(std::string&& name, fs::path&& path) : name(std::move(name)), path(std::move(path)), compiled(false), checked(false) {}
|
|
|
|
|
|
2026-04-27 07:04:42 +02:00
|
|
|
bool Module::Check(const fs::path& pcmDir, fs::file_time_type sourceFloor) {
|
2026-04-23 01:57:25 +02:00
|
|
|
if(!checked) {
|
|
|
|
|
checked = true;
|
2026-07-23 01:24:42 +02:00
|
|
|
std::string pcmPath = std::format("{}.pcm", (pcmDir/path.filename()).generic_string());
|
|
|
|
|
std::string cppmPath = std::format("{}.cppm", path.generic_string());
|
2026-07-31 11:11:38 +00:00
|
|
|
// See ModulePartition::Check — the depfile clang wrote beside the BMI
|
|
|
|
|
// is the only record of which headers this interface unit included.
|
|
|
|
|
if(fs::exists(pcmPath) && std::max({fs::last_write_time(cppmPath), sourceFloor, NewestPrerequisite(std::format("{}.d", pcmPath))}) < fs::last_write_time(pcmPath)) {
|
fix: track what a primary module interface imports
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>
2026-07-30 18:19:04 +00:00
|
|
|
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.
|
2026-04-23 01:57:25 +02:00
|
|
|
bool depCheck = false;
|
|
|
|
|
for(std::unique_ptr<ModulePartition>& partition : partitions) {
|
2026-04-27 07:04:42 +02:00
|
|
|
if(partition->Check(pcmDir, sourceFloor)) {
|
2026-04-23 01:57:25 +02:00
|
|
|
depCheck = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
fix: track what a primary module interface imports
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>
2026-07-30 18:19:04 +00:00
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-23 01:57:25 +02:00
|
|
|
if(depCheck) {
|
|
|
|
|
needsRecompiling = true;
|
|
|
|
|
return true;
|
|
|
|
|
} else {
|
|
|
|
|
needsRecompiling = false;
|
|
|
|
|
compiled.store(true);
|
fix: track what a primary module interface imports
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>
2026-07-30 18:19:04 +00:00
|
|
|
compiled.notify_all();
|
2026-04-23 01:57:25 +02:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
fix: track what a primary module interface imports
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>
2026-07-30 18:19:04 +00:00
|
|
|
// 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.
|
2026-04-23 01:57:25 +02:00
|
|
|
for(std::unique_ptr<ModulePartition>& partition : partitions) {
|
2026-04-27 07:04:42 +02:00
|
|
|
partition->Check(pcmDir, sourceFloor);
|
2026-04-23 01:57:25 +02:00
|
|
|
}
|
fix: track what a primary module interface imports
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>
2026-07-30 18:19:04 +00:00
|
|
|
for(Module* dependency : moduleDependencies) {
|
|
|
|
|
dependency->Check(pcmDir, sourceFloor);
|
|
|
|
|
}
|
2026-04-23 01:57:25 +02:00
|
|
|
needsRecompiling = true;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
return needsRecompiling;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-23 01:24:42 +02:00
|
|
|
void Module::Compile(const std::string_view clang, const fs::path& pcmDir, const fs::path& buildDir, std::atomic<bool>& buildCancelled, std::string& buildError) {
|
fix: track what a primary module interface imports
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>
2026-07-30 18:19:04 +00:00
|
|
|
// 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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-23 01:57:25 +02:00
|
|
|
std::vector<std::thread> threads;
|
2026-04-27 07:04:42 +02:00
|
|
|
threads.reserve(partitions.size());
|
|
|
|
|
for(std::unique_ptr<ModulePartition>& part : partitions) {
|
|
|
|
|
if(part->needsRecompiling) {
|
|
|
|
|
threads.emplace_back(&ModulePartition::Compile, part.get(), clang, pcmDir, buildDir, std::ref(buildCancelled), std::ref(buildError));
|
2026-04-23 01:57:25 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for(std::thread& thread : threads){
|
|
|
|
|
thread.join();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (buildCancelled.load(std::memory_order_relaxed)) {
|
|
|
|
|
compiled.store(true);
|
|
|
|
|
compiled.notify_all();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-31 11:11:38 +00:00
|
|
|
// -MD records every header the preamble pulled in, next to the BMI as
|
|
|
|
|
// <name>.pcm.d, so the next Check can see an edit to one of them.
|
fix: decide a build step by its exit code, not by whether it printed
RunCommand merged stderr into stdout, dropped pclose's status and returned
the text; every compile, link and archive site then read "printed something"
as "failed". A warning is printing something, so a translation unit that
warned failed the build — but only on the run that actually recompiled it,
since warnings aren't re-emitted for an object that's already up to date.
The same unchanged source therefore passed or failed depending on the state
of the build tree: flaky-looking tests locally, and a cold CI checkout
surfacing every latent warning in a project at once as unrelated failures.
RunCommand is gone, replaced by RunBuildCommand: it goes through
RunCommandChecked, returns "" when the command exited 0 (so every caller's
`if (!result.empty())` error path is unchanged) and hands any warnings to
the new Progress::Diagnostic instead of to the error path. That also closes
the quiet half of the bug — a compiler killed by the OOM killer prints
nothing, so it used to read as success and leave the build carrying on with
a missing object; it now reports the signal that killed it.
Warnings are now shown rather than swallowed, which they weren't in either
direction before: invisible on an incremental build, fatal on a cold one.
Failing on them stays a project's choice, via -Werror in compileFlags.
2026-08-26 00:51:15 +00:00
|
|
|
std::string result = RunBuildCommand(std::format("{0} {1}.cppm --precompile -MD -MF {2}.pcm.d -o {2}.pcm", clang, path.string(), (pcmDir/path.filename()).string()));
|
2026-04-23 01:57:25 +02:00
|
|
|
|
|
|
|
|
if (!result.empty()) {
|
2026-04-27 07:04:42 +02:00
|
|
|
bool expected = false;
|
|
|
|
|
if (buildCancelled.compare_exchange_strong(expected, true)) {
|
2026-04-23 01:57:25 +02:00
|
|
|
buildError = std::move(result);
|
|
|
|
|
compiled.store(true);
|
|
|
|
|
compiled.notify_all();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
compiled.store(true);
|
|
|
|
|
compiled.notify_all();
|
|
|
|
|
|
fix: decide a build step by its exit code, not by whether it printed
RunCommand merged stderr into stdout, dropped pclose's status and returned
the text; every compile, link and archive site then read "printed something"
as "failed". A warning is printing something, so a translation unit that
warned failed the build — but only on the run that actually recompiled it,
since warnings aren't re-emitted for an object that's already up to date.
The same unchanged source therefore passed or failed depending on the state
of the build tree: flaky-looking tests locally, and a cold CI checkout
surfacing every latent warning in a project at once as unrelated failures.
RunCommand is gone, replaced by RunBuildCommand: it goes through
RunCommandChecked, returns "" when the command exited 0 (so every caller's
`if (!result.empty())` error path is unchanged) and hands any warnings to
the new Progress::Diagnostic instead of to the error path. That also closes
the quiet half of the bug — a compiler killed by the OOM killer prints
nothing, so it used to read as success and leave the build carrying on with
a missing object; it now reports the signal that killed it.
Warnings are now shown rather than swallowed, which they weren't in either
direction before: invisible on an incremental build, fatal on a cold one.
Failing on them stays a project's choice, via -Werror in compileFlags.
2026-08-26 00:51:15 +00:00
|
|
|
result = RunBuildCommand(std::format("{} -Wno-unused-command-line-argument {}.pcm -c -o {}.o", clang, (pcmDir/path.filename()).string(), (buildDir/path.filename()).string()));
|
2026-04-23 01:57:25 +02:00
|
|
|
|
|
|
|
|
if (!result.empty()) {
|
2026-04-27 07:04:42 +02:00
|
|
|
bool expected = false;
|
|
|
|
|
if (buildCancelled.compare_exchange_strong(expected, true)) {
|
2026-04-23 01:57:25 +02:00
|
|
|
buildError = std::move(result);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|