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
|
|
|
|
|
};
|
|
|
|
|
|
feat(lint): AST layer over libclang cursors
Adds LintContext::Decls() — clang's view of the declarations written in the
file — plus AddAstLintRule to register a rule that reads it. No rule uses it
yet; the three that will are migrated separately.
The declarations come back as a flat vector with parent indices rather than an
opaque cursor handle: no lifetimes cross the project-DLL boundary, no callback
hops back into project.so per node, and "is this at namespace scope or inside a
function?" becomes an index lookup instead of a hand-rolled brace stack.
Three things had to be solved for this to work at all on this codebase.
libclang cannot see through `export`. A C++20 export declaration has no
CXCursorKind, so `export namespace Crafter { … }` arrives as a childless
CXCursor_UnexposedDecl and clang_visitChildren does not descend. Five of the
eleven interfaces here are written that way — 677 lines, including
Configuration and LintContext, yielding zero usable cursors. A plain
`namespace` IS descended into, so the fix is to blank the keyword before
parsing, byte-length preserving so every line and column still lands on the
original file. `export module` is left alone or the unit stops being a module
interface. Verified end-to-end against a fixture whose asserted line numbers
match the unblanked file.
PCMs are flag-locked, so each file has to parse with the flags that built it.
CollectConfigSources now records which Configuration owns each source instead
of flattening to a set, because three regimes coexist: the library, each test
(carrying its own target, defines and -march), and project.cpp, which Build
never touches and which therefore gets no command at all.
libclang resolves its builtin headers relative to its own install path, which
need not match the clang++ that wrote the PCMs. When it doesn't, every parse
dies on "'stddef.h' file not found", so -resource-dir is passed explicitly
from `clang++ -print-resource-dir`.
Two flags on each declaration replace what would otherwise become more
substring denylists: isExternC, and isForeignApi for a declaration that binds
to an entity declared outside the project root — resolved through
clang_getCursorReferenced and the same inside-the-root test the dependency
walk already uses. Parameters and fields inherit it, so an exemption covers a
whole signature rather than the one node that named the foreign entity.
Failure is never silent. A fatal diagnostic leaves a fragment that is
indistinguishable from a file declaring nothing, so it is reported as an error
instead: the rule is skipped, a finding explains why, and summary.errors makes
the run fail. --no-ast opts out deliberately and exits normally.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:30:41 +02:00
|
|
|
// What a LintDecl declares.
|
|
|
|
|
enum class LintDeclKind {
|
|
|
|
|
Namespace,
|
|
|
|
|
Class,
|
|
|
|
|
Struct,
|
|
|
|
|
Union,
|
|
|
|
|
Enum,
|
|
|
|
|
EnumConstant,
|
|
|
|
|
TypeAlias,
|
|
|
|
|
Function,
|
|
|
|
|
Method,
|
|
|
|
|
Constructor,
|
|
|
|
|
Destructor,
|
|
|
|
|
Field,
|
|
|
|
|
Variable, // local or namespace-scope variable
|
|
|
|
|
Parameter,
|
|
|
|
|
Other,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
inline constexpr std::size_t LintNoParent = static_cast<std::size_t>(-1);
|
|
|
|
|
|
|
|
|
|
// One declaration from LintContext::Decls(), for declarations written in
|
|
|
|
|
// the file under lint (never ones pulled in from a header or module).
|
|
|
|
|
//
|
|
|
|
|
// The vector is flattened depth-first with parents before children, and
|
|
|
|
|
// `parent` indexes back into it — that is how a rule asks "is this at
|
|
|
|
|
// namespace scope, or inside a function?" without re-deriving a brace
|
|
|
|
|
// stack from the text.
|
|
|
|
|
struct LintDecl {
|
|
|
|
|
LintDeclKind kind = LintDeclKind::Other;
|
|
|
|
|
std::string name; // unqualified spelling; empty when anonymous
|
|
|
|
|
std::string type; // clang's resolved spelling: "char *", "std::int32_t"
|
|
|
|
|
std::size_t line = 0;
|
|
|
|
|
std::size_t column = 0;
|
refactor(lint): single-declaration splits on the AST, per-declarator type
Tokens are not enough for this one, which is worth stating because it is the
opposite of the enum-class case. Splitting a multi-declarator statement by
copying the shared type prefix is wrong in C++:
int* a, b; -> int* a; int* b; // b was int, not int*
Only per-declarator types get it right, and clang has already resolved them —
`int *` for a, plain `int` for b. A token-based splitter cannot know.
The regex it replaces bailed on `*`, `&`, `<>`, parens and quotes, so pointers,
templates and call initialisers were all left alone. All three split now, and
the fixture proves the mixed pointer case above comes out correctly.
Groups are found structurally rather than by matching a line shape: the first
declarator's extent starts at the shared type, so begin < nameOffset, while a
continuation declarator's starts at its own name, so begin == nameOffset. That
signal comes from the AST itself. nameOffset is now on LintDecl, which is also
what lets the replacement reuse each declarator's original text verbatim
instead of reconstructing it.
Replacing a byte range rather than rewriting whole lines means a comment after
the ';' is outside the edit and survives — the line-based version refused to
touch any line carrying a comment. A comment INSIDE the statement still bails,
since the rewrite would swallow it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 00:35:52 +02:00
|
|
|
std::size_t nameOffset = 0; // byte offset of the declared name
|
feat(lint): AST layer over libclang cursors
Adds LintContext::Decls() — clang's view of the declarations written in the
file — plus AddAstLintRule to register a rule that reads it. No rule uses it
yet; the three that will are migrated separately.
The declarations come back as a flat vector with parent indices rather than an
opaque cursor handle: no lifetimes cross the project-DLL boundary, no callback
hops back into project.so per node, and "is this at namespace scope or inside a
function?" becomes an index lookup instead of a hand-rolled brace stack.
Three things had to be solved for this to work at all on this codebase.
libclang cannot see through `export`. A C++20 export declaration has no
CXCursorKind, so `export namespace Crafter { … }` arrives as a childless
CXCursor_UnexposedDecl and clang_visitChildren does not descend. Five of the
eleven interfaces here are written that way — 677 lines, including
Configuration and LintContext, yielding zero usable cursors. A plain
`namespace` IS descended into, so the fix is to blank the keyword before
parsing, byte-length preserving so every line and column still lands on the
original file. `export module` is left alone or the unit stops being a module
interface. Verified end-to-end against a fixture whose asserted line numbers
match the unblanked file.
PCMs are flag-locked, so each file has to parse with the flags that built it.
CollectConfigSources now records which Configuration owns each source instead
of flattening to a set, because three regimes coexist: the library, each test
(carrying its own target, defines and -march), and project.cpp, which Build
never touches and which therefore gets no command at all.
libclang resolves its builtin headers relative to its own install path, which
need not match the clang++ that wrote the PCMs. When it doesn't, every parse
dies on "'stddef.h' file not found", so -resource-dir is passed explicitly
from `clang++ -print-resource-dir`.
Two flags on each declaration replace what would otherwise become more
substring denylists: isExternC, and isForeignApi for a declaration that binds
to an entity declared outside the project root — resolved through
clang_getCursorReferenced and the same inside-the-root test the dependency
walk already uses. Parameters and fields inherit it, so an exemption covers a
whole signature rather than the one node that named the foreign entity.
Failure is never silent. A fatal diagnostic leaves a fragment that is
indistinguishable from a file declaring nothing, so it is reported as an error
instead: the rule is skipped, a finding explains why, and summary.errors makes
the run fail. --no-ast opts out deliberately and exits normally.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:30:41 +02:00
|
|
|
std::size_t begin = 0; // byte offsets of the whole declaration, for
|
|
|
|
|
std::size_t end = 0; // marking a region a transform must not touch
|
refactor(lint): single-declaration splits on the AST, per-declarator type
Tokens are not enough for this one, which is worth stating because it is the
opposite of the enum-class case. Splitting a multi-declarator statement by
copying the shared type prefix is wrong in C++:
int* a, b; -> int* a; int* b; // b was int, not int*
Only per-declarator types get it right, and clang has already resolved them —
`int *` for a, plain `int` for b. A token-based splitter cannot know.
The regex it replaces bailed on `*`, `&`, `<>`, parens and quotes, so pointers,
templates and call initialisers were all left alone. All three split now, and
the fixture proves the mixed pointer case above comes out correctly.
Groups are found structurally rather than by matching a line shape: the first
declarator's extent starts at the shared type, so begin < nameOffset, while a
continuation declarator's starts at its own name, so begin == nameOffset. That
signal comes from the AST itself. nameOffset is now on LintDecl, which is also
what lets the replacement reuse each declarator's original text verbatim
instead of reconstructing it.
Replacing a byte range rather than rewriting whole lines means a comment after
the ';' is outside the edit and survives — the line-based version refused to
touch any line carrying a comment. A comment INSIDE the statement still bails,
since the rewrite would swallow it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 00:35:52 +02:00
|
|
|
// For the FIRST declarator of a statement, `begin` is the start of the
|
|
|
|
|
// shared type and so precedes `nameOffset`. For a continuation
|
|
|
|
|
// declarator — the `b` of `int* a, b;` — the extent starts at the name,
|
|
|
|
|
// so begin == nameOffset. That is how a multi-declarator statement is
|
|
|
|
|
// recognised without re-parsing the text, and each declarator's `type`
|
|
|
|
|
// is its OWN resolved type: `int *` for a, plain `int` for b.
|
feat(lint): AST layer over libclang cursors
Adds LintContext::Decls() — clang's view of the declarations written in the
file — plus AddAstLintRule to register a rule that reads it. No rule uses it
yet; the three that will are migrated separately.
The declarations come back as a flat vector with parent indices rather than an
opaque cursor handle: no lifetimes cross the project-DLL boundary, no callback
hops back into project.so per node, and "is this at namespace scope or inside a
function?" becomes an index lookup instead of a hand-rolled brace stack.
Three things had to be solved for this to work at all on this codebase.
libclang cannot see through `export`. A C++20 export declaration has no
CXCursorKind, so `export namespace Crafter { … }` arrives as a childless
CXCursor_UnexposedDecl and clang_visitChildren does not descend. Five of the
eleven interfaces here are written that way — 677 lines, including
Configuration and LintContext, yielding zero usable cursors. A plain
`namespace` IS descended into, so the fix is to blank the keyword before
parsing, byte-length preserving so every line and column still lands on the
original file. `export module` is left alone or the unit stops being a module
interface. Verified end-to-end against a fixture whose asserted line numbers
match the unblanked file.
PCMs are flag-locked, so each file has to parse with the flags that built it.
CollectConfigSources now records which Configuration owns each source instead
of flattening to a set, because three regimes coexist: the library, each test
(carrying its own target, defines and -march), and project.cpp, which Build
never touches and which therefore gets no command at all.
libclang resolves its builtin headers relative to its own install path, which
need not match the clang++ that wrote the PCMs. When it doesn't, every parse
dies on "'stddef.h' file not found", so -resource-dir is passed explicitly
from `clang++ -print-resource-dir`.
Two flags on each declaration replace what would otherwise become more
substring denylists: isExternC, and isForeignApi for a declaration that binds
to an entity declared outside the project root — resolved through
clang_getCursorReferenced and the same inside-the-root test the dependency
walk already uses. Parameters and fields inherit it, so an exemption covers a
whole signature rather than the one node that named the foreign entity.
Failure is never silent. A fatal diagnostic leaves a fragment that is
indistinguishable from a file declaring nothing, so it is reported as an error
instead: the rule is skipped, a finding explains why, and summary.errors makes
the run fail. --no-ast opts out deliberately and exits normally.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:30:41 +02:00
|
|
|
std::size_t parent = LintNoParent;
|
|
|
|
|
bool isDefinition = false;
|
|
|
|
|
bool isStatic = false;
|
|
|
|
|
bool isConstexpr = false;
|
|
|
|
|
bool isScopedEnum = false; // `enum class` rather than plain `enum`
|
|
|
|
|
// ---- foreign-API boundary ----
|
|
|
|
|
// Set when this declaration's spelling is dictated by somebody else's
|
|
|
|
|
// header, so the type-modernising rules must leave its bytes alone.
|
|
|
|
|
// Replaces the hand-maintained substring denylists, which could only
|
|
|
|
|
// ever grow: a new external library needs no new entry here.
|
|
|
|
|
bool isExternC = false; // declared with C language linkage
|
|
|
|
|
bool isForeignApi = false; // its type or its body binds to an entity
|
|
|
|
|
// declared outside the project root
|
|
|
|
|
};
|
|
|
|
|
|
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
|
|
|
|
feat(lint): AST layer over libclang cursors
Adds LintContext::Decls() — clang's view of the declarations written in the
file — plus AddAstLintRule to register a rule that reads it. No rule uses it
yet; the three that will are migrated separately.
The declarations come back as a flat vector with parent indices rather than an
opaque cursor handle: no lifetimes cross the project-DLL boundary, no callback
hops back into project.so per node, and "is this at namespace scope or inside a
function?" becomes an index lookup instead of a hand-rolled brace stack.
Three things had to be solved for this to work at all on this codebase.
libclang cannot see through `export`. A C++20 export declaration has no
CXCursorKind, so `export namespace Crafter { … }` arrives as a childless
CXCursor_UnexposedDecl and clang_visitChildren does not descend. Five of the
eleven interfaces here are written that way — 677 lines, including
Configuration and LintContext, yielding zero usable cursors. A plain
`namespace` IS descended into, so the fix is to blank the keyword before
parsing, byte-length preserving so every line and column still lands on the
original file. `export module` is left alone or the unit stops being a module
interface. Verified end-to-end against a fixture whose asserted line numbers
match the unblanked file.
PCMs are flag-locked, so each file has to parse with the flags that built it.
CollectConfigSources now records which Configuration owns each source instead
of flattening to a set, because three regimes coexist: the library, each test
(carrying its own target, defines and -march), and project.cpp, which Build
never touches and which therefore gets no command at all.
libclang resolves its builtin headers relative to its own install path, which
need not match the clang++ that wrote the PCMs. When it doesn't, every parse
dies on "'stddef.h' file not found", so -resource-dir is passed explicitly
from `clang++ -print-resource-dir`.
Two flags on each declaration replace what would otherwise become more
substring denylists: isExternC, and isForeignApi for a declaration that binds
to an entity declared outside the project root — resolved through
clang_getCursorReferenced and the same inside-the-root test the dependency
walk already uses. Parameters and fields inherit it, so an exemption covers a
whole signature rather than the one node that named the foreign entity.
Failure is never silent. A fatal diagnostic leaves a fragment that is
indistinguishable from a file declaring nothing, so it is reported as an error
instead: the rule is skipped, a finding explains why, and summary.errors makes
the run fail. --no-ast opts out deliberately and exits normally.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:30:41 +02:00
|
|
|
// Declarations written in this file, flattened depth-first with
|
|
|
|
|
// parents before children (see LintDecl). Empty when the AST is not
|
|
|
|
|
// available — check AstAvailable() first, and do not read an empty
|
|
|
|
|
// result as "this file declares nothing".
|
|
|
|
|
//
|
|
|
|
|
// Requires the module PCMs, unlike Tokens(): a module unit's
|
|
|
|
|
// `import std;` cannot resolve without them, and clang treats that as
|
|
|
|
|
// fatal rather than recovering. RunLint builds them on demand for
|
|
|
|
|
// rules registered through AddAstLintRule and fails the run if it
|
|
|
|
|
// cannot, so a semantic rule never silently reports clean.
|
|
|
|
|
//
|
|
|
|
|
// One further asymmetry worth knowing: an AST is ONE configuration's
|
|
|
|
|
// slice. Declarations inside a preprocessor branch that is inactive
|
|
|
|
|
// for the host are absent here, though Tokens() still sees them. Rules
|
|
|
|
|
// that must cover every platform belong on tokens.
|
|
|
|
|
CRAFTER_API std::span<const LintDecl> Decls();
|
|
|
|
|
CRAFTER_API bool AstAvailable();
|
|
|
|
|
// Why AstAvailable() is false — a missing PCM, a parse error, a file
|
|
|
|
|
// that is not a translation unit. Empty when the AST is available.
|
|
|
|
|
CRAFTER_API std::string_view AstUnavailableReason();
|
|
|
|
|
|
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;
|
feat(lint): AST layer over libclang cursors
Adds LintContext::Decls() — clang's view of the declarations written in the
file — plus AddAstLintRule to register a rule that reads it. No rule uses it
yet; the three that will are migrated separately.
The declarations come back as a flat vector with parent indices rather than an
opaque cursor handle: no lifetimes cross the project-DLL boundary, no callback
hops back into project.so per node, and "is this at namespace scope or inside a
function?" becomes an index lookup instead of a hand-rolled brace stack.
Three things had to be solved for this to work at all on this codebase.
libclang cannot see through `export`. A C++20 export declaration has no
CXCursorKind, so `export namespace Crafter { … }` arrives as a childless
CXCursor_UnexposedDecl and clang_visitChildren does not descend. Five of the
eleven interfaces here are written that way — 677 lines, including
Configuration and LintContext, yielding zero usable cursors. A plain
`namespace` IS descended into, so the fix is to blank the keyword before
parsing, byte-length preserving so every line and column still lands on the
original file. `export module` is left alone or the unit stops being a module
interface. Verified end-to-end against a fixture whose asserted line numbers
match the unblanked file.
PCMs are flag-locked, so each file has to parse with the flags that built it.
CollectConfigSources now records which Configuration owns each source instead
of flattening to a set, because three regimes coexist: the library, each test
(carrying its own target, defines and -march), and project.cpp, which Build
never touches and which therefore gets no command at all.
libclang resolves its builtin headers relative to its own install path, which
need not match the clang++ that wrote the PCMs. When it doesn't, every parse
dies on "'stddef.h' file not found", so -resource-dir is passed explicitly
from `clang++ -print-resource-dir`.
Two flags on each declaration replace what would otherwise become more
substring denylists: isExternC, and isForeignApi for a declaration that binds
to an entity declared outside the project root — resolved through
clang_getCursorReferenced and the same inside-the-root test the dependency
walk already uses. Parameters and fields inherit it, so an exemption covers a
whole signature rather than the one node that named the foreign entity.
Failure is never silent. A fatal diagnostic leaves a fragment that is
indistinguishable from a file declaring nothing, so it is reported as an error
instead: the rule is skipped, a finding explains why, and summary.errors makes
the run fail. --no-ast opts out deliberately and exits normally.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:30:41 +02:00
|
|
|
// Flags this file parses with, from GetCompileCommand of the
|
|
|
|
|
// Configuration that owns it. Empty disables Decls().
|
|
|
|
|
std::string compileCommand;
|
|
|
|
|
// Declarations resolving outside this are foreign API (LintDecl).
|
|
|
|
|
fs::path projectRoot;
|
|
|
|
|
std::optional<std::vector<LintDecl>> declCache;
|
|
|
|
|
std::string astReason;
|
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;
|
feat(lint): AST layer over libclang cursors
Adds LintContext::Decls() — clang's view of the declarations written in the
file — plus AddAstLintRule to register a rule that reads it. No rule uses it
yet; the three that will are migrated separately.
The declarations come back as a flat vector with parent indices rather than an
opaque cursor handle: no lifetimes cross the project-DLL boundary, no callback
hops back into project.so per node, and "is this at namespace scope or inside a
function?" becomes an index lookup instead of a hand-rolled brace stack.
Three things had to be solved for this to work at all on this codebase.
libclang cannot see through `export`. A C++20 export declaration has no
CXCursorKind, so `export namespace Crafter { … }` arrives as a childless
CXCursor_UnexposedDecl and clang_visitChildren does not descend. Five of the
eleven interfaces here are written that way — 677 lines, including
Configuration and LintContext, yielding zero usable cursors. A plain
`namespace` IS descended into, so the fix is to blank the keyword before
parsing, byte-length preserving so every line and column still lands on the
original file. `export module` is left alone or the unit stops being a module
interface. Verified end-to-end against a fixture whose asserted line numbers
match the unblanked file.
PCMs are flag-locked, so each file has to parse with the flags that built it.
CollectConfigSources now records which Configuration owns each source instead
of flattening to a set, because three regimes coexist: the library, each test
(carrying its own target, defines and -march), and project.cpp, which Build
never touches and which therefore gets no command at all.
libclang resolves its builtin headers relative to its own install path, which
need not match the clang++ that wrote the PCMs. When it doesn't, every parse
dies on "'stddef.h' file not found", so -resource-dir is passed explicitly
from `clang++ -print-resource-dir`.
Two flags on each declaration replace what would otherwise become more
substring denylists: isExternC, and isForeignApi for a declaration that binds
to an entity declared outside the project root — resolved through
clang_getCursorReferenced and the same inside-the-root test the dependency
walk already uses. Parameters and fields inherit it, so an exemption covers a
whole signature rather than the one node that named the foreign entity.
Failure is never silent. A fatal diagnostic leaves a fragment that is
indistinguishable from a file declaring nothing, so it is reported as an error
instead: the rule is skipped, a finding explains why, and summary.errors makes
the run fail. --no-ast opts out deliberately and exits normally.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:30:41 +02:00
|
|
|
// Registered via AddAstLintRule: this rule reads Decls(), so the run
|
|
|
|
|
// has to produce the module PCMs first, and must fail rather than let
|
|
|
|
|
// the rule quietly find nothing.
|
|
|
|
|
bool needsAst = false;
|
2026-07-23 01:24:42 +02:00
|
|
|
};
|
|
|
|
|
|
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);
|
feat(lint): AST layer over libclang cursors
Adds LintContext::Decls() — clang's view of the declarations written in the
file — plus AddAstLintRule to register a rule that reads it. No rule uses it
yet; the three that will are migrated separately.
The declarations come back as a flat vector with parent indices rather than an
opaque cursor handle: no lifetimes cross the project-DLL boundary, no callback
hops back into project.so per node, and "is this at namespace scope or inside a
function?" becomes an index lookup instead of a hand-rolled brace stack.
Three things had to be solved for this to work at all on this codebase.
libclang cannot see through `export`. A C++20 export declaration has no
CXCursorKind, so `export namespace Crafter { … }` arrives as a childless
CXCursor_UnexposedDecl and clang_visitChildren does not descend. Five of the
eleven interfaces here are written that way — 677 lines, including
Configuration and LintContext, yielding zero usable cursors. A plain
`namespace` IS descended into, so the fix is to blank the keyword before
parsing, byte-length preserving so every line and column still lands on the
original file. `export module` is left alone or the unit stops being a module
interface. Verified end-to-end against a fixture whose asserted line numbers
match the unblanked file.
PCMs are flag-locked, so each file has to parse with the flags that built it.
CollectConfigSources now records which Configuration owns each source instead
of flattening to a set, because three regimes coexist: the library, each test
(carrying its own target, defines and -march), and project.cpp, which Build
never touches and which therefore gets no command at all.
libclang resolves its builtin headers relative to its own install path, which
need not match the clang++ that wrote the PCMs. When it doesn't, every parse
dies on "'stddef.h' file not found", so -resource-dir is passed explicitly
from `clang++ -print-resource-dir`.
Two flags on each declaration replace what would otherwise become more
substring denylists: isExternC, and isForeignApi for a declaration that binds
to an entity declared outside the project root — resolved through
clang_getCursorReferenced and the same inside-the-root test the dependency
walk already uses. Parameters and fields inherit it, so an exemption covers a
whole signature rather than the one node that named the foreign entity.
Failure is never silent. A fatal diagnostic leaves a fragment that is
indistinguishable from a file declaring nothing, so it is reported as an error
instead: the rule is skipped, a finding explains why, and summary.errors makes
the run fail. --no-ast opts out deliberately and exits normally.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:30:41 +02:00
|
|
|
// Same, for a rule that reads LintContext::Decls(). Declared
|
|
|
|
|
// separately rather than as a flag on AddLintRule so existing
|
|
|
|
|
// registrations keep compiling. Such a rule makes the run build the
|
|
|
|
|
// module PCMs if they are missing, and a file whose AST could not be
|
|
|
|
|
// produced becomes an error instead of a silent pass. Prefer
|
|
|
|
|
// report-only: a transform running after the first one forces a
|
|
|
|
|
// re-parse of everything it changed.
|
|
|
|
|
CRAFTER_API void AddAstLintRule(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
|
|
|
};
|
|
|
|
|
|
refactor: extract GetCompileCommand and StdPcmDir out of Build
The clang invocation was assembled inline across four regions of Build,
interleaved with the dependency-graph walk, so nothing else could ask "what
flags does this Configuration compile with". The linter's AST layer needs
exactly that, and it cannot approximate it: a precompiled module is rejected
outright by a translation unit whose target features differ from the one that
wrote it. Dropping just -march=native produces hundreds of "compiled with the
target feature '+avx512bw' but the current translation unit is not" errors and
no usable parse, so reconstructed flags fail hard rather than degrade.
GetCompileCommand is the config-pure part: target, arch, standard,
configuration defines, module search paths, includes, user compileFlags,
optimisation and LTO. Build appends only what depends on work having happened
— dependency public flags and external dependency flags. The sub-strings it
also needs on their own (includes, defines, user flags, LTO) come back as
struct members, so the .c compile path is unchanged.
Verified by probing `command` at the equivalent point before and after and
diffing: byte-identical across all 23 configurations exercised by a full build
plus the test suite.
Two incidental simplifications fell out. pcmDir was recomputing what
Configuration::PcmDir() already returns, and cmakeBuildType is now a
one-liner. GetCompileCommand is also most of what a compile_commands.json
would need, which this repo lacks.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:15:20 +02:00
|
|
|
// Directory holding the std module PCM for this Configuration. Keyed on
|
|
|
|
|
// target+march because a BMI is only loadable by a TU with matching target
|
|
|
|
|
// features, and suffixed with the wasm variant flags for the same reason.
|
|
|
|
|
CRAFTER_API fs::path StdPcmDir(const Configuration& config);
|
|
|
|
|
|
|
|
|
|
// The clang invocation every C++ translation unit in this Configuration is
|
|
|
|
|
// compiled with, before anything that depends on work having happened
|
|
|
|
|
// (dependency public flags, external dependency flags — Build appends
|
|
|
|
|
// those itself). A pure function of the Configuration.
|
|
|
|
|
//
|
|
|
|
|
// Anything that needs to PARSE this project's sources rather than build
|
|
|
|
|
// them — the linter's AST layer, a future compile_commands.json — must go
|
|
|
|
|
// through this instead of assembling its own flags. A precompiled module
|
|
|
|
|
// is rejected outright by a TU whose target features differ from the one
|
|
|
|
|
// that wrote it, so approximately-right flags fail hard rather than
|
|
|
|
|
// degrade: omitting -march=native alone produces hundreds of "compiled
|
|
|
|
|
// with the target feature '+avx512bw' but the current translation unit is
|
|
|
|
|
// not" errors and no usable parse.
|
|
|
|
|
struct CompileCommand {
|
|
|
|
|
std::string command; // full C++ compile prefix, shell-ready
|
|
|
|
|
// Sub-sets Build also needs on its own: the .c compile path takes the
|
|
|
|
|
// includes, defines and user flags but not the module-only bits.
|
|
|
|
|
std::string includeFlags;
|
|
|
|
|
std::string defineFlags;
|
|
|
|
|
std::string userFlags;
|
|
|
|
|
std::string ltoCompileFlags;
|
|
|
|
|
std::string ltoLinkFlags;
|
feat(lint): no-char-pointer reads the AST, retiring the interop denylist
The rule was `\bchar\s*\*` over the text minus a substring denylist — argv,
getenv, setenv, dlerror, c_str, .data(, reinterpret_cast, extern ". Every entry
was a patch for one interop site, the list could only grow as libraries
arrived, and each entry disabled the rule for the whole LINE it appeared on.
It now walks declarations and asks the question the denylist was approximating:
whose header dictates this spelling? A declaration with C language linkage, or
one whose initialiser binds to an entity declared outside the project root, is
somebody else's API and keeps its spelling. getenv, c_str and friends are
exempt because of where they are declared, not because they are named here, so
a new external library needs no new entry.
Two bugs found while testing this, both of which had made the rule silently
pass over the entire repository:
clang_getCursorLanguage cannot be used to detect extern "C". Its default answer
is CXLanguage_C for a plain function, variable or parameter even in a C++
translation unit, so isExternC was true almost everywhere and exempted
everything. Replaced by tracking CXCursor_LinkageSpec depth during the walk,
reading the extent text to tell extern "C" from extern "C++".
Attributing any foreign reference in a subtree to the enclosing declaration was
too broad: a function that merely touched libc++ somewhere in its body would
exempt its own signature. Narrowed to initialiser contexts — a variable, field
or parameter — which is where a binding to a foreign API actually occurs.
Also: functions now carry their RESULT type rather than the whole function
type, since the parameters arrive as their own declarations and would otherwise
be reported twice. main's parameters are exempt structurally, its signature
being fixed by the language rather than chosen here.
Two sites keep an explicit lint-disable, both Crafter::Run taking main's argv
verbatim. That is two visible, reasoned suppressions in place of a denylist
that silently disabled the rule for every line mentioning one of eight tokens.
ExternalCloneDir and ExternalIncludeFlags are now exposed from :External, so a
source that includes an external dependency's headers can be parsed without
running a build to discover where they are. BuildExternal derives its own
working directory through the same function, so the two cannot drift.
Crafter.Build-Shader.cpp needed this to parse at all.
A file with no compile command — project.cpp, which LoadProject builds with its
own flags — is not a translation unit of the build graph, so AST rules skip it
the way a rule self-filters by extension. That is distinct from a file that
should have parsed and did not, which stays an error.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:46:32 +02:00
|
|
|
// -I flags from external dependencies' declared includeDirs, for this
|
|
|
|
|
// configuration and its dependencies. Deliberately NOT folded into
|
|
|
|
|
// `command`: Build appends the authoritative set from the external build
|
|
|
|
|
// results instead. Exposed for callers that only PARSE sources and so
|
|
|
|
|
// cannot wait for a build to tell them where the headers are.
|
|
|
|
|
std::string externalIncludeFlags;
|
refactor: extract GetCompileCommand and StdPcmDir out of Build
The clang invocation was assembled inline across four regions of Build,
interleaved with the dependency-graph walk, so nothing else could ask "what
flags does this Configuration compile with". The linter's AST layer needs
exactly that, and it cannot approximate it: a precompiled module is rejected
outright by a translation unit whose target features differ from the one that
wrote it. Dropping just -march=native produces hundreds of "compiled with the
target feature '+avx512bw' but the current translation unit is not" errors and
no usable parse, so reconstructed flags fail hard rather than degrade.
GetCompileCommand is the config-pure part: target, arch, standard,
configuration defines, module search paths, includes, user compileFlags,
optimisation and LTO. Build appends only what depends on work having happened
— dependency public flags and external dependency flags. The sub-strings it
also needs on their own (includes, defines, user flags, LTO) come back as
struct members, so the .c compile path is unchanged.
Verified by probing `command` at the equivalent point before and after and
diffing: byte-identical across all 23 configurations exercised by a full build
plus the test suite.
Two incidental simplifications fell out. pcmDir was recomputing what
Configuration::PcmDir() already returns, and cmakeBuildType is now a
one-liner. GetCompileCommand is also most of what a compile_commands.json
would need, which this repo lacks.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:15:20 +02:00
|
|
|
// ThinLTO is on: objects hold bitcode, so archiving needs llvm-ar.
|
|
|
|
|
bool useLto = false;
|
|
|
|
|
fs::path stdPcmDir;
|
|
|
|
|
fs::path pcmDir;
|
|
|
|
|
};
|
|
|
|
|
CRAFTER_API CompileCommand GetCompileCommand(const Configuration& config);
|
|
|
|
|
|
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
|
|
|
|
feat(lint): no-char-pointer reads the AST, retiring the interop denylist
The rule was `\bchar\s*\*` over the text minus a substring denylist — argv,
getenv, setenv, dlerror, c_str, .data(, reinterpret_cast, extern ". Every entry
was a patch for one interop site, the list could only grow as libraries
arrived, and each entry disabled the rule for the whole LINE it appeared on.
It now walks declarations and asks the question the denylist was approximating:
whose header dictates this spelling? A declaration with C language linkage, or
one whose initialiser binds to an entity declared outside the project root, is
somebody else's API and keeps its spelling. getenv, c_str and friends are
exempt because of where they are declared, not because they are named here, so
a new external library needs no new entry.
Two bugs found while testing this, both of which had made the rule silently
pass over the entire repository:
clang_getCursorLanguage cannot be used to detect extern "C". Its default answer
is CXLanguage_C for a plain function, variable or parameter even in a C++
translation unit, so isExternC was true almost everywhere and exempted
everything. Replaced by tracking CXCursor_LinkageSpec depth during the walk,
reading the extent text to tell extern "C" from extern "C++".
Attributing any foreign reference in a subtree to the enclosing declaration was
too broad: a function that merely touched libc++ somewhere in its body would
exempt its own signature. Narrowed to initialiser contexts — a variable, field
or parameter — which is where a binding to a foreign API actually occurs.
Also: functions now carry their RESULT type rather than the whole function
type, since the parameters arrive as their own declarations and would otherwise
be reported twice. main's parameters are exempt structurally, its signature
being fixed by the language rather than chosen here.
Two sites keep an explicit lint-disable, both Crafter::Run taking main's argv
verbatim. That is two visible, reasoned suppressions in place of a denylist
that silently disabled the rule for every line mentioning one of eight tokens.
ExternalCloneDir and ExternalIncludeFlags are now exposed from :External, so a
source that includes an external dependency's headers can be parsed without
running a build to discover where they are. BuildExternal derives its own
working directory through the same function, so the two cannot drift.
Crafter.Build-Shader.cpp needed this to parse at all.
A file with no compile command — project.cpp, which LoadProject builds with its
own flags — is not a translation unit of the build graph, so AST rules skip it
the way a rule self-filters by extension. That is distinct from a file that
should have parsed and did not, which stays an error.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:46:32 +02:00
|
|
|
// Takes main's argv verbatim so the CLI entry point is a one-liner;
|
|
|
|
|
// the shape is inherited from the language, not chosen here.
|
|
|
|
|
// lint-disable-next-line no-char-pointer
|
feat(lint): fixed-width-types keeps widths that a foreign API chose
The rewrite itself stays token-shaped — it edits type SPELLINGS, which an AST
discards — but what it must not touch now comes from the AST.
The old exemption was per LINE: `int main`, `argc`, `argv`, `extern "`. Being a
transform, a missed exemption here does not over-report, it emits code that no
longer matches the API being called, so this is the rule where guessing from
substrings mattered most. And being per-line, it also disabled the rule for
anything sharing a line with one of those words.
Now a declaration with C language linkage, or one whose initialiser binds to an
entity declared outside the project, contributes a protected byte range and
keeps its spelling. That answers the case directly: a function declared in
somebody else's header taking `unsigned int` keeps `unsigned int`, and a local
initialised from strtoul keeps `unsigned long`, because of where those are
declared rather than because of what the line says.
main is protected from the start of its declaration to the opening brace of its
body, not for its whole extent. Its signature is fixed by the language; its body
is ordinary code. Three findings on this repository came out of that
distinction, all correct:
for (int i = 1; i < argc; ++i) -> for (std::int32_t i = 1; ...)
skipped before only because `argc` appeared on the line, plus Crafter::Run's own
`int argc` and return type, which are ours rather than the language's.
All three AST rules now share one interop test instead of carrying a denylist
each, and it is the same test: whose header dictates this spelling.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:59:46 +02:00
|
|
|
CRAFTER_API std::int32_t Run(std::int32_t 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
|
|
|
}
|