2026-07-23 01:24:42 +02:00
|
|
|
// SPDX-License-Identifier: LGPL-3.0-only
|
|
|
|
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
2026-04-23 01:57:25 +02:00
|
|
|
|
test runner, cross-target runners, lib/exe split
- subprocess-isolated test runner (replaces V1 dlopen-RunTest);
Pass/Fail/Crash/Timeout/Skipped outcomes via :Test partition
- TestRunner abstraction with command templates: Local, Ssh,
SshWin (cmd.exe-shell), QemuUser, FromEnv; probe-based skip
when runner unreachable
- transitive PCM-path propagation in Build(); resolveImport
walks deps recursively; depResults cache keyed by PcmDir()
so per-target builds don't collide
- cfg.sysroot threaded through BuildStdPcm + base compile/link
command (enables aarch64 cross via Arch Linux ARM rootfs)
- lib + exe split: project.cpp defines crafterBuildLib
(LibraryStatic) + crafterBuildExe (Executable depending on
it); build.sh produces lib/libcrafter-build.a alongside
bin/crafter-build for downstream static-link consumers
- Windows DLL+launcher: CRAFTER_API macro, /EXPORT flag for
project.dll's CrafterBuildProject; Crafter::Run as the real
entry point with main.cpp as a thin wrapper
- 18 tests: HelloWorld/WithModule/Defines/CrossProjectModule/
Diamond × (Linux + sshwin:winvm), plus Incremental,
BuildError, Libraries, RunnerClassification, QemuUser,
SshRunner, WindowsViaSsh, CrossArchAarch64
- single ./bin/crafter-build test runs everything; Windows
variants skip gracefully if winvm SSH alias unreachable
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 22:32:19 +02:00
|
|
|
module;
|
|
|
|
|
#include "Crafter.Build-Api.h"
|
2026-04-23 01:57:25 +02:00
|
|
|
export module Crafter.Build:Clang;
|
|
|
|
|
import std;
|
|
|
|
|
import :Shader;
|
2026-04-27 07:04:42 +02:00
|
|
|
import :Interface;
|
|
|
|
|
import :Implementation;
|
|
|
|
|
import :External;
|
2026-04-23 01:57:25 +02:00
|
|
|
namespace fs = std::filesystem;
|
|
|
|
|
|
|
|
|
|
export namespace Crafter {
|
|
|
|
|
struct BuildResult {
|
|
|
|
|
std::string result;
|
|
|
|
|
bool repack;
|
|
|
|
|
std::unordered_set<std::string> libs;
|
2026-05-01 19:02:14 +02:00
|
|
|
// Compile flags (typically -I include paths) the dep wants its
|
|
|
|
|
// consumers to see — sourced from its external dependencies' include
|
|
|
|
|
// dirs so headers a dep exposes in its public modules are reachable
|
|
|
|
|
// when a consumer #includes them directly. Propagates transitively.
|
|
|
|
|
std::unordered_set<std::string> publicCompileFlags;
|
2026-04-27 07:04:42 +02:00
|
|
|
};
|
2026-04-23 01:57:25 +02:00
|
|
|
|
2026-04-27 07:04:42 +02:00
|
|
|
struct Define {
|
2026-04-23 01:57:25 +02:00
|
|
|
std::string name;
|
|
|
|
|
std::string value;
|
|
|
|
|
};
|
|
|
|
|
|
feat: feature-detected browser-wasm variants (relaxed-SIMD) + variant-aware runtime
The browser wasm pipeline hardcoded -msimd128 for every wasm32 target and
baked a single wasm URL into index.html, so newer codegen features that
aren't yet baseline across engines (relaxed SIMD today; threads, future SIMD
revisions later) couldn't be adopted without dropping the browsers that lack
them.
Add a general, feature-parameterized mechanism owned entirely by
Crafter.Build:
- Configuration::wasmVariants declares N codegen variants (label, extra -m
flags, runtime probes). Build() compiles the baseline plus one
outputName.<label>.wasm per variant, recompiling the whole graph (incl.
dep libs + std PCM) with the variant's flags — relaxed-SIMD is per-TU
codegen, not a link switch. wasmVariantFlags folds into VariantId so each
variant's objects/PCMs land in their own build+bin dir.
- EnableWasiBrowserRuntime emits a variants.json manifest (label -> url +
probes), preferred-first with the baseline as the universal fallback.
- The shipped runtime.js runs inlined wasm-feature-detect probes
(relaxed-simd, simd, tail-call, bulk-memory, exception-handling, threads),
picks the first variant whose probes all pass, and falls back to the single
baked CRAFTER_WASM_URL when no manifest is present (backward compatible).
- EnableWasiRelaxedSimdVariant registers the relaxed-SIMD variant — the
motivating case (Chrome 114+/Firefox 120+ enable it by default; Safari
still flag-gates it as of mid-2026).
Verified end to end: a wasm32-wasip1 build emits both wasi-hello.wasm and
wasi-hello.relaxed-simd.wasm + variants.json; Firefox selects the
relaxed-simd variant and runs it.
Resolves #24
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 15:14:18 +00:00
|
|
|
// A browser-wasm codegen variant. The same wasm32 target is compiled more
|
|
|
|
|
// than once, each pass adding `flags` to every translation unit, so newer
|
|
|
|
|
// ISA features (relaxed SIMD today; threads, future SIMD revisions, … later)
|
|
|
|
|
// can be adopted without dropping engines that lack them. Each non-empty
|
|
|
|
|
// `label` emits an additional `outputName.<label>.wasm` alongside the
|
|
|
|
|
// baseline `outputName.wasm`; `probes` names the runtime feature-detect
|
|
|
|
|
// checks (see wasi-runtime/runtime.js's probe registry) that must all pass
|
|
|
|
|
// for a browser to be served this variant. The first variant whose probes
|
|
|
|
|
// pass wins; the baseline is the universal fallback.
|
|
|
|
|
struct WasmVariant {
|
|
|
|
|
std::string label;
|
|
|
|
|
std::vector<std::string> flags;
|
|
|
|
|
std::vector<std::string> probes;
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-23 01:57:25 +02:00
|
|
|
enum class ConfigurationType {
|
|
|
|
|
Executable,
|
|
|
|
|
LibraryStatic,
|
|
|
|
|
LibraryDynamic,
|
|
|
|
|
};
|
|
|
|
|
|
test runner, cross-target runners, lib/exe split
- subprocess-isolated test runner (replaces V1 dlopen-RunTest);
Pass/Fail/Crash/Timeout/Skipped outcomes via :Test partition
- TestRunner abstraction with command templates: Local, Ssh,
SshWin (cmd.exe-shell), QemuUser, FromEnv; probe-based skip
when runner unreachable
- transitive PCM-path propagation in Build(); resolveImport
walks deps recursively; depResults cache keyed by PcmDir()
so per-target builds don't collide
- cfg.sysroot threaded through BuildStdPcm + base compile/link
command (enables aarch64 cross via Arch Linux ARM rootfs)
- lib + exe split: project.cpp defines crafterBuildLib
(LibraryStatic) + crafterBuildExe (Executable depending on
it); build.sh produces lib/libcrafter-build.a alongside
bin/crafter-build for downstream static-link consumers
- Windows DLL+launcher: CRAFTER_API macro, /EXPORT flag for
project.dll's CrafterBuildProject; Crafter::Run as the real
entry point with main.cpp as a thin wrapper
- 18 tests: HelloWorld/WithModule/Defines/CrossProjectModule/
Diamond × (Linux + sshwin:winvm), plus Incremental,
BuildError, Libraries, RunnerClassification, QemuUser,
SshRunner, WindowsViaSsh, CrossArchAarch64
- single ./bin/crafter-build test runs everything; Windows
variants skip gracefully if winvm SSH alias unreachable
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 22:32:19 +02:00
|
|
|
struct Test;
|
|
|
|
|
|
|
|
|
|
struct TestRunner {
|
2026-05-27 18:07:33 +02:00
|
|
|
// Command template the harness executes to run the test binary.
|
|
|
|
|
// Local runners leave this empty; prefix runners (Cmd, Wine) set it to
|
|
|
|
|
// a template like "wine {bin} {args}" or "qemu-aarch64 {bin} {args}".
|
test runner, cross-target runners, lib/exe split
- subprocess-isolated test runner (replaces V1 dlopen-RunTest);
Pass/Fail/Crash/Timeout/Skipped outcomes via :Test partition
- TestRunner abstraction with command templates: Local, Ssh,
SshWin (cmd.exe-shell), QemuUser, FromEnv; probe-based skip
when runner unreachable
- transitive PCM-path propagation in Build(); resolveImport
walks deps recursively; depResults cache keyed by PcmDir()
so per-target builds don't collide
- cfg.sysroot threaded through BuildStdPcm + base compile/link
command (enables aarch64 cross via Arch Linux ARM rootfs)
- lib + exe split: project.cpp defines crafterBuildLib
(LibraryStatic) + crafterBuildExe (Executable depending on
it); build.sh produces lib/libcrafter-build.a alongside
bin/crafter-build for downstream static-link consumers
- Windows DLL+launcher: CRAFTER_API macro, /EXPORT flag for
project.dll's CrafterBuildProject; Crafter::Run as the real
entry point with main.cpp as a thin wrapper
- 18 tests: HelloWorld/WithModule/Defines/CrossProjectModule/
Diamond × (Linux + sshwin:winvm), plus Incremental,
BuildError, Libraries, RunnerClassification, QemuUser,
SshRunner, WindowsViaSsh, CrossArchAarch64
- single ./bin/crafter-build test runs everything; Windows
variants skip gracefully if winvm SSH alias unreachable
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 22:32:19 +02:00
|
|
|
std::string exec;
|
2026-05-27 18:07:33 +02:00
|
|
|
// Display name; also the cache key for the availability probe.
|
test runner, cross-target runners, lib/exe split
- subprocess-isolated test runner (replaces V1 dlopen-RunTest);
Pass/Fail/Crash/Timeout/Skipped outcomes via :Test partition
- TestRunner abstraction with command templates: Local, Ssh,
SshWin (cmd.exe-shell), QemuUser, FromEnv; probe-based skip
when runner unreachable
- transitive PCM-path propagation in Build(); resolveImport
walks deps recursively; depResults cache keyed by PcmDir()
so per-target builds don't collide
- cfg.sysroot threaded through BuildStdPcm + base compile/link
command (enables aarch64 cross via Arch Linux ARM rootfs)
- lib + exe split: project.cpp defines crafterBuildLib
(LibraryStatic) + crafterBuildExe (Executable depending on
it); build.sh produces lib/libcrafter-build.a alongside
bin/crafter-build for downstream static-link consumers
- Windows DLL+launcher: CRAFTER_API macro, /EXPORT flag for
project.dll's CrafterBuildProject; Crafter::Run as the real
entry point with main.cpp as a thin wrapper
- 18 tests: HelloWorld/WithModule/Defines/CrossProjectModule/
Diamond × (Linux + sshwin:winvm), plus Incremental,
BuildError, Libraries, RunnerClassification, QemuUser,
SshRunner, WindowsViaSsh, CrossArchAarch64
- single ./bin/crafter-build test runs everything; Windows
variants skip gracefully if winvm SSH alias unreachable
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 22:32:19 +02:00
|
|
|
std::string name;
|
|
|
|
|
// Runs once per RunTests invocation (cached by `name`). Exit 0 = runner
|
|
|
|
|
// is available; non-zero = skip every Test using this runner with a
|
|
|
|
|
// "runner unavailable" message. Empty = always available (e.g., Local).
|
|
|
|
|
std::string probe;
|
|
|
|
|
|
|
|
|
|
bool IsLocal() const { return exec.empty(); }
|
|
|
|
|
|
|
|
|
|
static CRAFTER_API TestRunner Local();
|
2026-05-27 18:07:33 +02:00
|
|
|
// Prefix runner: wraps the local binary in `<command> {bin} {args}`.
|
|
|
|
|
// Used for qemu-user, wasmtime, and similar single-binary wrappers.
|
V2: WASI, -r flag, CI pipeline, examples & tests cleanup
WASI / wasm32 target support
- Auto-detect /usr/share/wasi-sysroot on Linux when target starts_with("wasm32")
- Skip -march/-mtune for wasm (clang rejects them)
- Apply -fno-exceptions -fno-c++-static-destructors -mllvm -wasm-enable-sjlj
-D_WASI_EMULATED_SIGNAL to wasm builds (compile + std PCM, kept in sync)
- .wasm output extension in expectedOutputFor and link command
- EnableWasiBrowserRuntime(cfg): opt-in helper that drops index.html +
runtime.js next to the .wasm; runtime.js reads window.CRAFTER_WASM_URL
set in the templated index.html so a single shim handles any output name
-r run flag in the CLI: build then exec the artifact (host targets only;
rejects libraries; auto .exe/.wasm extension handling)
CI pipeline (.forgejo/workflows/ci.yaml)
- Triggers: PR/push to master + manual dispatch
- Single arch-latest container job: install deps, bootstrap, self-rebuild,
run tests, cross-compile mingw, package both archives, upload artifacts
- Rolling 'latest' release published only on push/dispatch to master
mingw cross-compile from Linux now works end-to-end:
- ExternalDependency cache key includes target so per-target glslang builds
don't collide; CMAKE_BUILD_TYPE=Release pinned (otherwise glslang appends
'd' to lib names and breaks linking); cross-compile cmake flags
(CMAKE_SYSTEM_NAME=Windows, CMAKE_*_COMPILER_TARGET=...)
- project.cpp accepts --target=<triple>; Linux-only -Wl,--export-dynamic
and -ldl are gated; mingw glslang skips the standalone exe (its libgcc_eh
link pulls pthread which mingw doesn't link by default)
- mingw compile uses -femulated-tls so std::__once_callable etc reference
the same emutls symbols libstdc++ provides
- mingw link auto-adds -lstdc++exp -lpthread
GetCrafterBuildHome() exposed from the Platform module; LoadProject (Linux
+ Windows) now both use it instead of duplicating the resolution.
Examples reorg: hello-world, library, with-module, wasi, tests — each with
its own README. Tests reorg: per-test directory with inner/ fixture, no
shared tests/fixtures/ tree. New Wasi test verifies .wasm magic bytes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:24:46 +02:00
|
|
|
static CRAFTER_API TestRunner Cmd(std::string command);
|
test: introduce test.toml + target-derived runners alongside existing machinery
Vendors toml++ v3.4.0 as lib/toml.hpp and wires it into Crafter.Build-Test
to parse a declarative test.toml manifest (target/march/mtune/sysroot/
requires/timeout/args/defines). Test discovery now treats project.cpp and
test.toml as mutually exclusive: project.cpp stays the escape hatch for
outer-driver tests, test.toml gives downstream test authors a no-boilerplate
path.
Adds:
- TestRunner::Wine() and TestRunner::ForTarget(cfg) — runner is now derived
from cfg.target (Local for host, Wine for Windows-on-Linux, wasmtime for
WASI, qemu-<arch> with QEMU_LD_PREFIX for non-host Linux). The env-var
override CRAFTER_BUILD_RUNNER_<target> still wins as a power-user escape
hatch via FromEnv.
- Declarative preconditions: tool:<name>, file:<path>, env:<VAR> are
evaluated before the build; missing preconditions Skip without paying
the compile cost.
- Hard-fail-unless-declared: when a derived runner's tool is missing AND
the test didn't declare 'tool:<that>' in requires, the missing runner
is a Fail instead of a silent Skip. Surfaces broken cross-arch CI
config that previously hid as "skipped".
- Multi-target sweep: bare `crafter-build test` (no --target=) now
iterates every distinct test.toml-declared target plus the host, so
cross-arch tests run by default without the user needing to know which
targets exist. `--target=X` bypasses the sweep.
Test struct gains a `requires_` vector so project.cpp users can declare
preconditions too (matching what test.toml writes there).
Existing tests, factories (Ssh/SshWin/Wsl/Cmd), and CRAFTER_BUILD_RUNNER_*
machinery remain intact — this commit only adds; migration and deletion
follow in subsequent commits.
Refs issue #8.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 17:47:39 +02:00
|
|
|
// Run a Windows .exe through Wine. Probes `wine` on PATH; on a Windows
|
|
|
|
|
// host the wine wrapper is pointless, so callers should route to Local
|
|
|
|
|
// before reaching here.
|
|
|
|
|
static CRAFTER_API TestRunner Wine();
|
2026-05-27 18:07:33 +02:00
|
|
|
// Parse a `<kind>[:<arg>]` spec used by CRAFTER_BUILD_RUNNER_<target>
|
|
|
|
|
// and --runner=. Supported: "local", "cmd:<binary>". Returns nullopt
|
|
|
|
|
// for an empty string; throws on a non-empty unrecognized spec.
|
V2: WASI, -r flag, CI pipeline, examples & tests cleanup
WASI / wasm32 target support
- Auto-detect /usr/share/wasi-sysroot on Linux when target starts_with("wasm32")
- Skip -march/-mtune for wasm (clang rejects them)
- Apply -fno-exceptions -fno-c++-static-destructors -mllvm -wasm-enable-sjlj
-D_WASI_EMULATED_SIGNAL to wasm builds (compile + std PCM, kept in sync)
- .wasm output extension in expectedOutputFor and link command
- EnableWasiBrowserRuntime(cfg): opt-in helper that drops index.html +
runtime.js next to the .wasm; runtime.js reads window.CRAFTER_WASM_URL
set in the templated index.html so a single shim handles any output name
-r run flag in the CLI: build then exec the artifact (host targets only;
rejects libraries; auto .exe/.wasm extension handling)
CI pipeline (.forgejo/workflows/ci.yaml)
- Triggers: PR/push to master + manual dispatch
- Single arch-latest container job: install deps, bootstrap, self-rebuild,
run tests, cross-compile mingw, package both archives, upload artifacts
- Rolling 'latest' release published only on push/dispatch to master
mingw cross-compile from Linux now works end-to-end:
- ExternalDependency cache key includes target so per-target glslang builds
don't collide; CMAKE_BUILD_TYPE=Release pinned (otherwise glslang appends
'd' to lib names and breaks linking); cross-compile cmake flags
(CMAKE_SYSTEM_NAME=Windows, CMAKE_*_COMPILER_TARGET=...)
- project.cpp accepts --target=<triple>; Linux-only -Wl,--export-dynamic
and -ldl are gated; mingw glslang skips the standalone exe (its libgcc_eh
link pulls pthread which mingw doesn't link by default)
- mingw compile uses -femulated-tls so std::__once_callable etc reference
the same emutls symbols libstdc++ provides
- mingw link auto-adds -lstdc++exp -lpthread
GetCrafterBuildHome() exposed from the Platform module; LoadProject (Linux
+ Windows) now both use it instead of duplicating the resolution.
Examples reorg: hello-world, library, with-module, wasi, tests — each with
its own README. Tests reorg: per-test directory with inner/ fixture, no
shared tests/fixtures/ tree. New Wasi test verifies .wasm magic bytes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:24:46 +02:00
|
|
|
static CRAFTER_API std::optional<TestRunner> FromSpec(std::string_view spec);
|
2026-05-27 18:07:33 +02:00
|
|
|
// Honor CRAFTER_BUILD_RUNNER_<target> (power-user override). Triple
|
|
|
|
|
// dashes/dots become underscores so they're valid in env-var names.
|
|
|
|
|
// Returns `fallback` when the env var is unset.
|
test runner, cross-target runners, lib/exe split
- subprocess-isolated test runner (replaces V1 dlopen-RunTest);
Pass/Fail/Crash/Timeout/Skipped outcomes via :Test partition
- TestRunner abstraction with command templates: Local, Ssh,
SshWin (cmd.exe-shell), QemuUser, FromEnv; probe-based skip
when runner unreachable
- transitive PCM-path propagation in Build(); resolveImport
walks deps recursively; depResults cache keyed by PcmDir()
so per-target builds don't collide
- cfg.sysroot threaded through BuildStdPcm + base compile/link
command (enables aarch64 cross via Arch Linux ARM rootfs)
- lib + exe split: project.cpp defines crafterBuildLib
(LibraryStatic) + crafterBuildExe (Executable depending on
it); build.sh produces lib/libcrafter-build.a alongside
bin/crafter-build for downstream static-link consumers
- Windows DLL+launcher: CRAFTER_API macro, /EXPORT flag for
project.dll's CrafterBuildProject; Crafter::Run as the real
entry point with main.cpp as a thin wrapper
- 18 tests: HelloWorld/WithModule/Defines/CrossProjectModule/
Diamond × (Linux + sshwin:winvm), plus Incremental,
BuildError, Libraries, RunnerClassification, QemuUser,
SshRunner, WindowsViaSsh, CrossArchAarch64
- single ./bin/crafter-build test runs everything; Windows
variants skip gracefully if winvm SSH alias unreachable
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 22:32:19 +02:00
|
|
|
static CRAFTER_API TestRunner FromEnv(std::string_view target, TestRunner fallback = Local());
|
test: introduce test.toml + target-derived runners alongside existing machinery
Vendors toml++ v3.4.0 as lib/toml.hpp and wires it into Crafter.Build-Test
to parse a declarative test.toml manifest (target/march/mtune/sysroot/
requires/timeout/args/defines). Test discovery now treats project.cpp and
test.toml as mutually exclusive: project.cpp stays the escape hatch for
outer-driver tests, test.toml gives downstream test authors a no-boilerplate
path.
Adds:
- TestRunner::Wine() and TestRunner::ForTarget(cfg) — runner is now derived
from cfg.target (Local for host, Wine for Windows-on-Linux, wasmtime for
WASI, qemu-<arch> with QEMU_LD_PREFIX for non-host Linux). The env-var
override CRAFTER_BUILD_RUNNER_<target> still wins as a power-user escape
hatch via FromEnv.
- Declarative preconditions: tool:<name>, file:<path>, env:<VAR> are
evaluated before the build; missing preconditions Skip without paying
the compile cost.
- Hard-fail-unless-declared: when a derived runner's tool is missing AND
the test didn't declare 'tool:<that>' in requires, the missing runner
is a Fail instead of a silent Skip. Surfaces broken cross-arch CI
config that previously hid as "skipped".
- Multi-target sweep: bare `crafter-build test` (no --target=) now
iterates every distinct test.toml-declared target plus the host, so
cross-arch tests run by default without the user needing to know which
targets exist. `--target=X` bypasses the sweep.
Test struct gains a `requires_` vector so project.cpp users can declare
preconditions too (matching what test.toml writes there).
Existing tests, factories (Ssh/SshWin/Wsl/Cmd), and CRAFTER_BUILD_RUNNER_*
machinery remain intact — this commit only adds; migration and deletion
follow in subsequent commits.
Refs issue #8.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 17:47:39 +02:00
|
|
|
// Derive a runner from a Configuration's target triple + sysroot.
|
|
|
|
|
// Returns Local() when target equals the host, Wine() for Windows
|
|
|
|
|
// targets on a non-Windows host, `qemu-<arch>` (with QEMU_LD_PREFIX
|
|
|
|
|
// set when cfg.sysroot is non-empty) for non-host -linux- triples,
|
|
|
|
|
// `wasmtime` for wasm32-wasi/wasm64-wasi, and Local() as a last
|
|
|
|
|
// resort. CRAFTER_BUILD_RUNNER_<target> still wins as an override
|
|
|
|
|
// upstream of this — see FromEnv.
|
|
|
|
|
static CRAFTER_API TestRunner ForTarget(const struct Configuration& cfg);
|
test runner, cross-target runners, lib/exe split
- subprocess-isolated test runner (replaces V1 dlopen-RunTest);
Pass/Fail/Crash/Timeout/Skipped outcomes via :Test partition
- TestRunner abstraction with command templates: Local, Ssh,
SshWin (cmd.exe-shell), QemuUser, FromEnv; probe-based skip
when runner unreachable
- transitive PCM-path propagation in Build(); resolveImport
walks deps recursively; depResults cache keyed by PcmDir()
so per-target builds don't collide
- cfg.sysroot threaded through BuildStdPcm + base compile/link
command (enables aarch64 cross via Arch Linux ARM rootfs)
- lib + exe split: project.cpp defines crafterBuildLib
(LibraryStatic) + crafterBuildExe (Executable depending on
it); build.sh produces lib/libcrafter-build.a alongside
bin/crafter-build for downstream static-link consumers
- Windows DLL+launcher: CRAFTER_API macro, /EXPORT flag for
project.dll's CrafterBuildProject; Crafter::Run as the real
entry point with main.cpp as a thin wrapper
- 18 tests: HelloWorld/WithModule/Defines/CrossProjectModule/
Diamond × (Linux + sshwin:winvm), plus Incremental,
BuildError, Libraries, RunnerClassification, QemuUser,
SshRunner, WindowsViaSsh, CrossArchAarch64
- single ./bin/crafter-build test runs everything; Windows
variants skip gracefully if winvm SSH alias unreachable
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 22:32:19 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
enum class TestOutcome { Pass, Fail, Crash, Timeout, Skipped };
|
|
|
|
|
|
|
|
|
|
struct TestResult {
|
|
|
|
|
std::string name;
|
|
|
|
|
TestOutcome outcome = TestOutcome::Pass;
|
2026-07-23 01:24:42 +02:00
|
|
|
std::int32_t exitCode = 0;
|
|
|
|
|
std::int32_t signal = 0;
|
test runner, cross-target runners, lib/exe split
- subprocess-isolated test runner (replaces V1 dlopen-RunTest);
Pass/Fail/Crash/Timeout/Skipped outcomes via :Test partition
- TestRunner abstraction with command templates: Local, Ssh,
SshWin (cmd.exe-shell), QemuUser, FromEnv; probe-based skip
when runner unreachable
- transitive PCM-path propagation in Build(); resolveImport
walks deps recursively; depResults cache keyed by PcmDir()
so per-target builds don't collide
- cfg.sysroot threaded through BuildStdPcm + base compile/link
command (enables aarch64 cross via Arch Linux ARM rootfs)
- lib + exe split: project.cpp defines crafterBuildLib
(LibraryStatic) + crafterBuildExe (Executable depending on
it); build.sh produces lib/libcrafter-build.a alongside
bin/crafter-build for downstream static-link consumers
- Windows DLL+launcher: CRAFTER_API macro, /EXPORT flag for
project.dll's CrafterBuildProject; Crafter::Run as the real
entry point with main.cpp as a thin wrapper
- 18 tests: HelloWorld/WithModule/Defines/CrossProjectModule/
Diamond × (Linux + sshwin:winvm), plus Incremental,
BuildError, Libraries, RunnerClassification, QemuUser,
SshRunner, WindowsViaSsh, CrossArchAarch64
- single ./bin/crafter-build test runs everything; Windows
variants skip gracefully if winvm SSH alias unreachable
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 22:32:19 +02:00
|
|
|
std::chrono::milliseconds duration{0};
|
|
|
|
|
std::string output;
|
|
|
|
|
};
|
|
|
|
|
|
2026-07-23 01:24:42 +02:00
|
|
|
// One lint diagnostic. Produced by LintContext::Report — or derived by
|
|
|
|
|
// the driver when a transform rule's SetContent output differs from the
|
|
|
|
|
// file on disk ("would reformat") — collected and printed compiler-style
|
|
|
|
|
// by RunLint (Crafter.Build:Lint):
|
|
|
|
|
// <file>:<line>: warning: <message> [<rule>]
|
|
|
|
|
struct LintFinding {
|
|
|
|
|
fs::path file;
|
|
|
|
|
std::size_t line = 0; // 1-based; 0 = whole-file finding
|
|
|
|
|
std::string rule; // name of the rule that produced it
|
|
|
|
|
std::string message;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Parsed `// lint-disable-*` suppression directives for one file (see
|
|
|
|
|
// LintContext::Suppressed). Line keys are 1-based and refer to the line a
|
|
|
|
|
// next-line directive TARGETS (the line after the comment).
|
|
|
|
|
struct LintSuppressions {
|
|
|
|
|
bool fileAll = false;
|
|
|
|
|
std::unordered_set<std::string> fileRules;
|
|
|
|
|
std::unordered_set<std::size_t> lineAll;
|
|
|
|
|
std::unordered_map<std::size_t, std::unordered_set<std::string>> lineRules;
|
|
|
|
|
};
|
|
|
|
|
|
feat(lint): libclang-backed token layer
Adds LintContext::Tokens() and friends, backed by clang_tokenize, as the
substrate the rules will move onto. Nothing consumes it yet.
libclang is dlopen'd rather than linked: -lclang would break the mingw and
MSVC cross-builds at link time and would put a libclang.so.NN runtime
dependency into the otherwise self-contained release tarballs. The clang-c
header is used for its declarations only, and the function-pointer table is
typed with decltype so the signatures cannot drift from the real API.
Three properties this buys that the hand-rolled scanners could not have:
- a raw string literal or block comment is ONE token, so the documented
"raw string literals are not recognized" limitation goes away;
- `//` inside a literal is not a comment, so LineHasComment() replaces the
Line(n).contains("//") probes that false-positive on it;
- tokens cover preprocessor branches that are inactive for the host, since
clang_tokenize lexes rather than evaluates #if. Token rules therefore
keep seeing every platform's code, which an AST could not offer.
The parse backing the tokenizer is expected to fail on module units — no
PCMs, no build flags — and that is fine, because lexing has no semantic
prerequisites. Verified in the new tests.
LintSummary::Clean() now counts `errors`. It previously ignored them, so an
infrastructure failure that produced no findings reported clean and exited
0; a missing libclang would have been exactly that.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 02:54:38 +02:00
|
|
|
// Lexical class of a LintToken, mirroring clang's token kinds one-to-one.
|
|
|
|
|
enum class LintTokenKind {
|
|
|
|
|
Punctuation,
|
|
|
|
|
Keyword,
|
|
|
|
|
Identifier,
|
|
|
|
|
Literal, // string, raw string, character, integer, floating literal
|
|
|
|
|
Comment, // // ... or /* ... */
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// One token from LintContext::Tokens(). `offset`/`length` are byte offsets
|
|
|
|
|
// into LintContext::content and stay valid until the next SetContent.
|
|
|
|
|
//
|
|
|
|
|
// A raw string literal or a block comment is ONE token and may span lines,
|
|
|
|
|
// which is exactly what the hand-rolled scanners could not represent. The
|
|
|
|
|
// stream also covers text inside preprocessor branches that are inactive
|
|
|
|
|
// for the host — clang_tokenize lexes, it does not evaluate #if — so token
|
|
|
|
|
// rules see every platform's code, not just the one being built.
|
|
|
|
|
struct LintToken {
|
|
|
|
|
LintTokenKind kind = LintTokenKind::Punctuation;
|
|
|
|
|
std::size_t offset = 0;
|
|
|
|
|
std::size_t length = 0;
|
|
|
|
|
std::size_t line = 0; // 1-based, of the token's first byte
|
|
|
|
|
std::size_t column = 0; // 1-based, of the token's first byte
|
|
|
|
|
};
|
|
|
|
|
|
2026-07-23 01:24:42 +02:00
|
|
|
// Per-file view handed to each LintRule's check callback. Every member
|
|
|
|
|
// function is out-of-line and CRAFTER_API (defined in Crafter.Build:Lint's
|
|
|
|
|
// implementation unit) because rule lambdas execute from the user's
|
|
|
|
|
// project DLL on Windows — clang does not emit module-attached in-class
|
|
|
|
|
// inline bodies into consumers (see ArgQuery below).
|
|
|
|
|
struct LintContext {
|
|
|
|
|
fs::path file; // absolute path of the file under lint
|
|
|
|
|
std::string content; // whole file as read from disk
|
|
|
|
|
std::vector<std::string_view> lines; // views into `content`, one per line, no '\n'
|
|
|
|
|
|
|
|
|
|
CRAFTER_API std::string Extension() const; // ".cppm", ".cpp", ".h", ...
|
|
|
|
|
CRAFTER_API std::string_view Line(std::size_t n) const; // 1-based; empty if out of range
|
refactor(lint): derive CommentStripped from tokens, drop the char scanner
StripComments hand-scanned five states over raw characters and could not see
a raw string literal, which was documented as a v1 limitation. It was worse
than "does not recognise them": an odd number of quotes inside a raw string
desynchronised the scanner for the rest of the file. Simulated on the new
fixture, the old output was
auto banner = R" "hi)" \n \n ... "MARKER text"
— the code after the raw string blanked away, and the *contents* of a later
string literal left standing as if it were code. Every rule reading
CommentStripped() saw that. There are 33 raw strings across 6 files here,
including the two largest.
The replacement projects the token stream onto a copy of the buffer, blanking
comment tokens whole and the bodies of string/character literals. Editing a
copy in place makes the length-preserving contract structural rather than
something each branch has to remember, and the token extents make raw
strings, escapes, encoding prefixes and '"' all fall out for free. A raw
string reduces to R"…" so rules that bracket a literal by counting quotes
keep working. Digit separators (1'000) are excluded by requiring the text
before the quote to be an encoding prefix.
The public contract is unchanged, so no rule needed editing, and `lint` over
this repo produces byte-identical output to the previous implementation.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 02:59:11 +02:00
|
|
|
// `content` with comments and string/char literal bodies blanked to
|
|
|
|
|
// spaces, newlines preserved — offsets and line numbers stay valid.
|
|
|
|
|
// Built from Tokens() on first call, cached per file, so raw strings,
|
|
|
|
|
// escapes, encoding prefixes and literals like '"' all come out right.
|
|
|
|
|
// A raw string is reduced to R"…" with the body and the delimiter
|
|
|
|
|
// scaffolding blanked, leaving exactly two quote characters.
|
|
|
|
|
//
|
|
|
|
|
// Convenient for a quick scan, but Tokens() is the better tool for
|
|
|
|
|
// anything structural: this view cannot tell an identifier from a
|
|
|
|
|
// keyword, and it has already thrown away where the literals were.
|
2026-07-23 01:24:42 +02:00
|
|
|
CRAFTER_API const std::string& CommentStripped();
|
|
|
|
|
// Record a finding at `line` (1-based; pass 0 for a whole-file finding).
|
|
|
|
|
CRAFTER_API void Report(std::size_t line, std::string message);
|
|
|
|
|
// Replace the file's content. Makes this rule a *transform*:
|
|
|
|
|
// `crafter-build format` writes the result back to disk; `lint`
|
|
|
|
|
// derives would-reformat findings from it (dry — never writes).
|
|
|
|
|
// `lines` is re-split and CommentStripped() re-derives on next call;
|
|
|
|
|
// string_views taken before this call are invalidated. Recommended
|
|
|
|
|
// pattern: build the new string, call SetContent once at the end. Do
|
|
|
|
|
// not assign `content` directly — that bypasses the re-split. May be
|
|
|
|
|
// combined with Report() in the same rule.
|
|
|
|
|
CRAFTER_API void SetContent(std::string newContent);
|
|
|
|
|
// True when `rule` is suppressed at `line` (1-based; 0 = whole-file,
|
|
|
|
|
// which only file-level directives cover) by a suppression comment:
|
|
|
|
|
// // lint-disable-next-line [rules...] applies to the following line
|
|
|
|
|
// // lint-disable-file [rules...] applies to the whole file
|
|
|
|
|
// Rule names are space- or comma-separated; none = all rules. The
|
|
|
|
|
// driver already filters Report()
|
|
|
|
|
// findings and reverts line-preserving transform edits on suppressed
|
|
|
|
|
// lines; a transform that MERGES or SPLITS lines must consult this
|
|
|
|
|
// itself for every line its edit touches (the driver cannot map lines
|
|
|
|
|
// across a count-changing rewrite). Parsed lazily from the raw lines;
|
|
|
|
|
// re-parsed after SetContent.
|
|
|
|
|
CRAFTER_API bool Suppressed(std::string_view rule, std::size_t line);
|
|
|
|
|
|
feat(lint): libclang-backed token layer
Adds LintContext::Tokens() and friends, backed by clang_tokenize, as the
substrate the rules will move onto. Nothing consumes it yet.
libclang is dlopen'd rather than linked: -lclang would break the mingw and
MSVC cross-builds at link time and would put a libclang.so.NN runtime
dependency into the otherwise self-contained release tarballs. The clang-c
header is used for its declarations only, and the function-pointer table is
typed with decltype so the signatures cannot drift from the real API.
Three properties this buys that the hand-rolled scanners could not have:
- a raw string literal or block comment is ONE token, so the documented
"raw string literals are not recognized" limitation goes away;
- `//` inside a literal is not a comment, so LineHasComment() replaces the
Line(n).contains("//") probes that false-positive on it;
- tokens cover preprocessor branches that are inactive for the host, since
clang_tokenize lexes rather than evaluates #if. Token rules therefore
keep seeing every platform's code, which an AST could not offer.
The parse backing the tokenizer is expected to fail on module units — no
PCMs, no build flags — and that is fine, because lexing has no semantic
prerequisites. Verified in the new tests.
LintSummary::Clean() now counts `errors`. It previously ignored them, so an
infrastructure failure that produced no findings reported clean and exited
0; a missing libclang would have been exactly that.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 02:54:38 +02:00
|
|
|
// The file lexed by clang, in source order. Built on first call, cached
|
|
|
|
|
// per file, re-lexed after SetContent. Empty for extensions that are
|
|
|
|
|
// not C or C++ (shaders): lexing GLSL as C++ would produce nonsense.
|
|
|
|
|
//
|
|
|
|
|
// Prefer this to scanning characters. It is the only view that gets
|
|
|
|
|
// raw strings, line splices, digraphs and nested quoting right, and
|
|
|
|
|
// offsets index straight into `content`, so a transform can find in
|
|
|
|
|
// the token stream and edit in place.
|
|
|
|
|
CRAFTER_API std::span<const LintToken> Tokens();
|
|
|
|
|
// The token's own bytes: content.substr(tok.offset, tok.length).
|
|
|
|
|
CRAFTER_API std::string_view TokenText(const LintToken& token) const;
|
|
|
|
|
// Tokens whose FIRST byte is on `line` (1-based). A token that starts
|
|
|
|
|
// earlier and spans into `line` — a raw string, a block comment — is
|
|
|
|
|
// not included; ask Tokens() directly when that matters.
|
|
|
|
|
CRAFTER_API std::span<const LintToken> TokensOnLine(std::size_t line);
|
|
|
|
|
// True when a comment token starts on `line`. Replaces `Line(n).contains("//")`,
|
|
|
|
|
// which false-positives on a `//` inside a string literal.
|
|
|
|
|
CRAFTER_API bool LineHasComment(std::size_t line);
|
fix(lint): make the reflow guards token-accurate instead of textual
The transforms guard themselves against comments and raw strings before
joining or rewriting a line, because pulling text up past a `//` buries it
and reflowing a multi-line literal changes the string. Those guards were
substring probes over the raw line, so they answered the wrong question:
Line(n).contains("//") fires on // inside a string literal
Line(n).contains("R\"") fires on the characters R" inside a literal, and
MISSES a raw string opened on an earlier line
Both misfire on this repo's own sources. "MARKER" ends in R", and any string
mentioning a lint-disable directive contains //. Two wrapped call sites in
tests/Lint were being left unjoined for exactly these reasons; they join now,
and the results are in this commit.
Replaced by LineHasComment() (added with the token layer) and a new
LineHasMultiLineToken(), which reports whether any token actually covering
that line spans a line boundary — a raw string or a block comment. Backed by
a per-line bitmap derived from the token cache and invalidated with it.
The guards themselves stay: joining across a real comment or a real
multi-line literal is still unsafe, and there are tests for both. What
changes is that they now fire on comments and literals rather than on the
characters that spell them.
format-concat gets narrower as a result. It used to refuse any line
containing R" and ask for a manual fix; now only a literal that genuinely
spans lines does that, because a single-line raw string reduces to R"…" in
the stripped view, fails the plain-literal test, and travels through as an
argument with its spelling intact.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 03:08:06 +02:00
|
|
|
// True when `line` (1-based) is touched by a token that spans more
|
|
|
|
|
// than one line — a raw string or a block comment. A transform that
|
|
|
|
|
// joins, splits or rewrites such a line changes what is inside that
|
|
|
|
|
// token, so this is the guard to consult before reflowing anything.
|
|
|
|
|
// Replaces `Line(n).contains("R\"")`, which both misses raw strings
|
|
|
|
|
// opened on an earlier line and fires on the characters R" appearing
|
|
|
|
|
// inside an ordinary literal.
|
|
|
|
|
CRAFTER_API bool LineHasMultiLineToken(std::size_t line);
|
feat(lint): libclang-backed token layer
Adds LintContext::Tokens() and friends, backed by clang_tokenize, as the
substrate the rules will move onto. Nothing consumes it yet.
libclang is dlopen'd rather than linked: -lclang would break the mingw and
MSVC cross-builds at link time and would put a libclang.so.NN runtime
dependency into the otherwise self-contained release tarballs. The clang-c
header is used for its declarations only, and the function-pointer table is
typed with decltype so the signatures cannot drift from the real API.
Three properties this buys that the hand-rolled scanners could not have:
- a raw string literal or block comment is ONE token, so the documented
"raw string literals are not recognized" limitation goes away;
- `//` inside a literal is not a comment, so LineHasComment() replaces the
Line(n).contains("//") probes that false-positive on it;
- tokens cover preprocessor branches that are inactive for the host, since
clang_tokenize lexes rather than evaluates #if. Token rules therefore
keep seeing every platform's code, which an AST could not offer.
The parse backing the tokenizer is expected to fail on module units — no
PCMs, no build flags — and that is fine, because lexing has no semantic
prerequisites. Verified in the new tests.
LintSummary::Clean() now counts `errors`. It previously ignored them, so an
infrastructure failure that produced no findings reported clean and exited
0; a missing libclang would have been exactly that.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 02:54:38 +02:00
|
|
|
|
2026-07-23 01:24:42 +02:00
|
|
|
// Driver wiring — set by RunLint before each check call. Not for rules.
|
|
|
|
|
std::string activeRule;
|
|
|
|
|
std::vector<LintFinding>* sink = nullptr;
|
|
|
|
|
std::optional<std::string> commentStrippedCache;
|
|
|
|
|
std::optional<LintSuppressions> suppressionsCache;
|
feat(lint): libclang-backed token layer
Adds LintContext::Tokens() and friends, backed by clang_tokenize, as the
substrate the rules will move onto. Nothing consumes it yet.
libclang is dlopen'd rather than linked: -lclang would break the mingw and
MSVC cross-builds at link time and would put a libclang.so.NN runtime
dependency into the otherwise self-contained release tarballs. The clang-c
header is used for its declarations only, and the function-pointer table is
typed with decltype so the signatures cannot drift from the real API.
Three properties this buys that the hand-rolled scanners could not have:
- a raw string literal or block comment is ONE token, so the documented
"raw string literals are not recognized" limitation goes away;
- `//` inside a literal is not a comment, so LineHasComment() replaces the
Line(n).contains("//") probes that false-positive on it;
- tokens cover preprocessor branches that are inactive for the host, since
clang_tokenize lexes rather than evaluates #if. Token rules therefore
keep seeing every platform's code, which an AST could not offer.
The parse backing the tokenizer is expected to fail on module units — no
PCMs, no build flags — and that is fine, because lexing has no semantic
prerequisites. Verified in the new tests.
LintSummary::Clean() now counts `errors`. It previously ignored them, so an
infrastructure failure that produced no findings reported clean and exited
0; a missing libclang would have been exactly that.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 02:54:38 +02:00
|
|
|
std::optional<std::vector<LintToken>> tokenCache;
|
fix(lint): make the reflow guards token-accurate instead of textual
The transforms guard themselves against comments and raw strings before
joining or rewriting a line, because pulling text up past a `//` buries it
and reflowing a multi-line literal changes the string. Those guards were
substring probes over the raw line, so they answered the wrong question:
Line(n).contains("//") fires on // inside a string literal
Line(n).contains("R\"") fires on the characters R" inside a literal, and
MISSES a raw string opened on an earlier line
Both misfire on this repo's own sources. "MARKER" ends in R", and any string
mentioning a lint-disable directive contains //. Two wrapped call sites in
tests/Lint were being left unjoined for exactly these reasons; they join now,
and the results are in this commit.
Replaced by LineHasComment() (added with the token layer) and a new
LineHasMultiLineToken(), which reports whether any token actually covering
that line spans a line boundary — a raw string or a block comment. Backed by
a per-line bitmap derived from the token cache and invalidated with it.
The guards themselves stay: joining across a real comment or a real
multi-line literal is still unsafe, and there are tests for both. What
changes is that they now fire on comments and literals rather than on the
characters that spell them.
format-concat gets narrower as a result. It used to refuse any line
containing R" and ask for a manual fix; now only a literal that genuinely
spans lines does that, because a single-line raw string reduces to R"…" in
the stripped view, fails the plain-literal test, and travels through as an
argument with its spelling intact.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 03:08:06 +02:00
|
|
|
// Per-line flag, 0-based, for LineHasMultiLineToken. Derived from
|
|
|
|
|
// tokenCache and invalidated with it.
|
|
|
|
|
std::optional<std::vector<bool>> spannedLineCache;
|
2026-07-23 01:24:42 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// A named lint rule: `check` runs once per (rule, file) over the project's
|
|
|
|
|
// own sources. Rules self-filter by ctx.Extension() / ctx.file. A rule
|
|
|
|
|
// that calls ctx.SetContent is a transform — defined once, it both gates
|
|
|
|
|
// `crafter-build lint` and fixes under `crafter-build format`.
|
|
|
|
|
struct LintRule {
|
|
|
|
|
std::string name;
|
|
|
|
|
std::function<void(LintContext&)> check;
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-29 18:59:01 +02:00
|
|
|
// The host target triple, detected once per process by running
|
|
|
|
|
// `clang++ -print-target-triple` and cached. Used as the default for
|
|
|
|
|
// Configuration::target so projects don't have to hardcode it for the
|
|
|
|
|
// no-cross-compile case. Returns "" if clang isn't on PATH or doesn't
|
|
|
|
|
// print a triple — those projects still need an explicit cfg.target.
|
|
|
|
|
CRAFTER_API std::string HostTarget();
|
|
|
|
|
|
2026-04-23 01:57:25 +02:00
|
|
|
struct Configuration {
|
2026-04-27 07:04:42 +02:00
|
|
|
fs::path path;
|
|
|
|
|
std::string outputName;
|
|
|
|
|
std::string name;
|
2026-04-23 01:57:25 +02:00
|
|
|
std::string march = "native";
|
|
|
|
|
std::string mtune = "native";
|
2026-04-29 18:59:01 +02:00
|
|
|
std::string target = HostTarget();
|
test runner, cross-target runners, lib/exe split
- subprocess-isolated test runner (replaces V1 dlopen-RunTest);
Pass/Fail/Crash/Timeout/Skipped outcomes via :Test partition
- TestRunner abstraction with command templates: Local, Ssh,
SshWin (cmd.exe-shell), QemuUser, FromEnv; probe-based skip
when runner unreachable
- transitive PCM-path propagation in Build(); resolveImport
walks deps recursively; depResults cache keyed by PcmDir()
so per-target builds don't collide
- cfg.sysroot threaded through BuildStdPcm + base compile/link
command (enables aarch64 cross via Arch Linux ARM rootfs)
- lib + exe split: project.cpp defines crafterBuildLib
(LibraryStatic) + crafterBuildExe (Executable depending on
it); build.sh produces lib/libcrafter-build.a alongside
bin/crafter-build for downstream static-link consumers
- Windows DLL+launcher: CRAFTER_API macro, /EXPORT flag for
project.dll's CrafterBuildProject; Crafter::Run as the real
entry point with main.cpp as a thin wrapper
- 18 tests: HelloWorld/WithModule/Defines/CrossProjectModule/
Diamond × (Linux + sshwin:winvm), plus Incremental,
BuildError, Libraries, RunnerClassification, QemuUser,
SshRunner, WindowsViaSsh, CrossArchAarch64
- single ./bin/crafter-build test runs everything; Windows
variants skip gracefully if winvm SSH alias unreachable
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 22:32:19 +02:00
|
|
|
std::string sysroot;
|
2026-04-23 01:57:25 +02:00
|
|
|
bool debug = false;
|
|
|
|
|
ConfigurationType type = ConfigurationType::Executable;
|
2026-04-27 07:04:42 +02:00
|
|
|
std::vector<std::unique_ptr<Module>> interfaces;
|
|
|
|
|
std::vector<Implementation> implementations;
|
|
|
|
|
std::vector<fs::path> cFiles;
|
|
|
|
|
std::vector<fs::path> cuda;
|
|
|
|
|
std::vector<Configuration*> dependencies;
|
|
|
|
|
std::vector<fs::path> files;
|
2026-05-02 21:08:51 +02:00
|
|
|
// Build-time-only source files this configuration exposes to its own
|
|
|
|
|
// and its consumers' shader compiles as glslang #include search
|
|
|
|
|
// paths. Each entry's parent directory (or the entry itself, if it's
|
|
|
|
|
// a directory) is added to the includer for every shader compiled in
|
|
|
|
|
// this configuration and in any configuration that transitively
|
|
|
|
|
// depends on it. Files are NOT copied — they're read in place from
|
|
|
|
|
// the dep's source tree, like C++ -I include dirs.
|
|
|
|
|
std::vector<fs::path> buildFiles;
|
2026-04-23 01:57:25 +02:00
|
|
|
std::vector<Define> defines;
|
|
|
|
|
std::vector<Shader> shaders;
|
2026-05-12 03:44:14 +02:00
|
|
|
// Source assets compressed via Crafter.Asset's SaveCompressed →
|
|
|
|
|
// .ctex/.cmesh in this configuration's bin dir.
|
|
|
|
|
//
|
|
|
|
|
// Each entry is either:
|
|
|
|
|
// - A single .png/.obj file. Output lands flat in the bin dir:
|
|
|
|
|
// bin/<filename>.ctex or bin/<filename>.cmesh.
|
|
|
|
|
// - A directory. The build recurses; .png/.obj are compressed
|
|
|
|
|
// with the relative tree mirrored under bin/<dirname>/, and
|
|
|
|
|
// every other file in the tree is copied through unchanged.
|
|
|
|
|
// Lets mod/map trees (mod.json + cannon/base.obj +
|
|
|
|
|
// cannon/color.png) keep their nested layout so JSON paths
|
|
|
|
|
// like "cannon/base.cmesh" resolve at runtime.
|
|
|
|
|
//
|
|
|
|
|
// Crafter.Asset must be reachable through cfg.dependencies (the
|
|
|
|
|
// build engine links its library API into crafter-build via a
|
|
|
|
|
// self-host pass). Forwarded to a consuming executable's bin dir
|
|
|
|
|
// alongside .spv shaders and cfg.files entries.
|
2026-05-12 01:16:40 +02:00
|
|
|
std::vector<fs::path> assets;
|
2026-04-27 07:04:42 +02:00
|
|
|
std::vector<ExternalDependency> externalDependencies;
|
|
|
|
|
std::vector<std::string> compileFlags;
|
|
|
|
|
std::vector<std::string> linkFlags;
|
feat: feature-detected browser-wasm variants (relaxed-SIMD) + variant-aware runtime
The browser wasm pipeline hardcoded -msimd128 for every wasm32 target and
baked a single wasm URL into index.html, so newer codegen features that
aren't yet baseline across engines (relaxed SIMD today; threads, future SIMD
revisions later) couldn't be adopted without dropping the browsers that lack
them.
Add a general, feature-parameterized mechanism owned entirely by
Crafter.Build:
- Configuration::wasmVariants declares N codegen variants (label, extra -m
flags, runtime probes). Build() compiles the baseline plus one
outputName.<label>.wasm per variant, recompiling the whole graph (incl.
dep libs + std PCM) with the variant's flags — relaxed-SIMD is per-TU
codegen, not a link switch. wasmVariantFlags folds into VariantId so each
variant's objects/PCMs land in their own build+bin dir.
- EnableWasiBrowserRuntime emits a variants.json manifest (label -> url +
probes), preferred-first with the baseline as the universal fallback.
- The shipped runtime.js runs inlined wasm-feature-detect probes
(relaxed-simd, simd, tail-call, bulk-memory, exception-handling, threads),
picks the first variant whose probes all pass, and falls back to the single
baked CRAFTER_WASM_URL when no manifest is present (backward compatible).
- EnableWasiRelaxedSimdVariant registers the relaxed-SIMD variant — the
motivating case (Chrome 114+/Firefox 120+ enable it by default; Safari
still flag-gates it as of mid-2026).
Verified end to end: a wasm32-wasip1 build emits both wasi-hello.wasm and
wasi-hello.relaxed-simd.wasm + variants.json; Firefox selects the
relaxed-simd variant and runs it.
Resolves #24
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 15:14:18 +00:00
|
|
|
// Browser-wasm feature-detected variants. Empty (the default) builds a
|
|
|
|
|
// single outputName.wasm with the baseline wasm flag set. Non-empty
|
|
|
|
|
// builds the baseline plus one outputName.<label>.wasm per entry, each
|
|
|
|
|
// recompiling the whole build graph with that entry's `flags`;
|
|
|
|
|
// EnableWasiBrowserRuntime emits a variants.json manifest the shipped
|
|
|
|
|
// runtime.js uses to pick the right variant per browser. Populate via
|
|
|
|
|
// EnableWasiRelaxedSimdVariant for the common case.
|
|
|
|
|
std::vector<WasmVariant> wasmVariants;
|
|
|
|
|
// Extra wasm codegen flags injected into this configuration's compile +
|
|
|
|
|
// link command for the active variant pass. Set across the whole build
|
|
|
|
|
// graph by the variant driver inside Build(); part of VariantId so each
|
|
|
|
|
// variant's objects/PCMs land in their own build+bin dir and never
|
|
|
|
|
// clobber the baseline's. Not for direct project use — declare
|
|
|
|
|
// wasmVariants instead.
|
|
|
|
|
std::vector<std::string> wasmVariantFlags;
|
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
|
|
|
// The project args ApplyStandardArgs did not interpret — i.e. the
|
|
|
|
|
// project's own flags, whose effect on the build the framework cannot
|
|
|
|
|
// see. Hashed into VariantId because such a flag typically decides what
|
|
|
|
|
// gets compiled or bundled (`--no-webgpu` dropping entries from
|
|
|
|
|
// cfg.files, say), and without it both flag settings share one bin dir
|
|
|
|
|
// and interleave their outputs there. Populated by ApplyStandardArgs;
|
|
|
|
|
// the args it does recognise are excluded, since their effect already
|
|
|
|
|
// shows up in target/march/mtune/debug/sysroot/type.
|
|
|
|
|
std::vector<std::string> projectArgs;
|
test runner, cross-target runners, lib/exe split
- subprocess-isolated test runner (replaces V1 dlopen-RunTest);
Pass/Fail/Crash/Timeout/Skipped outcomes via :Test partition
- TestRunner abstraction with command templates: Local, Ssh,
SshWin (cmd.exe-shell), QemuUser, FromEnv; probe-based skip
when runner unreachable
- transitive PCM-path propagation in Build(); resolveImport
walks deps recursively; depResults cache keyed by PcmDir()
so per-target builds don't collide
- cfg.sysroot threaded through BuildStdPcm + base compile/link
command (enables aarch64 cross via Arch Linux ARM rootfs)
- lib + exe split: project.cpp defines crafterBuildLib
(LibraryStatic) + crafterBuildExe (Executable depending on
it); build.sh produces lib/libcrafter-build.a alongside
bin/crafter-build for downstream static-link consumers
- Windows DLL+launcher: CRAFTER_API macro, /EXPORT flag for
project.dll's CrafterBuildProject; Crafter::Run as the real
entry point with main.cpp as a thin wrapper
- 18 tests: HelloWorld/WithModule/Defines/CrossProjectModule/
Diamond × (Linux + sshwin:winvm), plus Incremental,
BuildError, Libraries, RunnerClassification, QemuUser,
SshRunner, WindowsViaSsh, CrossArchAarch64
- single ./bin/crafter-build test runs everything; Windows
variants skip gracefully if winvm SSH alias unreachable
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 22:32:19 +02:00
|
|
|
std::vector<Test> tests;
|
2026-07-23 01:24:42 +02:00
|
|
|
// Lint rules for `crafter-build lint`. Populate via AddLintRule.
|
|
|
|
|
std::vector<LintRule> lintRules;
|
test runner, cross-target runners, lib/exe split
- subprocess-isolated test runner (replaces V1 dlopen-RunTest);
Pass/Fail/Crash/Timeout/Skipped outcomes via :Test partition
- TestRunner abstraction with command templates: Local, Ssh,
SshWin (cmd.exe-shell), QemuUser, FromEnv; probe-based skip
when runner unreachable
- transitive PCM-path propagation in Build(); resolveImport
walks deps recursively; depResults cache keyed by PcmDir()
so per-target builds don't collide
- cfg.sysroot threaded through BuildStdPcm + base compile/link
command (enables aarch64 cross via Arch Linux ARM rootfs)
- lib + exe split: project.cpp defines crafterBuildLib
(LibraryStatic) + crafterBuildExe (Executable depending on
it); build.sh produces lib/libcrafter-build.a alongside
bin/crafter-build for downstream static-link consumers
- Windows DLL+launcher: CRAFTER_API macro, /EXPORT flag for
project.dll's CrafterBuildProject; Crafter::Run as the real
entry point with main.cpp as a thin wrapper
- 18 tests: HelloWorld/WithModule/Defines/CrossProjectModule/
Diamond × (Linux + sshwin:winvm), plus Incremental,
BuildError, Libraries, RunnerClassification, QemuUser,
SshRunner, WindowsViaSsh, CrossArchAarch64
- single ./bin/crafter-build test runs everything; Windows
variants skip gracefully if winvm SSH alias unreachable
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 22:32:19 +02:00
|
|
|
CRAFTER_API void GetInterfacesAndImplementations(std::span<fs::path> interfaces, std::span<fs::path> implementations);
|
2026-07-30 17:12:24 +00:00
|
|
|
// Retry the `import X;` names GetInterfacesAndImplementations could not
|
|
|
|
|
// place, against the dependency DAG as it stands now. Sources are
|
|
|
|
|
// scanned when they're declared, but `dependencies` is often assigned
|
|
|
|
|
// afterwards (AddTest does exactly this), and an import that resolved to
|
|
|
|
|
// nothing leaves no staleness edge — so a dependency's interface could
|
|
|
|
|
// change and this Configuration's objects would be silently reused
|
|
|
|
|
// against the new layout. Build() calls this before checking mtimes;
|
|
|
|
|
// calling it again is harmless.
|
|
|
|
|
CRAFTER_API void ResolvePendingImports();
|
2026-05-27 19:45:05 +02:00
|
|
|
// Declare a test. Sources default to `tests/<name>/main.cpp` resolved
|
2026-07-22 18:25:39 +02:00
|
|
|
// against this Configuration's path; target/march/mtune/sysroot/debug
|
|
|
|
|
// are inherited from this Configuration so cross-arch projects don't
|
|
|
|
|
// have to re-specify. Returns a builder for chaining defines, deps,
|
|
|
|
|
// etc. Defined in Crafter.Build:Test.
|
2026-05-27 19:45:05 +02:00
|
|
|
CRAFTER_API struct TestBuilder AddTest(std::string_view name);
|
|
|
|
|
// Same as AddTest, but compiles the parent's `interfaces` directly
|
|
|
|
|
// into this test's Configuration (rather than going through a dep).
|
|
|
|
|
// Use when the test must rebuild those interfaces with its own
|
|
|
|
|
// compile flags — typically per-march SIMD codegen.
|
|
|
|
|
CRAFTER_API struct TestBuilder AddTest(std::string_view name, std::span<fs::path> interfaces);
|
|
|
|
|
// Math-style fan-out: one Test per MarchTier, all sharing the same
|
|
|
|
|
// `tests/<name>/main.cpp` source and the same interface set, each
|
|
|
|
|
// compiled with the tier's `-march`/`-mtune`. Test names are
|
|
|
|
|
// `<name>-<march>`.
|
2026-07-23 01:24:42 +02:00
|
|
|
CRAFTER_API void AddMarchVariants(std::string_view name, std::span<fs::path> interfaces, std::span<const struct MarchTier> tiers);
|
|
|
|
|
// Register a lint rule for `crafter-build lint` / `format`. Rules
|
|
|
|
|
// registered on any Configuration whose path lies inside the project
|
|
|
|
|
// root are collected (deduplicated by name, root-first) — attach them
|
|
|
|
|
// to the lib or the exe config, either works. Rules that call
|
|
|
|
|
// ctx.SetContent are transforms (see LintContext::SetContent).
|
|
|
|
|
// Defined in Crafter.Build:Lint.
|
|
|
|
|
CRAFTER_API void AddLintRule(std::string name, std::function<void(LintContext&)> check);
|
2026-04-30 02:20:19 +02:00
|
|
|
// Suffix that uniquely identifies this Configuration's compile state.
|
|
|
|
|
// target+march+mtune are spelled out for readability; the rest
|
|
|
|
|
// (type, debug, sysroot, defines, compileFlags) collapse into a short
|
|
|
|
|
// hash so two Configurations sharing a path but with different
|
|
|
|
|
// compile state can't clobber each other's outputs.
|
|
|
|
|
std::string VariantId() const {
|
|
|
|
|
std::string compileKey;
|
2026-07-23 01:24:42 +02:00
|
|
|
compileKey += std::to_string(static_cast<std::int32_t>(type));
|
2026-04-30 02:20:19 +02:00
|
|
|
compileKey += '|';
|
|
|
|
|
compileKey += debug ? '1' : '0';
|
|
|
|
|
compileKey += '|';
|
|
|
|
|
compileKey += sysroot;
|
|
|
|
|
for (const Define& d : defines) {
|
|
|
|
|
compileKey += "|D:";
|
|
|
|
|
compileKey += d.name;
|
|
|
|
|
compileKey += '=';
|
|
|
|
|
compileKey += d.value;
|
|
|
|
|
}
|
|
|
|
|
for (const std::string& f : compileFlags) {
|
|
|
|
|
compileKey += "|F:";
|
|
|
|
|
compileKey += f;
|
|
|
|
|
}
|
feat: feature-detected browser-wasm variants (relaxed-SIMD) + variant-aware runtime
The browser wasm pipeline hardcoded -msimd128 for every wasm32 target and
baked a single wasm URL into index.html, so newer codegen features that
aren't yet baseline across engines (relaxed SIMD today; threads, future SIMD
revisions later) couldn't be adopted without dropping the browsers that lack
them.
Add a general, feature-parameterized mechanism owned entirely by
Crafter.Build:
- Configuration::wasmVariants declares N codegen variants (label, extra -m
flags, runtime probes). Build() compiles the baseline plus one
outputName.<label>.wasm per variant, recompiling the whole graph (incl.
dep libs + std PCM) with the variant's flags — relaxed-SIMD is per-TU
codegen, not a link switch. wasmVariantFlags folds into VariantId so each
variant's objects/PCMs land in their own build+bin dir.
- EnableWasiBrowserRuntime emits a variants.json manifest (label -> url +
probes), preferred-first with the baseline as the universal fallback.
- The shipped runtime.js runs inlined wasm-feature-detect probes
(relaxed-simd, simd, tail-call, bulk-memory, exception-handling, threads),
picks the first variant whose probes all pass, and falls back to the single
baked CRAFTER_WASM_URL when no manifest is present (backward compatible).
- EnableWasiRelaxedSimdVariant registers the relaxed-SIMD variant — the
motivating case (Chrome 114+/Firefox 120+ enable it by default; Safari
still flag-gates it as of mid-2026).
Verified end to end: a wasm32-wasip1 build emits both wasi-hello.wasm and
wasi-hello.relaxed-simd.wasm + variants.json; Firefox selects the
relaxed-simd variant and runs it.
Resolves #24
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 15:14:18 +00:00
|
|
|
// Wasm variant codegen flags perturb every object/PCM, so they must
|
|
|
|
|
// key into a distinct build+bin dir from the baseline (and from
|
|
|
|
|
// each other) to avoid clobbering.
|
|
|
|
|
for (const std::string& f : wasmVariantFlags) {
|
|
|
|
|
compileKey += "|W:";
|
|
|
|
|
compileKey += f;
|
|
|
|
|
}
|
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
|
|
|
// Sorted by ApplyStandardArgs, so flag order on the command line
|
|
|
|
|
// doesn't split the cache.
|
|
|
|
|
for (const std::string& a : projectArgs) {
|
|
|
|
|
compileKey += "|A:";
|
|
|
|
|
compileKey += a;
|
|
|
|
|
}
|
2026-04-30 02:20:19 +02:00
|
|
|
std::size_t configHash = std::hash<std::string>{}(compileKey);
|
|
|
|
|
return std::format("{}-{}-{}-{}-{:08x}", name, target, march, mtune, configHash);
|
|
|
|
|
}
|
|
|
|
|
fs::path BuildDir() const { return path / "build" / VariantId(); }
|
|
|
|
|
fs::path BinDir() const { return path / "bin" / VariantId(); }
|
2026-04-27 07:04:42 +02:00
|
|
|
fs::path PcmDir() const {
|
2026-04-30 02:20:19 +02:00
|
|
|
return type == ConfigurationType::Executable ? BuildDir() : BinDir();
|
2026-04-27 07:04:42 +02:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
test runner, cross-target runners, lib/exe split
- subprocess-isolated test runner (replaces V1 dlopen-RunTest);
Pass/Fail/Crash/Timeout/Skipped outcomes via :Test partition
- TestRunner abstraction with command templates: Local, Ssh,
SshWin (cmd.exe-shell), QemuUser, FromEnv; probe-based skip
when runner unreachable
- transitive PCM-path propagation in Build(); resolveImport
walks deps recursively; depResults cache keyed by PcmDir()
so per-target builds don't collide
- cfg.sysroot threaded through BuildStdPcm + base compile/link
command (enables aarch64 cross via Arch Linux ARM rootfs)
- lib + exe split: project.cpp defines crafterBuildLib
(LibraryStatic) + crafterBuildExe (Executable depending on
it); build.sh produces lib/libcrafter-build.a alongside
bin/crafter-build for downstream static-link consumers
- Windows DLL+launcher: CRAFTER_API macro, /EXPORT flag for
project.dll's CrafterBuildProject; Crafter::Run as the real
entry point with main.cpp as a thin wrapper
- 18 tests: HelloWorld/WithModule/Defines/CrossProjectModule/
Diamond × (Linux + sshwin:winvm), plus Incremental,
BuildError, Libraries, RunnerClassification, QemuUser,
SshRunner, WindowsViaSsh, CrossArchAarch64
- single ./bin/crafter-build test runs everything; Windows
variants skip gracefully if winvm SSH alias unreachable
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 22:32:19 +02:00
|
|
|
struct Test {
|
|
|
|
|
Configuration config;
|
|
|
|
|
TestRunner runner;
|
|
|
|
|
std::chrono::seconds timeout{60};
|
|
|
|
|
std::vector<std::string> args;
|
test: introduce test.toml + target-derived runners alongside existing machinery
Vendors toml++ v3.4.0 as lib/toml.hpp and wires it into Crafter.Build-Test
to parse a declarative test.toml manifest (target/march/mtune/sysroot/
requires/timeout/args/defines). Test discovery now treats project.cpp and
test.toml as mutually exclusive: project.cpp stays the escape hatch for
outer-driver tests, test.toml gives downstream test authors a no-boilerplate
path.
Adds:
- TestRunner::Wine() and TestRunner::ForTarget(cfg) — runner is now derived
from cfg.target (Local for host, Wine for Windows-on-Linux, wasmtime for
WASI, qemu-<arch> with QEMU_LD_PREFIX for non-host Linux). The env-var
override CRAFTER_BUILD_RUNNER_<target> still wins as a power-user escape
hatch via FromEnv.
- Declarative preconditions: tool:<name>, file:<path>, env:<VAR> are
evaluated before the build; missing preconditions Skip without paying
the compile cost.
- Hard-fail-unless-declared: when a derived runner's tool is missing AND
the test didn't declare 'tool:<that>' in requires, the missing runner
is a Fail instead of a silent Skip. Surfaces broken cross-arch CI
config that previously hid as "skipped".
- Multi-target sweep: bare `crafter-build test` (no --target=) now
iterates every distinct test.toml-declared target plus the host, so
cross-arch tests run by default without the user needing to know which
targets exist. `--target=X` bypasses the sweep.
Test struct gains a `requires_` vector so project.cpp users can declare
preconditions too (matching what test.toml writes there).
Existing tests, factories (Ssh/SshWin/Wsl/Cmd), and CRAFTER_BUILD_RUNNER_*
machinery remain intact — this commit only adds; migration and deletion
follow in subsequent commits.
Refs issue #8.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 17:47:39 +02:00
|
|
|
// Declarative preconditions. Each entry is "tool:<name>",
|
|
|
|
|
// "file:<path>", or "env:<VAR>". Evaluated before the test runs; any
|
|
|
|
|
// unmet require turns the test into a Skip with a derived reason.
|
|
|
|
|
// Also doubles as the "I know this runner might not be here" opt-in:
|
|
|
|
|
// when the test's derived runner needs a tool (e.g. qemu-aarch64,
|
|
|
|
|
// wasmtime, wine) and the matching tool: entry isn't present, an
|
|
|
|
|
// unavailable runner becomes a Fail instead of a silent Skip — the
|
|
|
|
|
// dependency has to be declared to be allowed to be missing.
|
|
|
|
|
std::vector<std::string> requires_;
|
test runner, cross-target runners, lib/exe split
- subprocess-isolated test runner (replaces V1 dlopen-RunTest);
Pass/Fail/Crash/Timeout/Skipped outcomes via :Test partition
- TestRunner abstraction with command templates: Local, Ssh,
SshWin (cmd.exe-shell), QemuUser, FromEnv; probe-based skip
when runner unreachable
- transitive PCM-path propagation in Build(); resolveImport
walks deps recursively; depResults cache keyed by PcmDir()
so per-target builds don't collide
- cfg.sysroot threaded through BuildStdPcm + base compile/link
command (enables aarch64 cross via Arch Linux ARM rootfs)
- lib + exe split: project.cpp defines crafterBuildLib
(LibraryStatic) + crafterBuildExe (Executable depending on
it); build.sh produces lib/libcrafter-build.a alongside
bin/crafter-build for downstream static-link consumers
- Windows DLL+launcher: CRAFTER_API macro, /EXPORT flag for
project.dll's CrafterBuildProject; Crafter::Run as the real
entry point with main.cpp as a thin wrapper
- 18 tests: HelloWorld/WithModule/Defines/CrossProjectModule/
Diamond × (Linux + sshwin:winvm), plus Incremental,
BuildError, Libraries, RunnerClassification, QemuUser,
SshRunner, WindowsViaSsh, CrossArchAarch64
- single ./bin/crafter-build test runs everything; Windows
variants skip gracefully if winvm SSH alias unreachable
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 22:32:19 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
CRAFTER_API BuildResult Build(Configuration& config, std::unordered_map<fs::path, std::shared_future<BuildResult>>& depResults, std::mutex& depMutex);
|
2026-04-23 01:57:25 +02:00
|
|
|
|
test runner, cross-target runners, lib/exe split
- subprocess-isolated test runner (replaces V1 dlopen-RunTest);
Pass/Fail/Crash/Timeout/Skipped outcomes via :Test partition
- TestRunner abstraction with command templates: Local, Ssh,
SshWin (cmd.exe-shell), QemuUser, FromEnv; probe-based skip
when runner unreachable
- transitive PCM-path propagation in Build(); resolveImport
walks deps recursively; depResults cache keyed by PcmDir()
so per-target builds don't collide
- cfg.sysroot threaded through BuildStdPcm + base compile/link
command (enables aarch64 cross via Arch Linux ARM rootfs)
- lib + exe split: project.cpp defines crafterBuildLib
(LibraryStatic) + crafterBuildExe (Executable depending on
it); build.sh produces lib/libcrafter-build.a alongside
bin/crafter-build for downstream static-link consumers
- Windows DLL+launcher: CRAFTER_API macro, /EXPORT flag for
project.dll's CrafterBuildProject; Crafter::Run as the real
entry point with main.cpp as a thin wrapper
- 18 tests: HelloWorld/WithModule/Defines/CrossProjectModule/
Diamond × (Linux + sshwin:winvm), plus Incremental,
BuildError, Libraries, RunnerClassification, QemuUser,
SshRunner, WindowsViaSsh, CrossArchAarch64
- single ./bin/crafter-build test runs everything; Windows
variants skip gracefully if winvm SSH alias unreachable
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 22:32:19 +02:00
|
|
|
CRAFTER_API int Run(int argc, char** argv);
|
V2: WASI, -r flag, CI pipeline, examples & tests cleanup
WASI / wasm32 target support
- Auto-detect /usr/share/wasi-sysroot on Linux when target starts_with("wasm32")
- Skip -march/-mtune for wasm (clang rejects them)
- Apply -fno-exceptions -fno-c++-static-destructors -mllvm -wasm-enable-sjlj
-D_WASI_EMULATED_SIGNAL to wasm builds (compile + std PCM, kept in sync)
- .wasm output extension in expectedOutputFor and link command
- EnableWasiBrowserRuntime(cfg): opt-in helper that drops index.html +
runtime.js next to the .wasm; runtime.js reads window.CRAFTER_WASM_URL
set in the templated index.html so a single shim handles any output name
-r run flag in the CLI: build then exec the artifact (host targets only;
rejects libraries; auto .exe/.wasm extension handling)
CI pipeline (.forgejo/workflows/ci.yaml)
- Triggers: PR/push to master + manual dispatch
- Single arch-latest container job: install deps, bootstrap, self-rebuild,
run tests, cross-compile mingw, package both archives, upload artifacts
- Rolling 'latest' release published only on push/dispatch to master
mingw cross-compile from Linux now works end-to-end:
- ExternalDependency cache key includes target so per-target glslang builds
don't collide; CMAKE_BUILD_TYPE=Release pinned (otherwise glslang appends
'd' to lib names and breaks linking); cross-compile cmake flags
(CMAKE_SYSTEM_NAME=Windows, CMAKE_*_COMPILER_TARGET=...)
- project.cpp accepts --target=<triple>; Linux-only -Wl,--export-dynamic
and -ldl are gated; mingw glslang skips the standalone exe (its libgcc_eh
link pulls pthread which mingw doesn't link by default)
- mingw compile uses -femulated-tls so std::__once_callable etc reference
the same emutls symbols libstdc++ provides
- mingw link auto-adds -lstdc++exp -lpthread
GetCrafterBuildHome() exposed from the Platform module; LoadProject (Linux
+ Windows) now both use it instead of duplicating the resolution.
Examples reorg: hello-world, library, with-module, wasi, tests — each with
its own README. Tests reorg: per-test directory with inner/ fixture, no
shared tests/fixtures/ tree. New Wasi test verifies .wasm magic bytes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:24:46 +02:00
|
|
|
|
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
|
|
|
// Delete the bin/ and build/ trees beside `projectFile`, returning the paths
|
|
|
|
|
// that existed and were removed. Backs `crafter-build clean`.
|
|
|
|
|
//
|
|
|
|
|
// Deliberately does not load the project: cleaning is most often reached
|
|
|
|
|
// when something is already wrong, and a clean that first needs project.cpp
|
|
|
|
|
// to compile (and its git dependencies to be present) is useless exactly
|
|
|
|
|
// then. That means variant directories are not enumerated — the whole tree
|
|
|
|
|
// goes, which is what the by-hand `rm -rf bin build` did anyway.
|
|
|
|
|
CRAFTER_API std::vector<fs::path> CleanProject(const fs::path& projectFile);
|
|
|
|
|
|
V2: WASI, -r flag, CI pipeline, examples & tests cleanup
WASI / wasm32 target support
- Auto-detect /usr/share/wasi-sysroot on Linux when target starts_with("wasm32")
- Skip -march/-mtune for wasm (clang rejects them)
- Apply -fno-exceptions -fno-c++-static-destructors -mllvm -wasm-enable-sjlj
-D_WASI_EMULATED_SIGNAL to wasm builds (compile + std PCM, kept in sync)
- .wasm output extension in expectedOutputFor and link command
- EnableWasiBrowserRuntime(cfg): opt-in helper that drops index.html +
runtime.js next to the .wasm; runtime.js reads window.CRAFTER_WASM_URL
set in the templated index.html so a single shim handles any output name
-r run flag in the CLI: build then exec the artifact (host targets only;
rejects libraries; auto .exe/.wasm extension handling)
CI pipeline (.forgejo/workflows/ci.yaml)
- Triggers: PR/push to master + manual dispatch
- Single arch-latest container job: install deps, bootstrap, self-rebuild,
run tests, cross-compile mingw, package both archives, upload artifacts
- Rolling 'latest' release published only on push/dispatch to master
mingw cross-compile from Linux now works end-to-end:
- ExternalDependency cache key includes target so per-target glslang builds
don't collide; CMAKE_BUILD_TYPE=Release pinned (otherwise glslang appends
'd' to lib names and breaks linking); cross-compile cmake flags
(CMAKE_SYSTEM_NAME=Windows, CMAKE_*_COMPILER_TARGET=...)
- project.cpp accepts --target=<triple>; Linux-only -Wl,--export-dynamic
and -ldl are gated; mingw glslang skips the standalone exe (its libgcc_eh
link pulls pthread which mingw doesn't link by default)
- mingw compile uses -femulated-tls so std::__once_callable etc reference
the same emutls symbols libstdc++ provides
- mingw link auto-adds -lstdc++exp -lpthread
GetCrafterBuildHome() exposed from the Platform module; LoadProject (Linux
+ Windows) now both use it instead of duplicating the resolution.
Examples reorg: hello-world, library, with-module, wasi, tests — each with
its own README. Tests reorg: per-test directory with inner/ fixture, no
shared tests/fixtures/ tree. New Wasi test verifies .wasm magic bytes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:24:46 +02:00
|
|
|
// Add a small index.html + runtime.js pair next to the .wasm output so the
|
|
|
|
|
// build can be loaded directly in a browser (just `serve` the bin dir and
|
|
|
|
|
// open it). Opt-in: WASI builds destined for wasmtime/wasmer don't need
|
|
|
|
|
// this and shouldn't carry the extra files. Call from project.cpp after
|
|
|
|
|
// outputName is set; index.html is generated against the current
|
|
|
|
|
// outputName so renaming the binary later requires another call.
|
|
|
|
|
CRAFTER_API void EnableWasiBrowserRuntime(Configuration& cfg);
|
2026-04-29 18:59:01 +02:00
|
|
|
|
feat: feature-detected browser-wasm variants (relaxed-SIMD) + variant-aware runtime
The browser wasm pipeline hardcoded -msimd128 for every wasm32 target and
baked a single wasm URL into index.html, so newer codegen features that
aren't yet baseline across engines (relaxed SIMD today; threads, future SIMD
revisions later) couldn't be adopted without dropping the browsers that lack
them.
Add a general, feature-parameterized mechanism owned entirely by
Crafter.Build:
- Configuration::wasmVariants declares N codegen variants (label, extra -m
flags, runtime probes). Build() compiles the baseline plus one
outputName.<label>.wasm per variant, recompiling the whole graph (incl.
dep libs + std PCM) with the variant's flags — relaxed-SIMD is per-TU
codegen, not a link switch. wasmVariantFlags folds into VariantId so each
variant's objects/PCMs land in their own build+bin dir.
- EnableWasiBrowserRuntime emits a variants.json manifest (label -> url +
probes), preferred-first with the baseline as the universal fallback.
- The shipped runtime.js runs inlined wasm-feature-detect probes
(relaxed-simd, simd, tail-call, bulk-memory, exception-handling, threads),
picks the first variant whose probes all pass, and falls back to the single
baked CRAFTER_WASM_URL when no manifest is present (backward compatible).
- EnableWasiRelaxedSimdVariant registers the relaxed-SIMD variant — the
motivating case (Chrome 114+/Firefox 120+ enable it by default; Safari
still flag-gates it as of mid-2026).
Verified end to end: a wasm32-wasip1 build emits both wasi-hello.wasm and
wasi-hello.relaxed-simd.wasm + variants.json; Firefox selects the
relaxed-simd variant and runs it.
Resolves #24
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 15:14:18 +00:00
|
|
|
// Register the relaxed-SIMD codegen variant on cfg. Builds an additional
|
|
|
|
|
// outputName.relaxed-simd.wasm compiled with -mrelaxed-simd (FMA,
|
|
|
|
|
// dot-product, relaxed swizzle/laneselect, …); the shipped runtime serves
|
|
|
|
|
// it only to engines that report relaxed-SIMD support (Chrome 114+, Firefox
|
|
|
|
|
// 120+) and falls back to the baseline outputName.wasm everywhere else
|
|
|
|
|
// (e.g. Safari, which still gates relaxed SIMD behind a flag as of mid
|
|
|
|
|
// 2026). Idempotent. Call before EnableWasiBrowserRuntime so the emitted
|
|
|
|
|
// variants.json manifest includes it. A no-op for non-wasm targets at build
|
|
|
|
|
// time (no wasm is produced), but harmless to declare unconditionally.
|
|
|
|
|
CRAFTER_API void EnableWasiRelaxedSimdVariant(Configuration& cfg);
|
|
|
|
|
|
2026-04-30 04:15:29 +02:00
|
|
|
// View over the project's args with simple query helpers. Has(flag) for
|
|
|
|
|
// boolean switches; Get(prefix) for valued options (e.g. Get("--prefix=")
|
|
|
|
|
// returns the substring after the equals). Definitions are out-of-line
|
|
|
|
|
// and CRAFTER_API so they cross the Windows DLL boundary cleanly — clang
|
|
|
|
|
// does not emit module-attached in-class inline bodies into consumers.
|
2026-04-30 02:20:19 +02:00
|
|
|
struct ArgQuery {
|
|
|
|
|
std::span<const std::string_view> args;
|
2026-04-30 04:15:29 +02:00
|
|
|
CRAFTER_API bool Has(std::string_view flag) const;
|
|
|
|
|
CRAFTER_API std::optional<std::string> Get(std::string_view prefix) const;
|
2026-04-30 02:20:19 +02:00
|
|
|
};
|
|
|
|
|
|
2026-04-29 18:59:01 +02:00
|
|
|
// Apply the framework's standard CLI args + env vars onto cfg:
|
|
|
|
|
// --debug cfg.debug = true
|
|
|
|
|
// --target=<triple> cfg.target = <triple>
|
|
|
|
|
// --march=<value> cfg.march = <value>
|
|
|
|
|
// --mtune=<value> cfg.mtune = <value>
|
2026-04-30 02:20:19 +02:00
|
|
|
// --lib cfg.type promoted Executable → LibraryStatic
|
|
|
|
|
// --shared cfg.type promoted LibraryStatic → LibraryDynamic
|
|
|
|
|
// Promotions chain in priority order so `--lib --shared` lands on
|
|
|
|
|
// LibraryDynamic regardless of arg order; each is a no-op when the
|
|
|
|
|
// baseline doesn't match (e.g. --shared on an Executable, or --lib on a
|
|
|
|
|
// pre-set library).
|
2026-04-29 18:59:01 +02:00
|
|
|
// $CRAFTER_BUILD_MARCH / $CRAFTER_BUILD_MTUNE seed march/mtune.
|
|
|
|
|
// Env applies first, then args, so CLI wins over env wins over caller's
|
2026-04-30 02:20:19 +02:00
|
|
|
// pre-set defaults. Returns an ArgQuery over the same span so projects
|
|
|
|
|
// can query their own flags (`--timing`, ...) without re-rolling the
|
|
|
|
|
// for-arg-in-args loop.
|
|
|
|
|
CRAFTER_API ArgQuery ApplyStandardArgs(Configuration& cfg, std::span<const std::string_view> args);
|
2026-07-23 01:24:42 +02:00
|
|
|
}
|