154 lines
6.8 KiB
C++
154 lines
6.8 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;
|
||
|
|
|
||
|
|
// Freshness of the std PCM and of project.so used to be an mtime comparison
|
||
|
|
// against files that come with the install: std.cppm and the crafter-build
|
||
|
|
// executable. A Flatpak (any OSTree deployment) gives every installed file
|
||
|
|
// mtime 0, so those inputs always looked older than the cache: a clang bump
|
||
|
|
// would never rebuild std.pcm, a crafter-build update would never recompile
|
||
|
|
// project.so, and clang crashes on the stale PCM instead of saying so.
|
||
|
|
//
|
||
|
|
// Both are now keyed on content stamps (the compile command, the std module
|
||
|
|
// source, the crafter-build version, the toolchain key and the module source
|
||
|
|
// stamp) and the cache directory carries the toolchain key in its name. This
|
||
|
|
// test drives the shapes an mtime rule gets wrong: a std.pcm dated before its
|
||
|
|
// source must still be reused, a warm load must leave project.so alone, and a
|
||
|
|
// stamp that disagrees must rebuild even when the artifact is dated after
|
||
|
|
// every input.
|
||
|
|
namespace {
|
||
|
|
std::int32_t Failures = 0;
|
||
|
|
|
||
|
|
void Check(bool cond, std::string_view msg) {
|
||
|
|
if (!cond) {
|
||
|
|
std::println(std::cerr, "FAIL: {}", msg);
|
||
|
|
++Failures;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
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 {};
|
||
|
|
}
|
||
|
|
|
||
|
|
bool Load(const fs::path& projectFile, std::string_view what) {
|
||
|
|
std::array<std::string_view, 0> noArgs = {};
|
||
|
|
try {
|
||
|
|
(void)LoadProject(projectFile, noArgs);
|
||
|
|
return true;
|
||
|
|
} catch (const std::exception& e) {
|
||
|
|
std::println(std::cerr, "FAIL: LoadProject ({}) threw: {}", what, e.what());
|
||
|
|
++Failures;
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fs::path StampOf(const fs::path& artifact) {
|
||
|
|
fs::path stamp = artifact;
|
||
|
|
stamp += ".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-toolchain-key";
|
||
|
|
std::error_code ec;
|
||
|
|
fs::remove_all(scratch, ec);
|
||
|
|
fs::create_directories(scratch);
|
||
|
|
|
||
|
|
// A private copy of the module sources and a cold private cache, as in
|
||
|
|
// HostCacheSourceStamp.
|
||
|
|
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 = \"keyed\";\n"
|
||
|
|
" cfg.outputName = \"keyed\";\n"
|
||
|
|
" cfg.type = ConfigurationType::Executable;\n"
|
||
|
|
" return cfg;\n"
|
||
|
|
"}\n";
|
||
|
|
|
||
|
|
if (!Load(projectFile, "cold")) return 1;
|
||
|
|
|
||
|
|
const fs::path cacheDir = FindModuleCacheDir(cacheRoot);
|
||
|
|
Check(!cacheDir.empty(), "a module cache directory was created");
|
||
|
|
if (cacheDir.empty()) return 1;
|
||
|
|
Check(cacheDir.filename().string().contains(GetToolchain().Key()), std::format("the cache directory is named for the toolchain (got '{}', key '{}')", cacheDir.filename().string(), GetToolchain().Key()));
|
||
|
|
|
||
|
|
const fs::path stdPcm = cacheDir / "std.pcm";
|
||
|
|
const fs::path projectSo = projectDir / "build" / "project.so";
|
||
|
|
Check(fs::exists(stdPcm) && fs::exists(StampOf(stdPcm)), "std.pcm and its stamp exist after a cold load");
|
||
|
|
Check(fs::exists(projectSo) && fs::exists(StampOf(projectSo)), "project.so and its stamp exist after a cold load");
|
||
|
|
if (!fs::exists(StampOf(stdPcm)) || !fs::exists(StampOf(projectSo))) return 1;
|
||
|
|
|
||
|
|
// The OSTree shape, inverted: std.pcm dated before its source. A stamp
|
||
|
|
// rule reuses it; an mtime rule would rebuild it on every run. project.so
|
||
|
|
// keeps two mtime inputs that live next to it (project.cpp and dependency
|
||
|
|
// artifacts) but the executable is no longer one of them, so a warm load
|
||
|
|
// with nothing changed leaves it alone.
|
||
|
|
const fs::file_time_type epoch{};
|
||
|
|
fs::last_write_time(stdPcm, epoch);
|
||
|
|
const fs::file_time_type afterCold = fs::last_write_time(projectSo);
|
||
|
|
if (!Load(projectFile, "std.pcm dated at the epoch")) return 1;
|
||
|
|
Check(fs::last_write_time(stdPcm) == epoch, "std.pcm dated before its source is still reused");
|
||
|
|
Check(fs::last_write_time(projectSo) == afterCold, "project.so is left alone when nothing changed");
|
||
|
|
|
||
|
|
// The other half: artifacts dated after every input, with a stamp that
|
||
|
|
// disagrees. An mtime rule keeps them; the stamp rule rebuilds.
|
||
|
|
const fs::file_time_type future = fs::file_time_type::clock::now() + std::chrono::hours(24);
|
||
|
|
std::ofstream(StampOf(stdPcm)) << "stale";
|
||
|
|
fs::last_write_time(stdPcm, future);
|
||
|
|
if (!Load(projectFile, "stale std.pcm stamp")) return 1;
|
||
|
|
Check(fs::last_write_time(stdPcm) != future, "a disagreeing stamp rebuilds std.pcm even when it is dated after its source");
|
||
|
|
{
|
||
|
|
std::ifstream in(StampOf(stdPcm));
|
||
|
|
std::string stamp;
|
||
|
|
std::getline(in, stamp);
|
||
|
|
Check(stamp != "stale" && !stamp.empty(), "the rebuilt std.pcm gets its stamp back");
|
||
|
|
}
|
||
|
|
|
||
|
|
std::ofstream(StampOf(projectSo)) << "stale";
|
||
|
|
fs::last_write_time(projectSo, future);
|
||
|
|
if (!Load(projectFile, "stale project.so stamp")) return 1;
|
||
|
|
Check(fs::last_write_time(projectSo) != future, "a disagreeing stamp recompiles project.so even when it is dated after its inputs");
|
||
|
|
|
||
|
|
// The one mtime input that legitimately remains: the project file itself.
|
||
|
|
const fs::file_time_type beforeEdit = fs::last_write_time(projectSo);
|
||
|
|
{
|
||
|
|
std::ofstream append(projectFile, std::ios::app);
|
||
|
|
append << "// edited\n";
|
||
|
|
}
|
||
|
|
fs::last_write_time(projectFile, fs::file_time_type::clock::now() + std::chrono::seconds(2));
|
||
|
|
if (!Load(projectFile, "edited project.cpp")) return 1;
|
||
|
|
Check(fs::last_write_time(projectSo) != beforeEdit, "an edited project.cpp recompiles project.so");
|
||
|
|
|
||
|
|
fs::remove_all(scratch, ec);
|
||
|
|
return Failures == 0 ? 0 : 1;
|
||
|
|
}
|