// 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; }