The staleness check compared an artifact against its own source and its module imports. Headers were in neither set: the scanner reads `import` lines, and a .cppm or .cpp keeps its mtime when a header it includes is edited — so the build reported nothing to do and left objects compiled against the previous contents. Same silent mixed-layout binary as issue 27, reached through #include instead of import. Ask the compiler what it actually opened. Every C++ and C compile now passes -MD -MF <artifact>.d, and Check reads that depfile back through NewestPrerequisite, comparing every prerequisite's mtime against the artifact. A missing depfile (an object from a crafter-build that wrote none) or a prerequisite that no longer exists reads as "rebuild": neither is evidence of freshness. The parser unescapes make syntax rather than splitting on whitespace, since clang wraps depfiles onto continuation lines and escapes spaces in filenames.
230 lines
10 KiB
C++
230 lines
10 KiB
C++
// SPDX-License-Identifier: LGPL-3.0-only
|
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
|
|
module Crafter.Build:Interface_impl;
|
|
import std;
|
|
import :Interface;
|
|
import :Platform;
|
|
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) {}
|
|
|
|
bool ModulePartition::Check(const fs::path& pcmDir, fs::file_time_type sourceFloor) {
|
|
if(!checked) {
|
|
checked = true;
|
|
std::string pcmPath = std::format("{}.pcm", (pcmDir/path.filename()).generic_string());
|
|
std::string cppmPath = std::format("{}.cppm", path.generic_string());
|
|
// 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)) {
|
|
fs::file_time_type pcmTime = fs::last_write_time(pcmPath);
|
|
for(ModulePartition* dependency : partitionDependencies) {
|
|
if(dependency->Check(pcmDir, sourceFloor)) {
|
|
needsRecompiling = true;
|
|
return true;
|
|
}
|
|
}
|
|
for(Module* dependency : moduleDependencies) {
|
|
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) {
|
|
needsRecompiling = true;
|
|
return true;
|
|
}
|
|
}
|
|
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;
|
|
return true;
|
|
}
|
|
} else {
|
|
return needsRecompiling;
|
|
}
|
|
}
|
|
|
|
void ModulePartition::Compile(const std::string_view clang, const fs::path& pcmDir, const fs::path& buildDir, std::atomic<bool>& buildCancelled, std::string& buildError) {
|
|
for(ModulePartition* dependency : partitionDependencies) {
|
|
if(!dependency->compiled.load()) {
|
|
dependency->compiled.wait(false);
|
|
}
|
|
}
|
|
for(Module* dependency : moduleDependencies) {
|
|
if(!dependency->compiled.load()) {
|
|
dependency->compiled.wait(false);
|
|
}
|
|
}
|
|
|
|
if (buildCancelled.load(std::memory_order_relaxed)) {
|
|
compiled.store(true);
|
|
compiled.notify_all();
|
|
return;
|
|
}
|
|
|
|
// -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.
|
|
std::string result = RunCommand(std::format("{0} {1}.cppm --precompile -MD -MF {2}.pcm.d -o {2}.pcm", clang, path.string(), (pcmDir/path.filename()).string()));
|
|
|
|
if (!result.empty()) {
|
|
bool expected = false;
|
|
if (buildCancelled.compare_exchange_strong(expected, true)) {
|
|
buildError = std::move(result);
|
|
compiled.store(true);
|
|
compiled.notify_all();
|
|
return;
|
|
}
|
|
}
|
|
|
|
compiled.store(true);
|
|
compiled.notify_all();
|
|
|
|
result = RunCommand(std::format("{} -Wno-unused-command-line-argument {}.pcm -c -o {}.o", clang, (pcmDir/path.filename()).string(), (buildDir/path.filename()).string()));
|
|
|
|
if (!result.empty()) {
|
|
bool expected = false;
|
|
if (buildCancelled.compare_exchange_strong(expected, true)) {
|
|
buildError = std::move(result);
|
|
}
|
|
}
|
|
}
|
|
|
|
Module::Module(std::string&& name, fs::path&& path) : name(std::move(name)), path(std::move(path)), compiled(false), checked(false) {}
|
|
|
|
bool Module::Check(const fs::path& pcmDir, fs::file_time_type sourceFloor) {
|
|
if(!checked) {
|
|
checked = true;
|
|
std::string pcmPath = std::format("{}.pcm", (pcmDir/path.filename()).generic_string());
|
|
std::string cppmPath = std::format("{}.cppm", path.generic_string());
|
|
// 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)) {
|
|
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;
|
|
}
|
|
} else {
|
|
return needsRecompiling;
|
|
}
|
|
}
|
|
|
|
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) {
|
|
if(part->needsRecompiling) {
|
|
threads.emplace_back(&ModulePartition::Compile, part.get(), clang, pcmDir, buildDir, std::ref(buildCancelled), std::ref(buildError));
|
|
}
|
|
}
|
|
|
|
for(std::thread& thread : threads){
|
|
thread.join();
|
|
}
|
|
|
|
if (buildCancelled.load(std::memory_order_relaxed)) {
|
|
compiled.store(true);
|
|
compiled.notify_all();
|
|
return;
|
|
}
|
|
|
|
// -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.
|
|
std::string result = RunCommand(std::format("{0} {1}.cppm --precompile -MD -MF {2}.pcm.d -o {2}.pcm", clang, path.string(), (pcmDir/path.filename()).string()));
|
|
|
|
if (!result.empty()) {
|
|
bool expected = false;
|
|
if (buildCancelled.compare_exchange_strong(expected, true)) {
|
|
buildError = std::move(result);
|
|
compiled.store(true);
|
|
compiled.notify_all();
|
|
return;
|
|
}
|
|
}
|
|
|
|
compiled.store(true);
|
|
compiled.notify_all();
|
|
|
|
result = RunCommand(std::format("{} -Wno-unused-command-line-argument {}.pcm -c -o {}.o", clang, (pcmDir/path.filename()).string(), (buildDir/path.filename()).string()));
|
|
|
|
if (!result.empty()) {
|
|
bool expected = false;
|
|
if (buildCancelled.compare_exchange_strong(expected, true)) {
|
|
buildError = std::move(result);
|
|
}
|
|
}
|
|
}
|
|
}
|