Compare commits
34 changed files with 168 additions and 1876 deletions
23
AGENTS.md
23
AGENTS.md
|
|
@ -10,7 +10,7 @@ crafter-build is a C++26 modules build system. A project is described by a `proj
|
|||
|---|---|
|
||||
| `interfaces/Crafter.Build-<Partition>.cppm` | Module interface partitions. `Crafter.Build.cppm` is the umbrella that re-exports them. |
|
||||
| `implementations/Crafter.Build-<Partition>.cpp` | Implementation units, one per partition, named `Crafter.Build:<Partition>_impl`. `main.cpp` is a thin wrapper around `Crafter::Run`. |
|
||||
| `project.cpp` | The self-host build description: a `crafter.build-lib` library, a `crafter.build-exe` executable, the test declarations, the lint rules and the `flatpak` and `install` scripts. |
|
||||
| `project.cpp` | The self-host build description: a `crafter.build-lib` library, a `crafter.build-exe` executable, the test declarations and the lint rules. |
|
||||
| `tests/<Name>/main.cpp` | One self-test per directory; `tests/<Name>/fixture/` holds the sample project it builds. |
|
||||
| `examples/` | Standalone user-facing projects, MIT licensed. |
|
||||
| `build.sh` | Bootstrap script. It compiles everything by hand, without crafter-build. |
|
||||
|
|
@ -22,13 +22,12 @@ 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, test and script data types.
|
||||
- `:Clang` holds `Configuration`, `Build()`, `VariantId()`, `ApplyStandardArgs`, `HostTarget`, the CLI entry point `Crafter::Run`, and the lint and test 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
|
||||
|
|
@ -48,7 +47,7 @@ Three things to know about this loop:
|
|||
- Changes under `implementations/` are picked up by the self-host build and by the tests, which link the local library. Changes under `interfaces/` are not: the harness compiles against the copies in `share/crafter-build/` and the PCM cache built from them, so a new or changed declaration is invisible until you run `./build.sh` again. The symptom is `use of undeclared identifier` for the symbol you just added.
|
||||
- A new exported symbol that a `project.cpp` reaches on its default path also needs `./build.sh`: the old binary does not export it, and `LoadProject` fails with an undefined symbol.
|
||||
|
||||
`./build.sh` clones and builds glslang on first run and takes several minutes; later runs are faster. `./bin/crafter-build clean` deletes `bin/` and `build/` without loading `project.cpp`. That takes `bin/crafter-build` itself with it, and `build/flatpak`, the state of the local Flatpak build (see "CI and publishing"): about 2 GB, and the next `run flatpak` rebuilds LLVM. Set `CRAFTER_BUILD_HOME` only when running a binary from outside this tree; `CRAFTER_BUILD_MARCH` and `CRAFTER_BUILD_MTUNE` default to `native`.
|
||||
`./build.sh` clones and builds glslang on first run and takes several minutes; later runs are faster. `./bin/crafter-build clean` deletes `bin/` and `build/` without loading `project.cpp`. Set `CRAFTER_BUILD_HOME` only when running a binary from outside this tree; `CRAFTER_BUILD_MARCH` and `CRAFTER_BUILD_MTUNE` default to `native`.
|
||||
|
||||
## Build, test and style commands
|
||||
|
||||
|
|
@ -61,8 +60,6 @@ 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 <name> [args...] # run one; everything after the name is the script's
|
||||
./bin/crafter-build clean
|
||||
```
|
||||
|
||||
|
|
@ -145,7 +142,6 @@ 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:<command>` 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 <name>` 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,21 +160,10 @@ gpg --batch --pinentry-mode loopback --passphrase '' --export-secret-keys --armo
|
|||
printf '%s\n' "$KEYID" > /etc/crafter-flatpak/key-id && chmod 600 /etc/crafter-flatpak/signing-key.asc /etc/crafter-flatpak/key-id
|
||||
```
|
||||
|
||||
To release: add a `<release>` to `packaging/flatpak/net.catcrafts.CrafterBuild.metainfo.xml` (its description becomes the release notes), merge, and push the tag `X.Y.Z`. CI refuses a tag that does not match the newest entry.
|
||||
Local builds are two scripts in `project.cpp`. Run them with the native binary: the Flatpak build of crafter-build has no `flatpak` to drive.
|
||||
|
||||
```bash
|
||||
./bin/crafter-build run flatpak [--jobs=<N>] # build the Flatpak, then print its version from inside the sandbox
|
||||
./bin/crafter-build run install # install that build as branch `local` and make it current
|
||||
```
|
||||
|
||||
`run flatpak` needs `flatpak-builder` or the `org.flatpak.Builder` app, and the Sdk (`flatpak install flathub org.freedesktop.Sdk//26.08`). It builds the working tree, uncommitted changes included, runs the whole suite inside the sandbox and reports version `0.0.0-dev`. Its state is `build/flatpak/` (`state/`, `repo/`, `build-dir/`). The first run builds LLVM and the WASI sysroot, about 25 minutes on a 56-core machine; later runs rebuild only the crafter-build module, a few minutes, because flatpak-builder never caches a `type: dir` source. The checkout must not be under `/tmp`, which `org.flatpak.Builder` cannot see. Use it for anything toolchain-sensitive: the native loop compiles with the distro's clang and the Flatpak with its bundled LLVM, and the two can be a major version apart.
|
||||
|
||||
`run install` does not build; it installs what `run flatpak` last exported. It points the user remote `crafter-build-local` at `build/flatpak/repo`, installs `net.catcrafts.CrafterBuild//local` from it next to whichever published branch is installed, and makes it the branch `flatpak run net.catcrafts.CrafterBuild` starts. `flatpak make-current --user net.catcrafts.CrafterBuild <branch>` switches back. Before `crafter-build clean`, or before deleting the checkout, run `flatpak uninstall --user net.catcrafts.CrafterBuild//local` and `flatpak remote-delete --user crafter-build-local`; otherwise the remote is left pointing at a repo that is gone.
|
||||
To release: add a `<release>` to `packaging/flatpak/net.catcrafts.CrafterBuild.metainfo.xml` (its description becomes the release notes), merge, and push the tag `X.Y.Z`. CI refuses a tag that does not match the newest entry. Local builds: `flatpak-builder --user --force-clean --ccache --repo=repo build-dir packaging/flatpak/net.catcrafts.CrafterBuild.yaml`, then `flatpak-builder --run build-dir <manifest> crafter-build --version`.
|
||||
|
||||
## 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.
|
||||
|
|
|
|||
48
README.md
48
README.md
|
|
@ -60,7 +60,7 @@ glslang.includeDirs = { "" };
|
|||
glslang.libs = { "SPIRV", "glslang", "OSDependent" };
|
||||
```
|
||||
|
||||
Clones live at `~/.cache/crafter.build/external/<name>-<hash>/`, keyed by `(url, branch, commit, options)` so different projects sharing the same dep with the same spec reuse the clone and cmake build, while projects with diverging specs each get their own cache entry. The clone is resilient to partial-clone recovery and CMake reconfigure is triggered automatically on `options` changes. Submodules are checked out at the revisions the fetched commit records, after every clone, switch, checkout and pull, so a pinned commit builds against its own submodules and not the default branch's.
|
||||
Clones live at `~/.cache/crafter.build/external/<name>-<hash>/`, keyed by `(url, branch, commit, options)` so different projects sharing the same dep with the same spec reuse the clone and cmake build, while projects with diverging specs each get their own cache entry. The clone is resilient to partial-clone recovery and CMake reconfigure is triggered automatically on `options` changes.
|
||||
|
||||
**Header-only git deps** — same as cmake deps but `builder = ExternalBuilder::None`. Just clones and propagates `-I` flags.
|
||||
|
||||
|
|
@ -147,7 +147,7 @@ For elaborate setups (rsync, ssh ControlMaster, env vars on the spawn, `QEMU_LD_
|
|||
|
||||
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. Ordinary compiles are pointed at the same headers (`-nostdinc++ -isystem <sysroot>/usr/include/c++/v1`), so a unit that `#include`s a standard header gets the sysroot's libc++ and not the host toolchain's. 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, with libstdc++ 15 or newer for its std module. clang finds it by the `x86_64-w64-mingw32-gcc` on `PATH`; `CRAFTER_MINGW_DIR` names another prefix (the directory holding `x86_64-w64-mingw32/` and `lib/gcc/`), and inside the Flatpak the host's `/usr` is used when it has mingw-w64. Without one the build stops with a message saying where it looked. 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
|
||||
|
||||
|
|
@ -256,50 +256,6 @@ Suppression is comment-based :
|
|||
// lint-disable-file — all rules, whole file
|
||||
```
|
||||
|
||||
## Scripts
|
||||
|
||||
A project has chores that are not part of building it: packaging, installing, publishing, generating documentation. A script gives one a name. Scripts are C++ in `project.cpp` like everything else, a name and a callback pushed onto `cfg.scripts` before returning the `Configuration`:
|
||||
|
||||
```cpp
|
||||
cfg.scripts.push_back({"docs", [](Crafter::ScriptContext& ctx) {
|
||||
return ctx.Run("python3 tools/gen-docs.py");
|
||||
}});
|
||||
```
|
||||
|
||||
```bash
|
||||
crafter-build run # list the project's scripts
|
||||
crafter-build run docs # run one
|
||||
crafter-build run docs --open api # everything after the name is handed to the script
|
||||
```
|
||||
|
||||
The callback returns the exit status, and `crafter-build run` exits with it. `ctx.Run(command)` runs a shell command with the terminal attached, so its output appears as it is produced and it can ask questions, and returns the command's exit code (128 plus the signal number when it was killed). A script with several steps stops at the first failure by returning that code:
|
||||
|
||||
```cpp
|
||||
cfg.scripts.push_back({"package", [](Crafter::ScriptContext& ctx) {
|
||||
const std::string dist = Crafter::ShellQuote((ctx.projectDir / "dist").string());
|
||||
if (const std::int32_t code = ctx.Run(std::format("mkdir -p {}", dist)); code != 0) return code;
|
||||
return ctx.Run(std::format("tar -czf {}/myapp.tar.gz -C {} .", dist, Crafter::ShellQuote(ctx.project.BinDir().string())));
|
||||
}});
|
||||
```
|
||||
|
||||
`ScriptContext` gives each script:
|
||||
|
||||
| Member | Meaning |
|
||||
|---|---|
|
||||
| `project` | the `Configuration` that `project.cpp` returned. Read the project through this; a `cfg` captured by reference is gone by the time a script runs |
|
||||
| `projectDir` | absolute directory of the project file |
|
||||
| `args` | everything typed after the script's name, untouched |
|
||||
| `Run(command)` | run a shell command with the terminal attached and return its exit code |
|
||||
|
||||
Things to know:
|
||||
|
||||
- Everything after the script's name belongs to the script, including what looks like an option of crafter-build (`--help`, `-v`). Options and project args for crafter-build go before the name: `crafter-build --debug run package`.
|
||||
- `run` loads `project.cpp` but does not build the project. A script that needs the build output runs after `crafter-build`.
|
||||
- Commands run in the directory crafter-build was started from. Build paths from `ctx.projectDir`, and quote anything that is not a literal with `Crafter::ShellQuote`.
|
||||
- Only the scripts of the project you run crafter-build in are available. A dependency's scripts are not.
|
||||
- Inside the Flatpak, commands run in its sandbox: the host's files are visible, but the tools on `PATH` are the ones the Flatpak ships (`git`, `cmake`, `python3`, `tar`, ...), not everything installed on the host.
|
||||
- A return value outside 0 to 255 is not an exit status and becomes 1. So does a script that throws, with the exception's message printed.
|
||||
|
||||
## Examples
|
||||
|
||||
[examples/](examples/) contains progressively larger self-contained projects:
|
||||
|
|
|
|||
2
build.sh
2
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 Script Progress Asset"
|
||||
PARTITIONS="Toolchain Shader Platform Interface Implementation External Clang Test Lint Progress Asset"
|
||||
|
||||
cp interfaces/Crafter.Build.cppm share/crafter-build/
|
||||
for p in $PARTITIONS; do
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import :Platform;
|
|||
import :Toolchain;
|
||||
import :Test;
|
||||
import :Lint;
|
||||
import :Script;
|
||||
import :Progress;
|
||||
import :Asset;
|
||||
namespace fs = std::filesystem;
|
||||
|
|
@ -535,14 +534,14 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
|||
}
|
||||
for (const Shader& shader : config.shaders) {
|
||||
if (buildCancelled.load(std::memory_order_relaxed)) break;
|
||||
if (shader.Check(outputDir, buildDir, shaderIncludeDirs)) continue;
|
||||
threads.emplace_back([&shader, &outputDir, &buildDir, &shaderIncludeDirs, &buildError, &buildCancelled]() {
|
||||
if (shader.Check(outputDir)) continue;
|
||||
threads.emplace_back([&shader, &outputDir, &shaderIncludeDirs, &buildError, &buildCancelled]() {
|
||||
// A source registered more than once shows up once per variant;
|
||||
// name the variant so the lines can be told apart.
|
||||
Progress::Task task(shader.variant.empty() ? std::format("Compiling shader {}", shader.path.filename().string()) : std::format("Compiling shader {} ({})", shader.path.filename().string(), shader.variant));
|
||||
if (buildCancelled.load(std::memory_order_relaxed)) return;
|
||||
|
||||
std::string result = shader.Compile(outputDir, buildDir, shaderIncludeDirs);
|
||||
std::string result = shader.Compile(outputDir, shaderIncludeDirs);
|
||||
if (result.empty()) return;
|
||||
|
||||
bool expected = false;
|
||||
|
|
@ -699,13 +698,7 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
|||
std::string stdPcmResult;
|
||||
{
|
||||
Progress::Task task(std::format("Building std PCM ({}-{})", config.target, config.march));
|
||||
// The shader and external threads are running: an exception leaving
|
||||
// here would destroy them joinable, which terminates the process.
|
||||
try {
|
||||
stdPcmResult = BuildStdPcm(config, stdPcmDir/"std.pcm");
|
||||
} catch (const std::exception& e) {
|
||||
stdPcmResult = std::format("building the std module for {}: {}", config.target, e.what());
|
||||
}
|
||||
stdPcmResult = BuildStdPcm(config, stdPcmDir/"std.pcm");
|
||||
}
|
||||
if(!stdPcmResult.empty()) {
|
||||
buildCancelled.store(true);
|
||||
|
|
@ -734,8 +727,8 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
|||
const bool isWasm = config.target.starts_with("wasm32");
|
||||
|
||||
std::string files;
|
||||
std::set<std::string> libSet;
|
||||
std::set<std::string> publicFlagSet;
|
||||
std::unordered_set<std::string> libSet;
|
||||
std::unordered_set<std::string> publicFlagSet;
|
||||
std::mutex fileMutex;
|
||||
std::vector<std::thread> depThreads;
|
||||
depThreads.reserve(config.dependencies.size());
|
||||
|
|
@ -810,9 +803,6 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
|||
: std::format(" -march={} -mtune={}", config.march, config.mtune);
|
||||
if (!config.sysroot.empty()) {
|
||||
cArchFlags += std::format(" --sysroot={}", config.sysroot);
|
||||
} else if (config.target == "x86_64-w64-mingw32" && !GetToolchain().mingwSysroot.empty()) {
|
||||
// The C++ side gets it through GetBaseCommand.
|
||||
cArchFlags += std::format(" --sysroot={}", GetToolchain().mingwSysroot.string());
|
||||
}
|
||||
if (cIsWasm) {
|
||||
// Matches the C++ path's wasi flag set so any libc shim defines
|
||||
|
|
@ -1158,48 +1148,8 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
|||
}
|
||||
}
|
||||
|
||||
// The exact command the link (or archive) step runs, kept beside the
|
||||
// objects once it has succeeded. A different one relinks even with no
|
||||
// object newer than the output: a link flag changed in project.cpp, or a
|
||||
// dependency's library moved, as an ExternalDependency's does when its
|
||||
// options change and it is built into a new cache directory.
|
||||
std::string linkCommand;
|
||||
if (config.type == ConfigurationType::Executable) {
|
||||
if (config.target.starts_with("wasm32")) {
|
||||
linkCommand = std::format("{}{} -o {}.wasm -fuse-ld=lld{}", command, files, (outputDir/config.outputName).string(), linkExtras);
|
||||
} else {
|
||||
linkCommand = std::format("{}{} -o {} -fuse-ld=lld{}", command, files, (outputDir/config.outputName).string(), linkExtras);
|
||||
}
|
||||
} else if (config.type == ConfigurationType::LibraryStatic) {
|
||||
// ThinLTO emits LLVM bitcode objects; plain `ar` writes an archive
|
||||
// index that omits their symbols, so a consumer's lld link can't
|
||||
// pull the needed members. llvm-ar writes a bitcode-aware index
|
||||
// and handles plain ELF and COFF objects just as well, so every
|
||||
// build uses it.
|
||||
linkCommand = std::format("{} rcs {}.a {}", GetToolchain().llvmAr.string(), (outputDir/fs::path(std::string("lib")+config.outputName)).string(), files);
|
||||
} else if (config.target == "x86_64-w64-mingw32") {
|
||||
// LibraryDynamic. Output names follow each target's convention so
|
||||
// consumers can link via the standard linker search:
|
||||
// mingw: <name>.dll + lib<name>.dll.a (lld --out-implib)
|
||||
// unix: lib<name>.so (rpath $ORIGIN)
|
||||
fs::path dll = outputDir / std::format("{}.dll", config.outputName);
|
||||
fs::path implib = outputDir / std::format("lib{}.dll.a", config.outputName);
|
||||
linkCommand = std::format("{}{} -shared -o {} -Wl,--out-implib,{} -fuse-ld=lld{}", command, files, dll.string(), implib.string(), linkExtras);
|
||||
} else {
|
||||
linkCommand = std::format("{}{} -shared -o {}.so -Wl,-rpath,'$ORIGIN' -fuse-ld=lld{}", command, files, (outputDir/(std::string("lib")+config.outputName)).string(), linkExtras);
|
||||
}
|
||||
const fs::path linkRecord = buildDir / std::format("{}.link", config.outputName);
|
||||
if (!buildResult.repack) {
|
||||
std::ifstream recorded(linkRecord, std::ios::binary);
|
||||
const std::string previous((std::istreambuf_iterator<char>(recorded)), std::istreambuf_iterator<char>());
|
||||
if (previous != linkCommand) buildResult.repack = true;
|
||||
}
|
||||
|
||||
if(buildResult.repack) {
|
||||
Progress::Task task(std::format("Linking {}", config.outputName));
|
||||
// The record goes first, so a link that fails leaves it stale.
|
||||
std::error_code removed;
|
||||
fs::remove(linkRecord, removed);
|
||||
if(config.type == ConfigurationType::Executable) {
|
||||
if(config.target == "x86_64-w64-mingw32") {
|
||||
try {
|
||||
|
|
@ -1230,10 +1180,30 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
|||
return {e.what(), false, {}};
|
||||
}
|
||||
}
|
||||
}
|
||||
buildResult.result = RunBuildCommand(linkCommand);
|
||||
if (buildResult.result.empty()) {
|
||||
std::ofstream(linkRecord, std::ios::binary | std::ios::trunc) << linkCommand;
|
||||
if (config.target.starts_with("wasm32")) {
|
||||
buildResult.result = RunBuildCommand(std::format("{}{} -o {}.wasm -fuse-ld=lld{}", command, files, (outputDir/config.outputName).string(), linkExtras));
|
||||
} else {
|
||||
buildResult.result = RunBuildCommand(std::format("{}{} -o {} -fuse-ld=lld{}", command, files, (outputDir/config.outputName).string(), linkExtras));
|
||||
}
|
||||
} else if(config.type == ConfigurationType::LibraryStatic) {
|
||||
// ThinLTO emits LLVM bitcode objects; plain `ar` writes an archive
|
||||
// index that omits their symbols, so a consumer's lld link can't
|
||||
// pull the needed members. llvm-ar writes a bitcode-aware index
|
||||
// and handles plain ELF and COFF objects just as well, so every
|
||||
// build uses it.
|
||||
buildResult.result = RunBuildCommand(std::format("{} rcs {}.a {}", GetToolchain().llvmAr.string(), (outputDir/fs::path(std::string("lib")+config.outputName)).string(), files));
|
||||
} else {
|
||||
// LibraryDynamic. Output names follow each target's convention so
|
||||
// consumers can link via the standard linker search:
|
||||
// mingw: <name>.dll + lib<name>.dll.a (lld --out-implib)
|
||||
// unix: lib<name>.so (rpath $ORIGIN)
|
||||
if (config.target == "x86_64-w64-mingw32") {
|
||||
fs::path dll = outputDir / std::format("{}.dll", config.outputName);
|
||||
fs::path implib = outputDir / std::format("lib{}.dll.a", config.outputName);
|
||||
buildResult.result = RunBuildCommand(std::format("{}{} -shared -o {} -Wl,--out-implib,{} -fuse-ld=lld{}", command, files, dll.string(), implib.string(), linkExtras));
|
||||
} else {
|
||||
buildResult.result = RunBuildCommand(std::format("{}{} -shared -o {}.so -Wl,-rpath,'$ORIGIN' -fuse-ld=lld{}", command, files, (outputDir/(std::string("lib")+config.outputName)).string(), linkExtras));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1624,7 +1594,6 @@ 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
|
||||
|
|
@ -1677,23 +1646,6 @@ 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):
|
||||
<script> [args...] Run the named script. Everything after the name is
|
||||
handed to the script untouched, including what
|
||||
looks like an option of crafter-build itself, so
|
||||
-v, -q, --project= and project args go before it.
|
||||
--list List the project's scripts. A bare `run` does too.
|
||||
|
||||
Scripts are defined in project.cpp, as entries of cfg.scripts:
|
||||
cfg.scripts.push_back({{"docs", [](Crafter::ScriptContext& ctx) {{
|
||||
return ctx.Run("python3 tools/gen-docs.py");
|
||||
}}}});
|
||||
The callback returns the exit status. ctx.Run runs a shell command with the
|
||||
terminal attached and returns its status; ctx.args holds the arguments and
|
||||
ctx.projectDir the project's directory. Commands run in the directory
|
||||
crafter-build was started from. Only the scripts of the project being run
|
||||
are available, never a dependency's.
|
||||
|
||||
Clean (after the `clean` subcommand):
|
||||
Removes bin/ and build/ next to the project file. Does not load project.cpp,
|
||||
so it works when the project itself no longer compiles. Every target, variant
|
||||
|
|
@ -1718,18 +1670,13 @@ Environment:
|
|||
instead of the compiler's own.
|
||||
CRAFTER_BUILD_LIBCLANG Exact libclang to load for lint.
|
||||
CRAFTER_BUILD_WASI_SYSROOT WASI sysroot for wasm32 targets.
|
||||
CRAFTER_MINGW_DIR mingw-w64 prefix (holding x86_64-w64-mingw32/
|
||||
and lib/gcc/), passed as --sysroot. Default:
|
||||
the mingw gcc on PATH; in the Flatpak, the
|
||||
host's /usr when it has mingw-w64.
|
||||
CRAFTER_MINGW_DIR mingw-w64 sysroot, passed as --sysroot.
|
||||
CRAFTER_BUILD_HOME Directory holding the module sources.
|
||||
|
||||
Exit status:
|
||||
0 success / all non-skipped tests passed / lint clean / format applied
|
||||
1 build failure, one or more tests failed, lint findings, no rules defined,
|
||||
format write errors, or `format --check` found files that would change
|
||||
`run` exits with the status its script returned, and with 1 when there is no
|
||||
such script, when the script throws, or when it returns a value outside 0..255
|
||||
)", argv0);
|
||||
}
|
||||
|
||||
|
|
@ -1745,29 +1692,14 @@ std::int32_t Crafter::Run(std::int32_t argc, char** argv) {
|
|||
bool runLint = false;
|
||||
bool runFormat = false;
|
||||
bool runClean = false;
|
||||
bool runScript = false;
|
||||
bool runAfterBuild = false;
|
||||
RunTestsOptions testOpts;
|
||||
RunLintOptions lintOpts;
|
||||
RunScriptOptions scriptOpts;
|
||||
Progress::Verbosity verbosity = Progress::Verbosity::Default;
|
||||
|
||||
for (std::int32_t i = 1; i < argc; ++i) {
|
||||
std::string_view arg = argv[i];
|
||||
// The two `run` branches come before everything else on purpose.
|
||||
// What follows a script's name belongs to the script, whatever
|
||||
// it looks like: `run package --help` must not print this
|
||||
// tool's help, `run package clean` must not delete build/, and
|
||||
// none of it may reach projectArgs, where it would change the
|
||||
// variant hash and so the directory the project builds into.
|
||||
if (scriptOpts.named) {
|
||||
scriptOpts.args.emplace_back(arg);
|
||||
} else if (runScript && !arg.starts_with("-")) {
|
||||
// The first word after `run`, even one that is a verb
|
||||
// elsewhere. Options before it keep their usual meaning.
|
||||
scriptOpts.name = arg;
|
||||
scriptOpts.named = true;
|
||||
} else if (arg == "--version") {
|
||||
if (arg == "--version") {
|
||||
// Answered here, before the project file is looked for, so
|
||||
// it works in any directory and is never forwarded to
|
||||
// project.cpp as an unknown argument.
|
||||
|
|
@ -1780,15 +1712,11 @@ std::int32_t Crafter::Run(std::int32_t argc, char** argv) {
|
|||
runTests = true;
|
||||
} else if (!runTests && !runFormat && !runClean && arg == "lint") {
|
||||
runLint = true;
|
||||
} else if (!runTests && !runLint && !runFormat && arg == "clean") {
|
||||
} else if (!runTests && !runLint && !runClean && arg == "clean") {
|
||||
runClean = true;
|
||||
} else if (!runTests && !runLint && !runClean && arg == "format") {
|
||||
runFormat = true;
|
||||
lintOpts.mode = LintMode::Apply;
|
||||
} else if (!runTests && !runLint && !runFormat && !runClean && arg == "run") {
|
||||
runScript = true;
|
||||
} else if (runScript && arg == "--list") {
|
||||
scriptOpts.listOnly = true;
|
||||
} else if (runFormat && arg == "--check") {
|
||||
lintOpts.mode = LintMode::Check;
|
||||
} else if (arg == "-r") {
|
||||
|
|
@ -1850,11 +1778,6 @@ std::int32_t Crafter::Run(std::int32_t argc, char** argv) {
|
|||
Configuration config = LoadProject(projectFile, projectArgs);
|
||||
SetParentProject(&config);
|
||||
|
||||
if (runScript) {
|
||||
scriptOpts.projectFile = fs::absolute(projectFile).lexically_normal();
|
||||
return RunScript(config, scriptOpts);
|
||||
}
|
||||
|
||||
if (runLint || runFormat) {
|
||||
// lexically_normal: fs::absolute keeps a "./" component, which
|
||||
// would leak into displayed paths and the project-root derivation.
|
||||
|
|
|
|||
|
|
@ -21,6 +21,16 @@ std::string DeriveName(const GitSource& source) {
|
|||
return slash == std::string::npos ? url : url.substr(slash + 1);
|
||||
}
|
||||
|
||||
std::string ShellQuote(std::string_view s) {
|
||||
std::string out = "'";
|
||||
for (char c : s) {
|
||||
if (c == '\'') out += "'\\''";
|
||||
else out += c;
|
||||
}
|
||||
out += "'";
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string JoinOptions(std::span<const std::string> options) {
|
||||
std::string out;
|
||||
for (const std::string& opt : options) {
|
||||
|
|
@ -36,18 +46,6 @@ std::string FetchGit(const GitSource& source, const fs::path& cloneDir) {
|
|||
if (r.exitCode != 0) return std::format("`{}` failed (exit {}): {}", cmd, r.exitCode, r.output);
|
||||
return std::nullopt;
|
||||
};
|
||||
// Submodules follow the checkout only when asked to: after every clone,
|
||||
// switch, checkout or pull, so they match the revision that was fetched
|
||||
// instead of whichever one was checked out before, from the URLs that
|
||||
// revision's .gitmodules gives (sync). A no-op, and offline, when they
|
||||
// already match, which puts back submodules moved by hand; and no git at
|
||||
// all without any.
|
||||
auto updateSubmodules = [&runGit](const fs::path& dir) -> std::optional<std::string> {
|
||||
if (!fs::exists(dir / ".gitmodules")) return std::nullopt;
|
||||
const std::string quoted = ShellQuote(dir.string());
|
||||
if (auto err = runGit(std::format("git -C {} submodule sync --recursive", quoted))) return err;
|
||||
return runGit(std::format("git -C {} submodule update --init --recursive", quoted));
|
||||
};
|
||||
|
||||
if (fs::exists(cloneDir)) {
|
||||
CommandResult remote = RunCommandChecked(std::format("git -C {} remote get-url origin", ShellQuote(cloneDir.string())));
|
||||
|
|
@ -63,9 +61,6 @@ std::string FetchGit(const GitSource& source, const fs::path& cloneDir) {
|
|||
if (auto err = runGit(std::format("git -C {} fetch origin", ShellQuote(cloneDir.string())))) return *err;
|
||||
if (auto err = runGit(std::format("git -C {} checkout {}", ShellQuote(cloneDir.string()), ShellQuote(source.commit)))) return *err;
|
||||
}
|
||||
// Also when the commit already matched: a clone made before
|
||||
// submodules were updated has them at the default branch's.
|
||||
if (auto err = updateSubmodules(cloneDir)) return *err;
|
||||
return "";
|
||||
} else {
|
||||
// No commit pinned — pull latest. The user opted into "track latest"
|
||||
|
|
@ -76,7 +71,6 @@ std::string FetchGit(const GitSource& source, const fs::path& cloneDir) {
|
|||
} else {
|
||||
if (auto err = runGit(std::format("git -C {} pull", ShellQuote(cloneDir.string())))) return *err;
|
||||
}
|
||||
if (auto err = updateSubmodules(cloneDir)) return *err;
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
|
@ -89,9 +83,7 @@ std::string FetchGit(const GitSource& source, const fs::path& cloneDir) {
|
|||
fs::path tmpDir = cloneDir;
|
||||
tmpDir += std::format(".tmp.{:08x}{:08x}", rd(), rd());
|
||||
|
||||
// Not --recursive: that checks out the default branch's submodules, and
|
||||
// the requested revision's come once it is checked out.
|
||||
if (auto err = runGit(std::format("git clone {} {}", ShellQuote(source.url), ShellQuote(tmpDir.string())))) {
|
||||
if (auto err = runGit(std::format("git clone --recursive {} {}", ShellQuote(source.url), ShellQuote(tmpDir.string())))) {
|
||||
if (fs::exists(tmpDir)) fs::remove_all(tmpDir);
|
||||
return *err;
|
||||
}
|
||||
|
|
@ -107,10 +99,6 @@ std::string FetchGit(const GitSource& source, const fs::path& cloneDir) {
|
|||
return *err;
|
||||
}
|
||||
}
|
||||
if (auto err = updateSubmodules(tmpDir)) {
|
||||
fs::remove_all(tmpDir);
|
||||
return *err;
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
fs::rename(tmpDir, cloneDir, ec);
|
||||
|
|
@ -146,10 +134,6 @@ std::string BuildInjectedCMakeFlags(const fs::path& cmakeBuildDir, std::string_v
|
|||
out += " -DCMAKE_SYSTEM_NAME=Windows";
|
||||
out += std::format(" -DCMAKE_C_COMPILER_TARGET={}", target);
|
||||
out += std::format(" -DCMAKE_CXX_COMPILER_TARGET={}", target);
|
||||
// The libstdc++ and the mingw-w64 install the project's own compiles
|
||||
// use (GetBaseCommand).
|
||||
out += " -DCMAKE_CXX_FLAGS=-stdlib=libstdc++";
|
||||
if (!tc.mingwSysroot.empty()) out += std::format(" -DCMAKE_SYSROOT={}", ShellQuote(tc.mingwSysroot.string()));
|
||||
}
|
||||
fs::path absBuildDir = fs::absolute(cmakeBuildDir);
|
||||
out += std::format(" -DCMAKE_ARCHIVE_OUTPUT_DIRECTORY={}", ShellQuote(absBuildDir.string()));
|
||||
|
|
@ -253,9 +237,6 @@ ExternalBuildResult Crafter::BuildExternal(const ExternalDependency& dep, std::s
|
|||
}
|
||||
|
||||
fs::path cloneDir = ExternalCloneDir(dep, target);
|
||||
// Builds running side by side (tests built in parallel, two
|
||||
// crafter-builds) share the clone: one fetches and builds it at a time.
|
||||
FileLock lock(fs::path(std::format("{}.lock", cloneDir.string())));
|
||||
|
||||
std::string fetchErr = FetchGit(dep.source, cloneDir);
|
||||
if (!fetchErr.empty()) {
|
||||
|
|
@ -302,20 +283,13 @@ ExternalBuildResult Crafter::BuildExternal(const ExternalDependency& dep, std::s
|
|||
result.linkFlags.push_back(std::format("-l{}", lib));
|
||||
}
|
||||
|
||||
// The newest library in every directory the link searches: the build
|
||||
// directory and the libDirs, where projects like msquic put theirs,
|
||||
// versioned names (libmsquic.so.2) included.
|
||||
auto isLibrary = [](const std::string& name) {
|
||||
return name.ends_with(".a") || name.ends_with(".so") || name.contains(".so.") || name.ends_with(".dll");
|
||||
};
|
||||
std::vector<fs::path> searched = { cmakeBuildDir };
|
||||
for (const fs::path& libDir : dep.libDirs) searched.push_back(libDir.is_absolute() ? libDir : cmakeBuildDir / libDir);
|
||||
for (const fs::path& dir : searched) {
|
||||
std::error_code listEc;
|
||||
for (fs::directory_iterator it(dir, listEc), end; !listEc && it != end; it.increment(listEc)) {
|
||||
if (fs::exists(cmakeBuildDir)) {
|
||||
for (const auto& entry : fs::directory_iterator(cmakeBuildDir)) {
|
||||
if (!entry.is_regular_file()) continue;
|
||||
const fs::path& p = entry.path();
|
||||
if (p.extension() != ".a" && p.extension() != ".so") continue;
|
||||
std::error_code ec;
|
||||
if (!it->is_regular_file(ec) || !isLibrary(it->path().filename().string())) continue;
|
||||
fs::file_time_type t = it->last_write_time(ec);
|
||||
fs::file_time_type t = entry.last_write_time(ec);
|
||||
if (!ec && t > result.latestArtifact) result.latestArtifact = t;
|
||||
}
|
||||
}
|
||||
|
|
@ -391,8 +365,6 @@ Configuration* Crafter::GitProject(const GitProjectSpec& spec) {
|
|||
fs::path cloneDir = externalRoot / std::format("{}-{}", name, srcHash);
|
||||
|
||||
{
|
||||
// One fetch of a clone at a time, as in BuildExternal.
|
||||
FileLock lock(fs::path(std::format("{}.lock", cloneDir.string())));
|
||||
const bool exists = fs::exists(cloneDir);
|
||||
Progress::Task task(std::format("{} {}", exists ? "Updating" : "Cloning", name));
|
||||
if (std::string err = FetchGit(spec.source, cloneDir); !err.empty()) {
|
||||
|
|
@ -434,8 +406,6 @@ fs::path Crafter::GitFetch(const GitSource& source) {
|
|||
fs::path cloneDir = externalRoot / std::format("{}-{:016x}", name, key);
|
||||
|
||||
{
|
||||
// One fetch of a clone at a time, as in BuildExternal.
|
||||
FileLock lock(fs::path(std::format("{}.lock", cloneDir.string())));
|
||||
const bool exists = fs::exists(cloneDir);
|
||||
Progress::Task task(std::format("{} {}", exists ? "Updating" : "Cloning", name));
|
||||
if (std::string err = FetchGit(source, cloneDir); !err.empty()) {
|
||||
|
|
|
|||
|
|
@ -33,10 +33,27 @@ namespace {
|
|||
// pass through the lock before it reaches the compile steps that *read* those
|
||||
// PCMs, so a reader never observes a half-written file.
|
||||
//
|
||||
// A FileLock on a `.lock` file in the cache dir.
|
||||
struct CacheLock {
|
||||
FileLock lock;
|
||||
explicit CacheLock(const fs::path& cacheDir) : lock(cacheDir / ".lock") {}
|
||||
// The lock lives in the OS, keyed on a `.lock` file in the cache dir, and is
|
||||
// released automatically when the descriptor/handle closes — including on
|
||||
// process death — so an interrupted build can never leave a stale lock. The
|
||||
// descriptor is close-on-exec / non-inheritable so spawned clang processes
|
||||
// don't keep the lock alive past our own release.
|
||||
class CacheLock {
|
||||
std::int32_t fd_ = -1;
|
||||
public:
|
||||
explicit CacheLock(const fs::path& cacheDir) {
|
||||
fd_ = ::open((cacheDir / ".lock").c_str(), O_RDWR | O_CREAT | O_CLOEXEC, 0644);
|
||||
if (fd_ >= 0) {
|
||||
while (::flock(fd_, LOCK_EX) != 0 && errno == EINTR) {}
|
||||
}
|
||||
}
|
||||
~CacheLock() {
|
||||
if (fd_ >= 0) {
|
||||
::close(fd_); // closing the descriptor drops the flock
|
||||
}
|
||||
}
|
||||
CacheLock(const CacheLock&) = delete;
|
||||
CacheLock& operator=(const CacheLock&) = delete;
|
||||
};
|
||||
|
||||
// The cached Crafter.Build PCMs are keyed by `<target>-<march>` alone, so
|
||||
|
|
@ -103,51 +120,6 @@ namespace {
|
|||
buffer << in.rdbuf();
|
||||
return buffer.str();
|
||||
}
|
||||
|
||||
// The string value of `"key": "..."` in `object`, if it has one.
|
||||
std::optional<std::string> JsonString(std::string_view object, std::string_view key) {
|
||||
const std::string quoted = std::format("\"{}\"", key);
|
||||
std::size_t at = object.find(quoted);
|
||||
if (at == std::string_view::npos) return std::nullopt;
|
||||
at = object.find_first_not_of(" \t\r\n", at + quoted.size());
|
||||
if (at == std::string_view::npos || object[at] != ':') return std::nullopt;
|
||||
at = object.find_first_not_of(" \t\r\n", at + 1);
|
||||
if (at == std::string_view::npos || object[at] != '"') return std::nullopt;
|
||||
std::string value;
|
||||
for (std::size_t i = at + 1; i < object.size(); ++i) {
|
||||
if (object[i] == '"') return value;
|
||||
if (object[i] == '\\' && i + 1 < object.size()) ++i;
|
||||
value += object[i];
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// The std module's "source-path" in a libstdc++.modules.json: the object
|
||||
// in its "modules" array whose "logical-name" is "std". The manifest is
|
||||
// a flat list of such objects, so each one is the text between its
|
||||
// braces.
|
||||
std::optional<std::string> StdModuleSourcePath(std::string_view manifest) {
|
||||
for (std::size_t open = manifest.find('{', manifest.find("\"modules\"")); open != std::string_view::npos; open = manifest.find('{', open + 1)) {
|
||||
const std::size_t close = manifest.find('}', open);
|
||||
if (close == std::string_view::npos) break;
|
||||
const std::string_view object = manifest.substr(open, close - open);
|
||||
if (JsonString(object, "logical-name") == "std") return JsonString(object, "source-path");
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
Crafter::FileLock::FileLock(const fs::path& path) {
|
||||
fd = ::open(path.c_str(), O_RDWR | O_CREAT | O_CLOEXEC, 0644);
|
||||
if (fd >= 0) {
|
||||
while (::flock(fd, LOCK_EX) != 0 && errno == EINTR) {}
|
||||
}
|
||||
}
|
||||
|
||||
Crafter::FileLock::~FileLock() {
|
||||
if (fd >= 0) {
|
||||
::close(fd); // closing the descriptor drops the flock
|
||||
}
|
||||
}
|
||||
|
||||
fs::path Crafter::GetCrafterBuildHome() {
|
||||
|
|
@ -492,7 +464,7 @@ namespace {
|
|||
std::mutex depMutex;
|
||||
std::unordered_set<Configuration*> forced;
|
||||
std::unordered_set<Configuration*> walked;
|
||||
std::set<std::string> libs;
|
||||
std::unordered_set<std::string> libs;
|
||||
|
||||
for (const PragmaDependency& pragma : pragmas) {
|
||||
Configuration* cfg = nullptr;
|
||||
|
|
@ -580,35 +552,6 @@ bool Crafter::IsTopLevelProject() {
|
|||
return LoadingFrames.size() <= 1;
|
||||
}
|
||||
|
||||
namespace {
|
||||
// The one reading of a wait status (pclose, system) for every helper
|
||||
// that runs a command: a signal death is reported the way a shell
|
||||
// reports it, as 128+signal.
|
||||
void DecodeWaitStatus(std::int32_t status, CommandResult& result) {
|
||||
if (WIFEXITED(status)) {
|
||||
result.exitCode = WEXITSTATUS(status);
|
||||
} else if (WIFSIGNALED(status)) {
|
||||
result.signal = WTERMSIG(status);
|
||||
result.exitCode = 128 + result.signal;
|
||||
result.crashed = true;
|
||||
} else {
|
||||
result.exitCode = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::string Crafter::ShellQuote(std::string_view s) {
|
||||
std::string out;
|
||||
out.reserve(s.size() + 2);
|
||||
out.push_back('\'');
|
||||
for (char c : s) {
|
||||
if (c == '\'') out += "'\\''";
|
||||
else out.push_back(c);
|
||||
}
|
||||
out.push_back('\'');
|
||||
return out;
|
||||
}
|
||||
|
||||
CommandResult Crafter::RunCommandChecked(std::string_view cmd) {
|
||||
std::array<char, 128> buffer;
|
||||
CommandResult result{};
|
||||
|
|
@ -621,27 +564,19 @@ CommandResult Crafter::RunCommandChecked(std::string_view cmd) {
|
|||
result.output += buffer.data();
|
||||
}
|
||||
|
||||
DecodeWaitStatus(pclose(pipe), result);
|
||||
std::int32_t status = pclose(pipe);
|
||||
if (WIFEXITED(status)) {
|
||||
result.exitCode = WEXITSTATUS(status);
|
||||
} else if (WIFSIGNALED(status)) {
|
||||
result.signal = WTERMSIG(status);
|
||||
result.exitCode = 128 + result.signal;
|
||||
result.crashed = true;
|
||||
} else {
|
||||
result.exitCode = -1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::int32_t Crafter::RunCommandAttached(std::string_view cmd) {
|
||||
// The child writes straight to the terminal. Anything of ours that is
|
||||
// still pending — the in-place status line, buffered stdout when it is
|
||||
// a pipe — has to be out before the child's first byte, or it lands in
|
||||
// the middle of the child's output or after it.
|
||||
Progress::Clear();
|
||||
std::fflush(stdout);
|
||||
std::fflush(stderr);
|
||||
|
||||
const std::string command(cmd);
|
||||
const std::int32_t status = std::system(command.c_str());
|
||||
if (status == -1) throw std::runtime_error(std::format("could not start a shell for: {}", cmd));
|
||||
CommandResult result{};
|
||||
DecodeWaitStatus(status, result);
|
||||
return result.exitCode;
|
||||
}
|
||||
|
||||
CommandResult Crafter::RunCommandWithTimeout(std::string_view cmd, std::chrono::seconds timeout) {
|
||||
std::array<char, 128> buffer;
|
||||
CommandResult result{};
|
||||
|
|
@ -657,16 +592,25 @@ CommandResult Crafter::RunCommandWithTimeout(std::string_view cmd, std::chrono::
|
|||
result.output += buffer.data();
|
||||
}
|
||||
|
||||
DecodeWaitStatus(pclose(pipe), result);
|
||||
// What comes back is `timeout`'s own exit, and it speaks through the
|
||||
// code: 124 for a command it had to stop, 128+signal for one that died.
|
||||
if (!result.crashed) {
|
||||
if (result.exitCode == 124) {
|
||||
std::int32_t status = pclose(pipe);
|
||||
if (WIFEXITED(status)) {
|
||||
std::int32_t code = WEXITSTATUS(status);
|
||||
if (code == 124) {
|
||||
result.timedOut = true;
|
||||
} else if (result.exitCode >= 128) {
|
||||
result.signal = result.exitCode - 128;
|
||||
result.exitCode = 124;
|
||||
} else if (code >= 128) {
|
||||
result.signal = code - 128;
|
||||
result.exitCode = code;
|
||||
result.crashed = true;
|
||||
} else {
|
||||
result.exitCode = code;
|
||||
}
|
||||
} else if (WIFSIGNALED(status)) {
|
||||
result.signal = WTERMSIG(status);
|
||||
result.exitCode = 128 + result.signal;
|
||||
result.crashed = true;
|
||||
} else {
|
||||
result.exitCode = -1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
|
@ -674,52 +618,23 @@ CommandResult Crafter::RunCommandWithTimeout(std::string_view cmd, std::chrono::
|
|||
std::string Crafter::BuildStdPcm(const Configuration& config, fs::path stdPcm) {
|
||||
CacheLock lock(stdPcm.parent_path());
|
||||
if(config.target == "x86_64-w64-mingw32") {
|
||||
// libstdc++ 15 and newer install libstdc++.modules.json beside the
|
||||
// library, naming the std module's source relative to itself. It is
|
||||
// looked for in the library directories the compiler searches for
|
||||
// this target, as it would look for libstdc++ itself, but only in
|
||||
// those that belong to the mingw toolchain: the list also has the
|
||||
// generic <prefix>/lib, where the host's own (Linux) libstdc++ keeps
|
||||
// its manifest. A toolchain that isn't there is a build error, not
|
||||
// an exception: Build() runs this with its worker threads started.
|
||||
const Toolchain& tc = GetToolchain();
|
||||
const std::string where = tc.mingwSysroot.empty()
|
||||
? std::string("where clang finds it by the x86_64-w64-mingw32 gcc on PATH")
|
||||
: std::format("under {}", tc.mingwSysroot.string());
|
||||
const std::string missing = std::format("no mingw-w64 libstdc++ with the std module (libstdc++ 15 or newer) {}. Install mingw-w64, or point CRAFTER_MINGW_DIR at its prefix, the directory holding x86_64-w64-mingw32/ and lib/gcc/", where);
|
||||
// Asked once a run for each compiler and target: every mingw
|
||||
// configuration comes through here, up to date or not, and the answer
|
||||
// costs a clang.
|
||||
static std::mutex probeMutex;
|
||||
static std::unordered_map<std::string, CommandResult> probes;
|
||||
const std::string probe = std::format("{} --target={} -print-search-dirs", GetBaseCommand(config), config.target);
|
||||
CommandResult dirs;
|
||||
{
|
||||
std::lock_guard guard(probeMutex);
|
||||
auto found = probes.find(probe);
|
||||
if (found == probes.end()) found = probes.emplace(probe, RunCommandChecked(probe)).first;
|
||||
dirs = found->second;
|
||||
}
|
||||
if (dirs.exitCode != 0) return std::format("{}: {}", missing, dirs.output);
|
||||
fs::path manifestPath;
|
||||
constexpr std::string_view librariesLine = "libraries: =";
|
||||
if (const std::size_t at = dirs.output.find(librariesLine); at != std::string::npos) {
|
||||
const std::size_t end = dirs.output.find('\n', at);
|
||||
const std::string list = dirs.output.substr(at + librariesLine.size(), end == std::string::npos ? std::string::npos : end - at - librariesLine.size());
|
||||
for (const auto part : std::views::split(list, ':')) {
|
||||
const fs::path dir(std::string_view(part.begin(), part.end()));
|
||||
const bool mingwOwn = std::ranges::contains(dir, fs::path("x86_64-w64-mingw32"));
|
||||
if (mingwOwn && fs::exists(dir / "libstdc++.modules.json")) {
|
||||
manifestPath = dir / "libstdc++.modules.json";
|
||||
break;
|
||||
}
|
||||
std::vector<std::string> folders;
|
||||
|
||||
// Iterate through the directory and collect all subdirectories
|
||||
for (const auto& entry : fs::directory_iterator(GetToolchain().mingwSysroot / "include" / "c++")) {
|
||||
if (entry.is_directory()) {
|
||||
folders.push_back(entry.path().filename().string());
|
||||
}
|
||||
}
|
||||
if (manifestPath.empty()) return missing;
|
||||
const std::optional<std::string> source = StdModuleSourcePath(FileBytes(manifestPath));
|
||||
if (!source) return std::format("{} names no std module", manifestPath.string());
|
||||
const fs::path stdCc = (manifestPath.parent_path() / *source).lexically_normal();
|
||||
if (!fs::exists(stdCc)) return std::format("{}, which {} names as the std module, is missing", stdCc.string(), manifestPath.string());
|
||||
|
||||
// Sort the folders by version in descending order
|
||||
std::sort(folders.begin(), folders.end(), [](const std::string& a, const std::string& b) {
|
||||
return std::lexicographical_compare(b.begin(), b.end(), a.begin(), a.end());
|
||||
});
|
||||
|
||||
std::string mingWversion = folders.front();
|
||||
|
||||
fs::path stdCc = GetToolchain().mingwSysroot / "include" / "c++" / mingWversion / "bits" / "std.cc";
|
||||
|
||||
// -femulated-tls keeps PCM TLS access matching libstdc++'s emutls
|
||||
// definitions; mismatch surfaces as undefined std::__once_callable.
|
||||
|
|
@ -808,11 +723,9 @@ std::string Crafter::GetBaseCommand(const Configuration& config) {
|
|||
const Toolchain& tc = GetToolchain();
|
||||
std::string command = tc.clangxx.string();
|
||||
if (config.target == "x86_64-w64-mingw32") {
|
||||
// libstdc++ from the mingw toolchain, said outright: a clang built to
|
||||
// default to libc++ (the Flatpak's) would otherwise reach for that.
|
||||
// Found by clang's own mingw discovery unless there is a sysroot.
|
||||
command += " -stdlib=libstdc++";
|
||||
if (!tc.mingwSysroot.empty()) command += std::format(" --sysroot={}", tc.mingwSysroot.string());
|
||||
// libstdc++ from the mingw toolchain, found by clang's own mingw
|
||||
// discovery unless the sysroot was given explicitly.
|
||||
if (tc.mingwSysrootExplicit) command += std::format(" --sysroot={}", tc.mingwSysroot.string());
|
||||
return command;
|
||||
}
|
||||
command += " -stdlib=libc++";
|
||||
|
|
@ -823,7 +736,7 @@ std::string Crafter::GetBaseCommand(const Configuration& config) {
|
|||
}
|
||||
|
||||
namespace {
|
||||
constexpr std::array<std::string_view, 13> CrafterBuildModules = {
|
||||
constexpr std::array<std::string_view, 12> CrafterBuildModules = {
|
||||
"Crafter.Build-Toolchain",
|
||||
"Crafter.Build-Shader",
|
||||
"Crafter.Build-Platform",
|
||||
|
|
@ -833,7 +746,6 @@ namespace {
|
|||
"Crafter.Build-Clang",
|
||||
"Crafter.Build-Test",
|
||||
"Crafter.Build-Lint",
|
||||
"Crafter.Build-Script",
|
||||
"Crafter.Build-Progress",
|
||||
"Crafter.Build-Asset",
|
||||
"Crafter.Build",
|
||||
|
|
|
|||
|
|
@ -1,104 +0,0 @@
|
|||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
module Crafter.Build:Script_impl;
|
||||
import std;
|
||||
import :Script;
|
||||
import :Clang;
|
||||
import :Platform;
|
||||
import :Progress;
|
||||
namespace fs = std::filesystem;
|
||||
using namespace Crafter;
|
||||
|
||||
namespace {
|
||||
std::string JoinNames(std::span<const Script> scripts) {
|
||||
std::string out;
|
||||
for (const Script& script : scripts) {
|
||||
if (!out.empty()) out += ", ";
|
||||
out += script.name;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
std::int32_t ScriptContext::Run(std::string_view command) const {
|
||||
// Echoed by default, unlike a compile step: a script's commands are the
|
||||
// only account of what it is doing to the machine.
|
||||
if (Progress::GetVerbosity() != Progress::Verbosity::Quiet) std::println("$ {}", command);
|
||||
return RunCommandAttached(command);
|
||||
}
|
||||
|
||||
std::int32_t Crafter::RunScript(Configuration& projectCfg, const RunScriptOptions& opts) {
|
||||
// Loading the project may have left an in-place status line behind.
|
||||
Progress::Clear();
|
||||
|
||||
const std::span<const Script> scripts = projectCfg.scripts;
|
||||
if (scripts.empty()) {
|
||||
std::println(std::cerr,
|
||||
R"msg(No scripts defined.
|
||||
Add them in project.cpp before returning the Configuration:
|
||||
|
||||
cfg.scripts.push_back({{"docs", [](Crafter::ScriptContext& ctx) {{
|
||||
return ctx.Run("python3 tools/gen-docs.py");
|
||||
}}}});
|
||||
|
||||
`crafter-build run docs [args...]` then runs it and exits with what it
|
||||
returned; ctx.args holds the arguments.)msg");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Checked for every script on every run, the list included, so a bad
|
||||
// declaration is reported the first time anyone uses `run` and not only
|
||||
// when that one name is asked for.
|
||||
std::unordered_set<std::string_view> seen;
|
||||
for (const Script& script : scripts) {
|
||||
if (script.name.empty() || script.name.starts_with('-')) {
|
||||
std::println(std::cerr, "script name '{}' cannot be run: a name must not be empty or start with '-'", script.name);
|
||||
return 1;
|
||||
}
|
||||
// Running the first or the last of two would both be a guess, and
|
||||
// the usual cause is a pasted line whose name was never changed.
|
||||
if (!seen.insert(script.name).second) {
|
||||
std::println(std::cerr, "script '{}' is defined twice", script.name);
|
||||
return 1;
|
||||
}
|
||||
if (!script.run) {
|
||||
std::println(std::cerr, "script '{}' has no callback", script.name);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (opts.listOnly || !opts.named) {
|
||||
for (const Script& script : scripts) std::println("{}", script.name);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const auto found = std::ranges::find(scripts, opts.name, &Script::name);
|
||||
if (found == scripts.end()) {
|
||||
std::println(std::cerr, "No script named '{}'. Scripts: {}", opts.name, JoinNames(scripts));
|
||||
return 1;
|
||||
}
|
||||
|
||||
ScriptContext ctx{
|
||||
.project = projectCfg,
|
||||
// lexically_normal: fs::absolute keeps a "./" component.
|
||||
.projectDir = opts.projectFile.empty() ? fs::absolute(projectCfg.path).lexically_normal() : opts.projectFile.parent_path(),
|
||||
.args = opts.args,
|
||||
};
|
||||
try {
|
||||
const std::int32_t code = found->run(ctx);
|
||||
if (code >= 0 && code <= 255) return code;
|
||||
std::println(std::cerr, "script '{}' returned {}, which is not an exit status (0..255); exiting with 1", found->name, code);
|
||||
return 1;
|
||||
} catch (const std::exception& e) {
|
||||
// Never let a script's exception unwind across the project.so
|
||||
// boundary and out of Run: report it as this script's failure.
|
||||
Progress::Clear();
|
||||
std::println(std::cerr, "script '{}' failed: {}", found->name, e.what());
|
||||
return 1;
|
||||
} catch (...) {
|
||||
Progress::Clear();
|
||||
std::println(std::cerr, "script '{}' failed: it threw something that isn't a std::exception", found->name);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,6 @@ module;
|
|||
#include "../lib/DirStackFileIncluder.h"
|
||||
module Crafter.Build:Shader_impl;
|
||||
import :Shader;
|
||||
import :Platform;
|
||||
import std;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
|
|
@ -32,39 +31,6 @@ namespace {
|
|||
}
|
||||
return EShLangVertex;
|
||||
}
|
||||
|
||||
// A path as a Make prerequisite, escaped the way NewestPrerequisite reads it.
|
||||
std::string MakeEscaped(const std::string& path) {
|
||||
std::string escaped;
|
||||
for (const char c : path) {
|
||||
if (c == ' ' || c == '\t' || c == '#') escaped += '\\';
|
||||
if (c == '$') escaped += '$';
|
||||
escaped += c;
|
||||
}
|
||||
return escaped;
|
||||
}
|
||||
|
||||
std::string ReadFile(const fs::path& file) {
|
||||
std::ifstream stream(file, std::ios::binary);
|
||||
return std::string(std::istreambuf_iterator<char>(stream), std::istreambuf_iterator<char>());
|
||||
}
|
||||
|
||||
// What a compile's output depends on besides files' contents, one line
|
||||
// each: two entries of one source differ here, and an #include searched
|
||||
// for from another source directory or in other directories can find
|
||||
// another file.
|
||||
std::string Settings(const Crafter::Shader& shader, std::span<const fs::path> includeDirs) {
|
||||
std::string settings = std::format("source {}\nstage {}\nentry {}\n", fs::absolute(shader.path).string(), static_cast<int>(shader.type), shader.entrypoint);
|
||||
for (const std::string& define : shader.defines) settings += std::format("define {}\n", define);
|
||||
for (const fs::path& dir : includeDirs) settings += std::format("include {}\n", dir.string());
|
||||
return settings;
|
||||
}
|
||||
|
||||
// Where Compile records, for Check, what the output in the bin dir was
|
||||
// built from: beside the configuration's objects, as their depfiles are.
|
||||
fs::path Record(const Crafter::Shader& shader, const fs::path& buildDir, std::string_view kind) {
|
||||
return buildDir / std::format("{}.{}", shader.OutputName().string(), kind);
|
||||
}
|
||||
}
|
||||
|
||||
namespace Crafter {
|
||||
|
|
@ -79,25 +45,11 @@ namespace Crafter {
|
|||
name.replace_extension(variant.empty() ? std::string("spv") : std::format("{}.spv", variant));
|
||||
return name;
|
||||
}
|
||||
bool Shader::Check(const fs::path& outputDir, const fs::path& buildDir, std::span<const fs::path> includeDirs) const {
|
||||
std::error_code ec;
|
||||
const fs::file_time_type built = fs::last_write_time(outputDir / OutputName(), ec);
|
||||
if (ec) return false;
|
||||
// The source and every file it #included, as the last compile
|
||||
// recorded them: without a record, or with an included file since
|
||||
// deleted, NewestPrerequisite reads max(), which is stale.
|
||||
if (std::max(fs::last_write_time(path), NewestPrerequisite(Record(*this, buildDir, "d"))) >= built) return false;
|
||||
return ReadFile(Record(*this, buildDir, "settings")) == Settings(*this, includeDirs);
|
||||
bool Shader::Check(const fs::path& outputDir) const {
|
||||
fs::path spv = outputDir / OutputName();
|
||||
return fs::exists(spv) && fs::last_write_time(path) < fs::last_write_time(spv);
|
||||
}
|
||||
std::string Shader::Compile(const fs::path& outputDir, const fs::path& buildDir, std::span<const fs::path> includeDirs) const {
|
||||
// The records go first, so a compile that fails leaves the output
|
||||
// stale, whatever it wrote.
|
||||
const fs::path depFile = Record(*this, buildDir, "d");
|
||||
const fs::path settingsFile = Record(*this, buildDir, "settings");
|
||||
std::error_code removed;
|
||||
fs::remove(depFile, removed);
|
||||
fs::remove(settingsFile, removed);
|
||||
|
||||
std::string Shader::Compile(const fs::path& outputDir, std::span<const fs::path> includeDirs) const {
|
||||
EShLanguage glslangType = ToEShLanguage(type);
|
||||
glslang::InitializeProcess();
|
||||
|
||||
|
|
@ -205,25 +157,6 @@ namespace Crafter {
|
|||
if (!spvLog.empty()) {
|
||||
return fail("SPIR-V codegen reported issues", std::move(spvLog));
|
||||
}
|
||||
|
||||
// What Check compares the output against next time: the source and
|
||||
// the files it included, as absolute paths so that reading them back
|
||||
// doesn't depend on the directory a build runs from, and the settings.
|
||||
std::ofstream dep(depFile, std::ios::binary);
|
||||
dep << MakeEscaped(fs::absolute(outputDir / filename).string()) << ": " << MakeEscaped(fs::absolute(path).string());
|
||||
for (const std::string& included : includeDir.getIncludedFiles()) {
|
||||
dep << " \\\n " << MakeEscaped(fs::absolute(included).string());
|
||||
}
|
||||
dep << '\n';
|
||||
dep.close();
|
||||
std::ofstream settings(settingsFile, std::ios::binary);
|
||||
settings << Settings(*this, includeDirs);
|
||||
settings.close();
|
||||
if (!dep || !settings) {
|
||||
fs::remove(depFile, removed);
|
||||
fs::remove(settingsFile, removed);
|
||||
return fail("failed to record what the shader was built from", buildDir.string());
|
||||
}
|
||||
glslang::FinalizeProcess();
|
||||
return {};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,11 +23,23 @@ namespace {
|
|||
|
||||
// MatchGlob/MatchAny live in :Platform — shared with the lint verb.
|
||||
|
||||
std::string ShellQuoteSh(std::string_view s) {
|
||||
std::string out;
|
||||
out.reserve(s.size() + 2);
|
||||
out.push_back('\'');
|
||||
for (char c : s) {
|
||||
if (c == '\'') out += "'\\''";
|
||||
else out.push_back(c);
|
||||
}
|
||||
out.push_back('\'');
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string JoinAndQuoteArgs(std::span<const std::string> args) {
|
||||
std::string out;
|
||||
for (const auto& a : args) {
|
||||
if (!out.empty()) out.push_back(' ');
|
||||
out += ShellQuote(a);
|
||||
out += ShellQuoteSh(a);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
|
@ -261,7 +273,7 @@ TestResult Crafter::RunSingleTest(const Test& test, const fs::path& binary, std:
|
|||
|
||||
if (test.runner.exec.empty()) {
|
||||
// Local runner: spawn the binary directly through the host shell.
|
||||
std::string cmd = std::format("{} {}", ShellQuote(binary.string()), ph["{args}"]);
|
||||
std::string cmd = std::format("{} {}", ShellQuoteSh(binary.string()), ph["{args}"]);
|
||||
r = RunCommandWithTimeout(cmd, timeout);
|
||||
} else {
|
||||
// Prefix runner (qemu-user, wasmtime, wine, ...): templated exec
|
||||
|
|
|
|||
|
|
@ -137,10 +137,9 @@ Toolchain Crafter::ResolveToolchain(const ToolchainInputs& in) {
|
|||
}
|
||||
if (std::string_view dir = Env(in, "CRAFTER_MINGW_DIR"); !dir.empty()) {
|
||||
tc.mingwSysroot = fs::path(dir);
|
||||
} else if (!in.hostRoot.empty() && fs::exists(in.hostRoot / "usr" / "x86_64-w64-mingw32")) {
|
||||
// The sandbox has no mingw-w64 of its own, and no mingw gcc on PATH
|
||||
// for clang to find one by.
|
||||
tc.mingwSysroot = in.hostRoot / "usr";
|
||||
tc.mingwSysrootExplicit = true;
|
||||
} else {
|
||||
tc.mingwSysroot = "/usr/x86_64-w64-mingw32";
|
||||
}
|
||||
return tc;
|
||||
}
|
||||
|
|
@ -158,7 +157,6 @@ const Toolchain& Crafter::GetToolchain() {
|
|||
try {
|
||||
in.crafterPrefix = GetCrafterBuildHome().parent_path().parent_path();
|
||||
} catch (const std::exception&) {}
|
||||
if (fs::exists("/.flatpak-info")) in.hostRoot = "/run/host";
|
||||
|
||||
Toolchain tc = ResolveToolchain(in);
|
||||
if (!tc.error.empty()) return tc;
|
||||
|
|
|
|||
|
|
@ -14,15 +14,12 @@ export namespace Crafter {
|
|||
struct BuildResult {
|
||||
std::string result;
|
||||
bool repack;
|
||||
// Sets in order: the commands they go into are compared with the ones
|
||||
// recorded last time, so they come out the same however the
|
||||
// dependency threads that fill them happen to finish.
|
||||
std::set<std::string> libs;
|
||||
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::set<std::string> publicCompileFlags;
|
||||
std::unordered_set<std::string> publicCompileFlags;
|
||||
};
|
||||
|
||||
struct Define {
|
||||
|
|
@ -359,45 +356,6 @@ export namespace Crafter {
|
|||
bool needsAst = false;
|
||||
};
|
||||
|
||||
struct Configuration;
|
||||
|
||||
// What a Script's callback is handed when `crafter-build run <name>`
|
||||
// starts it.
|
||||
struct ScriptContext {
|
||||
// The Configuration project.cpp returned. Read the project through
|
||||
// this rather than capturing the local in CrafterBuildProject by
|
||||
// reference: that local is gone by the time a script runs.
|
||||
Configuration& project;
|
||||
// Absolute directory of the project file. Commands run in the
|
||||
// directory crafter-build was started from, like everything else it
|
||||
// does, so a script names its files through this.
|
||||
fs::path projectDir;
|
||||
// Everything the user typed after the script's name, untouched:
|
||||
// crafter-build interprets none of it.
|
||||
std::span<const std::string> args;
|
||||
// Run `command` through sh with the terminal attached — output
|
||||
// appears as it is produced, the command can prompt, Ctrl-C reaches
|
||||
// it — after echoing it as `$ command` (not under -q). Returns its
|
||||
// exit code, 128+signal when it was killed; return that from the
|
||||
// script to make it the exit code of crafter-build. Quote anything
|
||||
// that is not a literal with ShellQuote. Out-of-line like
|
||||
// LintContext's members: the callback runs from project.so.
|
||||
std::int32_t Run(std::string_view command) const;
|
||||
};
|
||||
|
||||
// A project script: a chore that belongs to the project but is not part
|
||||
// of building it (packaging, installing, publishing, serving), run by
|
||||
// name with `crafter-build run <name> [args...]`. `run` returns the
|
||||
// process exit code. It is only stored when project.cpp is loaded, which
|
||||
// happens for every verb and for every project that depends on this one,
|
||||
// so whatever a script needs to probe it probes when it runs. Only the
|
||||
// scripts of the project crafter-build was pointed at can be run, never
|
||||
// a dependency's.
|
||||
struct Script {
|
||||
std::string name;
|
||||
std::function<std::int32_t(ScriptContext&)> run;
|
||||
};
|
||||
|
||||
// The host target triple: what the resolved toolchain's clang++ prints
|
||||
// for -print-target-triple (see GetToolchain). Used as the default for
|
||||
// Configuration::target so projects don't have to hardcode it for the
|
||||
|
|
@ -488,12 +446,6 @@ export namespace Crafter {
|
|||
std::vector<Test> tests;
|
||||
// Lint rules for `crafter-build lint`. Populate via AddLintRule.
|
||||
std::vector<LintRule> lintRules;
|
||||
// Scripts for `crafter-build run <name>`, a plain list like defines:
|
||||
// cfg.scripts.push_back({"docs", [](ScriptContext& ctx) {
|
||||
// return ctx.Run("python3 tools/gen-docs.py");
|
||||
// }});
|
||||
// Not part of VariantId: a script changes nothing that is compiled.
|
||||
std::vector<Script> scripts;
|
||||
void GetInterfacesAndImplementations(std::span<fs::path> interfaces, std::span<fs::path> implementations);
|
||||
// Retry the `import X;` names GetInterfacesAndImplementations could not
|
||||
// place, against the dependency DAG as it stands now. Sources are
|
||||
|
|
|
|||
|
|
@ -14,21 +14,6 @@ namespace Crafter {
|
|||
bool timedOut = false;
|
||||
std::int32_t signal = 0;
|
||||
};
|
||||
// An exclusive lock on the file `path` (created if missing), held from
|
||||
// construction to destruction. An flock, so it keeps out the other
|
||||
// threads of this process (each opens the file anew) and other
|
||||
// crafter-build processes alike. The OS drops it when the descriptor
|
||||
// closes, process death included, so an interrupted build never leaves a
|
||||
// stale lock; the descriptor is close-on-exec, so the tools a holder
|
||||
// spawns don't keep it past its release.
|
||||
class FileLock {
|
||||
std::int32_t fd = -1;
|
||||
public:
|
||||
explicit FileLock(const fs::path& path);
|
||||
~FileLock();
|
||||
FileLock(const FileLock&) = delete;
|
||||
FileLock& operator=(const FileLock&) = delete;
|
||||
};
|
||||
std::string BuildStdPcm(const Configuration& config, fs::path stdPcm);
|
||||
fs::path GetCacheDir();
|
||||
CommandResult RunCommandChecked(std::string_view command);
|
||||
|
|
@ -44,18 +29,6 @@ namespace Crafter {
|
|||
// therefore keep the `if (!result.empty())` shape they already had.
|
||||
std::string RunBuildCommand(std::string_view command);
|
||||
export CommandResult RunCommandWithTimeout(std::string_view command, std::chrono::seconds timeout);
|
||||
// Runs `command` through sh with this process's own stdin, stdout and
|
||||
// stderr, so its output appears as it is produced and it can prompt.
|
||||
// Every helper above captures instead, which suits a compile step and
|
||||
// not a command that runs for half an hour. Returns the exit code, or
|
||||
// 128+signal when the command was killed; a nonzero exit is the caller's
|
||||
// to judge and never throws. While it runs, Ctrl-C goes to the command
|
||||
// and not to crafter-build, which then sees it as exit code 130.
|
||||
// Project scripts reach this through ScriptContext::Run.
|
||||
std::int32_t RunCommandAttached(std::string_view command);
|
||||
// `s` as one single-quoted sh word, safe to splice into a command line
|
||||
// whatever it contains: paths with spaces, arguments a user typed.
|
||||
export std::string ShellQuote(std::string_view s);
|
||||
std::string GetBaseCommand(const Configuration& config);
|
||||
export Configuration LoadProject(const fs::path& projectFile, std::span<const std::string_view> args);
|
||||
// A dependency the project file currently running its CrafterBuildProject
|
||||
|
|
|
|||
|
|
@ -1,40 +0,0 @@
|
|||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
export module Crafter.Build:Script;
|
||||
import std;
|
||||
import :Clang;
|
||||
|
||||
export namespace Crafter {
|
||||
struct RunScriptOptions {
|
||||
// The script to run; read only when `named` is set.
|
||||
std::string name;
|
||||
// A name was given. Kept apart from `name` because an empty string
|
||||
// is a name someone can type (`run ""`): it has to be looked up and
|
||||
// refused, not read as a bare `run`, which lists.
|
||||
bool named = false;
|
||||
// What followed the name on the command line, handed to the script
|
||||
// as ScriptContext::args.
|
||||
std::vector<std::string> args;
|
||||
// Print the script names instead of running one.
|
||||
bool listOnly = false;
|
||||
// Absolute path of the loaded project.cpp; its directory becomes
|
||||
// ScriptContext::projectDir. Empty => fall back to cfg.path.
|
||||
std::filesystem::path projectFile;
|
||||
};
|
||||
|
||||
// Backs `crafter-build run`. Runs the script `opts` names out of
|
||||
// `projectCfg.scripts` and returns what it returned, which the CLI uses
|
||||
// as the process exit code. Without a name, or with listOnly, prints the
|
||||
// names and returns 0.
|
||||
//
|
||||
// Returns 1, with the reason on stderr and without running anything,
|
||||
// when the project defines no scripts, when a script is declared under a
|
||||
// name that could never be typed (empty, or starting with '-') or under
|
||||
// one name twice, and when no script has the requested name. A script
|
||||
// that throws also yields 1, as does one that returns a nonzero value
|
||||
// outside 1..255: an exit status is eight bits, and a value such as 256
|
||||
// (a raw wait status passed along by mistake) would otherwise wrap to
|
||||
// success.
|
||||
std::int32_t RunScript(Configuration& projectCfg, const RunScriptOptions& opts);
|
||||
}
|
||||
|
|
@ -38,23 +38,16 @@ namespace Crafter {
|
|||
// gives "<name>.spv", "sets" gives "<name>.sets.spv". One file-name
|
||||
// component: no path separators. Every entry of a configuration must
|
||||
// end up with its own OutputName(); Build() fails if two collide, so
|
||||
// each extra compile of a source needs its own variant.
|
||||
// each extra compile of a source needs its own variant. Check()
|
||||
// compares only the source's mtime: changing defines without touching
|
||||
// the source leaves the old output in place.
|
||||
std::string variant;
|
||||
Shader(fs::path&& path, std::string&& entrypoint, ShaderType type);
|
||||
// File name of the compiled SPIR-V, relative to the output directory.
|
||||
// The single source of truth for it: Check, Compile and the artifact
|
||||
// forwarding in Build() all go through here.
|
||||
fs::path OutputName() const;
|
||||
// Whether the output in `outputDir` is up to date: newer than the
|
||||
// source and than every file the source #included, and compiled with
|
||||
// this entry's stage, entry point and defines, searching
|
||||
// `includeDirs`. Compile records the last two in `buildDir`; without
|
||||
// its record an output is stale.
|
||||
bool Check(const fs::path& outputDir, const fs::path& buildDir, std::span<const fs::path> includeDirs) const;
|
||||
// Compiles into `outputDir`, searching `includeDirs` for #include, and
|
||||
// records what Check needs in `buildDir`: a depfile naming the source
|
||||
// and every file it included, and the settings it was compiled with.
|
||||
// Returns an error message, empty on success.
|
||||
std::string Compile(const fs::path& outputDir, const fs::path& buildDir, std::span<const fs::path> includeDirs) const;
|
||||
bool Check(const fs::path& outputDir) const;
|
||||
std::string Compile(const fs::path& outputDir, std::span<const fs::path> includeDirs) const;
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,13 +37,12 @@ export namespace Crafter {
|
|||
// Empty when the crafter prefix is unknown; need not exist.
|
||||
fs::path stdModulesDir;
|
||||
fs::path wasiSysroot;
|
||||
// The mingw-w64 install prefix, the directory that holds
|
||||
// x86_64-w64-mingw32/ and lib/gcc/x86_64-w64-mingw32/ (/usr for a
|
||||
// distro package), passed to clang as --sysroot for the mingw target.
|
||||
// Empty means clang's own discovery, which follows the mingw gcc on
|
||||
// PATH. Set by CRAFTER_MINGW_DIR, else the host's /usr when running
|
||||
// sandboxed and it has mingw-w64 (the sandbox's own /usr never does).
|
||||
// Holds include/c++/<ver>/bits/std.cc for the mingw target. Passed to
|
||||
// clang as --sysroot only when it was set explicitly: clang's own
|
||||
// mingw discovery handles the distro layout, and a --sysroot pointing
|
||||
// at the triple directory would lose crtbegin.o and libgcc.
|
||||
fs::path mingwSysroot;
|
||||
bool mingwSysrootExplicit = false;
|
||||
// `clang++ -print-target-triple` and the first line of
|
||||
// `clang++ --version`; both empty when clang++ could not be run.
|
||||
std::string hostTriple;
|
||||
|
|
@ -73,9 +72,6 @@ export namespace Crafter {
|
|||
std::map<std::string, std::string, std::less<>> env;
|
||||
// The <prefix> of <prefix>/share/crafter-build; empty when unknown.
|
||||
fs::path crafterPrefix;
|
||||
// Where the host's root shows inside a sandbox (/run/host in a
|
||||
// Flatpak); empty outside one.
|
||||
fs::path hostRoot;
|
||||
};
|
||||
// Pure path resolution: an env override wins, then a toolchain installed
|
||||
// beside crafter-build (<crafterPrefix>/bin/clang++), then bare names on
|
||||
|
|
|
|||
|
|
@ -10,6 +10,5 @@ export import :Shader;
|
|||
export import :External;
|
||||
export import :Test;
|
||||
export import :Lint;
|
||||
export import :Script;
|
||||
export import :Progress;
|
||||
export import :Asset;
|
||||
|
|
|
|||
146
project.cpp
146
project.cpp
|
|
@ -8,134 +8,6 @@ import Crafter.LintRules;
|
|||
namespace fs = std::filesystem;
|
||||
using namespace Crafter;
|
||||
|
||||
// The local Flatpak: `crafter-build run flatpak` builds it from this checkout
|
||||
// and `crafter-build run install` installs that build. The development box
|
||||
// compiles with its own clang, the shipped artifact with the LLVM bundled in
|
||||
// the Flatpak, so this is how a change is tried against the real thing.
|
||||
namespace {
|
||||
struct FlatpakPaths {
|
||||
fs::path manifest;
|
||||
// flatpak-builder's downloads, ccache and per-module cache. The
|
||||
// LLVM build lives here: about 2 GB, and 25 minutes to rebuild.
|
||||
fs::path state;
|
||||
// The OSTree repo the build is exported to; `install` reads it.
|
||||
fs::path repo;
|
||||
fs::path buildDir;
|
||||
};
|
||||
|
||||
// Everything under <project>/build/flatpak, and a fixed name on purpose
|
||||
// instead of a BuildDir(): the Flatpak is compiled by the toolchain
|
||||
// inside the sandbox, so it does not depend on the host variant, and a
|
||||
// variant-keyed directory would strand the LLVM cache on every host
|
||||
// compiler update. The manifest's `skip: build` keeps it out of the
|
||||
// sources the sandbox copies. flatpak-builder keys its cache on the
|
||||
// manifest's absolute path, so the manifest is used where it is, never
|
||||
// through a rewritten copy the way CI stamps the version in; a local
|
||||
// build therefore reports version 0.0.0-dev.
|
||||
FlatpakPaths FlatpakLayout(const fs::path& projectDir) {
|
||||
const fs::path root = projectDir / "build" / "flatpak";
|
||||
return {
|
||||
.manifest = projectDir / "packaging" / "flatpak" / "net.catcrafts.CrafterBuild.yaml",
|
||||
.state = root / "state",
|
||||
.repo = root / "repo",
|
||||
.buildDir = root / "build-dir",
|
||||
};
|
||||
}
|
||||
|
||||
// Both scripts drive the host's flatpak, and the sandbox crafter-build
|
||||
// itself runs in as a Flatpak has none.
|
||||
bool RefusedInSandbox(std::string_view script) {
|
||||
if (!fs::exists("/.flatpak-info")) return false;
|
||||
std::println(std::cerr, "run {0}: needs the host's flatpak, which the Flatpak build of crafter-build cannot reach. Use a native build: ./build.sh, then ./bin/crafter-build run {0}", script);
|
||||
return true;
|
||||
}
|
||||
|
||||
// flatpak-builder as a host package, else the org.flatpak.Builder app.
|
||||
// Empty when neither is installed.
|
||||
std::string FlatpakBuilder() {
|
||||
if (RunCommandWithTimeout("flatpak-builder --version", std::chrono::seconds(10)).exitCode == 0) return "flatpak-builder";
|
||||
if (RunCommandWithTimeout("flatpak info org.flatpak.Builder", std::chrono::seconds(10)).exitCode == 0) return "flatpak run org.flatpak.Builder";
|
||||
return "";
|
||||
}
|
||||
|
||||
// A repo path as the file:// URL of a remote. Percent-encoded, because
|
||||
// `remote-modify --url=` stores what it is given as it is.
|
||||
std::string FileUrl(const fs::path& path) {
|
||||
std::string url = "file://";
|
||||
for (const char c : path.string()) {
|
||||
const bool plain = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || std::string_view("/-._~").contains(c);
|
||||
if (plain) url.push_back(c);
|
||||
else url += std::format("%{:02X}", static_cast<std::uint8_t>(c));
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
std::int32_t BuildFlatpak(ScriptContext& ctx) {
|
||||
if (RefusedInSandbox("flatpak")) return 1;
|
||||
// Nothing else is passed through: a flag such as --stop-at or
|
||||
// --download-only would end the run without a build and leave the
|
||||
// smoke test below running against the previous one.
|
||||
std::string jobs;
|
||||
for (const std::string& arg : ctx.args) {
|
||||
const std::string_view count = arg.starts_with("--jobs=") ? std::string_view(arg).substr(7) : std::string_view();
|
||||
if (count.empty() || !std::ranges::all_of(count, [](char c) { return c >= '0' && c <= '9'; })) {
|
||||
std::println(std::cerr, "run flatpak: unknown argument '{}' (only --jobs=<N> is accepted)", arg);
|
||||
return 1;
|
||||
}
|
||||
jobs = std::format(" --jobs={}", count);
|
||||
}
|
||||
const std::string builder = FlatpakBuilder();
|
||||
if (builder.empty()) {
|
||||
std::println(std::cerr, "run flatpak: neither flatpak-builder nor the org.flatpak.Builder app is installed (flatpak install flathub org.flatpak.Builder)");
|
||||
return 1;
|
||||
}
|
||||
const FlatpakPaths paths = FlatpakLayout(ctx.projectDir);
|
||||
const std::string buildDir = ShellQuote(paths.buildDir.string());
|
||||
if (const std::int32_t code = ctx.Run(std::format("{}{} --user --force-clean --ccache --state-dir={} --repo={} --default-branch=local {} {}", builder, jobs, ShellQuote(paths.state.string()), ShellQuote(paths.repo.string()), buildDir, ShellQuote(paths.manifest.string()))); code != 0) return code;
|
||||
// The smoke test CI runs on its build, but through `flatpak build`
|
||||
// and not `<builder> --run`: the org.flatpak.Builder app (1.4.9)
|
||||
// exits 1 when the command it ran succeeded and 0 when it failed,
|
||||
// and --run also wants the state dir again or it drops a
|
||||
// .flatpak-builder/ into the working directory.
|
||||
return ctx.Run(std::format("flatpak build {} crafter-build --version", buildDir));
|
||||
}
|
||||
|
||||
std::int32_t InstallFlatpak(ScriptContext& ctx) {
|
||||
if (RefusedInSandbox("install")) return 1;
|
||||
if (!ctx.args.empty()) {
|
||||
std::println(std::cerr, "run install: takes no arguments (got '{}')", ctx.args.front());
|
||||
return 1;
|
||||
}
|
||||
const FlatpakPaths paths = FlatpakLayout(ctx.projectDir);
|
||||
// Deliberately not built from here: flatpak-builder never caches a
|
||||
// `type: dir` module, so every build reruns build.sh and the whole
|
||||
// suite inside the sandbox. Installing is the cheap half.
|
||||
if (!fs::exists(paths.repo / "config")) {
|
||||
std::println(std::cerr, "run install: nothing built yet, run: crafter-build run flatpak");
|
||||
return 1;
|
||||
}
|
||||
const std::string url = ShellQuote(FileUrl(paths.repo));
|
||||
// Its own branch from its own remote, next to whichever published
|
||||
// branch is installed. The remote may already exist and point at
|
||||
// another checkout's repo; --if-not-exists would keep that silently.
|
||||
if (const std::int32_t code = ctx.Run(std::format("flatpak remote-add --user --if-not-exists --no-gpg-verify crafter-build-local {}", url)); code != 0) return code;
|
||||
if (const std::int32_t code = ctx.Run(std::format("flatpak remote-modify --user --url={} crafter-build-local", url)); code != 0) return code;
|
||||
if (const std::int32_t code = ctx.Run("flatpak install --user --noninteractive --or-update crafter-build-local net.catcrafts.CrafterBuild//local"); code != 0) return code;
|
||||
// Only a first install becomes the branch `flatpak run` starts; an
|
||||
// update leaves that choice where it was.
|
||||
if (const std::int32_t code = ctx.Run("flatpak make-current --user net.catcrafts.CrafterBuild local"); code != 0) return code;
|
||||
std::println(
|
||||
R"(
|
||||
`flatpak run net.catcrafts.CrafterBuild` now starts this build.
|
||||
Back to a published branch, keeping this one installed:
|
||||
flatpak make-current --user net.catcrafts.CrafterBuild <branch>
|
||||
Remove it (do this before `crafter-build clean`, which deletes the repo the remote points at):
|
||||
flatpak uninstall --user net.catcrafts.CrafterBuild//local
|
||||
flatpak remote-delete --user crafter-build-local)");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> args) {
|
||||
std::vector<std::string> depArgs(args.begin(), args.end());
|
||||
Configuration* math = GitProject({
|
||||
|
|
@ -179,7 +51,7 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
|||
// it reaches clang, which must still see a string literal.
|
||||
CrafterBuildLib->defines.push_back({"CRAFTER_BUILD_VERSION", std::format("\\\"{}\\\"", version)});
|
||||
{
|
||||
std::array<fs::path, 13> interfaces = {
|
||||
std::array<fs::path, 12> interfaces = {
|
||||
"interfaces/Crafter.Build",
|
||||
"interfaces/Crafter.Build-Toolchain",
|
||||
"interfaces/Crafter.Build-Shader",
|
||||
|
|
@ -190,11 +62,10 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
|||
"interfaces/Crafter.Build-Clang",
|
||||
"interfaces/Crafter.Build-Test",
|
||||
"interfaces/Crafter.Build-Lint",
|
||||
"interfaces/Crafter.Build-Script",
|
||||
"interfaces/Crafter.Build-Progress",
|
||||
"interfaces/Crafter.Build-Asset",
|
||||
};
|
||||
std::array<fs::path, 12> implementations = {
|
||||
std::array<fs::path, 11> implementations = {
|
||||
"implementations/Crafter.Build-Toolchain",
|
||||
"implementations/Crafter.Build-Shader",
|
||||
"implementations/Crafter.Build-Platform",
|
||||
|
|
@ -204,7 +75,6 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
|||
"implementations/Crafter.Build-Clang",
|
||||
"implementations/Crafter.Build-Test",
|
||||
"implementations/Crafter.Build-Lint",
|
||||
"implementations/Crafter.Build-Script",
|
||||
"implementations/Crafter.Build-Progress",
|
||||
"implementations/Crafter.Build-Asset",
|
||||
};
|
||||
|
|
@ -256,12 +126,6 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
|||
cfg.AddTest("CleanProject").Dependencies({ CrafterBuildLib.get() });
|
||||
cfg.AddTest("ShaderCompile").Dependencies({ CrafterBuildLib.get() });
|
||||
cfg.AddTest("ShaderVariantBuild").Dependencies({ CrafterBuildLib.get() });
|
||||
cfg.AddTest("ShaderIncludeChange").Dependencies({ CrafterBuildLib.get() });
|
||||
cfg.AddTest("MingwMissing").Dependencies({ CrafterBuildLib.get() });
|
||||
cfg.AddTest("GitSubmodules").Dependencies({ CrafterBuildLib.get() });
|
||||
cfg.AddTest("LinkFlagsChange").Dependencies({ CrafterBuildLib.get() });
|
||||
cfg.AddTest("ExternalArtifactTime").Dependencies({ CrafterBuildLib.get() });
|
||||
cfg.AddTest("ConcurrentExternalFetch").Dependencies({ CrafterBuildLib.get() });
|
||||
cfg.AddTest("StandardArgs").Dependencies({ CrafterBuildLib.get() });
|
||||
cfg.AddTest("TestRunnerSpec").Dependencies({ CrafterBuildLib.get() });
|
||||
cfg.AddTest("VariantId").Dependencies({ CrafterBuildLib.get() });
|
||||
|
|
@ -286,8 +150,6 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
|||
.LinkFlag("-Wl,--export-dynamic").LinkFlag("-ldl");
|
||||
cfg.AddTest("ProjectDependency").Dependencies({ CrafterBuildLib.get() })
|
||||
.LinkFlag("-Wl,--export-dynamic").LinkFlag("-ldl");
|
||||
cfg.AddTest("Script").Dependencies({ CrafterBuildLib.get() })
|
||||
.LinkFlag("-Wl,--export-dynamic").LinkFlag("-ldl");
|
||||
cfg.AddTest("ConcurrentDependencyReset").Dependencies({ CrafterBuildLib.get() });
|
||||
cfg.AddTest("Lint").Dependencies({ CrafterBuildLib.get() });
|
||||
}
|
||||
|
|
@ -297,9 +159,5 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
|||
// rules also auto-fix under `crafter-build format`.
|
||||
LintRules::AddRules(cfg);
|
||||
|
||||
// See the top of this file.
|
||||
cfg.scripts.push_back({"flatpak", BuildFlatpak});
|
||||
cfg.scripts.push_back({"install", InstallFlatpak});
|
||||
|
||||
return cfg;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,12 +29,6 @@ namespace {
|
|||
std::ofstream(root / "keep-me.txt") << "untouched";
|
||||
return root;
|
||||
}
|
||||
|
||||
std::int32_t RunWith(std::vector<std::string> args) {
|
||||
std::vector<char*> argv;
|
||||
for (std::string& arg : args) argv.push_back(arg.data());
|
||||
return Run(static_cast<std::int32_t>(argv.size()), argv.data());
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
|
|
@ -77,22 +71,6 @@ int main() {
|
|||
Check(fs::exists(cwdBin) == cwdBinExisted, "the cwd's bin/ is untouched");
|
||||
}
|
||||
|
||||
{
|
||||
// `clean` is a verb only where a verb can stand. After `format` it
|
||||
// is a rule-name glob like any other word, and a glob must never
|
||||
// delete the build trees. The project file cannot compile, so the
|
||||
// run fails once it gets as far as loading it — which is the proof
|
||||
// that it went on to format instead of cleaning and exiting 0.
|
||||
fs::path root = StageProject("format-glob");
|
||||
fs::path projectFile = root / "project.cpp";
|
||||
std::ofstream(projectFile) << "this is not valid C++ at all\n";
|
||||
|
||||
const std::int32_t exitCode = RunWith({"crafter-build", "format", "clean", std::format("--project={}", projectFile.string())});
|
||||
Check(fs::exists(root / "bin" / "app-x86_64-pc-linux-gnu-native-native-0badf00d" / "app"), "`format clean` leaves bin/ alone");
|
||||
Check(fs::exists(root / "build" / "app-x86_64-pc-linux-gnu-native-native-0badf00d" / "main_impl.o"), "`format clean` leaves build/ alone");
|
||||
Check(exitCode != 0, "`format clean` goes on to load the project instead of cleaning");
|
||||
}
|
||||
|
||||
if (Failures > 0) {
|
||||
std::println(std::cerr, "{} assertions failed", Failures);
|
||||
return 1;
|
||||
|
|
|
|||
|
|
@ -1,95 +0,0 @@
|
|||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
import std;
|
||||
import Crafter.Build;
|
||||
namespace fs = std::filesystem;
|
||||
using namespace Crafter;
|
||||
|
||||
// Builds that run at the same time share their dependencies' clones in the
|
||||
// cache: the tests `crafter-build test --jobs=N` builds side by side each
|
||||
// declare the same ExternalDependency, and two crafter-builds can run at
|
||||
// once. Fetching a clone that already exists ran `git pull` (and the CMake
|
||||
// build) in it with nothing to keep a second fetch out, and the second git
|
||||
// fails on the first one's index.lock. A clone's fetch and build now hold a
|
||||
// lock of their own.
|
||||
//
|
||||
// Eight threads fetch the same tracked branch of a local repository at
|
||||
// once, a few rounds over, the branch moving between rounds so every pull
|
||||
// has work to do; every fetch has to succeed. Everything is local;
|
||||
// XDG_CACHE_HOME keeps the clone out of the real cache.
|
||||
|
||||
extern "C" int setenv(const char* name, const char* value, int overwrite);
|
||||
|
||||
namespace {
|
||||
std::int32_t failures = 0;
|
||||
|
||||
void Check(bool cond, std::string_view msg) {
|
||||
if (!cond) {
|
||||
std::println(std::cerr, "FAIL: {}", msg);
|
||||
++failures;
|
||||
}
|
||||
}
|
||||
|
||||
void Git(const fs::path& dir, std::string_view args) {
|
||||
const std::string command = std::format("git -C {} {}", ShellQuote(dir.string()), args);
|
||||
auto result = RunCommandWithTimeout(command, std::chrono::seconds(60));
|
||||
if (result.exitCode != 0) throw std::runtime_error(std::format("{} failed: {}", command, result.output));
|
||||
}
|
||||
|
||||
void Commit(const fs::path& repo, std::uint32_t round) {
|
||||
std::ofstream(repo / "round.txt", std::ios::binary | std::ios::trunc) << round;
|
||||
Git(repo, "add round.txt");
|
||||
Git(repo, std::format("commit -q -m round-{}", round));
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
const fs::path scratch = fs::temp_directory_path() / "crafter-build-concurrent-external-fetch";
|
||||
fs::remove_all(scratch);
|
||||
fs::create_directories(scratch);
|
||||
setenv("XDG_CACHE_HOME", (scratch / "cache").c_str(), 1);
|
||||
setenv("GIT_CONFIG_NOSYSTEM", "1", 1);
|
||||
setenv("GIT_CONFIG_GLOBAL", "/dev/null", 1);
|
||||
for (const char* name : { "GIT_AUTHOR_NAME", "GIT_COMMITTER_NAME" }) setenv(name, "crafter-build test", 1);
|
||||
for (const char* name : { "GIT_AUTHOR_EMAIL", "GIT_COMMITTER_EMAIL" }) setenv(name, "test@example.invalid", 1);
|
||||
|
||||
const fs::path repo = scratch / "shared";
|
||||
fs::create_directories(repo);
|
||||
Git(repo, "init -q -b main");
|
||||
Commit(repo, 0);
|
||||
const GitSource source { .url = std::format("file://{}", repo.string()), .branch = "main" };
|
||||
const fs::path clone = GitFetch(source);
|
||||
|
||||
constexpr std::uint32_t threads = 8;
|
||||
constexpr std::uint32_t rounds = 4;
|
||||
std::vector<std::string> errors;
|
||||
std::mutex errorsMutex;
|
||||
for (std::uint32_t round = 1; round <= rounds; ++round) {
|
||||
Commit(repo, round);
|
||||
std::vector<std::jthread> fetchers;
|
||||
for (std::uint32_t i = 0; i < threads; ++i) {
|
||||
fetchers.emplace_back([&] {
|
||||
try {
|
||||
(void)GitFetch(source);
|
||||
} catch (const std::exception& e) {
|
||||
std::lock_guard lock(errorsMutex);
|
||||
errors.push_back(e.what());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Check(errors.empty(), std::format("{} fetches at once, {} rounds: every one succeeds ({} failed{}{})", threads, rounds, errors.size(), errors.empty() ? "" : ", the first: ", errors.empty() ? "" : errors.front()));
|
||||
std::ifstream round(clone / "round.txt");
|
||||
std::uint32_t last = 0;
|
||||
round >> last;
|
||||
Check(last == rounds, std::format("and the clone ends at the last round ({})", last));
|
||||
|
||||
std::error_code ec;
|
||||
fs::remove_all(scratch, ec);
|
||||
if (failures > 0) {
|
||||
std::println(std::cerr, "{} assertions failed", failures);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -1,89 +0,0 @@
|
|||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
import std;
|
||||
import Crafter.Build;
|
||||
namespace fs = std::filesystem;
|
||||
using namespace Crafter;
|
||||
|
||||
// A CMake dependency's newest library decides when its consumers are stale
|
||||
// (BuildExternal's latestArtifact). Only files directly in the CMake build
|
||||
// directory named *.a or *.so were looked at, so msquic's
|
||||
// bin/Release/libmsquic.so.2, in one of the dependency's libDirs and with a
|
||||
// version after the .so, never counted: a rebuilt msquic left everything
|
||||
// built against the old one in place.
|
||||
//
|
||||
// The dependency here is a local repository whose CMake project puts a
|
||||
// versioned shared library in bin/Release, declared as a libDir. Everything
|
||||
// is local; XDG_CACHE_HOME keeps the clone out of the real cache.
|
||||
|
||||
extern "C" int setenv(const char* name, const char* value, int overwrite);
|
||||
|
||||
namespace {
|
||||
std::int32_t failures = 0;
|
||||
|
||||
void Check(bool cond, std::string_view msg) {
|
||||
if (!cond) {
|
||||
std::println(std::cerr, "FAIL: {}", msg);
|
||||
++failures;
|
||||
}
|
||||
}
|
||||
|
||||
void Git(const fs::path& dir, std::string_view args) {
|
||||
const std::string command = std::format("git -C {} {}", ShellQuote(dir.string()), args);
|
||||
auto result = RunCommandWithTimeout(command, std::chrono::seconds(60));
|
||||
if (result.exitCode != 0) throw std::runtime_error(std::format("{} failed: {}", command, result.output));
|
||||
}
|
||||
|
||||
void Write(const fs::path& file, std::string_view text) {
|
||||
std::ofstream(file, std::ios::binary | std::ios::trunc) << text;
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
const fs::path scratch = fs::temp_directory_path() / "crafter-build-external-artifact-time";
|
||||
fs::remove_all(scratch);
|
||||
fs::create_directories(scratch);
|
||||
setenv("XDG_CACHE_HOME", (scratch / "cache").c_str(), 1);
|
||||
setenv("GIT_CONFIG_NOSYSTEM", "1", 1);
|
||||
setenv("GIT_CONFIG_GLOBAL", "/dev/null", 1);
|
||||
for (const char* name : { "GIT_AUTHOR_NAME", "GIT_COMMITTER_NAME" }) setenv(name, "crafter-build test", 1);
|
||||
for (const char* name : { "GIT_AUTHOR_EMAIL", "GIT_COMMITTER_EMAIL" }) setenv(name, "test@example.invalid", 1);
|
||||
|
||||
const fs::path repo = scratch / "versioned";
|
||||
fs::create_directories(repo);
|
||||
Write(repo / "lib.c", "int Versioned(void) { return 2; }\n");
|
||||
Write(repo / "CMakeLists.txt",
|
||||
"cmake_minimum_required(VERSION 3.16)\n"
|
||||
"project(versioned C)\n"
|
||||
"add_library(versioned SHARED lib.c)\n"
|
||||
"set_target_properties(versioned PROPERTIES VERSION 2.0.0 SOVERSION 2\n"
|
||||
" LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin/Release)\n");
|
||||
Git(repo, "init -q -b main");
|
||||
Git(repo, "add .");
|
||||
Git(repo, "commit -q -m versioned");
|
||||
|
||||
ExternalDependency dep;
|
||||
dep.name = "versioned";
|
||||
dep.source.url = std::format("file://{}", repo.string());
|
||||
dep.builder = ExternalBuilder::CMake;
|
||||
dep.libDirs = { "bin/Release" };
|
||||
dep.libs = { "versioned" };
|
||||
std::atomic<bool> cancelled = false;
|
||||
const ExternalBuildResult result = BuildExternal(dep, HostTarget(), cancelled);
|
||||
Check(result.error.empty(), std::format("the dependency builds: {}", result.error));
|
||||
|
||||
const fs::path library = ExternalCloneDir(dep, HostTarget()) / "build" / "bin" / "Release" / "libversioned.so.2.0.0";
|
||||
Check(fs::exists(library), std::format("the library is where its CMake project puts it: {}", library.string()));
|
||||
if (fs::exists(library)) {
|
||||
Check(result.latestArtifact == fs::last_write_time(library), "its newest artifact is the versioned library in the libDir");
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
fs::remove_all(scratch, ec);
|
||||
if (failures > 0) {
|
||||
std::println(std::cerr, "{} assertions failed", failures);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -1,128 +0,0 @@
|
|||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
import std;
|
||||
import Crafter.Build;
|
||||
namespace fs = std::filesystem;
|
||||
using namespace Crafter;
|
||||
|
||||
// A fetched dependency's submodules have to match the revision it was fetched
|
||||
// at. They used to come from the remote's default branch: a fresh clone was
|
||||
// `git clone --recursive`, which checks out the default branch's submodules,
|
||||
// and the `switch` or `checkout` to the requested branch or commit that came
|
||||
// after it left them where they were. A branch that was pulled forward kept
|
||||
// the submodules of its old tip as well. So pinning msquic to a release built
|
||||
// that release against the main branch's quictls.
|
||||
//
|
||||
// The test makes a repository whose submodule moves between two of its
|
||||
// commits, then fetches it four ways through GitFetch:
|
||||
// - pinned to the older commit;
|
||||
// - by a branch at the older commit;
|
||||
// - by a branch that is then moved to the newer commit and fetched again;
|
||||
// - pinned again after the cached clone's submodule was checked out
|
||||
// elsewhere, as an older crafter-build left it.
|
||||
// Everything is local; XDG_CACHE_HOME keeps the clones out of the real cache.
|
||||
|
||||
extern "C" int setenv(const char* name, const char* value, int overwrite);
|
||||
|
||||
namespace {
|
||||
std::int32_t failures = 0;
|
||||
|
||||
void Check(bool cond, std::string_view msg) {
|
||||
if (!cond) {
|
||||
std::println(std::cerr, "FAIL: {}", msg);
|
||||
++failures;
|
||||
}
|
||||
}
|
||||
|
||||
// Runs git in `dir`; the output, or an exception naming the command.
|
||||
std::string Git(const fs::path& dir, std::string_view args) {
|
||||
const std::string command = std::format("git -C {} {}", ShellQuote(dir.string()), args);
|
||||
auto result = RunCommandWithTimeout(command, std::chrono::seconds(60));
|
||||
if (result.exitCode != 0) throw std::runtime_error(std::format("{} failed: {}", command, result.output));
|
||||
while (!result.output.empty() && (result.output.back() == '\n' || result.output.back() == '\r')) result.output.pop_back();
|
||||
return result.output;
|
||||
}
|
||||
|
||||
void Write(const fs::path& file, std::string_view text) {
|
||||
std::ofstream(file, std::ios::binary | std::ios::trunc) << text;
|
||||
}
|
||||
|
||||
std::string Read(const fs::path& file) {
|
||||
std::ifstream stream(file, std::ios::binary);
|
||||
return std::string(std::istreambuf_iterator<char>(stream), std::istreambuf_iterator<char>());
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
const fs::path scratch = fs::temp_directory_path() / "crafter-build-git-submodules";
|
||||
fs::remove_all(scratch);
|
||||
fs::create_directories(scratch);
|
||||
setenv("XDG_CACHE_HOME", (scratch / "cache").c_str(), 1);
|
||||
// A known git: no user or system configuration, an identity for the
|
||||
// commits, and file:// allowed for submodules (git refuses it by default),
|
||||
// also for the git that GitFetch runs.
|
||||
setenv("GIT_CONFIG_NOSYSTEM", "1", 1);
|
||||
setenv("GIT_CONFIG_GLOBAL", "/dev/null", 1);
|
||||
setenv("GIT_CONFIG_COUNT", "1", 1);
|
||||
setenv("GIT_CONFIG_KEY_0", "protocol.file.allow", 1);
|
||||
setenv("GIT_CONFIG_VALUE_0", "always", 1);
|
||||
for (const char* name : { "GIT_AUTHOR_NAME", "GIT_COMMITTER_NAME" }) setenv(name, "crafter-build test", 1);
|
||||
for (const char* name : { "GIT_AUTHOR_EMAIL", "GIT_COMMITTER_EMAIL" }) setenv(name, "test@example.invalid", 1);
|
||||
|
||||
// The submodule: "one", then "two".
|
||||
const fs::path sub = scratch / "sub";
|
||||
fs::create_directories(sub);
|
||||
Git(sub, "init -q -b main");
|
||||
Write(sub / "value.txt", "one");
|
||||
Git(sub, "add value.txt");
|
||||
Git(sub, "commit -q -m one");
|
||||
const std::string one = Git(sub, "rev-parse HEAD");
|
||||
Write(sub / "value.txt", "two");
|
||||
Git(sub, "commit -q -am two");
|
||||
const std::string two = Git(sub, "rev-parse HEAD");
|
||||
|
||||
// The superproject: `older` has the submodule at "one", main at "two".
|
||||
const fs::path super = scratch / "super";
|
||||
fs::create_directories(super);
|
||||
Git(super, "init -q -b main");
|
||||
Git(super, std::format("submodule --quiet add {} lib", ShellQuote(std::format("file://{}", sub.string()))));
|
||||
Git(super / "lib", std::format("checkout -q {}", one));
|
||||
Git(super, "add lib");
|
||||
Git(super, "commit -q -m older");
|
||||
const std::string older = Git(super, "rev-parse HEAD");
|
||||
Git(super, "branch older");
|
||||
Git(super, "branch track");
|
||||
Git(super / "lib", std::format("checkout -q {}", two));
|
||||
Git(super, "add lib");
|
||||
Git(super, "commit -q -m newer");
|
||||
const std::string url = std::format("file://{}", super.string());
|
||||
|
||||
// ── Pinned to the older commit ───────────────────────────────────────────
|
||||
const fs::path pinned = GitFetch({ .url = url, .commit = older });
|
||||
Check(Read(pinned / "lib" / "value.txt") == "one", std::format("a commit pin gets that commit's submodule (lib says {})", Read(pinned / "lib" / "value.txt")));
|
||||
|
||||
// ── By a branch at the older commit ──────────────────────────────────────
|
||||
const fs::path branch = GitFetch({ .url = url, .branch = "older" });
|
||||
Check(Read(branch / "lib" / "value.txt") == "one", std::format("a branch gets its own tip's submodule (lib says {})", Read(branch / "lib" / "value.txt")));
|
||||
|
||||
// ── A tracked branch moving forward ──────────────────────────────────────
|
||||
const fs::path tracked = GitFetch({ .url = url, .branch = "track" });
|
||||
Check(Read(tracked / "lib" / "value.txt") == "one", "a tracked branch starts with its tip's submodule");
|
||||
Git(super, "branch -f track main");
|
||||
const fs::path pulled = GitFetch({ .url = url, .branch = "track" });
|
||||
Check(pulled == tracked && Read(pulled / "lib" / "value.txt") == "two", std::format("pulling it forward moves the submodule along (lib says {})", Read(pulled / "lib" / "value.txt")));
|
||||
|
||||
// ── A cached clone with its submodule out of step ────────────────────────
|
||||
Git(pinned / "lib", std::format("checkout -q {}", two));
|
||||
const fs::path repaired = GitFetch({ .url = url, .commit = older });
|
||||
Check(repaired == pinned && Read(repaired / "lib" / "value.txt") == "one", std::format("a cached clone at the pin gets its submodule put back (lib says {})", Read(repaired / "lib" / "value.txt")));
|
||||
|
||||
std::error_code ec;
|
||||
fs::remove_all(scratch, ec);
|
||||
if (failures > 0) {
|
||||
std::println(std::cerr, "{} assertions failed", failures);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
int main() {
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -1,119 +0,0 @@
|
|||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
import std;
|
||||
import Crafter.Build;
|
||||
namespace fs = std::filesystem;
|
||||
using namespace Crafter;
|
||||
|
||||
// A change to what an executable is linked with has to relink it, even when
|
||||
// no object changed. The relink check compared modification times only, so a
|
||||
// link flag added in project.cpp, or a dependency's library moving (an
|
||||
// ExternalDependency whose options changed is fetched and built into a new
|
||||
// cache directory, and its -L and rpath follow), left the old binary in
|
||||
// place: Crafter.Network's tests kept loading the msquic they were first
|
||||
// linked against.
|
||||
//
|
||||
// The test adds an rpath to an executable's link flags, takes it away again,
|
||||
// and then builds once more with nothing changed, which must not relink.
|
||||
// The flags come out of sets the dependency threads fill as they finish, so
|
||||
// the recorded command lists them sorted: in the order they happened to land
|
||||
// in, an unchanged build could look like a changed one.
|
||||
|
||||
namespace {
|
||||
std::int32_t failures = 0;
|
||||
|
||||
void Check(bool cond, std::string_view msg) {
|
||||
if (!cond) {
|
||||
std::println(std::cerr, "FAIL: {}", msg);
|
||||
++failures;
|
||||
}
|
||||
}
|
||||
|
||||
constexpr std::string_view marker = "/crafter-build-link-flags-change-marker";
|
||||
|
||||
BuildResult BuildOnce(const fs::path& project, std::vector<std::string> linkFlags) {
|
||||
Configuration app;
|
||||
app.path = project;
|
||||
app.name = "linked";
|
||||
app.outputName = "linked";
|
||||
app.target = HostTarget();
|
||||
app.type = ConfigurationType::Executable;
|
||||
app.linkFlags = std::move(linkFlags);
|
||||
std::array<fs::path, 0> ifaces = {};
|
||||
std::array<fs::path, 1> impls = { "main" };
|
||||
app.GetInterfacesAndImplementations(ifaces, impls);
|
||||
std::unordered_map<fs::path, std::shared_future<BuildResult>> depResults;
|
||||
std::mutex depMutex;
|
||||
BuildResult result = Build(app, depResults, depMutex);
|
||||
return result;
|
||||
}
|
||||
|
||||
Configuration Plain(const fs::path& project) {
|
||||
Configuration app;
|
||||
app.path = project;
|
||||
app.name = "linked";
|
||||
app.outputName = "linked";
|
||||
app.target = HostTarget();
|
||||
app.type = ConfigurationType::Executable;
|
||||
return app;
|
||||
}
|
||||
|
||||
fs::path Binary(const fs::path& project) {
|
||||
return Plain(project).BinDir() / "linked";
|
||||
}
|
||||
|
||||
bool HasMarker(const fs::path& binary) {
|
||||
std::ifstream stream(binary, std::ios::binary);
|
||||
const std::string bytes((std::istreambuf_iterator<char>(stream)), std::istreambuf_iterator<char>());
|
||||
return bytes.find(marker) != std::string::npos;
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
const fs::path project = fs::temp_directory_path() / "crafter-build-link-flags-change";
|
||||
fs::remove_all(project);
|
||||
fs::copy(fs::current_path() / "tests" / "LinkFlagsChange" / "fixture", project, fs::copy_options::recursive);
|
||||
const std::string rpath = std::format("-Wl,-rpath,{}", marker);
|
||||
|
||||
const BuildResult first = BuildOnce(project, {});
|
||||
Check(first.result.empty(), std::format("the plain build succeeds: {}", first.result));
|
||||
const fs::path binary = Binary(project);
|
||||
Check(fs::exists(binary) && !HasMarker(binary), "the plain binary has no rpath to the marker");
|
||||
|
||||
const BuildResult added = BuildOnce(project, { rpath });
|
||||
Check(added.result.empty(), std::format("the build with the rpath succeeds: {}", added.result));
|
||||
Check(HasMarker(binary), "adding a link flag relinks the binary");
|
||||
|
||||
const BuildResult removed = BuildOnce(project, {});
|
||||
Check(removed.result.empty(), std::format("the build without it succeeds: {}", removed.result));
|
||||
Check(!HasMarker(binary), "taking it away relinks it again");
|
||||
|
||||
const fs::file_time_type linked = fs::last_write_time(binary);
|
||||
const BuildResult again = BuildOnce(project, {});
|
||||
Check(again.result.empty() && fs::last_write_time(binary) == linked, "an unchanged build doesn't relink");
|
||||
|
||||
const std::vector<std::string> flags = { "-Wl,-z,now", "-Wl,-O1", "-Wl,--as-needed", "-Wl,-z,relro", "-Wl,--build-id", "-Wl,--hash-style=gnu" };
|
||||
const BuildResult many = BuildOnce(project, flags);
|
||||
Check(many.result.empty(), std::format("the build with six flags succeeds: {}", many.result));
|
||||
std::ifstream recorded(Plain(project).BuildDir() / "linked.link", std::ios::binary);
|
||||
const std::string record((std::istreambuf_iterator<char>(recorded)), std::istreambuf_iterator<char>());
|
||||
std::vector<std::size_t> at;
|
||||
for (const std::string& flag : flags) at.push_back(record.find(std::format(" {}", flag)));
|
||||
std::vector<std::string> sorted = flags;
|
||||
std::ranges::sort(sorted);
|
||||
bool inOrder = !std::ranges::contains(at, std::string::npos);
|
||||
for (std::size_t k = 0; inOrder && k + 1 < sorted.size(); ++k) {
|
||||
const auto index = [&](const std::string& flag) { return at[static_cast<std::size_t>(std::ranges::find(flags, flag) - flags.begin())]; };
|
||||
inOrder = index(sorted[k]) < index(sorted[k + 1]);
|
||||
}
|
||||
Check(inOrder, std::format("the link record lists the flags sorted: {}", record));
|
||||
|
||||
std::error_code ec;
|
||||
fs::remove_all(project, ec);
|
||||
if (failures > 0) {
|
||||
std::println(std::cerr, "{} assertions failed", failures);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
import std;
|
||||
|
||||
int main() {
|
||||
std::println("hello from crafter-build");
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -1,97 +0,0 @@
|
|||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
import std;
|
||||
import Crafter.Build;
|
||||
namespace fs = std::filesystem;
|
||||
using namespace Crafter;
|
||||
|
||||
// Building for x86_64-w64-mingw32 without a usable mingw-w64 libstdc++ has to
|
||||
// fail the build with a message that says what is missing. It used to abort
|
||||
// the whole process with a bare "libc++abi: terminating": the std module's
|
||||
// source was found by listing <mingw sysroot>/include/c++ with a throwing
|
||||
// directory_iterator, the exception unwound through Build() while its worker
|
||||
// threads were still joinable, and ~thread called std::terminate. That is
|
||||
// what every mingw build inside the Flatpak did, as the sandbox has no
|
||||
// /usr/x86_64-w64-mingw32.
|
||||
//
|
||||
// The std module's source is now found through libstdc++.modules.json in the
|
||||
// mingw toolchain's library directories. CRAFTER_MINGW_DIR points the
|
||||
// toolchain at a scratch prefix, which the test takes through four states:
|
||||
// - missing;
|
||||
// - with only a manifest in the prefix's generic lib/, where the host's
|
||||
// Linux libstdc++ keeps one: not the mingw library, so still missing;
|
||||
// - with a mingw manifest that names no std module;
|
||||
// - with one whose std module source isn't there.
|
||||
|
||||
extern "C" int setenv(const char* name, const char* value, int overwrite);
|
||||
|
||||
namespace {
|
||||
std::int32_t failures = 0;
|
||||
|
||||
void Check(bool cond, std::string_view msg) {
|
||||
if (!cond) {
|
||||
std::println(std::cerr, "FAIL: {}", msg);
|
||||
++failures;
|
||||
}
|
||||
}
|
||||
|
||||
BuildResult BuildOnce(const fs::path& project) {
|
||||
Configuration app;
|
||||
app.path = project;
|
||||
app.name = "hello";
|
||||
app.outputName = "hello";
|
||||
app.target = "x86_64-w64-mingw32";
|
||||
app.type = ConfigurationType::Executable;
|
||||
std::array<fs::path, 0> ifaces = {};
|
||||
std::array<fs::path, 1> impls = { "main" };
|
||||
app.GetInterfacesAndImplementations(ifaces, impls);
|
||||
std::unordered_map<fs::path, std::shared_future<BuildResult>> depResults;
|
||||
std::mutex depMutex;
|
||||
return Build(app, depResults, depMutex);
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
const fs::path scratch = fs::temp_directory_path() / "crafter-build-mingw-missing";
|
||||
fs::remove_all(scratch);
|
||||
fs::create_directories(scratch);
|
||||
const fs::path project = scratch / "project";
|
||||
fs::copy(fs::current_path() / "tests" / "MingwMissing" / "fixture", project, fs::copy_options::recursive);
|
||||
const fs::path sysroot = scratch / "mingw";
|
||||
// Before anything resolves the toolchain, which reads both once. The std
|
||||
// module cache goes to the scratch directory too.
|
||||
setenv("CRAFTER_MINGW_DIR", sysroot.c_str(), 1);
|
||||
setenv("XDG_CACHE_HOME", (scratch / "cache").c_str(), 1);
|
||||
|
||||
const BuildResult missing = BuildOnce(project);
|
||||
Check(!missing.result.empty(), "a missing mingw prefix fails the build");
|
||||
Check(missing.result.contains("mingw") && missing.result.contains(sysroot.string()) && missing.result.contains("CRAFTER_MINGW_DIR"), std::format("and says where it looked and how to point elsewhere: {}", missing.result));
|
||||
|
||||
auto write = [](const fs::path& file, std::string_view text) {
|
||||
fs::create_directories(file.parent_path());
|
||||
std::ofstream(file, std::ios::binary | std::ios::trunc) << text;
|
||||
};
|
||||
write(sysroot / "lib" / "libstdc++.modules.json", R"({"version": 1, "modules": [{"logical-name": "std", "source-path": "../include/c++/16/bits/std.cc", "is-std-library": true}]})");
|
||||
write(sysroot / "include" / "c++" / "16" / "bits" / "std.cc", "// the host's Linux std module\n");
|
||||
const BuildResult linux = BuildOnce(project);
|
||||
Check(linux.result.contains("no mingw-w64 libstdc++"), std::format("a manifest outside the mingw toolchain's own directories isn't taken for it: {}", linux.result));
|
||||
|
||||
const fs::path manifest = sysroot / "x86_64-w64-mingw32" / "lib" / "libstdc++.modules.json";
|
||||
write(manifest, R"({"version": 1, "modules": [{"logical-name": "std.compat", "source-path": "../include/c++/15.1.0/bits/std.compat.cc"}]})");
|
||||
const BuildResult noStd = BuildOnce(project);
|
||||
Check(noStd.result.contains(manifest.string()) && noStd.result.contains("no std module"), std::format("a manifest without the std module fails the build, naming the manifest: {}", noStd.result));
|
||||
|
||||
write(manifest, R"({"version": 1, "modules": [{"source-path": "../include/c++/15.1.0/bits/std.cc", "logical-name": "std"}]})");
|
||||
const BuildResult noSource = BuildOnce(project);
|
||||
Check(noSource.result.contains((sysroot / "x86_64-w64-mingw32" / "include" / "c++" / "15.1.0" / "bits" / "std.cc").string()) && noSource.result.contains("missing"),
|
||||
std::format("a std module source that isn't there fails the build, naming it: {}", noSource.result));
|
||||
|
||||
std::error_code ec;
|
||||
fs::remove_all(scratch, ec);
|
||||
if (failures > 0) {
|
||||
std::println(std::cerr, "{} assertions failed", failures);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
import std;
|
||||
import Crafter.Build;
|
||||
using namespace Crafter;
|
||||
|
||||
// Scripts that report what they were handed. The test that loads this file
|
||||
// drives the real command line and can only observe a script through its
|
||||
// exit code and through what it leaves in the project directory.
|
||||
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> args) {
|
||||
Configuration cfg;
|
||||
cfg.path = "./";
|
||||
cfg.name = "script-fixture";
|
||||
cfg.outputName = "script-fixture";
|
||||
// Applied so that a script argument which leaked into the project's
|
||||
// args would be visible: in cfg.projectArgs, or as cfg.debug.
|
||||
ApplyStandardArgs(cfg, args);
|
||||
|
||||
cfg.scripts.push_back({"record", [](ScriptContext& ctx) {
|
||||
std::ofstream out(ctx.projectDir / "record.txt");
|
||||
for (const std::string& arg : ctx.args) std::println(out, "arg:{}", arg);
|
||||
for (const std::string& arg : ctx.project.projectArgs) std::println(out, "projectArg:{}", arg);
|
||||
std::println(out, "debug:{}", ctx.project.debug);
|
||||
std::println(out, "project:{}", ctx.project.name);
|
||||
return 42;
|
||||
}});
|
||||
cfg.scripts.push_back({"shell", [](ScriptContext& ctx) { return ctx.Run("exit 7"); }});
|
||||
return cfg;
|
||||
}
|
||||
|
|
@ -1,241 +0,0 @@
|
|||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
import std;
|
||||
import Crafter.Build;
|
||||
namespace fs = std::filesystem;
|
||||
using namespace Crafter;
|
||||
|
||||
// `crafter-build run <name> [args...]` runs a script project.cpp put in
|
||||
// cfg.scripts and exits with what the script returned.
|
||||
//
|
||||
// Two things are easy to get wrong and expensive when they are. The words
|
||||
// after the script's name belong to the script: read by crafter-build
|
||||
// instead, `run package clean` deletes the build tree, `run package --help`
|
||||
// prints the wrong help, and anything else lands in the project's args,
|
||||
// where it changes the variant hash and so the directory the project builds
|
||||
// into. And the exit code is the script's whole result: collapsed to 0 or 1
|
||||
// it loses what a caller branches on, and passed through unchecked a value
|
||||
// such as 256 wraps to success.
|
||||
namespace {
|
||||
std::int32_t Failures = 0;
|
||||
|
||||
void Check(bool cond, std::string_view msg) {
|
||||
if (!cond) {
|
||||
std::println(std::cerr, "FAIL: {}", msg);
|
||||
++Failures;
|
||||
}
|
||||
}
|
||||
|
||||
std::int32_t RunWith(std::vector<std::string> args) {
|
||||
std::vector<char*> argv;
|
||||
for (std::string& arg : args) argv.push_back(arg.data());
|
||||
return Run(static_cast<std::int32_t>(argv.size()), argv.data());
|
||||
}
|
||||
|
||||
std::vector<std::string> ReadLines(const fs::path& file) {
|
||||
std::vector<std::string> lines;
|
||||
std::ifstream in(file);
|
||||
for (std::string line; std::getline(in, line);) lines.push_back(line);
|
||||
return lines;
|
||||
}
|
||||
|
||||
// One script that returns `code`, run by name.
|
||||
std::int32_t RunReturning(std::int32_t code) {
|
||||
Configuration cfg;
|
||||
cfg.scripts.push_back({"it", [code](ScriptContext&) { return code; }});
|
||||
return RunScript(cfg, {.name = "it", .named = true});
|
||||
}
|
||||
|
||||
// LoadProject compiles a project file against the installed module
|
||||
// sources; without them the command-line cases cannot run at all.
|
||||
bool ModuleSourcesInstalled() {
|
||||
try {
|
||||
return fs::exists(GetCrafterBuildHome() / "Crafter.Build.cppm");
|
||||
} catch (const std::exception&) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
const fs::path fixture = fs::current_path() / "tests" / "Script" / "fixture";
|
||||
const fs::path scratch = fs::temp_directory_path() / "crafter-build-script";
|
||||
std::error_code ec;
|
||||
fs::remove_all(scratch, ec);
|
||||
fs::create_directories(scratch);
|
||||
fs::copy(fixture, scratch, fs::copy_options::recursive);
|
||||
|
||||
// The script gets the project, the project's directory and its
|
||||
// arguments exactly as given, and its return value comes back.
|
||||
{
|
||||
Configuration cfg;
|
||||
std::int32_t calls = 0;
|
||||
std::vector<std::string> seen;
|
||||
const Configuration* seenProject = nullptr;
|
||||
fs::path seenDir;
|
||||
cfg.scripts.push_back({"other", [&](ScriptContext&) {
|
||||
++calls;
|
||||
return 9;
|
||||
}});
|
||||
cfg.scripts.push_back({"pass", [&](ScriptContext& ctx) {
|
||||
++calls;
|
||||
seen.assign(ctx.args.begin(), ctx.args.end());
|
||||
seenProject = &ctx.project;
|
||||
seenDir = ctx.projectDir;
|
||||
return 3;
|
||||
}});
|
||||
const std::vector<std::string> given = {"a", "--help", "", "two words", "it's"};
|
||||
const std::int32_t code = RunScript(cfg, {.name = "pass", .named = true, .args = given, .projectFile = scratch / "project.cpp"});
|
||||
Check(code == 3, std::format("the script's return value is the result (got {})", code));
|
||||
Check(calls == 1, std::format("only the named script ran (ran {})", calls));
|
||||
Check(seen == given, "the arguments arrive verbatim, empty ones and option-like ones included");
|
||||
Check(seenProject == &cfg, "ctx.project is the configuration the script was declared on");
|
||||
Check(seenDir == scratch, std::format("ctx.projectDir is the project file's directory (got '{}')", seenDir.string()));
|
||||
|
||||
// Asking for the list runs nothing, with or without a name.
|
||||
calls = 0;
|
||||
Check(RunScript(cfg, {}) == 0, "a run without a name lists and succeeds");
|
||||
Check(RunScript(cfg, {.name = "pass", .named = true, .listOnly = true}) == 0, "--list succeeds");
|
||||
Check(calls == 0, "listing runs no script");
|
||||
|
||||
// A name nobody declared. The empty string is one: it is a name
|
||||
// that was given, not the absence of a name.
|
||||
Check(RunScript(cfg, {.name = "nope", .named = true}) == 1, "an unknown name fails");
|
||||
Check(RunScript(cfg, {.name = "", .named = true}) == 1, "an empty name is looked up and fails instead of listing");
|
||||
Check(RunScript(cfg, {.name = "pas", .named = true}) == 1, "names match exactly, not by prefix");
|
||||
Check(calls == 0, "an unknown name runs no script");
|
||||
}
|
||||
|
||||
// The exit status passes through whole, and only an exit status does.
|
||||
Check(RunReturning(0) == 0, "0 stays 0");
|
||||
Check(RunReturning(1) == 1, "1 stays 1");
|
||||
Check(RunReturning(77) == 77, "77 stays 77");
|
||||
Check(RunReturning(255) == 255, "255 stays 255");
|
||||
Check(RunReturning(256) == 1, "256 would wrap to success as an exit status and becomes 1");
|
||||
Check(RunReturning(-1) == 1, "a negative value becomes 1");
|
||||
|
||||
// A script that throws is that script's failure, not a crash.
|
||||
{
|
||||
Configuration cfg;
|
||||
cfg.scripts.push_back({"throws", [](ScriptContext&) -> std::int32_t { throw std::runtime_error("no such tool"); }});
|
||||
Check(RunScript(cfg, {.name = "throws", .named = true}) == 1, "a throwing script exits 1");
|
||||
cfg.scripts.push_back({"throwsInt", [](ScriptContext&) -> std::int32_t { throw 7; }});
|
||||
Check(RunScript(cfg, {.name = "throwsInt", .named = true}) == 1, "and so does one throwing something that isn't a std::exception");
|
||||
}
|
||||
|
||||
// Declarations that cannot work are refused before anything runs, also
|
||||
// when a different, valid script was asked for.
|
||||
{
|
||||
Configuration none;
|
||||
Check(RunScript(none, {.name = "it", .named = true}) == 1, "a project without scripts fails");
|
||||
Check(RunScript(none, {}) == 1, "listing a project without scripts fails too");
|
||||
|
||||
std::int32_t calls = 0;
|
||||
const auto count = [&](ScriptContext&) {
|
||||
++calls;
|
||||
return 0;
|
||||
};
|
||||
Configuration twice;
|
||||
twice.scripts.push_back({"a", count});
|
||||
twice.scripts.push_back({"b", count});
|
||||
twice.scripts.push_back({"a", count});
|
||||
Check(RunScript(twice, {.name = "b", .named = true}) == 1, "a name declared twice fails the run");
|
||||
Check(RunScript(twice, {}) == 1, "a name declared twice fails the list");
|
||||
|
||||
Configuration dashed;
|
||||
dashed.scripts.push_back({"ok", count});
|
||||
dashed.scripts.push_back({"-x", count});
|
||||
Check(RunScript(dashed, {.name = "ok", .named = true}) == 1, "a name starting with '-' could never be typed and is refused");
|
||||
|
||||
Configuration unnamed;
|
||||
unnamed.scripts.push_back({"", count});
|
||||
Check(RunScript(unnamed, {.name = "", .named = true}) == 1, "an empty name is refused");
|
||||
|
||||
Configuration hollow;
|
||||
hollow.scripts.push_back({"hollow", nullptr});
|
||||
Check(RunScript(hollow, {.name = "hollow", .named = true}) == 1, "a script without a callback is refused");
|
||||
Check(calls == 0, "none of these ran a script");
|
||||
}
|
||||
|
||||
// ctx.Run: the command's real exit code, a signal death the way a shell
|
||||
// reports it, and ShellQuote carrying awkward text through sh intact.
|
||||
{
|
||||
const std::string awkward = "it's a \"test\" $HOME `id` \\ *";
|
||||
const fs::path echoed = scratch / "echoed file.txt";
|
||||
Configuration cfg;
|
||||
cfg.scripts.push_back({"exit", [](ScriptContext& ctx) { return ctx.Run("exit 7"); }});
|
||||
cfg.scripts.push_back({"signal", [](ScriptContext& ctx) { return ctx.Run("kill -TERM $$"); }});
|
||||
cfg.scripts.push_back({"quote", [&](ScriptContext& ctx) { return ctx.Run(std::format("printf %s {} > {}", ShellQuote(awkward), ShellQuote(echoed.string()))); }});
|
||||
// A script is usually several commands: stop at the first failure.
|
||||
cfg.scripts.push_back({"steps", [&](ScriptContext& ctx) {
|
||||
if (const std::int32_t code = ctx.Run("exit 5"); code != 0) return code;
|
||||
return ctx.Run(std::format("touch {}", ShellQuote((scratch / "not-reached").string())));
|
||||
}});
|
||||
Check(RunScript(cfg, {.name = "exit", .named = true}) == 7, "ctx.Run returns the command's exit code");
|
||||
Check(RunScript(cfg, {.name = "signal", .named = true}) == 143, "a command killed by SIGTERM reads as 128+15");
|
||||
Check(RunScript(cfg, {.name = "quote", .named = true}) == 0, "a quoted command line runs");
|
||||
const std::vector<std::string> lines = ReadLines(echoed);
|
||||
Check(lines.size() == 1 && lines[0] == awkward, "ShellQuote passes quotes, spaces and expansions through sh untouched");
|
||||
Check(RunScript(cfg, {.name = "steps", .named = true}) == 5, "a failed step's code can be returned as the script's");
|
||||
Check(!fs::exists(scratch / "not-reached"), "and the steps after it do not run");
|
||||
}
|
||||
|
||||
if (!ModuleSourcesInstalled()) {
|
||||
// A skip must never cover for a failure above.
|
||||
if (Failures > 0) return 1;
|
||||
std::println(std::cerr, "SKIP: no module sources to load the fixture project with");
|
||||
return 77;
|
||||
}
|
||||
|
||||
// The same through the command line, where the parsing lives. Sentinels
|
||||
// stand in for a built project: a word after `run` that is read as the
|
||||
// clean verb takes them with it.
|
||||
const std::string project = std::format("--project={}", (scratch / "project.cpp").string());
|
||||
const fs::path binSentinel = scratch / "bin" / "sentinel";
|
||||
const fs::path buildSentinel = scratch / "build" / "sentinel";
|
||||
fs::create_directories(binSentinel.parent_path());
|
||||
fs::create_directories(buildSentinel.parent_path());
|
||||
std::ofstream(binSentinel) << "binary";
|
||||
std::ofstream(buildSentinel) << "object";
|
||||
|
||||
// Everything after the name is the script's, including this tool's own
|
||||
// options and verbs; what stands before `run` is still the project's.
|
||||
{
|
||||
const std::int32_t code = RunWith({"crafter-build", project, "--flavour=x", "run", "record", "a", "--help", "-v", "clean", "--debug", "--version", "run"});
|
||||
Check(code == 42, std::format("the script ran and its return value is the exit code (got {})", code));
|
||||
const std::vector<std::string> expected = {
|
||||
"arg:a", "arg:--help", "arg:-v", "arg:clean", "arg:--debug", "arg:--version", "arg:run",
|
||||
"projectArg:--flavour=x",
|
||||
"debug:false",
|
||||
"project:script-fixture",
|
||||
};
|
||||
const std::vector<std::string> record = ReadLines(scratch / "record.txt");
|
||||
Check(record == expected, "the script got every word after its name and the project args got none of them");
|
||||
if (record != expected) {
|
||||
for (const std::string& line : record) std::println(std::cerr, " recorded: {}", line);
|
||||
}
|
||||
Check(fs::exists(binSentinel) && fs::exists(buildSentinel), "`clean` as a script argument deleted nothing");
|
||||
}
|
||||
|
||||
// The first word after `run` is the name even when it is a verb.
|
||||
{
|
||||
fs::remove(scratch / "record.txt");
|
||||
Check(RunWith({"crafter-build", project, "run", "clean"}) == 1, "`run clean` looks for a script called clean");
|
||||
Check(fs::exists(binSentinel) && fs::exists(buildSentinel), "`run clean` deleted nothing");
|
||||
Check(RunWith({"crafter-build", project, "run", "help"}) == 1, "`run help` looks for a script called help");
|
||||
}
|
||||
|
||||
Check(RunWith({"crafter-build", project, "run", "shell"}) == 7, "a command's exit code reaches the process exit code");
|
||||
Check(RunWith({"crafter-build", project, "run", "-q", "shell"}) == 7, "an option between `run` and the name is still this tool's");
|
||||
Check(RunWith({"crafter-build", project, "run"}) == 0, "a bare `run` lists");
|
||||
Check(RunWith({"crafter-build", project, "run", "--list"}) == 0, "`run --list` lists");
|
||||
Check(!fs::exists(scratch / "record.txt"), "listing ran no script");
|
||||
Check(RunWith({"crafter-build", project, "run", "nope"}) == 1, "an unknown script fails");
|
||||
|
||||
if (Failures > 0) {
|
||||
std::println(std::cerr, "{} assertions failed", Failures);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -27,7 +27,7 @@ int main() {
|
|||
Shader shader(fs::path(src), "main", ShaderType::Fragment);
|
||||
|
||||
std::array<fs::path, 0> includeDirs = {};
|
||||
std::string err = shader.Compile(outDir, outDir, includeDirs);
|
||||
std::string err = shader.Compile(outDir, includeDirs);
|
||||
if (!err.empty()) {
|
||||
std::println(std::cerr, "shader compile failed: {}", err);
|
||||
return 1;
|
||||
|
|
@ -51,27 +51,10 @@ int main() {
|
|||
}
|
||||
|
||||
// Compiling again should be a no-op since the source isn't newer.
|
||||
if (!shader.Check(outDir, outDir, includeDirs)) {
|
||||
if (!shader.Check(outDir)) {
|
||||
std::println(std::cerr, "Shader::Check returned false on an up-to-date spv");
|
||||
return 1;
|
||||
}
|
||||
// Searched elsewhere, an #include could find another file: stale.
|
||||
const std::array<fs::path, 1> elsewhere = { outDir / "elsewhere" };
|
||||
if (shader.Check(outDir, outDir, elsewhere)) {
|
||||
std::println(std::cerr, "Shader::Check called an spv up to date for other include directories");
|
||||
return 1;
|
||||
}
|
||||
// So could a source of the same name moved to another directory, which
|
||||
// its #includes are searched from first.
|
||||
const fs::path movedDir = outDir / "moved";
|
||||
fs::create_directories(movedDir);
|
||||
fs::copy_file(src, movedDir / src.filename());
|
||||
fs::last_write_time(movedDir / src.filename(), fs::last_write_time(src));
|
||||
Shader moved(movedDir / src.filename(), "main", ShaderType::Fragment);
|
||||
if (moved.Check(outDir, outDir, includeDirs)) {
|
||||
std::println(std::cerr, "Shader::Check called an spv up to date for a source of the same name elsewhere");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// One source registered twice: the second entry carries defines and a
|
||||
// variant name. It must land in its own file instead of overwriting the
|
||||
|
|
@ -94,16 +77,16 @@ int main() {
|
|||
// exists, the variant is still due. A Check() that ignored the
|
||||
// variant would call it up to date here, and Build() would then
|
||||
// never compile a variant added to an already built project.
|
||||
if (each == &red && red.Check(outDir, outDir, includeDirs)) {
|
||||
if (each == &red && red.Check(outDir)) {
|
||||
std::println(std::cerr, "Shader::Check called the variant up to date before it was ever compiled");
|
||||
return 1;
|
||||
}
|
||||
std::string variantErr = each->Compile(outDir, outDir, includeDirs);
|
||||
std::string variantErr = each->Compile(outDir, includeDirs);
|
||||
if (!variantErr.empty()) {
|
||||
std::println(std::cerr, "variant shader compile failed: {}", variantErr);
|
||||
return 1;
|
||||
}
|
||||
if (!each->Check(outDir, outDir, includeDirs)) {
|
||||
if (!each->Check(outDir)) {
|
||||
std::println(std::cerr, "Shader::Check returned false for {}", each->OutputName().string());
|
||||
return 1;
|
||||
}
|
||||
|
|
@ -116,14 +99,14 @@ int main() {
|
|||
Shader malformed(fs::path(variantSrc), "main", ShaderType::Fragment);
|
||||
malformed.defines = {bad};
|
||||
malformed.variant = "malformed";
|
||||
if (!malformed.Compile(outDir, outDir, includeDirs).contains("invalid shader define")) {
|
||||
if (!malformed.Compile(outDir, includeDirs).contains("invalid shader define")) {
|
||||
std::println(std::cerr, "a malformed define was not rejected by name");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
Shader nested(fs::path(variantSrc), "main", ShaderType::Fragment);
|
||||
nested.variant = "sub/red";
|
||||
if (!nested.Compile(outDir, outDir, includeDirs).contains("path separator")) {
|
||||
if (!nested.Compile(outDir, includeDirs).contains("path separator")) {
|
||||
std::println(std::cerr, "a variant with a path separator was not rejected by name");
|
||||
return 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
const vec4 kTint = vec4(0.0, 1.0, 0.0, 1.0);
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
#version 450
|
||||
#extension GL_GOOGLE_include_directive : require
|
||||
|
||||
// Built by the ShaderIncludeChange test, which edits palette.glsl and this
|
||||
// entry's defines between builds.
|
||||
#include "palette.glsl"
|
||||
|
||||
layout(location = 0) out vec4 outColor;
|
||||
|
||||
void main() {
|
||||
#ifdef LIT_DIM
|
||||
outColor = kTint * 0.5;
|
||||
#else
|
||||
outColor = kTint;
|
||||
#endif
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
int main() {
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
#version 450
|
||||
|
||||
// Includes nothing: the ShaderIncludeChange test checks it is left alone.
|
||||
layout(location = 0) out vec4 outColor;
|
||||
|
||||
void main() {
|
||||
outColor = vec4(0.0, 0.0, 1.0, 1.0);
|
||||
}
|
||||
|
|
@ -1,126 +0,0 @@
|
|||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
import std;
|
||||
import Crafter.Build;
|
||||
namespace fs = std::filesystem;
|
||||
using namespace Crafter;
|
||||
|
||||
// A shader's output is stale when anything that went into it changed, not
|
||||
// only its own source. Check() used to compare the .spv against the source's
|
||||
// mtime alone, so:
|
||||
// - editing a file the shader #includes left the .spv built against the old
|
||||
// contents, the GLSL twin of IncrementalHeaderChange;
|
||||
// - changing an entry's defines without touching the source did too.
|
||||
// Each pass changes one thing and checks both directions: the shader it
|
||||
// reaches is recompiled, to new contents, and the shader that includes
|
||||
// nothing is left alone. A build with nothing changed compiles nothing.
|
||||
namespace {
|
||||
std::int32_t failures = 0;
|
||||
|
||||
void Check(bool cond, std::string_view msg) {
|
||||
if (!cond) {
|
||||
std::println(std::cerr, "FAIL: {}", msg);
|
||||
++failures;
|
||||
}
|
||||
}
|
||||
|
||||
// The fixture is edited between passes, so work on a copy outside the repo.
|
||||
fs::path StageFixture() {
|
||||
fs::path source = fs::current_path() / "tests" / "ShaderIncludeChange" / "fixture";
|
||||
fs::path staged = fs::temp_directory_path() / "crafter-build-shader-include-change";
|
||||
fs::remove_all(staged);
|
||||
fs::copy(source, staged, fs::copy_options::recursive);
|
||||
return staged;
|
||||
}
|
||||
|
||||
Configuration MakeApp(const fs::path& staged) {
|
||||
Configuration app;
|
||||
app.path = staged;
|
||||
app.name = "lit";
|
||||
app.outputName = "lit";
|
||||
app.target = HostTarget();
|
||||
app.type = ConfigurationType::Executable;
|
||||
std::array<fs::path, 0> ifaces = {};
|
||||
std::array<fs::path, 1> impls = { "main" };
|
||||
app.GetInterfacesAndImplementations(ifaces, impls);
|
||||
app.shaders.emplace_back(staged / "lit.frag", std::string("main"), ShaderType::Fragment);
|
||||
app.shaders.emplace_back(staged / "plain.frag", std::string("main"), ShaderType::Fragment);
|
||||
// Puts include/ on the shaders' #include path.
|
||||
app.buildFiles = { staged / "include" / "palette.glsl" };
|
||||
return app;
|
||||
}
|
||||
|
||||
bool BuildOk(Configuration& app, std::string_view label) {
|
||||
// A fresh depResults per pass: the map memoizes a Configuration's
|
||||
// build for the duration of one pass.
|
||||
std::unordered_map<fs::path, std::shared_future<BuildResult>> depResults;
|
||||
std::mutex depMutex;
|
||||
BuildResult r = Build(app, depResults, depMutex);
|
||||
Check(r.result.empty(), std::format("{} builds: {}", label, r.result));
|
||||
return r.result.empty();
|
||||
}
|
||||
|
||||
std::string ReadAll(const fs::path& file) {
|
||||
std::ifstream stream(file, std::ios::binary);
|
||||
return std::string(std::istreambuf_iterator<char>(stream), std::istreambuf_iterator<char>());
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
const fs::path staged = StageFixture();
|
||||
Configuration app = MakeApp(staged);
|
||||
const fs::path lit = app.BinDir() / "lit.spv";
|
||||
const fs::path plain = app.BinDir() / "plain.spv";
|
||||
const fs::path palette = staged / "include" / "palette.glsl";
|
||||
|
||||
// Pass 1: everything from scratch.
|
||||
if (!BuildOk(app, "the first pass")) return 1;
|
||||
Check(fs::exists(lit) && fs::exists(plain), "both shaders are in the bin dir");
|
||||
const fs::file_time_type plainBuilt = fs::last_write_time(plain);
|
||||
const std::string green = ReadAll(lit);
|
||||
|
||||
// Pass 2: nothing changed.
|
||||
fs::file_time_type litBuilt = fs::last_write_time(lit);
|
||||
BuildOk(app, "an unchanged project");
|
||||
Check(fs::last_write_time(lit) == litBuilt && fs::last_write_time(plain) == plainBuilt, "with nothing changed, no shader is recompiled");
|
||||
|
||||
// Pass 3: the included file changes. Dated just after the output it must
|
||||
// outdate, so the rebuilt output is newer than it again.
|
||||
{
|
||||
std::ofstream out(palette, std::ios::trunc);
|
||||
out << "const vec4 kTint = vec4(1.0, 0.0, 0.0, 1.0);\n";
|
||||
}
|
||||
fs::last_write_time(palette, litBuilt + std::chrono::milliseconds(1));
|
||||
BuildOk(app, "an edited include");
|
||||
const std::string red = ReadAll(lit);
|
||||
Check(fs::last_write_time(lit) > litBuilt && red != green, "editing an included file recompiles the shader that includes it, to new contents");
|
||||
Check(fs::last_write_time(plain) == plainBuilt, "and leaves the shader that includes nothing alone");
|
||||
|
||||
// Pass 4: the entry's defines change, no file does.
|
||||
app.shaders[0].defines = { "LIT_DIM" };
|
||||
litBuilt = fs::last_write_time(lit);
|
||||
BuildOk(app, "a new define");
|
||||
const std::string dim = ReadAll(lit);
|
||||
Check(fs::last_write_time(lit) > litBuilt && dim != red, "a define added to the entry recompiles it, taking the #ifdef branch");
|
||||
Check(fs::last_write_time(plain) == plainBuilt, "and leaves the other entry alone");
|
||||
|
||||
// Pass 5: and back.
|
||||
app.shaders[0].defines = {};
|
||||
BuildOk(app, "the define taken out");
|
||||
Check(ReadAll(lit) == red, "taking the define out again recompiles it as it was");
|
||||
|
||||
// Pass 6: nothing changed again; what the passes above recorded holds.
|
||||
litBuilt = fs::last_write_time(lit);
|
||||
BuildOk(app, "an unchanged project, again");
|
||||
Check(fs::last_write_time(lit) == litBuilt && fs::last_write_time(plain) == plainBuilt, "with nothing changed since, no shader is recompiled");
|
||||
|
||||
std::error_code ec;
|
||||
fs::remove_all(staged, ec);
|
||||
|
||||
if (failures > 0) {
|
||||
std::println(std::cerr, "{} assertions failed", failures);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -61,7 +61,7 @@ int main() {
|
|||
Check(tc.libcxxVersion == LibcxxVersion("/usr/include/c++/v1"), "the system libc++ headers state the release of the system module source");
|
||||
Check(tc.stdModulesDir.empty(), "no crafter-build prefix, no shipped module sources");
|
||||
Check(tc.wasiSysroot == "/usr/share/wasi-sysroot", "the system wasi sysroot is the default");
|
||||
Check(tc.mingwSysroot.empty(), "outside a sandbox, clang finds mingw-w64 itself: no --sysroot");
|
||||
Check(tc.mingwSysroot == "/usr/x86_64-w64-mingw32" && !tc.mingwSysrootExplicit, "the default mingw dir is not passed as --sysroot");
|
||||
Check(tc.LibcxxCompileFlags().empty() && tc.LibcxxLinkFlags().empty(), "the PATH fallback adds no libc++ flags");
|
||||
}
|
||||
{
|
||||
|
|
@ -144,21 +144,7 @@ int main() {
|
|||
in.env["CRAFTER_MINGW_DIR"] = "/opt/mingw";
|
||||
Toolchain tc = ResolveToolchain(in);
|
||||
Check(tc.wasiSysroot == "/opt/wasi", "CRAFTER_BUILD_WASI_SYSROOT overrides the wasi sysroot");
|
||||
Check(tc.mingwSysroot == "/opt/mingw", "CRAFTER_MINGW_DIR names the mingw prefix passed as --sysroot");
|
||||
}
|
||||
{
|
||||
// Sandboxed, the host's /usr stands in for the missing mingw-w64,
|
||||
// unless CRAFTER_MINGW_DIR says otherwise.
|
||||
const fs::path host = fs::temp_directory_path() / "crafter-build-toolchain-host";
|
||||
fs::remove_all(host);
|
||||
ToolchainInputs in;
|
||||
in.hostRoot = host;
|
||||
Check(ResolveToolchain(in).mingwSysroot.empty(), "a host without mingw-w64 gives no --sysroot");
|
||||
fs::create_directories(host / "usr" / "x86_64-w64-mingw32");
|
||||
Check(ResolveToolchain(in).mingwSysroot == host / "usr", "a host with mingw-w64 gives its /usr as the mingw prefix");
|
||||
in.env["CRAFTER_MINGW_DIR"] = "/opt/mingw";
|
||||
Check(ResolveToolchain(in).mingwSysroot == "/opt/mingw", "CRAFTER_MINGW_DIR wins over the host's");
|
||||
fs::remove_all(host);
|
||||
Check(tc.mingwSysroot == "/opt/mingw" && tc.mingwSysrootExplicit, "CRAFTER_MINGW_DIR overrides the mingw dir and is passed as --sysroot");
|
||||
}
|
||||
{
|
||||
// The cache key separates toolchains that would build different
|
||||
|
|
|
|||
Loading…
Reference in a new issue