From 561398013c090011a0be443f865cce456930e6dc Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Tue, 6 Oct 2026 18:16:00 +0200 Subject: [PATCH] feat(script): scripts in project.cpp, run with `crafter-build run` A project.cpp could describe how a project is built, tested and linted, but not the chores around it: packaging, installing, publishing. Those stayed shell lines in a README or in someone's history. Configuration gains `scripts`, a plain list like `defines`. A Script is a name and a C++ callback, as a lint rule is; there is no script language and no config file. The callback gets a ScriptContext (the project, the project file's directory, the arguments) and returns the exit status. ctx.Run runs a shell command with the terminal attached and returns its status. It is only stored when project.cpp loads, which happens for every verb and for every project that depends on this one. `crafter-build run [args...]` runs one, a bare `run` lists them. Only the scripts of the project crafter-build was pointed at are runnable, never a dependency's. Everything after the script's name is handed to the script untouched. The two branches that do this sit ahead of every other branch of the parse loop, --version and --help included. Below the verbs, `run package clean` would delete build/, and any other word would land in the project's args and so in the variant hash, moving the build to another directory. Options before the name keep their meaning, so project args go there. The driver is its own partition, :Script. It refuses a project with no scripts, a name that is empty or starts with '-' (it could never be typed), a name declared twice (running either would be a guess) and an unknown name, each with exit 1 and without running anything. A script that throws exits 1. A return value outside 0..255 becomes 1: an exit status is eight bits, and a raw wait status such as 256 would otherwise wrap to success. Two helpers in :Platform: - RunCommandAttached runs a command with inherited stdio. Every helper so far captured through popen, which is right for a compile step and useless for a command that runs for half an hour or asks a question. It reads the wait status through DecodeWaitStatus, which RunCommandChecked and RunCommandWithTimeout now share. - ShellQuote is exported. It existed twice, as ShellQuote in :External and ShellQuoteSh in :Test, with the same behaviour; both are gone. A script that puts a path into a command needs it too. Not included: a string shorthand for one-command scripts (a one-line lambda covers it) and a ctx.Build() helper (Build() is already exported). A new partition and new exported symbols: run ./build.sh. Co-Authored-By: Claude Fable 5.1 --- AGENTS.md | 7 +- build.sh | 2 +- implementations/Crafter.Build-Clang.cpp | 47 +++++++++- implementations/Crafter.Build-External.cpp | 10 --- implementations/Crafter.Build-Platform.cpp | 83 +++++++++++------ implementations/Crafter.Build-Script.cpp | 100 +++++++++++++++++++++ implementations/Crafter.Build-Test.cpp | 16 +--- interfaces/Crafter.Build-Clang.cppm | 45 ++++++++++ interfaces/Crafter.Build-Platform.cppm | 12 +++ interfaces/Crafter.Build-Script.cppm | 40 +++++++++ interfaces/Crafter.Build.cppm | 1 + project.cpp | 6 +- 12 files changed, 313 insertions(+), 56 deletions(-) create mode 100644 implementations/Crafter.Build-Script.cpp create mode 100644 interfaces/Crafter.Build-Script.cppm diff --git a/AGENTS.md b/AGENTS.md index 574abeb..7451d75 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,12 +22,13 @@ crafter-build is a C++26 modules build system. A project is described by a `proj Partitions and what they own: - `:Toolchain` resolves the compiler once per process (`GetToolchain`): clang++, clang, llvm-ar, libclang, libc++'s std module source and the default sysroots, from `CRAFTER_BUILD_LLVM_DIR`, else a toolchain installed beside crafter-build, else PATH. It also holds the version baked in at build time. `SysrootStdModule` picks the std module source for a sysroot build by the sysroot's libc++ release. -- `:Clang` holds `Configuration`, `Build()`, `VariantId()`, `ApplyStandardArgs`, `HostTarget`, the CLI entry point `Crafter::Run`, and the lint and test data types. +- `:Clang` holds `Configuration`, `Build()`, `VariantId()`, `ApplyStandardArgs`, `HostTarget`, the CLI entry point `Crafter::Run`, and the lint, test and script data types. - `:Platform` runs commands, implements `LoadProject` (compile and load `project.cpp`), the host PCM cache, `#pragma crafter dependency`, and all OS-specific code. - `:Interface` and `:Implementation` model module units, their staleness checks and their compiles. - `:External` handles git + cmake dependencies, `GitProject` and `LocalProject`. - `:Test` holds `AddTest`, `RunTests` and `TestRunner`. - `:Lint` is the lint/format driver plus the libclang token and AST layer. +- `:Script` is the driver behind `crafter-build run`: it looks a name up in `cfg.scripts`, calls the callback and turns what it returns into the exit code. - `:Progress` is console output. `:Shader` compiles shaders through glslang. `:Asset` is the hook into Crafter.Asset. ## Dev environment @@ -60,6 +61,8 @@ Three things to know about this loop: ./bin/crafter-build lint # dry style gate, exit 1 on any finding ./bin/crafter-build format # apply the transform rules in place ./bin/crafter-build format --check # CI-style dry run +./bin/crafter-build run # list the scripts project.cpp defines +./bin/crafter-build run [args...] # run one; everything after the name is the script's ./bin/crafter-build clean ``` @@ -142,6 +145,7 @@ These were decided deliberately. Do not undo them without the maintainer's say-s - `project.cpp` is compiled and loaded into the crafter-build process, and every `GitProject`, `#pragma crafter dependency` and `ExternalDependency` clones a repository and runs its build on the host. Treat a dependency URL as code you are choosing to run. This repo's own dependencies are sibling Catcrafts repos tracking their default branch on purpose; pin with `commit` when adding anything else, and whenever reproducibility matters. - Unpinned dependencies `git pull` on every invocation, so builds and tests need network. A missing or private repo surfaces as a clone failure before anything is built. - `--runner=cmd:` executes the named wrapper around every test binary. Wrappers come from `--runner=` or `CRAFTER_BUILD_RUNNER_*`; do not hardcode hosts, wrappers or credentials in `project.cpp` or tests. +- `crafter-build run ` executes whatever the script does, with your permissions. Only the scripts of the project crafter-build was pointed at can be run, never a dependency's; keep it that way. - The linter loads `libclang` at runtime by name. `CRAFTER_BUILD_LIBCLANG` pins an exact path and must fail loudly when wrong, never fall back to another library. - CI force-pushes the rolling `latest` tag and publishes a prerelease on every push to `master` with the workflow's own token, and signs the Flatpak repo with the key mounted from the runner host. Do not add secrets to the workflow or echo the environment in it. - Tests write only under `fs::temp_directory_path()` and the fixture's own `bin/` and `build/`. Keep it that way. @@ -164,6 +168,7 @@ To release: add a `` to `packaging/flatpak/net.catcrafts.CrafterBuild.m ## Gotchas +- A new partition is listed in four places, in import order: `PARTITIONS` in `build.sh`, `CrafterBuildModules` in `Crafter.Build-Platform.cpp`, both arrays in `project.cpp`, and the umbrella `Crafter.Build.cppm`. Then `./build.sh`. - A `#pragma crafter dependency` target is rebuilt on every load on purpose: clang crashes rather than erroring when it imports a BMI whose own imported PCM was rebuilt underneath it, and the depfile check is what catches a regenerated host cache. Do not optimise the rebuild away. - A host-tooling library such as Crafter.LintRules may `import Crafter.Build;` without declaring it as a dependency, because `LoadProject` fills the cache it resolves from. Listing Crafter.Build in its `cfg.dependencies` would clone and build all of crafter-build for every consumer. Its test does link crafter-build, so that test is wired behind `IsTopLevelProject()`: without the guard, crafter-build loading the library as a dependency would load its own `project.cpp` again through the test's `GitProject` and hit the cycle guard. - `crafter-build clean` leaves a pragma dependency's build alone; it lives with the clone in the cache. diff --git a/build.sh b/build.sh index 2d7df47..312abde 100755 --- a/build.sh +++ b/build.sh @@ -48,7 +48,7 @@ cp wasi-runtime/index.html.in share/crafter-build/wasi-runtime/ # Partition order is the import order: every unit is listed before the # units that import it, and the umbrella last. -PARTITIONS="Toolchain Shader Platform Interface Implementation External Clang Test Lint Progress Asset" +PARTITIONS="Toolchain Shader Platform Interface Implementation External Clang Test Lint Script Progress Asset" cp interfaces/Crafter.Build.cppm share/crafter-build/ for p in $PARTITIONS; do diff --git a/implementations/Crafter.Build-Clang.cpp b/implementations/Crafter.Build-Clang.cpp index 3c48078..a7d12b8 100644 --- a/implementations/Crafter.Build-Clang.cpp +++ b/implementations/Crafter.Build-Clang.cpp @@ -12,6 +12,7 @@ import :Platform; import :Toolchain; import :Test; import :Lint; +import :Script; import :Progress; import :Asset; namespace fs = std::filesystem; @@ -1594,6 +1595,7 @@ R"(Usage: {0} test [test-options] [globs...] Build and run the project's tests {0} lint [lint-options] [globs...] Run the project's lint rules over its sources {0} format [format-options] [globs...] Apply the project's transform rules (rewrites files) + {0} run [script [args...]] Run a script the project defines, or list them {0} clean Delete the project's bin/ and build/ trees {0} help | -h | --help Show this help {0} --version Print the version and the compiler in use @@ -1646,6 +1648,23 @@ Format options (after the `format` subcommand): transform. `format` rewrites changed files in place; `lint` reports the same transforms as would-reformat findings without writing. +Run (after the `run` subcommand): +