Crafter.Build/tests/VariantId/main.cpp
Jorijn van der Graaf b162cc0552
test: a mingw build sees no header from its prefix's include/
MingwSysroot stages a prefix shaped like a Linux system's /usr with a
mingw-w64 cross toolchain in it, without needing a real one: glibc's
<sys/single_threaded.h> in include/, gcc's per-target directory, and a
stand-in mingw-w64 tree whose libstdc++ has an empty std module. A
static library for x86_64-w64-mingw32, so that nothing is linked,
compiles a C++ and a C source that stop with #error when they see the
prefix's include/ or miss mingw-w64's or libstdc++'s headers. Against
the library before the fix, the C++ source stopped on
<sys/single_threaded.h>. The test also checks that the std module is
not cached in the directory older releases built it in against the
prefix itself. It needs only clang, so it runs in CI too, where no
mingw-w64 is installed.

The stand-in std module's source is written through std::format: the
build's import scanner reads string literals as code, and the literal
"export module std;" made the test itself a unit of a module std.

VariantId checks that a prefix gives a mingw configuration a variant of
its own, that two prefixes give two, and that other targets ignore it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 20:48:06 +01:00

188 lines
8 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");
}
if (Failures > 0) {
std::println(std::cerr, "{} assertions failed", Failures);
return 1;
}
return 0;
}