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>
437 lines
23 KiB
C++
437 lines
23 KiB
C++
// SPDX-License-Identifier: LGPL-3.0-only
|
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
|
|
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<std::string> 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<std::string> publicCompileFlags;
|
|
};
|
|
|
|
struct Define {
|
|
std::string name;
|
|
std::string value;
|
|
};
|
|
|
|
// 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;
|
|
};
|
|
|
|
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 `<command> {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 `<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.
|
|
static CRAFTER_API std::optional<TestRunner> FromSpec(std::string_view spec);
|
|
// 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.
|
|
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-<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);
|
|
};
|
|
|
|
enum class TestOutcome { Pass, Fail, Crash, Timeout, Skipped };
|
|
|
|
struct TestResult {
|
|
std::string name;
|
|
TestOutcome outcome = TestOutcome::Pass;
|
|
std::int32_t exitCode = 0;
|
|
std::int32_t signal = 0;
|
|
std::chrono::milliseconds duration{0};
|
|
std::string output;
|
|
};
|
|
|
|
// 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;
|
|
};
|
|
|
|
// 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
|
|
};
|
|
|
|
// 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
|
|
// `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.
|
|
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);
|
|
|
|
// 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);
|
|
|
|
// 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;
|
|
std::optional<std::vector<LintToken>> tokenCache;
|
|
};
|
|
|
|
// 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;
|
|
};
|
|
|
|
// 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<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;
|
|
// 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;
|
|
std::vector<Define> defines;
|
|
std::vector<Shader> 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/<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.
|
|
std::vector<fs::path> assets;
|
|
std::vector<ExternalDependency> externalDependencies;
|
|
std::vector<std::string> compileFlags;
|
|
std::vector<std::string> linkFlags;
|
|
// 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;
|
|
std::vector<Test> tests;
|
|
// Lint rules for `crafter-build lint`. Populate via AddLintRule.
|
|
std::vector<LintRule> lintRules;
|
|
CRAFTER_API void GetInterfacesAndImplementations(std::span<fs::path> interfaces, std::span<fs::path> implementations);
|
|
// Declare a test. Sources default to `tests/<name>/main.cpp` resolved
|
|
// 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.
|
|
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>`.
|
|
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);
|
|
// 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<std::int32_t>(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;
|
|
}
|
|
// 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;
|
|
}
|
|
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(); }
|
|
fs::path PcmDir() const {
|
|
return type == ConfigurationType::Executable ? BuildDir() : BinDir();
|
|
}
|
|
};
|
|
|
|
struct Test {
|
|
Configuration config;
|
|
TestRunner runner;
|
|
std::chrono::seconds timeout{60};
|
|
std::vector<std::string> args;
|
|
// 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_;
|
|
};
|
|
|
|
CRAFTER_API BuildResult Build(Configuration& config, std::unordered_map<fs::path, std::shared_future<BuildResult>>& 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);
|
|
|
|
// 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);
|
|
|
|
// 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<const std::string_view> args;
|
|
CRAFTER_API bool Has(std::string_view flag) const;
|
|
CRAFTER_API std::optional<std::string> Get(std::string_view prefix) const;
|
|
};
|
|
|
|
// 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>
|
|
// --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<const std::string_view> args);
|
|
}
|