/* Crafter® Build Copyright (C) 2026 Catcrafts® Catcrafts.net This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License version 3.0 as published by the Free Software Foundation; This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ module; #include "Crafter.Build-Api.h" export module Crafter.Build:Clang; import std; import :Shader; import :Interface; import :Implementation; import :External; namespace fs = std::filesystem; export namespace Crafter { struct BuildResult { std::string result; bool repack; std::unordered_set libs; // 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 publicCompileFlags; }; struct Define { std::string name; std::string value; }; enum class ConfigurationType { Executable, LibraryStatic, LibraryDynamic, }; struct Test; struct TestRunner { // 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}". std::string exec; // Display name; also the cache key for the availability probe. 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(); // Prefix runner: wraps the local binary in ` {bin} {args}`. // Used for qemu-user, wasmtime, and similar single-binary wrappers. static CRAFTER_API TestRunner Cmd(std::string command); // 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(); // Parse a `[:]` spec used by CRAFTER_BUILD_RUNNER_ // and --runner=. Supported: "local", "cmd:". Returns nullopt // for an empty string; throws on a non-empty unrecognized spec. static CRAFTER_API std::optional FromSpec(std::string_view spec); // Honor CRAFTER_BUILD_RUNNER_ (power-user override). Triple // dashes/dots become underscores so they're valid in env-var names. // Returns `fallback` when the env var is unset. static CRAFTER_API TestRunner FromEnv(std::string_view target, TestRunner fallback = Local()); // 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-` (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_ still wins as an override // upstream of this — see FromEnv. static CRAFTER_API TestRunner ForTarget(const struct Configuration& cfg); }; enum class TestOutcome { Pass, Fail, Crash, Timeout, Skipped }; struct TestResult { std::string name; TestOutcome outcome = TestOutcome::Pass; int exitCode = 0; int signal = 0; std::chrono::milliseconds duration{0}; std::string output; }; // 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(); struct Configuration { fs::path path; std::string outputName; std::string name; std::string march = "native"; std::string mtune = "native"; std::string target = HostTarget(); std::string sysroot; bool debug = false; ConfigurationType type = ConfigurationType::Executable; std::vector> interfaces; std::vector implementations; std::vector cFiles; std::vector cuda; std::vector dependencies; std::vector files; // 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 buildFiles; std::vector defines; std::vector shaders; // 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/.ctex or bin/.cmesh. // - A directory. The build recurses; .png/.obj are compressed // with the relative tree mirrored under bin//, 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. std::vector assets; std::vector externalDependencies; std::vector compileFlags; std::vector linkFlags; std::vector tests; CRAFTER_API void GetInterfacesAndImplementations(std::span interfaces, std::span implementations); // Declare a test. Sources default to `tests//main.cpp` resolved // against this Configuration's path; target/march/mtune/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. 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 interfaces); // Math-style fan-out: one Test per MarchTier, all sharing the same // `tests//main.cpp` source and the same interface set, each // compiled with the tier's `-march`/`-mtune`. Test names are // `-`. CRAFTER_API void AddMarchVariants(std::string_view name, std::span interfaces, std::span tiers); // 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; compileKey += std::to_string(static_cast(type)); 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; } std::size_t configHash = std::hash{}(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(); } fs::path PcmDir() const { return type == ConfigurationType::Executable ? BuildDir() : BinDir(); } }; struct Test { Configuration config; TestRunner runner; std::chrono::seconds timeout{60}; std::vector args; // Declarative preconditions. Each entry is "tool:", // "file:", or "env:". 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 requires_; }; CRAFTER_API BuildResult Build(Configuration& config, std::unordered_map>& depResults, std::mutex& depMutex); CRAFTER_API int Run(int argc, char** argv); // 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); // 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. struct ArgQuery { std::span args; CRAFTER_API bool Has(std::string_view flag) const; CRAFTER_API std::optional Get(std::string_view prefix) const; }; // Apply the framework's standard CLI args + env vars onto cfg: // --debug cfg.debug = true // --target= cfg.target = // --march= cfg.march = // --mtune= cfg.mtune = // --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). // $CRAFTER_BUILD_MARCH / $CRAFTER_BUILD_MTUNE seed march/mtune. // Env applies first, then args, so CLI wins over env wins over caller's // 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 args); }