readme change
All checks were successful
Flatpak / flatpak (push) Successful in 13m43s

This commit is contained in:
Jorijn van der Graaf 2026-10-03 21:16:01 +02:00
commit c6b0bbaf25
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB

View file

@ -1,6 +1,6 @@
# Crafter Build # Crafter Build
A C++26 modules build system. Project descriptions are written in C++ with no JSON, no Lua, no embedded DSL. You write a `project.cpp` that constructs a `Configuration` and returns it; Crafter Build compiles, loads, and executes it. A C++26 modules build system. Project descriptions are written in C++ with no JSON, no Lua, no embedded DSL. You write a `project.cpp` that constructs a `Configuration` and returns it. Crafter Build compiles, loads, and executes it.
## Install ## Install
@ -39,7 +39,7 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
} }
``` ```
Run `crafter-build` in that directory; outputs land at `bin/myapp-<target>-<march>-<mtune>-<hash>/myapp` and intermediates at `build/myapp-<target>-<march>-<mtune>-<hash>/`. The hash covers everything that changes what is compiled (build type, defines, flags, sysroot, the compiler itself), so two such builds never share a directory. After a compiler update, a new crafter-build release included, the first build starts a fresh directory and recompiles; `crafter-build clean` removes the ones left behind. Add `-r` to run the produced executable straight after a successful build. Run `crafter-build` in that directory, outputs land at `bin/myapp-<target>-<march>-<mtune>-<hash>/myapp` and intermediates at `build/myapp-<target>-<march>-<mtune>-<hash>/`. The hash covers everything that changes what is compiled (build type, defines, flags, sysroot, the compiler itself), so two such builds never share a directory. After a compiler update, a new crafter-build release included, the first build starts a fresh directory and recompiles. `crafter-build clean` removes the ones left behind. Add `-r` to run the produced executable straight after a successful build.
## Dependencies ## Dependencies
@ -75,7 +75,7 @@ Configuration* sibling = LocalProject({ .projectFile = "../Crafter.Math/project.
cfg.dependencies = { math }; cfg.dependencies = { math };
``` ```
Clones share the external cache and identical specs share one `Configuration`. An unpinned source is `git pull`ed on every invocation; set `.commit` for reproducible, offline-after-first-fetch builds. Clones share the external cache and identical specs share one `Configuration`. An unpinned source is `git pull`ed on every invocation, set `.commit` for reproducible, offline-after-first-fetch builds.
A project loaded this way runs its `CrafterBuildProject` as a dependency, and `IsTopLevelProject()` is `false` while it does (`true` in the project crafter-build was pointed at, and outside any project file). Wire test-only dependencies behind it: a library whose test links crafter-build itself would otherwise load crafter-build's `project.cpp` from inside crafter-build and recurse. A project loaded this way runs its `CrafterBuildProject` as a dependency, and `IsTopLevelProject()` is `false` while it does (`true` in the project crafter-build was pointed at, and outside any project file). Wire test-only dependencies behind it: a library whose test links crafter-build itself would otherwise load crafter-build's `project.cpp` from inside crafter-build and recurse.
@ -112,7 +112,7 @@ Test conventions:
- **Exit codes**: `0` = ✅ pass, nonzero = ❌ fail, **`77`** = ⏭ skipped (autoconf convention; use it for runtime opt-outs like "tool not on PATH"). - **Exit codes**: `0` = ✅ pass, nonzero = ❌ fail, **`77`** = ⏭ skipped (autoconf convention; use it for runtime opt-outs like "tool not on PATH").
- Each test runs in its own subprocess — a segfault doesn't take the runner down. - Each test runs in its own subprocess — a segfault doesn't take the runner down.
- Default timeout is 60 s per test; runs in parallel up to `hardware_concurrency()`. - Default timeout is 60 s per test, runs in parallel up to `hardware_concurrency()`.
- Stdout+stderr is captured and printed atomically on completion. Failed/crashed/timed-out tests also write to `build/test-logs/<name>.log`. - Stdout+stderr is captured and printed atomically on completion. Failed/crashed/timed-out tests also write to `build/test-logs/<name>.log`.
- A broken fixture `project.cpp` is reported as a single failed test, not a fatal error — other tests still run. - A broken fixture `project.cpp` is reported as a single failed test, not a fatal error — other tests still run.
@ -130,7 +130,7 @@ crafter-build test --timeout=5 # override per-t
Outcomes: ✅ pass, ❌ fail, 💥 crash (with signal name), ⏱ timeout, ⏭ skipped. The runner exits 0 only if every non-skipped test passed. Outcomes: ✅ pass, ❌ fail, 💥 crash (with signal name), ⏱ timeout, ⏭ skipped. The runner exits 0 only if every non-skipped test passed.
`--target=` filters the run to tests whose `cfg.target` matches; default is the host triple. Per-target tests parse `--target=...` from their own args span and build for it; tests with hardcoded targets (e.g. host-only meta-tests) are filtered out under non-matching `--target=`. `--target=` filters the run to tests whose `cfg.target` matches, default is the host triple. Per-target tests parse `--target=...` from their own args span and build for it, tests with hardcoded targets (e.g. host-only meta-tests) are filtered out under non-matching `--target=`.
### Cross-target test runners ### Cross-target test runners
@ -145,9 +145,9 @@ TestRunner::Cmd("wine") // local exec via prefix co
For elaborate setups (rsync, ssh ControlMaster, env vars on the spawn, `QEMU_LD_PREFIX` for sysroots), construct `TestRunner` directly — `copy`/`exec`/`cleanup`/`probe` are public string fields with `{bundle}`, `{remote_bundle}`, `{bin}`, `{bin_name}`, `{args}` placeholders. For elaborate setups (rsync, ssh ControlMaster, env vars on the spawn, `QEMU_LD_PREFIX` for sysroots), construct `TestRunner` directly — `copy`/`exec`/`cleanup`/`probe` are public string fields with `{bundle}`, `{remote_bundle}`, `{bin}`, `{bin_name}`, `{args}` placeholders.
For cross-arch builds, set `cfg.sysroot` on the `Configuration` to point at a sysroot for the target. `BuildStdPcm` and the compile/link commands both consume it: `--sysroot=<path>` is passed to clang, and the std module is precompiled against the sysroot's libc++ headers. The module source has to come from the same libc++ release as those headers, so it is taken from `<sysroot>/usr/share/libc++/v1/std.cppm` when the sysroot ships it, else from the toolchain when both are the same major release, else from the sources crafter-build carries for other releases (`share/crafter-build/libc++-modules/<major>/`; `crafter-build --version` lists them). Alpine is the common case for the last two: its `libc++-dev` has the headers and no module source. When nothing matches, the build stops with one line naming both releases; installing that release's `share/libc++/v1` directory into the sysroot resolves it. The cheapest aarch64 sysroot is an extracted [Arch Linux ARM rootfs](http://os.archlinuxarm.org/) plus `libc++` + `libc++abi` + `gcc` installed into it via `pacman --root <rootfs> --config <alarm-pacman.conf> --dbpath <rootfs>/var/lib/pacman -Sy …` (point pacman at ALARM's mirrors via explicit config to avoid pulling host-arch packages). For cross-arch builds, set `cfg.sysroot` on the `Configuration` to point at a sysroot for the target. `BuildStdPcm` and the compile/link commands both consume it: `--sysroot=<path>` is passed to clang, and the std module is precompiled against the sysroot's libc++ headers. The module source has to come from the same libc++ release as those headers, so it is taken from `<sysroot>/usr/share/libc++/v1/std.cppm` when the sysroot ships it, else from the toolchain when both are the same major release, else from the sources crafter-build carries for other releases (`share/crafter-build/libc++-modules/<major>/`, `crafter-build --version` lists them). Alpine is the common case for the last two: its `libc++-dev` has the headers and no module source. When nothing matches, the build stops with one line naming both releases, installing that release's `share/libc++/v1` directory into the sysroot resolves it. The cheapest aarch64 sysroot is an extracted [Arch Linux ARM rootfs](http://os.archlinuxarm.org/) plus `libc++` + `libc++abi` + `gcc` installed into it via `pacman --root <rootfs> --config <alarm-pacman.conf> --dbpath <rootfs>/var/lib/pacman -Sy …` (point pacman at ALARM's mirrors via explicit config to avoid pulling host-arch packages).
For `--target=x86_64-w64-mingw32` (Windows from Linux), the pipeline cross-compiles as long as `mingw-w64` is installed. Pair it with `--runner=cmd:wine` to run the result. Only the MinGW ABI is supported; there is no MSVC target and no Windows host. `-fuse-ld=lld` is auto-injected on every link, and `-lstdc++exp` is auto-linked for the mingw target so user projects don't need to declare those flags. For `--target=x86_64-w64-mingw32` (Windows from Linux), the pipeline cross-compiles as long as `mingw-w64` is installed. Pair it with `--runner=cmd:wine` to run the result. Only the MinGW ABI is supported, there is no MSVC target and no Windows host. `-fuse-ld=lld` is auto-injected on every link, and `-lstdc++exp` is auto-linked for the mingw target so user projects don't need to declare those flags.
### Configuring runners ### Configuring runners
@ -176,7 +176,7 @@ local
cmd:<command> # cmd:wine, cmd:qemu-aarch64, cmd:valgrind, ... cmd:<command> # cmd:wine, cmd:qemu-aarch64, cmd:valgrind, ...
``` ```
Each runner carries a `probe` command (e.g. `which wine`); if it fails, every test using that runner is reported as ⏭ skipped and the rest of the suite continues. Skipped tests count toward the summary but don't fail the suite (`AllPassed()` returns true on skip-only runs). Each runner carries a `probe` command (e.g. `which wine`), if it fails, every test using that runner is reported as ⏭ skipped and the rest of the suite continues. Skipped tests count toward the summary but don't fail the suite (`AllPassed()` returns true on skip-only runs).
### Linking the parent project's library ### Linking the parent project's library
@ -203,7 +203,7 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view>)
## Linting & formatting ## Linting & formatting
Lint rules are C++ in `project.cpp` with no `.lintrc`, no YAML, and no built-in rules. A rule is a named callback that runs once per source file; register it on the `Configuration` before returning: Lint rules are C++ in `project.cpp` with no `.lintrc`, no YAML, and no built-in rules. A rule is a named callback that runs once per source file, register it on the `Configuration` before returning:
```cpp ```cpp
cfg.AddLintRule("no-tabs", [](Crafter::LintContext& ctx) { cfg.AddLintRule("no-tabs", [](Crafter::LintContext& ctx) {
@ -244,7 +244,7 @@ Lint findings print compiler-style — `file:line: warning: message [rule]` —
| `Extension()` | file extension, e.g. `".cppm"` | | `Extension()` | file extension, e.g. `".cppm"` |
| `CommentStripped()` | `content` with comments and string/char literal bodies blanked to spaces — newlines kept, so line numbers stay valid (raw strings not recognized yet) | | `CommentStripped()` | `content` with comments and string/char literal bodies blanked to spaces — newlines kept, so line numbers stay valid (raw strings not recognized yet) |
| `Report(line, msg)` | record a finding (`line == 0` for whole-file) | | `Report(line, msg)` | record a finding (`line == 0` for whole-file) |
| `SetContent(str)` | replace the file's content — makes the rule a transform; `lines` re-splits, `CommentStripped()` re-derives. Build the new string, call once at the end; don't assign `content` directly | | `SetContent(str)` | replace the file's content — makes the rule a transform, `lines` re-splits, `CommentStripped()` re-derives. Build the new string, call once at the end, don't assign `content` directly |
Suppression is comment-based : Suppression is comment-based :
@ -275,7 +275,7 @@ This project is licensed under the GNU Lesser General Public License, version 3
The example code under [examples/](examples/) is licensed under the MIT license (see [examples/LICENSE](examples/LICENSE)), so it can be freely copied into your own projects. The example code under [examples/](examples/) is licensed under the MIT license (see [examples/LICENSE](examples/LICENSE)), so it can be freely copied into your own projects.
The [lib/](lib/) directory contains vendored third-party code (nlohmann/json, glslang's DirStackFileIncluder) that remains under its own permissive licenses; see the headers of those files. The [lib/](lib/) directory contains vendored third-party code (nlohmann/json, glslang's DirStackFileIncluder) that remains under its own permissive licenses, see the headers of those files.
## Copyright ## Copyright