150 lines
5.9 KiB
C++
150 lines
5.9 KiB
C++
|
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||
|
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||
|
|
|
||
|
|
#include <stdlib.h>
|
||
|
|
import std;
|
||
|
|
import Crafter.Build;
|
||
|
|
namespace fs = std::filesystem;
|
||
|
|
using namespace Crafter;
|
||
|
|
|
||
|
|
// The host PCM cache is keyed by `<target>-<march>` alone, so every
|
||
|
|
// crafter-build on a machine — a package install and a working checkout, two
|
||
|
|
// checkouts of different versions — writes the same `Crafter.Build-*.pcm`
|
||
|
|
// files. Freshness was a per-file mtime comparison, which says "the PCM is
|
||
|
|
// newer than my source, reuse it" whether or not it was built from *my*
|
||
|
|
// source. The loser then compiled its project.cpp against the other install's
|
||
|
|
// declarations: no error, and a project.so built against a layout that doesn't
|
||
|
|
// match the library it's loaded into — the same silent shape as issue #27.
|
||
|
|
//
|
||
|
|
// So the invalidation has to key on the source bytes, and this test asserts it
|
||
|
|
// notices a change the mtime rule provably cannot see: different content with a
|
||
|
|
// deliberately *older* timestamp.
|
||
|
|
namespace {
|
||
|
|
std::int32_t Failures = 0;
|
||
|
|
|
||
|
|
void Check(bool cond, std::string_view msg) {
|
||
|
|
if (!cond) {
|
||
|
|
std::println(std::cerr, "FAIL: {}", msg);
|
||
|
|
++Failures;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// The cache subdirectory is named for the host target and march; glob for
|
||
|
|
// it rather than reconstructing the naming rule here.
|
||
|
|
fs::path FindModuleCacheDir(const fs::path& cacheRoot) {
|
||
|
|
for (const fs::directory_entry& entry : fs::directory_iterator(cacheRoot / "crafter.build")) {
|
||
|
|
if (entry.is_directory()) return entry.path();
|
||
|
|
}
|
||
|
|
return {};
|
||
|
|
}
|
||
|
|
|
||
|
|
std::string ReadStamp(const fs::path& moduleCacheDir) {
|
||
|
|
std::ifstream in(moduleCacheDir / "crafter-build-sources.stamp");
|
||
|
|
std::string stamp;
|
||
|
|
std::getline(in, stamp);
|
||
|
|
return stamp;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
int main() {
|
||
|
|
fs::path installedHome = GetCrafterBuildHome();
|
||
|
|
if (!fs::exists(installedHome / "Crafter.Build.cppm")) {
|
||
|
|
std::println(std::cerr, "SKIP: no module sources at {}", installedHome.string());
|
||
|
|
return 77;
|
||
|
|
}
|
||
|
|
|
||
|
|
fs::path scratch = fs::temp_directory_path() / "crafter-build-host-cache-source-stamp";
|
||
|
|
std::error_code ec;
|
||
|
|
fs::remove_all(scratch, ec);
|
||
|
|
fs::create_directories(scratch);
|
||
|
|
|
||
|
|
// A private copy of the module sources, so the mutation below can't touch
|
||
|
|
// the real install, and a cold private cache so the first LoadProject has
|
||
|
|
// to populate it.
|
||
|
|
fs::path home = scratch / "share" / "crafter-build";
|
||
|
|
fs::create_directories(home.parent_path());
|
||
|
|
fs::copy(installedHome, home, fs::copy_options::recursive);
|
||
|
|
setenv("CRAFTER_BUILD_HOME", home.string().c_str(), 1);
|
||
|
|
|
||
|
|
fs::path cacheRoot = scratch / "cache";
|
||
|
|
fs::create_directories(cacheRoot);
|
||
|
|
setenv("XDG_CACHE_HOME", cacheRoot.string().c_str(), 1);
|
||
|
|
|
||
|
|
fs::path projectDir = scratch / "project";
|
||
|
|
fs::create_directories(projectDir);
|
||
|
|
fs::path projectFile = projectDir / "project.cpp";
|
||
|
|
std::ofstream(projectFile) <<
|
||
|
|
"import std;\n"
|
||
|
|
"import Crafter.Build;\n"
|
||
|
|
"using namespace Crafter;\n"
|
||
|
|
"extern \"C\" Configuration CrafterBuildProject(std::span<const std::string_view>) {\n"
|
||
|
|
" Configuration cfg;\n"
|
||
|
|
" cfg.path = \"./\";\n"
|
||
|
|
" cfg.name = \"stamped\";\n"
|
||
|
|
" cfg.outputName = \"stamped\";\n"
|
||
|
|
" cfg.target = HostTarget();\n"
|
||
|
|
" cfg.type = ConfigurationType::Executable;\n"
|
||
|
|
" return cfg;\n"
|
||
|
|
"}\n";
|
||
|
|
|
||
|
|
std::array<std::string_view, 0> noArgs = {};
|
||
|
|
|
||
|
|
try {
|
||
|
|
(void)LoadProject(projectFile, noArgs);
|
||
|
|
} catch (const std::exception& e) {
|
||
|
|
std::println(std::cerr, "FAIL: first LoadProject threw: {}", e.what());
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
fs::path moduleCacheDir = FindModuleCacheDir(cacheRoot);
|
||
|
|
Check(!moduleCacheDir.empty(), "a module cache directory was created");
|
||
|
|
if (moduleCacheDir.empty()) return 1;
|
||
|
|
|
||
|
|
fs::path probePcm = moduleCacheDir / "Crafter.Build-Clang.pcm";
|
||
|
|
Check(fs::exists(probePcm), "the cold run populated the cache");
|
||
|
|
if (!fs::exists(probePcm)) return 1;
|
||
|
|
|
||
|
|
std::string firstStamp = ReadStamp(moduleCacheDir);
|
||
|
|
Check(!firstStamp.empty(), "the cold run recorded a source stamp");
|
||
|
|
fs::file_time_type afterCold = fs::last_write_time(probePcm);
|
||
|
|
|
||
|
|
// Unchanged sources must be a no-op — the stamp is an invalidation key, not
|
||
|
|
// an excuse to precompile eleven modules on every invocation.
|
||
|
|
try {
|
||
|
|
(void)LoadProject(projectFile, noArgs);
|
||
|
|
} catch (const std::exception& e) {
|
||
|
|
std::println(std::cerr, "FAIL: warm LoadProject threw: {}", e.what());
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
Check(fs::last_write_time(probePcm) == afterCold, "unchanged sources do not rebuild the cache");
|
||
|
|
Check(ReadStamp(moduleCacheDir) == firstStamp, "unchanged sources keep the same stamp");
|
||
|
|
|
||
|
|
// Now the case the mtime rule cannot see. The edit lands in a module the
|
||
|
|
// probe does not name, because the cached PCMs import each other: a change
|
||
|
|
// anywhere in the set invalidates all of them.
|
||
|
|
fs::path edited = home / "Crafter.Build-Progress.cppm";
|
||
|
|
{
|
||
|
|
std::ofstream append(edited, std::ios::app);
|
||
|
|
append << "\n// Content change with a backdated timestamp.\n";
|
||
|
|
}
|
||
|
|
fs::last_write_time(edited, afterCold - std::chrono::hours(24));
|
||
|
|
Check(fs::last_write_time(edited) < fs::last_write_time(probePcm), "the edited source really is older than the cached PCM");
|
||
|
|
|
||
|
|
try {
|
||
|
|
(void)LoadProject(projectFile, noArgs);
|
||
|
|
} catch (const std::exception& e) {
|
||
|
|
std::println(std::cerr, "FAIL: LoadProject after the edit threw: {}", e.what());
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
Check(fs::last_write_time(probePcm) > afterCold, "a content change rebuilds the cache even when the source's mtime is older");
|
||
|
|
Check(ReadStamp(moduleCacheDir) != firstStamp, "the recorded stamp follows the sources");
|
||
|
|
|
||
|
|
fs::remove_all(scratch, ec);
|
||
|
|
|
||
|
|
if (Failures > 0) {
|
||
|
|
std::println(std::cerr, "{} assertions failed", Failures);
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
return 0;
|
||
|
|
}
|