Compare commits
5 changed files with 57 additions and 257 deletions
|
|
@ -107,7 +107,7 @@ import Crafter.LintRules;
|
||||||
<prefix>/bin/crafter-build # the executable
|
<prefix>/bin/crafter-build # the executable
|
||||||
<prefix>/lib/libcrafter-build.a # static archive for downstream consumers
|
<prefix>/lib/libcrafter-build.a # static archive for downstream consumers
|
||||||
<prefix>/share/crafter-build/*.cppm # module sources (rebuilt on user machine on first run)
|
<prefix>/share/crafter-build/*.cppm # module sources (rebuilt on user machine on first run)
|
||||||
~/.cache/crafter.build/<target>-<march>-<clang>-<hash>/ # std.pcm + Crafter.Build*.pcm, built locally per toolchain
|
~/.cache/crafter.build/<target>-<march>/ # std.pcm + Crafter.Build*.pcm, built locally
|
||||||
```
|
```
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
|
||||||
|
|
@ -246,14 +246,11 @@ void Configuration::GetInterfacesAndImplementations(std::span<fs::path> interfac
|
||||||
}
|
}
|
||||||
|
|
||||||
fs::path Crafter::StdPcmDir(const Configuration& config) {
|
fs::path Crafter::StdPcmDir(const Configuration& config) {
|
||||||
// The std PCM is cached per target+march+toolchain: a PCM is only valid
|
// The std PCM is cached per target+march, but a wasm variant pass compiles
|
||||||
// for the exact clang that wrote it, so a compiler bump starts a cold
|
// it with extra codegen flags (e.g. -mrelaxed-simd) — its BMI must not be
|
||||||
// directory instead of handing the new clang a PCM it will crash on. A
|
// shared with the baseline's, or the consuming TUs see a target-feature
|
||||||
// wasm variant pass compiles it with extra codegen flags (e.g.
|
// mismatch. Suffix the cache dir with the variant flags when present.
|
||||||
// -mrelaxed-simd) — its BMI must not be shared with the baseline's, or
|
std::string stdPcmKey = std::format("{}-{}", config.target, config.march);
|
||||||
// the consuming TUs see a target-feature mismatch. Suffix the cache dir
|
|
||||||
// with the variant flags when present.
|
|
||||||
std::string stdPcmKey = std::format("{}-{}-{}", config.target, config.march, GetToolchain().Key());
|
|
||||||
for (const std::string& f : config.wasmVariantFlags) {
|
for (const std::string& f : config.wasmVariantFlags) {
|
||||||
stdPcmKey += "+";
|
stdPcmKey += "+";
|
||||||
for (char c : f) stdPcmKey += (c == '/' || c == '\\') ? '_' : c;
|
for (char c : f) stdPcmKey += (c == '/' || c == '\\') ? '_' : c;
|
||||||
|
|
|
||||||
|
|
@ -90,35 +90,15 @@ namespace {
|
||||||
return cacheDir / "crafter-build-sources.stamp";
|
return cacheDir / "crafter-build-sources.stamp";
|
||||||
}
|
}
|
||||||
|
|
||||||
// A stamp is a one-line content hash kept next to the artifact it
|
std::string ReadCacheStamp(const fs::path& cacheDir) {
|
||||||
// describes. Written last, after the artifact, so an interrupted build
|
std::ifstream in(CacheStampPath(cacheDir), std::ios::binary);
|
||||||
// leaves a stamp that disagrees and the next run starts over.
|
|
||||||
std::string ReadStamp(const fs::path& file) {
|
|
||||||
std::ifstream in(file, std::ios::binary);
|
|
||||||
std::string stamp;
|
std::string stamp;
|
||||||
std::getline(in, stamp);
|
std::getline(in, stamp);
|
||||||
return stamp;
|
return stamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
void WriteStamp(const fs::path& file, std::string_view stamp) {
|
void WriteCacheStamp(const fs::path& cacheDir, const std::string& stamp) {
|
||||||
std::ofstream(file, std::ios::binary | std::ios::trunc) << stamp << '\n';
|
std::ofstream(CacheStampPath(cacheDir), std::ios::binary | std::ios::trunc) << stamp << '\n';
|
||||||
}
|
|
||||||
|
|
||||||
fs::path StampPath(const fs::path& artifact) {
|
|
||||||
fs::path stamp = artifact;
|
|
||||||
stamp += ".stamp";
|
|
||||||
return stamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string HashStamp(std::string_view content) {
|
|
||||||
return std::format("{:016x}", std::hash<std::string_view>{}(content));
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string FileBytes(const fs::path& file) {
|
|
||||||
std::ifstream in(file, std::ios::binary);
|
|
||||||
std::ostringstream buffer;
|
|
||||||
buffer << in.rdbuf();
|
|
||||||
return buffer.str();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -515,17 +495,12 @@ namespace {
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
// project.so is stale when its stamp (crafter-build version, toolchain,
|
// project.so predates its source, the crafter-build that will load it, or
|
||||||
// module sources and compile command) disagrees, or when it predates its
|
// a pre-dependency artifact — any of them means recompiling.
|
||||||
// source or a pre-dependency artifact. The executable's mtime is not an
|
bool ProjectFileStale(const fs::path& artifact, const fs::path& project, const fs::path& hostExe, const ProjectFileDependencies& deps) {
|
||||||
// input any more: an OSTree deployment dates every installed file at the
|
|
||||||
// epoch, so a comparison against it never fired after an update and the
|
|
||||||
// old project.so was loaded into a binary it did not match.
|
|
||||||
bool ProjectFileStale(const fs::path& artifact, const fs::path& project, std::string_view stamp, const ProjectFileDependencies& deps) {
|
|
||||||
if (!fs::exists(artifact)) return true;
|
if (!fs::exists(artifact)) return true;
|
||||||
if (ReadStamp(StampPath(artifact)) != stamp) return true;
|
|
||||||
const fs::file_time_type built = fs::last_write_time(artifact);
|
const fs::file_time_type built = fs::last_write_time(artifact);
|
||||||
return built < fs::last_write_time(project) || built < deps.newestArtifact;
|
return built < fs::last_write_time(project) || built < fs::last_write_time(hostExe) || built < deps.newestArtifact;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -636,22 +611,21 @@ std::string Crafter::BuildStdPcm(const Configuration& config, fs::path stdPcm) {
|
||||||
|
|
||||||
fs::path stdCc = GetToolchain().mingwSysroot / "include" / "c++" / mingWversion / "bits" / "std.cc";
|
fs::path stdCc = GetToolchain().mingwSysroot / "include" / "c++" / mingWversion / "bits" / "std.cc";
|
||||||
|
|
||||||
|
if(!fs::exists(stdPcm) || fs::last_write_time(stdPcm) < fs::last_write_time(stdCc)) {
|
||||||
// -femulated-tls keeps PCM TLS access matching libstdc++'s emutls
|
// -femulated-tls keeps PCM TLS access matching libstdc++'s emutls
|
||||||
// definitions; mismatch surfaces as undefined std::__once_callable.
|
// definitions; mismatch surfaces as undefined std::__once_callable.
|
||||||
// The std.cc → std.cppm copy uses a plain filename: held under the
|
// The std.cc → std.cppm copy uses a plain filename: held under the
|
||||||
// cache lock, no other builder writes this path concurrently.
|
// cache lock, no other builder writes this path concurrently.
|
||||||
fs::path stdCppm = stdPcm.parent_path() / "std.cppm";
|
fs::path stdCppm = stdPcm.parent_path() / "std.cppm";
|
||||||
std::string command = std::format("{} --target={} -march={} -mtune={} -femulated-tls -O3 -std=c++26 -Wno-reserved-identifier -Wno-reserved-module-identifier --precompile {} -o {}", GetBaseCommand(config), config.target, config.march, config.mtune, stdCppm.string(), stdPcm.string());
|
|
||||||
std::string stamp = HashStamp(command + '\0' + FileBytes(stdCc));
|
|
||||||
if (fs::exists(stdPcm) && ReadStamp(StampPath(stdPcm)) == stamp) return "";
|
|
||||||
std::error_code copyEc;
|
std::error_code copyEc;
|
||||||
fs::copy_file(stdCc, stdCppm, fs::copy_options::overwrite_existing, copyEc);
|
fs::copy_file(stdCc, stdCppm, fs::copy_options::overwrite_existing, copyEc);
|
||||||
if (copyEc) {
|
if (copyEc) {
|
||||||
return std::format("copy {} -> {}: {}", stdCc.string(), stdCppm.string(), copyEc.message());
|
return std::format("copy {} -> {}: {}", stdCc.string(), stdCppm.string(), copyEc.message());
|
||||||
}
|
}
|
||||||
std::string result = RunBuildCommand(command);
|
return RunBuildCommand(std::format("{} --target={} -march={} -mtune={} -femulated-tls -O3 -std=c++26 -Wno-reserved-identifier -Wno-reserved-module-identifier --precompile {} -o {}", GetBaseCommand(config), config.target, config.march, config.mtune, stdCppm.string(), stdPcm.string()));
|
||||||
if (result.empty()) WriteStamp(StampPath(stdPcm), stamp);
|
} else {
|
||||||
return result;
|
return "";
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const bool isWasm = config.target.starts_with("wasm32");
|
const bool isWasm = config.target.starts_with("wasm32");
|
||||||
// wasi-sdk drops std.cppm at <sysroot>/share/libc++/v1/, the rest of
|
// wasi-sdk drops std.cppm at <sysroot>/share/libc++/v1/, the rest of
|
||||||
|
|
@ -695,16 +669,11 @@ std::string Crafter::BuildStdPcm(const Configuration& config, fs::path stdPcm) {
|
||||||
if (!isWasm && !config.sysroot.empty()) {
|
if (!isWasm && !config.sysroot.empty()) {
|
||||||
archFlags += std::format(" -nostdinc++ -isystem {}/usr/include/c++/v1", config.sysroot);
|
archFlags += std::format(" -nostdinc++ -isystem {}/usr/include/c++/v1", config.sysroot);
|
||||||
}
|
}
|
||||||
// Keyed on the command and the module source's bytes, never on
|
if(!fs::exists(stdPcm) || fs::last_write_time(stdPcm) < fs::last_write_time(stdCppm)) {
|
||||||
// mtimes: an installed std.cppm can be dated at the epoch (OSTree),
|
return RunBuildCommand(std::format("{} --target={} -std=c++26{}{} -O3 -Wno-reserved-identifier -Wno-reserved-module-identifier --precompile {} -o {}", GetBaseCommand(config), config.target, sysrootFlag, archFlags, stdCppm, stdPcm.string()));
|
||||||
// and a libc++ update under the same clang changes the bytes but
|
} else {
|
||||||
// not the path.
|
return "";
|
||||||
std::string command = std::format("{} --target={} -std=c++26{}{} -O3 -Wno-reserved-identifier -Wno-reserved-module-identifier --precompile {} -o {}", GetBaseCommand(config), config.target, sysrootFlag, archFlags, stdCppm, stdPcm.string());
|
}
|
||||||
std::string stamp = HashStamp(command + '\0' + FileBytes(stdCppm));
|
|
||||||
if (fs::exists(stdPcm) && ReadStamp(StampPath(stdPcm)) == stamp) return "";
|
|
||||||
std::string result = RunBuildCommand(command);
|
|
||||||
if (result.empty()) WriteStamp(StampPath(stdPcm), stamp);
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -752,17 +721,12 @@ namespace {
|
||||||
"Crafter.Build",
|
"Crafter.Build",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Returns the stamp of the module sources the cache was built from; it is
|
void EnsureCrafterBuildPcms(const fs::path& sourceDir, const fs::path& cacheDir, const Configuration& host) {
|
||||||
// one of project.so's inputs.
|
|
||||||
std::string EnsureCrafterBuildPcms(const fs::path& sourceDir, const fs::path& cacheDir, const Configuration& host) {
|
|
||||||
CacheLock lock(cacheDir);
|
CacheLock lock(cacheDir);
|
||||||
// Re-derived under the lock: another builder may have refreshed the
|
// Re-derived under the lock: another builder may have refreshed the
|
||||||
// cache from its own sources while we waited.
|
// cache from its own sources while we waited.
|
||||||
// The std PCM is an input to every module PCM: its stamp is folded in
|
std::string stamp = CrafterBuildSourceStamp(sourceDir, CrafterBuildModules);
|
||||||
// so a rebuilt std.pcm (a libc++ update under the same clang) rebuilds
|
const bool upToDate = ReadCacheStamp(cacheDir) == stamp;
|
||||||
// them too, instead of leaving PCMs that import a std.pcm that is gone.
|
|
||||||
std::string stamp = HashStamp(CrafterBuildSourceStamp(sourceDir, CrafterBuildModules) + '\0' + ReadStamp(StampPath(cacheDir / "std.pcm")));
|
|
||||||
const bool upToDate = ReadStamp(CacheStampPath(cacheDir)) == stamp;
|
|
||||||
for (std::string_view name : CrafterBuildModules) {
|
for (std::string_view name : CrafterBuildModules) {
|
||||||
fs::path cppmPath = sourceDir / std::format("{}.cppm", name);
|
fs::path cppmPath = sourceDir / std::format("{}.cppm", name);
|
||||||
fs::path pcmPath = cacheDir / std::format("{}.pcm", name);
|
fs::path pcmPath = cacheDir / std::format("{}.pcm", name);
|
||||||
|
|
@ -787,8 +751,7 @@ namespace {
|
||||||
// Last, so an interrupted rebuild leaves the stamp disagreeing with the
|
// Last, so an interrupted rebuild leaves the stamp disagreeing with the
|
||||||
// sources and the next run starts over rather than trusting a half-built
|
// sources and the next run starts over rather than trusting a half-built
|
||||||
// set of PCMs.
|
// set of PCMs.
|
||||||
if (!upToDate) WriteStamp(CacheStampPath(cacheDir), stamp);
|
if (!upToDate) WriteCacheStamp(cacheDir, stamp);
|
||||||
return stamp;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -802,6 +765,8 @@ Configuration Crafter::LoadProject(const fs::path& projectFile, std::span<const
|
||||||
}
|
}
|
||||||
fs::path soPath = buildDir / std::format("{}.so", absProject.stem().string());
|
fs::path soPath = buildDir / std::format("{}.so", absProject.stem().string());
|
||||||
|
|
||||||
|
fs::path hostExe = fs::read_symlink("/proc/self/exe");
|
||||||
|
|
||||||
fs::path sourceDir = Crafter::GetCrafterBuildHome();
|
fs::path sourceDir = Crafter::GetCrafterBuildHome();
|
||||||
|
|
||||||
// The host toolchain's own triple (x86_64-pc-linux-gnu on glibc distros,
|
// The host toolchain's own triple (x86_64-pc-linux-gnu on glibc distros,
|
||||||
|
|
@ -811,9 +776,7 @@ Configuration Crafter::LoadProject(const fs::path& projectFile, std::span<const
|
||||||
hostConfig.target = HostTarget();
|
hostConfig.target = HostTarget();
|
||||||
hostConfig.march = "native";
|
hostConfig.march = "native";
|
||||||
hostConfig.mtune = "native";
|
hostConfig.mtune = "native";
|
||||||
// The same directory Build() resolves the std PCM from, toolchain key
|
fs::path cacheDir = GetCacheDir() / std::format("{}-{}", hostConfig.target, hostConfig.march);
|
||||||
// included (see StdPcmDir).
|
|
||||||
fs::path cacheDir = StdPcmDir(hostConfig);
|
|
||||||
if (!fs::exists(cacheDir)) fs::create_directories(cacheDir);
|
if (!fs::exists(cacheDir)) fs::create_directories(cacheDir);
|
||||||
|
|
||||||
std::string stdResult = BuildStdPcm(hostConfig, cacheDir / "std.pcm");
|
std::string stdResult = BuildStdPcm(hostConfig, cacheDir / "std.pcm");
|
||||||
|
|
@ -821,7 +784,7 @@ Configuration Crafter::LoadProject(const fs::path& projectFile, std::span<const
|
||||||
throw std::runtime_error(std::format("Failed to build std.pcm: {}", stdResult));
|
throw std::runtime_error(std::format("Failed to build std.pcm: {}", stdResult));
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::string sourcesStamp = EnsureCrafterBuildPcms(sourceDir, cacheDir, hostConfig);
|
EnsureCrafterBuildPcms(sourceDir, cacheDir, hostConfig);
|
||||||
|
|
||||||
// After the host PCMs exist (a dependency's build imports Crafter.Build
|
// After the host PCMs exist (a dependency's build imports Crafter.Build
|
||||||
// from that cache) and before the staleness check (its artifacts are
|
// from that cache) and before the staleness check (its artifacts are
|
||||||
|
|
@ -830,6 +793,9 @@ Configuration Crafter::LoadProject(const fs::path& projectFile, std::span<const
|
||||||
// clang crashes on it.
|
// clang crashes on it.
|
||||||
ProjectFileDependencies preDeps = ResolveProjectFileDependencies(absProject, hostConfig);
|
ProjectFileDependencies preDeps = ResolveProjectFileDependencies(absProject, hostConfig);
|
||||||
|
|
||||||
|
const bool stale = ProjectFileStale(soPath, absProject, hostExe, preDeps);
|
||||||
|
|
||||||
|
if (stale) {
|
||||||
// -fuse-ld=lld like every other link the tool performs: lld is a hard
|
// -fuse-ld=lld like every other link the tool performs: lld is a hard
|
||||||
// dependency, binutils' ld is not (Alpine's clang would default to it),
|
// dependency, binutils' ld is not (Alpine's clang would default to it),
|
||||||
// and a dependency archive built in release holds ThinLTO bitcode,
|
// and a dependency archive built in release holds ThinLTO bitcode,
|
||||||
|
|
@ -842,18 +808,11 @@ Configuration Crafter::LoadProject(const fs::path& projectFile, std::span<const
|
||||||
"{} -o {}{}{}",
|
"{} -o {}{}{}",
|
||||||
GetBaseCommand(hostConfig), hostConfig.target, cacheDir.string(), preDeps.compileFlags,
|
GetBaseCommand(hostConfig), hostConfig.target, cacheDir.string(), preDeps.compileFlags,
|
||||||
absProject.string(), soPath.string(), preDeps.linkFlags, GetToolchain().LibcxxLinkFlags());
|
absProject.string(), soPath.string(), preDeps.linkFlags, GetToolchain().LibcxxLinkFlags());
|
||||||
// What project.so was compiled against. The module source stamp stands
|
|
||||||
// in for the executable: project.so resolves Crafter:: symbols from the
|
|
||||||
// running binary, so only the declarations (the .cppm files) matter,
|
|
||||||
// and an implementation-only rebuild of crafter-build leaves it valid.
|
|
||||||
const std::string projectStamp = HashStamp(std::format("{}\0{}\0{}\0{}", CrafterBuildVersion(), GetToolchain().Key(), sourcesStamp, compileCmd));
|
|
||||||
|
|
||||||
if (ProjectFileStale(soPath, absProject, projectStamp, preDeps)) {
|
|
||||||
std::string result = RunBuildCommand(compileCmd);
|
std::string result = RunBuildCommand(compileCmd);
|
||||||
if (!result.empty()) {
|
if (!result.empty()) {
|
||||||
throw std::runtime_error(std::format("Failed to compile project {}: {}", absProject.string(), result));
|
throw std::runtime_error(std::format("Failed to compile project {}: {}", absProject.string(), result));
|
||||||
}
|
}
|
||||||
WriteStamp(StampPath(soPath), projectStamp);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void* handle = dlopen(soPath.c_str(), RTLD_NOW | RTLD_GLOBAL);
|
void* handle = dlopen(soPath.c_str(), RTLD_NOW | RTLD_GLOBAL);
|
||||||
|
|
|
||||||
|
|
@ -142,8 +142,6 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
// Same LoadProject wiring as ConcurrentCacheRace above.
|
// Same LoadProject wiring as ConcurrentCacheRace above.
|
||||||
cfg.AddTest("HostCacheSourceStamp").Dependencies({ CrafterBuildLib.get() })
|
cfg.AddTest("HostCacheSourceStamp").Dependencies({ CrafterBuildLib.get() })
|
||||||
.LinkFlag("-Wl,--export-dynamic").LinkFlag("-ldl");
|
.LinkFlag("-Wl,--export-dynamic").LinkFlag("-ldl");
|
||||||
cfg.AddTest("HostCacheToolchainKey").Dependencies({ CrafterBuildLib.get() })
|
|
||||||
.LinkFlag("-Wl,--export-dynamic").LinkFlag("-ldl");
|
|
||||||
cfg.AddTest("ProjectDependency").Dependencies({ CrafterBuildLib.get() })
|
cfg.AddTest("ProjectDependency").Dependencies({ CrafterBuildLib.get() })
|
||||||
.LinkFlag("-Wl,--export-dynamic").LinkFlag("-ldl");
|
.LinkFlag("-Wl,--export-dynamic").LinkFlag("-ldl");
|
||||||
cfg.AddTest("ConcurrentDependencyReset").Dependencies({ CrafterBuildLib.get() });
|
cfg.AddTest("ConcurrentDependencyReset").Dependencies({ CrafterBuildLib.get() });
|
||||||
|
|
|
||||||
|
|
@ -1,154 +0,0 @@
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue