Crafter.Build/tests/VariantId/main.cpp
catbot e8fde57582 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.
2026-07-30 17:43:09 +00:00

142 lines
5.3 KiB
C++

// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
import std;
import Crafter.Build;
using namespace Crafter;
namespace {
std::int32_t Failures = 0;
void Check(bool cond, std::string_view msg) {
if (!cond) {
std::println(std::cerr, "FAIL: {}", msg);
++Failures;
}
}
Configuration MakeBase() {
Configuration cfg;
cfg.path = "/tmp/variant-id-test";
cfg.name = "vt";
cfg.outputName = "vt";
cfg.target = "x86_64-pc-linux-gnu";
cfg.march = "native";
cfg.mtune = "native";
cfg.type = ConfigurationType::Executable;
return cfg;
}
}
// VariantId is the cache key for build outputs. Two Configurations that
// differ only in something that affects codegen must produce different
// VariantId / BuildDir / BinDir, otherwise their .o files collide.
int main() {
{
// Baseline against itself: VariantId is deterministic.
Configuration a = MakeBase();
Configuration b = MakeBase();
Check(a.VariantId() == b.VariantId(), "identical configs have identical VariantId");
Check(a.BuildDir() == b.BuildDir(), "identical configs have identical BuildDir");
}
{
// type differs -> VariantId differs.
Configuration a = MakeBase();
Configuration b = MakeBase();
b.type = ConfigurationType::LibraryStatic;
Check(a.VariantId() != b.VariantId(), "type change perturbs VariantId");
}
{
// debug differs -> VariantId differs.
Configuration a = MakeBase();
Configuration b = MakeBase();
b.debug = true;
Check(a.VariantId() != b.VariantId(), "debug change perturbs VariantId");
}
{
// sysroot differs -> VariantId differs.
Configuration a = MakeBase();
Configuration b = MakeBase();
b.sysroot = "/opt/sysroot";
Check(a.VariantId() != b.VariantId(), "sysroot change perturbs VariantId");
}
{
// defines differ -> VariantId differs.
Configuration a = MakeBase();
Configuration b = MakeBase();
b.defines.push_back({"FOO", "1"});
Check(a.VariantId() != b.VariantId(), "define added perturbs VariantId");
}
{
// Same define name, different value -> VariantId differs.
Configuration a = MakeBase();
a.defines.push_back({"FOO", "1"});
Configuration b = MakeBase();
b.defines.push_back({"FOO", "2"});
Check(a.VariantId() != b.VariantId(), "define value change perturbs VariantId");
}
{
// compileFlags differ -> VariantId differs.
Configuration a = MakeBase();
Configuration b = MakeBase();
b.compileFlags.push_back("-fno-omit-frame-pointer");
Check(a.VariantId() != b.VariantId(), "compile flag added perturbs VariantId");
}
{
// target differs -> VariantId differs.
Configuration a = MakeBase();
Configuration b = MakeBase();
b.target = "aarch64-linux-gnu";
Check(a.VariantId() != b.VariantId(), "target change perturbs VariantId");
}
{
// march differs -> VariantId differs.
Configuration a = MakeBase();
Configuration b = MakeBase();
b.march = "x86-64-v3";
Check(a.VariantId() != b.VariantId(), "march change perturbs VariantId");
}
{
// A project flag the framework can't interpret still decides what gets
// compiled and bundled, so it has to key into VariantId. Otherwise both
// settings share one bin dir and interleave their outputs there — an
// index.html from one alongside a script from the other.
Configuration a = MakeBase();
Configuration b = MakeBase();
b.projectArgs.push_back("--no-webgpu");
Check(a.VariantId() != b.VariantId(), "project arg perturbs VariantId");
Check(a.BinDir() != b.BinDir(), "project arg yields a distinct BinDir");
}
{
// Distinct project args must not collide with each other either.
Configuration a = MakeBase();
a.projectArgs.push_back("--no-webgpu");
Configuration b = MakeBase();
b.projectArgs.push_back("--no-audio");
Check(a.VariantId() != b.VariantId(), "different project args yield different VariantId");
}
{
// PcmDir() differs between Executable (in BuildDir) and Library
// (in BinDir) — Library PCMs land in the installable bin dir so
// downstream consumers can find them.
Configuration exe = MakeBase();
Configuration lib = MakeBase();
lib.type = ConfigurationType::LibraryStatic;
Check(exe.PcmDir() == exe.BuildDir(), "Executable PcmDir == BuildDir");
Check(lib.PcmDir() == lib.BinDir(), "Library PcmDir == BinDir");
}
{
// VariantId is embedded in the path so distinct ids produce distinct dirs.
Configuration a = MakeBase();
Configuration b = MakeBase();
b.debug = true;
Check(a.BuildDir() != b.BuildDir(), "different VariantId yields different BuildDir");
Check(a.BinDir() != b.BinDir(), "different VariantId yields different BinDir");
}
if (Failures > 0) {
std::println(std::cerr, "{} assertions failed", Failures);
return 1;
}
return 0;
}