StdPcmDir named the directory by target, march and toolchain, then grew a case per target for the rest: the wasm flags, and with the first fix for #43 the mingw prefix. Every other flag BuildStdPcm passes went unkeyed, so configurations that differed only there took turns rebuilding one std PCM in place, under the PCMs built against it: a project with -mtune=generic against project.cpp's -mtune=native on every run, or two sysroots for one target. And the mingw prefix was keyed only when there was one, so a build without a prefix landed in the directory an older release had filled with a CRAFTER_MINGW_DIR=/usr build, and relinked its objects. StdPcmFlags is now the one source of the flags BuildStdPcm compiles with, and StdPcmKey names the directory by target, march and toolchain followed by a hash of those flags and, for the mingw target, of the prefix behind its --sysroot. VariantId hashes StdPcmKey in place of the mingw prefix: units do not notice a std module that moved directories, so a build has to move with it. Every std PCM directory and build directory moves once, as after a compiler update; the old ones stay until `clean` and the cache is cleared by hand. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
228 lines
11 KiB
C++
228 lines
11 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");
|
|
}
|
|
{
|
|
// The compiler is compile state as much as any flag: a PCM can only
|
|
// be read by the clang release that wrote it, and nothing in a
|
|
// project's sources changes when the compiler does. With the
|
|
// toolchain left out of the id, a project built with clang 22 kept
|
|
// its PCMs after crafter-build moved to clang 23; the build after
|
|
// the update found nothing stale, and the first edit failed with
|
|
// "module file ... uses an older format that is no longer supported"
|
|
// until the project was cleaned by hand.
|
|
Configuration cfg = MakeBase();
|
|
Toolchain older;
|
|
older.clangxx = "/app/bin/clang++";
|
|
older.clangVersion = "clang version 22.1.8";
|
|
Toolchain newer = older;
|
|
newer.clangVersion = "clang version 23.1.2";
|
|
Check(cfg.VariantId(older) != cfg.VariantId(newer), "a compiler update at the same path perturbs VariantId");
|
|
Check(cfg.VariantId(older) == cfg.VariantId(older), "VariantId under one toolchain is deterministic");
|
|
|
|
// Two installs of one release are still two toolchains: their
|
|
// libc++ need not be the same build.
|
|
Toolchain elsewhere = older;
|
|
elsewhere.clangxx = "/usr/bin/clang++";
|
|
Check(cfg.VariantId(older) != cfg.VariantId(elsewhere), "the same release installed elsewhere perturbs VariantId");
|
|
|
|
Check(cfg.VariantId() == cfg.VariantId(GetToolchain()), "VariantId() is the variant under the running toolchain");
|
|
Check(cfg.BuildDir().filename() == cfg.VariantId(GetToolchain()), "BuildDir is named for the running toolchain's variant");
|
|
}
|
|
{
|
|
// The mingw-w64 prefix decides what a mingw build compiles against.
|
|
// Before issue #43 it was clang's sysroot as is, which put the host's
|
|
// headers on the include path, and with the prefix left out of the
|
|
// id, the release that fixed that reused the objects built against
|
|
// them: the next build only relinked, and failed to link as before.
|
|
Configuration mingw = MakeBase();
|
|
mingw.target = "x86_64-w64-mingw32";
|
|
Toolchain discovered;
|
|
discovered.clangxx = "/app/bin/clang++";
|
|
Toolchain hostUsr = discovered;
|
|
hostUsr.mingwPrefix = "/run/host/usr";
|
|
Toolchain optMingw = discovered;
|
|
optMingw.mingwPrefix = "/opt/mingw";
|
|
Check(mingw.VariantId(discovered) != mingw.VariantId(hostUsr), "a mingw prefix perturbs a mingw configuration's VariantId");
|
|
Check(mingw.VariantId(hostUsr) != mingw.VariantId(optMingw), "two mingw prefixes are two variants");
|
|
Configuration host = MakeBase();
|
|
Check(host.VariantId(discovered) == host.VariantId(hostUsr), "the mingw prefix leaves other targets' variants alone");
|
|
|
|
// The std module's directory is keyed the same way, and so is a
|
|
// prefix given whole against one reached through a view.
|
|
Check(StdPcmKey(mingw, discovered) != StdPcmKey(mingw, hostUsr), "a mingw prefix moves the mingw std module");
|
|
Check(StdPcmKey(mingw, hostUsr) != StdPcmKey(mingw, optMingw), "two mingw prefixes are two std modules");
|
|
Toolchain hostUsrWhole = hostUsr;
|
|
hostUsrWhole.mingwPrefixIsSystem = !hostUsr.mingwPrefixIsSystem;
|
|
Check(StdPcmKey(mingw, hostUsr) != StdPcmKey(mingw, hostUsrWhole), "a prefix given whole and one shown through a view are two std modules");
|
|
Check(mingw.VariantId(hostUsr) != mingw.VariantId(hostUsrWhole), "and two variants, through the std module");
|
|
Check(StdPcmKey(host, discovered) == StdPcmKey(host, hostUsr), "the mingw prefix leaves other targets' std modules alone");
|
|
}
|
|
{
|
|
// Everything the std module is compiled with keys its directory, not
|
|
// only target and march. -mtune was left out, so a project built
|
|
// with -mtune=generic shared one std PCM with project.cpp's
|
|
// -mtune=native, and the two rebuilt it in turn on every run, under
|
|
// every module built against it; two sysroots did the same.
|
|
Toolchain tc;
|
|
tc.clangxx = "/app/bin/clang++";
|
|
tc.clangVersion = "clang version 23.1.2";
|
|
Configuration native = MakeBase();
|
|
Configuration generic = MakeBase();
|
|
generic.mtune = "generic";
|
|
Check(StdPcmKey(native, tc) != StdPcmKey(generic, tc), "-mtune keys the std module");
|
|
Configuration sysrootA = MakeBase();
|
|
sysrootA.sysroot = "/opt/sysroot-a";
|
|
Configuration sysrootB = MakeBase();
|
|
sysrootB.sysroot = "/opt/sysroot-b";
|
|
Check(StdPcmKey(sysrootA, tc) != StdPcmKey(sysrootB, tc) && StdPcmKey(sysrootA, tc) != StdPcmKey(native, tc), "the sysroot keys the std module");
|
|
Configuration wasm = MakeBase();
|
|
wasm.target = "wasm32-wasip1";
|
|
wasm.sysroot = "/opt/wasi-sysroot";
|
|
Configuration relaxed = MakeBase();
|
|
relaxed.target = wasm.target;
|
|
relaxed.sysroot = wasm.sysroot;
|
|
relaxed.wasmVariantFlags = {"-mrelaxed-simd"};
|
|
Check(StdPcmKey(wasm, tc) != StdPcmKey(relaxed, tc), "a wasm variant pass's flags key the std module");
|
|
Check(StdPcmKey(native, tc) == StdPcmKey(MakeBase(), tc), "one configuration, one std module");
|
|
Check(StdPcmKey(native, tc).starts_with(std::format("{}-{}-{}-", native.target, native.march, tc.Key())), std::format("the directory still says target, march and toolchain (got '{}')", StdPcmKey(native, tc)));
|
|
Check(native.VariantId(tc) != generic.VariantId(tc) && sysrootA.VariantId(tc) != sysrootB.VariantId(tc), "a build moves with its std module");
|
|
}
|
|
|
|
if (Failures > 0) {
|
|
std::println(std::cerr, "{} assertions failed", Failures);
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|