fix: key the host PCM cache on source content, add clean, hash project args
Three follow-ons to the stale-build report, all cases of an identity not capturing something that changes the output. The host PCM cache under <cache>/crafter.build/<target>-<march>/ is shared by every crafter-build on the machine, and freshness was a per-file mtime comparison. That cannot tell "this PCM is newer than my source" from "this PCM was built from different sources that happen to be newer", so a package install and a working checkout — or two checkouts of different versions — silently compiled their project.cpp against each other's declarations. Invalidation now keys on a stamp over the bytes of every module source, which also covers the case one file's mtime never could: the cached PCMs import each other, so a change to :Interface invalidates :Clang's PCM with Crafter.Build-Clang.cppm untouched. Project args ApplyStandardArgs does not itself interpret are now folded into VariantId. Such a flag typically decides what gets compiled or bundled — the report's example is --no-webgpu dropping entries from cfg.files — and without it both settings shared one bin dir and interleaved their outputs there, leaving a bundle matching neither. Sorted and deduplicated so flag order doesn't split the cache, and inherited by test Configurations. `crafter-build clean` removes the project's bin/ and build/ trees. It deliberately does not load project.cpp: cleaning is most often reached when something is already wrong, and a clean that first needs the project to compile is useless exactly then.
This commit is contained in:
parent
13697cd026
commit
e8fde57582
9 changed files with 439 additions and 15 deletions
|
|
@ -74,6 +74,54 @@ namespace {
|
|||
CacheLock(const CacheLock&) = delete;
|
||||
CacheLock& operator=(const CacheLock&) = delete;
|
||||
};
|
||||
|
||||
// The cached Crafter.Build PCMs are keyed by `<target>-<march>` alone, so
|
||||
// every crafter-build on the machine writes to the same files regardless of
|
||||
// which share/crafter-build its module sources came from. Freshness used to
|
||||
// be a per-file mtime comparison, which cannot distinguish "this PCM is
|
||||
// newer than my source" from "this PCM was built from *different* sources
|
||||
// that happen to be newer" — so a second install or checkout silently
|
||||
// compiled its project against the other's declarations. Same failure shape
|
||||
// as issue #27: no error, a binary built against a layout nobody linked.
|
||||
//
|
||||
// A stamp over the bytes of every module source answers what the mtime
|
||||
// can't. It covers the whole set rather than one file at a time because the
|
||||
// PCMs import each other: a change to :Interface invalidates :Clang's PCM
|
||||
// even though Crafter.Build-Clang.cppm is untouched.
|
||||
std::string CrafterBuildSourceStamp(const fs::path& sourceDir, std::span<const std::string_view> moduleNames) {
|
||||
std::string all;
|
||||
auto append = [&all, &sourceDir](const fs::path& relative) {
|
||||
std::ifstream in(sourceDir / relative, std::ios::binary);
|
||||
std::ostringstream buffer;
|
||||
buffer << in.rdbuf();
|
||||
all += relative.string();
|
||||
all += '\0';
|
||||
all += buffer.str();
|
||||
all += '\0';
|
||||
};
|
||||
for (std::string_view name : moduleNames) {
|
||||
append(fs::path(std::format("{}.cppm", name)));
|
||||
}
|
||||
// Exported through `module;` preambles, so its contents land in the PCMs
|
||||
// too.
|
||||
append("Crafter.Build-Api.h");
|
||||
return std::format("{:016x}", std::hash<std::string>{}(all));
|
||||
}
|
||||
|
||||
fs::path CacheStampPath(const fs::path& cacheDir) {
|
||||
return cacheDir / "crafter-build-sources.stamp";
|
||||
}
|
||||
|
||||
std::string ReadCacheStamp(const fs::path& cacheDir) {
|
||||
std::ifstream in(CacheStampPath(cacheDir), std::ios::binary);
|
||||
std::string stamp;
|
||||
std::getline(in, stamp);
|
||||
return stamp;
|
||||
}
|
||||
|
||||
void WriteCacheStamp(const fs::path& cacheDir, const std::string& stamp) {
|
||||
std::ofstream(CacheStampPath(cacheDir), std::ios::binary | std::ios::trunc) << stamp << '\n';
|
||||
}
|
||||
}
|
||||
|
||||
fs::path Crafter::GetCrafterBuildHome() {
|
||||
|
|
@ -328,13 +376,17 @@ std::string Crafter::GetBaseCommand(const Configuration& config) {
|
|||
namespace {
|
||||
void EnsureCrafterBuildPcms(const fs::path& sourceDir, const fs::path& cacheDir) {
|
||||
CacheLock lock(cacheDir);
|
||||
// Re-derived under the lock: another builder may have refreshed the
|
||||
// cache from its own sources while we waited.
|
||||
std::string stamp = CrafterBuildSourceStamp(sourceDir, CrafterBuildModules);
|
||||
bool upToDate = ReadCacheStamp(cacheDir) == stamp;
|
||||
for (std::string_view name : CrafterBuildModules) {
|
||||
fs::path cppmPath = sourceDir / std::format("{}.cppm", name);
|
||||
fs::path pcmPath = cacheDir / std::format("{}.pcm", name);
|
||||
if (!fs::exists(cppmPath)) {
|
||||
throw std::runtime_error(std::format("module source {} not found in {} (set CRAFTER_BUILD_HOME)", name, sourceDir.string()));
|
||||
}
|
||||
if (fs::exists(pcmPath) && fs::last_write_time(cppmPath) < fs::last_write_time(pcmPath)) {
|
||||
if (upToDate && fs::exists(pcmPath)) {
|
||||
continue;
|
||||
}
|
||||
std::string cmd = std::format(
|
||||
|
|
@ -350,6 +402,10 @@ namespace {
|
|||
throw std::runtime_error(std::format("Failed to precompile {} (exit {}): {}", name, r.exitCode, r.output));
|
||||
}
|
||||
}
|
||||
// Last, so an interrupted rebuild leaves the stamp disagreeing with the
|
||||
// sources and the next run starts over rather than trusting a half-built
|
||||
// set of PCMs.
|
||||
if (!upToDate) WriteCacheStamp(cacheDir, stamp);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -523,6 +579,10 @@ std::string Crafter::GetBaseCommand(const Configuration& config) {
|
|||
namespace {
|
||||
void EnsureCrafterBuildPcms(const fs::path& sourceDir, const fs::path& cacheDir) {
|
||||
CacheLock lock(cacheDir);
|
||||
// Re-derived under the lock: another builder may have refreshed the
|
||||
// cache from its own sources while we waited.
|
||||
std::string stamp = CrafterBuildSourceStamp(sourceDir, CrafterBuildModules);
|
||||
bool upToDate = ReadCacheStamp(cacheDir) == stamp;
|
||||
fs::path prefix = MingwPrefix();
|
||||
for (std::string_view name : CrafterBuildModules) {
|
||||
fs::path cppmPath = sourceDir / std::format("{}.cppm", name);
|
||||
|
|
@ -530,7 +590,7 @@ namespace {
|
|||
if (!fs::exists(cppmPath)) {
|
||||
throw std::runtime_error(std::format("module source {} not found in {} (set CRAFTER_BUILD_HOME)", name, sourceDir.string()));
|
||||
}
|
||||
if (fs::exists(pcmPath) && fs::last_write_time(cppmPath) < fs::last_write_time(pcmPath)) {
|
||||
if (upToDate && fs::exists(pcmPath)) {
|
||||
continue;
|
||||
}
|
||||
std::string cmd = std::format(
|
||||
|
|
@ -546,6 +606,10 @@ namespace {
|
|||
throw std::runtime_error(std::format("Failed to precompile {} (exit {}): {}", name, r.exitCode, r.output));
|
||||
}
|
||||
}
|
||||
// Last, so an interrupted rebuild leaves the stamp disagreeing with the
|
||||
// sources and the next run starts over rather than trusting a half-built
|
||||
// set of PCMs.
|
||||
if (!upToDate) WriteCacheStamp(cacheDir, stamp);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -839,13 +903,17 @@ namespace {
|
|||
|
||||
void EnsureCrafterBuildPcms(const fs::path& sourceDir, const fs::path& cacheDir) {
|
||||
CacheLock lock(cacheDir);
|
||||
// Re-derived under the lock: another builder may have refreshed the
|
||||
// cache from its own sources while we waited.
|
||||
std::string stamp = CrafterBuildSourceStamp(sourceDir, CrafterBuildModules);
|
||||
bool upToDate = ReadCacheStamp(cacheDir) == stamp;
|
||||
for (std::string_view name : CrafterBuildModules) {
|
||||
fs::path cppmPath = sourceDir / std::format("{}.cppm", name);
|
||||
fs::path pcmPath = cacheDir / std::format("{}.pcm", name);
|
||||
if (!fs::exists(cppmPath)) {
|
||||
throw std::runtime_error(std::format("module source {} not found in {} (set CRAFTER_BUILD_HOME)", name, sourceDir.string()));
|
||||
}
|
||||
if (fs::exists(pcmPath) && fs::last_write_time(cppmPath) < fs::last_write_time(pcmPath)) {
|
||||
if (upToDate && fs::exists(pcmPath)) {
|
||||
continue;
|
||||
}
|
||||
std::string cmd = std::format(
|
||||
|
|
@ -860,6 +928,10 @@ namespace {
|
|||
throw std::runtime_error(std::format("Failed to precompile {} (exit {}): {}", name, r.exitCode, r.output));
|
||||
}
|
||||
}
|
||||
// Last, so an interrupted rebuild leaves the stamp disagreeing with the
|
||||
// sources and the next run starts over rather than trusting a half-built
|
||||
// set of PCMs.
|
||||
if (!upToDate) WriteCacheStamp(cacheDir, stamp);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue