Compare commits

..
27 changed files with 1111 additions and 154 deletions

View file

@ -51,8 +51,9 @@ jobs:
persist-credentials: true persist-credentials: true
- name: Cache glslang clone+build - name: Cache glslang clone+build
# Cache covers crafter-build's external dep cache. The first run builds # Cache covers crafter-build's per-(target, march) external dep cache.
# glslang; the cache makes subsequent runs reuse that build. # First run builds glslang up to 6 times (3 marches × 2 targets); the
# cache makes subsequent runs reuse those builds.
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
path: | path: |
@ -88,14 +89,27 @@ jobs:
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/master' if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/master'
run: CRAFTER_BUILD_HOME=$PWD/share/crafter-build CRAFTER_BUILD_MARCH=x86-64-v4 ./bin/crafter-build run: CRAFTER_BUILD_HOME=$PWD/share/crafter-build CRAFTER_BUILD_MARCH=x86-64-v4 ./bin/crafter-build
# - name: Build Windows x86-64-v2 (mingw)
# if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/master'
# run: CRAFTER_BUILD_HOME=$PWD/share/crafter-build ./bin/crafter-build --target=x86_64-w64-mingw32
# - name: Build Windows x86-64-v3 (mingw)
# if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/master'
# run: CRAFTER_BUILD_HOME=$PWD/share/crafter-build CRAFTER_BUILD_MARCH=x86-64-v3 ./bin/crafter-build --target=x86_64-w64-mingw32
# - name: Build Windows x86-64-v4 (mingw)
# if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/master'
# run: CRAFTER_BUILD_HOME=$PWD/share/crafter-build CRAFTER_BUILD_MARCH=x86-64-v4 ./bin/crafter-build --target=x86_64-w64-mingw32
- name: Package artifacts - name: Package artifacts
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/master' if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/master'
run: | run: |
set -eux set -eux
mkdir -p dist mkdir -p dist
# 3 archives, one per march level. Each carries the executable, the # 6 archives — 3 marches × 2 OSes. Each is self-contained: the
# static library and share/; libc++ comes from the host. # Linux launcher is statically linked; the Windows launcher has
# crafter-build.dll + libcrafter-build.dll.a alongside the exe.
for march in x86-64-v2 x86-64-v3 x86-64-v4; do for march in x86-64-v2 x86-64-v3 x86-64-v4; do
# Short suffix (v2/v3/v4) for archive filenames; the upload-artifact # Short suffix (v2/v3/v4) for archive filenames; the upload-artifact
# steps below reference these short names. # steps below reference these short names.
@ -110,12 +124,18 @@ jobs:
cp bin/crafter.build-lib-x86_64-pc-linux-gnu-$march-*/libcrafter-build.a "$stage_lin/lib/" cp bin/crafter.build-lib-x86_64-pc-linux-gnu-$march-*/libcrafter-build.a "$stage_lin/lib/"
cp -r share "$stage_lin/" cp -r share "$stage_lin/"
tar czf "dist/crafter-build-linux-x86_64-$short.tar.gz" -C "$stage_lin" . tar czf "dist/crafter-build-linux-x86_64-$short.tar.gz" -C "$stage_lin" .
# stage_win=$(mktemp -d)
# mkdir -p "$stage_win/bin"
# cp bin/crafter.build-exe-x86_64-w64-mingw32-$march-*/* "$stage_win/bin/"
# cp -r share "$stage_win/"
# (cd "$stage_win" && zip -r "$GITHUB_WORKSPACE/dist/crafter-build-windows-x86_64-$short.zip" .)
done done
ls -la dist/ ls -la dist/
# Upload each archive as its own workflow artifact (one upload per file) # Upload each archive as its own workflow artifact (one upload per file)
# so users browsing the PR / run page get small, individually-named # so users browsing the PR / run page get six small, individually-named
# downloads instead of one wrapper zip containing the lot. # downloads instead of one wrapper zip containing the lot.
- name: Upload Linux v2 artifact - name: Upload Linux v2 artifact
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/master' if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/master'
@ -129,6 +149,18 @@ jobs:
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/master' if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/master'
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: { name: crafter-build-linux-x86_64-v4, path: dist/crafter-build-linux-x86_64-v4.tar.gz, if-no-files-found: error } with: { name: crafter-build-linux-x86_64-v4, path: dist/crafter-build-linux-x86_64-v4.tar.gz, if-no-files-found: error }
# - name: Upload Windows v2 artifact
# if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/master'
# uses: actions/upload-artifact@v3
# with: { name: crafter-build-windows-x86_64-v2, path: dist/crafter-build-windows-x86_64-v2.zip, if-no-files-found: error }
# - name: Upload Windows v3 artifact
# if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/master'
# uses: actions/upload-artifact@v3
# with: { name: crafter-build-windows-x86_64-v3, path: dist/crafter-build-windows-x86_64-v3.zip, if-no-files-found: error }
# - name: Upload Windows v4 artifact
# if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/master'
# uses: actions/upload-artifact@v3
# with: { name: crafter-build-windows-x86_64-v4, path: dist/crafter-build-windows-x86_64-v4.zip, if-no-files-found: error }
- name: Update rolling 'latest' tag - name: Update rolling 'latest' tag
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/master' if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/master'
@ -140,7 +172,7 @@ jobs:
- name: Publish rolling 'latest' release - name: Publish rolling 'latest' release
# forgejo-release uploads each file in release-dir as a separate # forgejo-release uploads each file in release-dir as a separate
# release asset, so the Releases page shows individually # release asset, so the Releases page shows six individually
# downloadable archives — pick the v-level matching your CPU. # downloadable archives — pick the v-level matching your CPU.
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/master' if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/master'
uses: https://code.forgejo.org/actions/forgejo-release@v2 uses: https://code.forgejo.org/actions/forgejo-release@v2

View file

@ -8,12 +8,12 @@ crafter-build is a C++26 modules build system. A project is described by a `proj
| Path | What lives there | | Path | What lives there |
|---|---| |---|---|
| `interfaces/Crafter.Build-<Partition>.cppm` | Module interface partitions. `Crafter.Build.cppm` is the umbrella that re-exports them. | | `interfaces/Crafter.Build-<Partition>.cppm` | Module interface partitions. `Crafter.Build.cppm` is the umbrella that re-exports them. `Crafter.Build-Api.h` defines `CRAFTER_API`. |
| `implementations/Crafter.Build-<Partition>.cpp` | Implementation units, one per partition, named `Crafter.Build:<Partition>_impl`. `main.cpp` is a thin wrapper around `Crafter::Run`. | | `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 and the lint rules. | | `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. | | `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. | | `examples/` | Standalone user-facing projects, MIT licensed. |
| `build.sh` | Bootstrap script. It compiles everything by hand, without crafter-build. | | `build.sh`, `build.cmd` | Bootstrap scripts for Linux and Windows. They compile everything by hand, without crafter-build. |
| `wasi-runtime/` | `runtime.js` and `index.html.in`, shipped for browser targets. | | `wasi-runtime/` | `runtime.js` and `index.html.in`, shipped for browser targets. |
| `lib/` | Vendored third-party headers (nlohmann/json, glslang's DirStackFileIncluder). Do not edit. | | `lib/` | Vendored third-party headers (nlohmann/json, glslang's DirStackFileIncluder). Do not edit. |
| `share/crafter-build/`, `bin/`, `build/`, `lib/*.a` | Generated and gitignored. | | `share/crafter-build/`, `bin/`, `build/`, `lib/*.a` | Generated and gitignored. |
@ -42,10 +42,12 @@ Three things to know about this loop:
- `bin/crafter-build` is the bootstrap binary and the harness for every command below. The self-host build writes to `bin/crafter.build-exe-<variant>/crafter-build` and does not replace it. - `bin/crafter-build` is the bootstrap binary and the harness for every command below. The self-host build writes to `bin/crafter.build-exe-<variant>/crafter-build` and does not replace it.
- 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. - 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. - A new `CRAFTER_API` 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`. 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`.
Windows builds use `build.cmd` with `LIBCXX_DIR` pointing at a libc++ install; the Linux-to-mingw cross-compile is a separate artifact with a different ABI. See the README. Windows branches of recent features are wired but often unverified: say so in the PR when you touch one and cannot test it.
## Build, test and style commands ## Build, test and style commands
```bash ```bash
@ -102,9 +104,10 @@ Suppress one finding with `// lint-disable-next-line <rule>` or a whole file wit
Beyond the rules: Beyond the rules:
- `import std;` everywhere; standard headers are never `#include`d. C headers (`clang-c/Index.h`, `dlfcn.h`) go in the global module fragment after `module;`. - `import std;` everywhere; standard headers are never `#include`d. C headers (`clang-c/Index.h`, `dlfcn.h`, `windows.h`) go in the global module fragment after `module;`.
- Mark every function or method that `project.cpp` or a downstream consumer calls with `CRAFTER_API` in the interface. It is the Windows DLL boundary.
- Comments explain why: the failure mode, the constraint, the rejected alternative. The codebase carries long comments on purpose. Do not strip them, and write them for new non-obvious code. Commit bodies follow the same rule. - Comments explain why: the failure mode, the constraint, the rejected alternative. The codebase carries long comments on purpose. Do not strip them, and write them for new non-obvious code. Commit bodies follow the same rule.
- There is no host-platform preprocessor split: the host is Linux. Target differences (mingw, wasm) branch on `cfg.target` strings at runtime, in the library as well as in `project.cpp` and tests. - Platform splits live in `:Platform`, guarded by `CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc` and `_x86_64_w64_mingw32`. In `project.cpp` and tests, branch on `cfg.target` strings instead of preprocessor macros.
- A build step fails when its command exits nonzero, never because it printed. Use `RunBuildCommand` or `RunCommandChecked`; warnings go to `Progress::Diagnostic`. - A build step fails when its command exits nonzero, never because it printed. Use `RunBuildCommand` or `RunCommandChecked`; warnings go to `Progress::Diagnostic`.
- Every output path goes through `Configuration::BinDir()`, `BuildDir()` and `PcmDir()`. Hand-rolled `path / "bin" / ...` clobbered variants before; do not add new ones. - Every output path goes through `Configuration::BinDir()`, `BuildDir()` and `PcmDir()`. Hand-rolled `path / "bin" / ...` clobbered variants before; do not add new ones.
- Anything that changes what gets compiled or bundled belongs in `VariantId()`. It already hashes type, debug, sysroot, defines, compile flags, wasm variant flags and uninterpreted project args. - Anything that changes what gets compiled or bundled belongs in `VariantId()`. It already hashes type, debug, sysroot, defines, compile flags, wasm variant flags and uninterpreted project args.
@ -116,7 +119,7 @@ These were decided deliberately. Do not undo them without the maintainer's say-s
1. **Configuration is C++.** No JSON, YAML, TOML or DSL for project, runner or lint configuration. Lint rules are C++ callbacks; there is no clang-format or clang-tidy integration and no separate "format rule" type. A rule that calls `ctx.SetContent` is a transform, and that is the whole mechanism. 1. **Configuration is C++.** No JSON, YAML, TOML or DSL for project, runner or lint configuration. Lint rules are C++ callbacks; there is no clang-format or clang-tidy integration and no separate "format rule" type. A rule that calls `ctx.SetContent` is a transform, and that is the whole mechanism.
2. **Per-project artifacts go in `<project>/build` and `<project>/bin`. Only cross-project things go in `~/.cache/crafter.build/`:** the std PCM and Crafter.Build PCMs per target and march, and external clones keyed by their spec hash. 2. **Per-project artifacts go in `<project>/build` and `<project>/bin`. Only cross-project things go in `~/.cache/crafter.build/`:** the std PCM and Crafter.Build PCMs per target and march, and external clones keyed by their spec hash.
3. **Sources ship, PCMs build locally.** Packages install `*.cppm` under `share/crafter-build/`. Nothing target-baked ships except the executable itself, and that defaults to a portable `-march` in `PKGBUILD` and CI. 3. **Sources ship, PCMs build locally.** Packages install `*.cppm` under `share/crafter-build/`. Nothing target-baked ships except the executable itself, and that defaults to a portable `-march` in `PKGBUILD` and CI.
4. **crafter-build is a monolithic executable with `--export-dynamic`.** `project.so` resolves `Crafter::` symbols from the running binary at dlopen time. There is no Windows host; `x86_64-w64-mingw32` is a cross-compile target for user projects only. 4. **Linux is a monolithic executable with `--export-dynamic`; Windows is a DLL plus a launcher.** The asymmetry is intentional.
5. **Convention over configuration.** Lead with the zero-config path. Env var, then CLI flag, then full `project.cpp` are the escalation, not the entry point. 5. **Convention over configuration.** Lead with the zero-config path. Env var, then CLI flag, then full `project.cpp` are the escalation, not the entry point.
6. **One skip mechanism** (runner probe plus exit 77) and **one `TestRunner` shape** (template fields with `{bin}`, `{bundle}` and similar placeholders; `Cmd(...)` covers any wrapper). Do not add a special-case runner class or a hardcoded "skip on this OS" branch. 6. **One skip mechanism** (runner probe plus exit 77) and **one `TestRunner` shape** (template fields with `{bin}`, `{bundle}` and similar placeholders; `Cmd(...)` covers any wrapper). Do not add a special-case runner class or a hardcoded "skip on this OS" branch.
7. **The house-style rules stay in their own repo.** They are not library API of crafter-build and are not forced on downstream projects. 7. **The house-style rules stay in their own repo.** They are not library API of crafter-build and are not forced on downstream projects.
@ -151,3 +154,4 @@ These were decided deliberately. Do not undo them without the maintainer's say-s
- The bootstrap binary is compiled without `CRAFTER_BUILD_HAS_ASSET`, so `cfg.assets` only works in the self-hosted binary. Changing asset extensions means updating two maps in `Crafter.Build-Clang.cpp`: `assetOutput` and the transitive copy loop. - The bootstrap binary is compiled without `CRAFTER_BUILD_HAS_ASSET`, so `cfg.assets` only works in the self-hosted binary. Changing asset extensions means updating two maps in `Crafter.Build-Clang.cpp`: `assetOutput` and the transitive copy loop.
- A warning is printed only when its unit actually recompiles. A build with nothing to do is silent, which is not evidence that a warning is gone. - A warning is printed only when its unit actually recompiles. A build with nothing to do is silent, which is not evidence that a warning is gone.
- `VariantId()` names the output directory, so CI globs `bin/crafter.build-exe-<target>-<march>-*/` rather than predicting the hash. - `VariantId()` names the output directory, so CI globs `bin/crafter.build-exe-<target>-<march>-*/` rather than predicting the hash.
- When driving `build.cmd` from a Windows `cmd` shell over SSH: write `set VAR=value&&next` without a space before `&&`, and `if exist X rmdir /S /Q X` instead of chaining `rmdir` with `&&`.

View file

@ -29,6 +29,7 @@ package() {
install -Dm644 lib/libcrafter-build.a "$pkgdir/usr/lib/libcrafter-build.a" install -Dm644 lib/libcrafter-build.a "$pkgdir/usr/lib/libcrafter-build.a"
install -dm755 "$pkgdir/usr/share/crafter-build" install -dm755 "$pkgdir/usr/share/crafter-build"
install -m644 share/crafter-build/*.cppm "$pkgdir/usr/share/crafter-build/" install -m644 share/crafter-build/*.cppm "$pkgdir/usr/share/crafter-build/"
install -m644 share/crafter-build/*.h "$pkgdir/usr/share/crafter-build/"
install -dm755 "$pkgdir/usr/share/crafter-build/wasi-runtime" install -dm755 "$pkgdir/usr/share/crafter-build/wasi-runtime"
install -m644 share/crafter-build/wasi-runtime/runtime.js "$pkgdir/usr/share/crafter-build/wasi-runtime/" install -m644 share/crafter-build/wasi-runtime/runtime.js "$pkgdir/usr/share/crafter-build/wasi-runtime/"
install -m644 share/crafter-build/wasi-runtime/index.html.in "$pkgdir/usr/share/crafter-build/wasi-runtime/" install -m644 share/crafter-build/wasi-runtime/index.html.in "$pkgdir/usr/share/crafter-build/wasi-runtime/"

View file

@ -154,15 +154,18 @@ Outcomes: ✅ pass, ❌ fail, 💥 crash (with signal name), ⏱ timeout, ⏭ sk
```cpp ```cpp
TestRunner::Local() // run on this machine (default) TestRunner::Local() // run on this machine (default)
TestRunner::Cmd("wine") // local exec via prefix command TestRunner::Cmd("wine") // local exec via prefix command
TestRunner::Ssh("hostname") // remote linux via scp + ssh
TestRunner::SshWin("winhost") // remote windows via scp + ssh-cmd.exe
TestRunner::Wsl() // Linux binaries via WSL2 (Windows host)
``` ```
`Cmd(cmd)` is a generic `<cmd> <bin> <args>` prefix runner — covers wine, qemu-user, valgrind, gdbserver, anything with that shape. Probes via `which <cmd>`. `Cmd(cmd)` is a generic `<cmd> <bin> <args>` prefix runner — covers wine, qemu-user, valgrind, gdbserver, anything with that shape. Probes via `which <cmd>` (or `where` on Windows hosts). The `Ssh` variants `scp -r` the test's whole bin dir, run the binary remotely, then clean up; each invocation uses a unique remote subdir so parallel runs don't collide. `Wsl()` mirrors the SSH model but copies the bundle into WSL's native filesystem (faster than executing in-place from `/mnt/c`) — useful when a Windows host wants to exercise Linux test binaries.
For elaborate setups (rsync, ssh ControlMaster, env vars on the spawn, `QEMU_LD_PREFIX` for sysroots), construct `TestRunner` directly — `copy`/`exec`/`cleanup`/`probe` are public string fields with `{bundle}`, `{remote_bundle}`, `{bin}`, `{bin_name}`, `{args}` placeholders. For elaborate setups (rsync, ssh ControlMaster, env vars on the spawn, `QEMU_LD_PREFIX` for sysroots), construct `TestRunner` directly — `copy`/`exec`/`cleanup`/`probe` are public string fields with `{bundle}`, `{remote_bundle}`, `{bin}`, `{bin_name}`, `{args}` placeholders.
For cross-arch builds, set `cfg.sysroot` on the `Configuration` to point at a sysroot for the target. `BuildStdPcm` and the compile/link commands both consume it: `--sysroot=<path>` is passed to clang, and `<sysroot>/usr/share/libc++/v1/std.cppm` is precompiled instead of the host's. The cheapest aarch64 sysroot is an extracted [Arch Linux ARM rootfs](http://os.archlinuxarm.org/) plus `libc++` + `libc++abi` + `gcc` installed into it via `pacman --root <rootfs> --config <alarm-pacman.conf> --dbpath <rootfs>/var/lib/pacman -Sy …` (point pacman at ALARM's mirrors via explicit config to avoid pulling host-arch packages). For cross-arch builds, set `cfg.sysroot` on the `Configuration` to point at a sysroot for the target. `BuildStdPcm` and the compile/link commands both consume it: `--sysroot=<path>` is passed to clang, and `<sysroot>/usr/share/libc++/v1/std.cppm` is precompiled instead of the host's. 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). See `tests/CrossArchAarch64/inner/` for a working sysroot+qemu example.
For `--target=x86_64-w64-mingw32` (Windows from Linux), the pipeline cross-compiles as long as `mingw-w64` is installed. Pair it with `--runner=cmd:wine` to run the result. Only the MinGW ABI is supported; there is no MSVC target and no Windows host. `-fuse-ld=lld` is auto-injected on every link, and `-lstdc++exp` is auto-linked for the mingw target so user projects don't need to declare those flags. For `--target=x86_64-w64-mingw32` (Windows from Linux), V2's pipeline cross-compiles as long as `mingw-w64` is installed. Pair it with `--runner=cmd:wine` (no VM needed) or `--runner=sshwin:winhost` (real Windows). Full MSVC ABI (`x86_64-pc-windows-msvc`) cross-compile from Linux is not yet wired; use MinGW for now. `-fuse-ld=lld` is auto-injected on every link, and `-lstdc++exp` is auto-linked for the mingw target so user projects don't need to declare those flags.
### Configuring runners ### Configuring runners
@ -189,9 +192,12 @@ Spec grammar:
``` ```
local local
cmd:<command> # cmd:wine, cmd:qemu-aarch64, cmd:valgrind, ... cmd:<command> # cmd:wine, cmd:qemu-aarch64, cmd:valgrind, ...
ssh:<host>[:<remoteDir>]
sshwin:<host>[:<remoteDir>] # cmd.exe-shell variant for Windows remotes
wsl[:<remoteDir>] # Windows host -> Linux binaries via WSL2
``` ```
Each runner carries a `probe` command (e.g. `which wine`); if it fails, every test using that runner is reported as ⏭ skipped and the rest of the suite continues. Skipped tests count toward the summary but don't fail the suite (`AllPassed()` returns true on skip-only runs). Each runner carries a `probe` command (e.g. `which wine`, `ssh -BatchMode=yes <host> true`); if it fails, every test using that runner is reported as ⏭ skipped and the rest of the suite continues. Skipped tests count toward the summary but don't fail the suite (`AllPassed()` returns true on skip-only runs).
### Linking the parent project's library ### Linking the parent project's library

137
build.cmd Normal file
View file

@ -0,0 +1,137 @@
@REM SPDX-License-Identifier: LGPL-3.0-only
@REM SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
@echo OFF
mkdir build 2>nul
mkdir bin 2>nul
mkdir lib 2>nul
mkdir share\crafter-build 2>nul
mkdir share\crafter-build\wasi-runtime 2>nul
copy /Y wasi-runtime\runtime.js share\crafter-build\wasi-runtime\
copy /Y wasi-runtime\index.html.in share\crafter-build\wasi-runtime\
copy /Y interfaces\Crafter.Build.cppm share\crafter-build\
copy /Y interfaces\Crafter.Build-Shader.cppm share\crafter-build\
copy /Y interfaces\Crafter.Build-Platform.cppm share\crafter-build\
copy /Y interfaces\Crafter.Build-Interface.cppm share\crafter-build\
copy /Y interfaces\Crafter.Build-Implementation.cppm share\crafter-build\
copy /Y interfaces\Crafter.Build-External.cppm share\crafter-build\
copy /Y interfaces\Crafter.Build-Clang.cppm share\crafter-build\
copy /Y interfaces\Crafter.Build-Test.cppm share\crafter-build\
copy /Y interfaces\Crafter.Build-Lint.cppm share\crafter-build\
copy /Y interfaces\Crafter.Build-Progress.cppm share\crafter-build\
copy /Y interfaces\Crafter.Build-Asset.cppm share\crafter-build\
copy /Y interfaces\Crafter.Build-Api.h share\crafter-build\
if not exist .\build\glslang\NUL git clone https://github.com/KhronosGroup/glslang.git .\build\glslang
set useLibcCommon=-nostdinc++ -nostdlib++
set useLibcLinker=-L %LIBCXX_DIR%\lib -lc++ %useLibcCommon%
set useLibcSource=-isystem %LIBCXX_DIR%\include\c++\v1 %useLibcCommon%
set buildDir=%CD%\build
if "%CRAFTER_BUILD_MARCH%"=="" set CRAFTER_BUILD_MARCH=native
if "%CRAFTER_BUILD_MTUNE%"=="" set CRAFTER_BUILD_MTUNE=native
cd .\build\glslang
cmake -G Ninja -B build ^
-DCMAKE_C_COMPILER=clang ^
-DCMAKE_CXX_COMPILER=clang++ ^
-DCMAKE_CXX_FLAGS="%useLibcSource%" ^
-DCMAKE_EXE_LINKER_FLAGS="%useLibcLinker%" ^
-DCMAKE_SHARED_LINKER_FLAGS="%useLibcLinker%" ^
-DCMAKE_BUILD_TYPE=Release ^
-DCMAKE_ARCHIVE_OUTPUT_DIRECTORY="%buildDir%" ^
-DCMAKE_LIBRARY_OUTPUT_DIRECTORY="%buildDir%" ^
-DENABLE_OPT=OFF
cmake --build build --config Release
cd ..\..\
set common_options=-I.\build\glslang -std=c++26 -O3 -march=%CRAFTER_BUILD_MARCH% -mtune=%CRAFTER_BUILD_MTUNE% -fprebuilt-module-path=.\build -D CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc -D CRAFTER_BUILD_CONFIGURATION_TYPE_EXECUTABLE -D _CRT_SECURE_NO_WARNINGS %useLibcSource% -c
clang++ %useLibcSource% -std=c++26 -march=%CRAFTER_BUILD_MARCH% -mtune=%CRAFTER_BUILD_MTUNE% -O3 -Wno-reserved-identifier -Wno-reserved-module-identifier --precompile %LIBCXX_DIR%\modules\c++\v1\std.cppm -o .\build\std.pcm
clang++ %common_options% -fmodule-output interfaces\Crafter.Build-Shader.cppm -o .\build\Crafter.Build-Shader.o
clang++ %common_options% -fmodule-output interfaces\Crafter.Build-Platform.cppm -o .\build\Crafter.Build-Platform.o
clang++ %common_options% -fmodule-output interfaces\Crafter.Build-Interface.cppm -o .\build\Crafter.Build-Interface.o
clang++ %common_options% -fmodule-output interfaces\Crafter.Build-Implementation.cppm -o .\build\Crafter.Build-Implementation.o
clang++ %common_options% -fmodule-output interfaces\Crafter.Build-External.cppm -o .\build\Crafter.Build-External.o
clang++ %common_options% -fmodule-output interfaces\Crafter.Build-Clang.cppm -o .\build\Crafter.Build-Clang.o
clang++ %common_options% -fmodule-output interfaces\Crafter.Build-Test.cppm -o .\build\Crafter.Build-Test.o
clang++ %common_options% -fmodule-output interfaces\Crafter.Build-Lint.cppm -o .\build\Crafter.Build-Lint.o
clang++ %common_options% -fmodule-output interfaces\Crafter.Build-Progress.cppm -o .\build\Crafter.Build-Progress.o
REM Asset partition: bootstrap compiles it WITHOUT CRAFTER_BUILD_HAS_ASSET so
REM CompressAsset takes the stub branch (no Crafter.Asset PCM exists yet).
clang++ %common_options% -fmodule-output interfaces\Crafter.Build-Asset.cppm -o .\build\Crafter.Build-Asset.o
clang++ %common_options% -fmodule-output interfaces\Crafter.Build.cppm -o .\build\Crafter.Build.o
clang++ %common_options% .\implementations\Crafter.Build-Shader.cpp -o .\build\Crafter.Build-Shader_impl.o
clang++ %common_options% .\implementations\Crafter.Build-Platform.cpp -o .\build\Crafter.Build-Platform_impl.o
clang++ %common_options% .\implementations\Crafter.Build-Interface.cpp -o .\build\Crafter.Build-Interface_impl.o
clang++ %common_options% .\implementations\Crafter.Build-Implementation.cpp -o .\build\Crafter.Build-Implementation_impl.o
clang++ %common_options% .\implementations\Crafter.Build-External.cpp -o .\build\Crafter.Build-External_impl.o
clang++ %common_options% .\implementations\Crafter.Build-Clang.cpp -o .\build\Crafter.Build-Clang_impl.o
clang++ %common_options% .\implementations\Crafter.Build-Test.cpp -o .\build\Crafter.Build-Test_impl.o
clang++ %common_options% .\implementations\Crafter.Build-Lint.cpp -o .\build\Crafter.Build-Lint_impl.o
clang++ %common_options% .\implementations\Crafter.Build-Progress.cpp -o .\build\Crafter.Build-Progress_impl.o
clang++ %common_options% .\implementations\Crafter.Build-Asset.cpp -o .\build\Crafter.Build-Asset_impl.o
clang++ %common_options% .\implementations\main.cpp -o .\build\main.o
REM Step 1: link all impl .o files into crafter-build.dll, generating crafter-build.lib import lib
clang++ %useLibcLinker% -shared -std=c++26 -O3 -march=%CRAFTER_BUILD_MARCH% -mtune=%CRAFTER_BUILD_MTUNE% -L.\build -fuse-ld=lld ^
-Wl,/IMPLIB:.\bin\crafter-build.lib ^
-lSPIRV -lGenericCodeGen -lglslang -lOSDependent -lMachineIndependent -lglslang-default-resource-limits ^
.\build\Crafter.Build-Shader.o ^
.\build\Crafter.Build-Platform.o ^
.\build\Crafter.Build-Interface.o ^
.\build\Crafter.Build-Implementation.o ^
.\build\Crafter.Build-External.o ^
.\build\Crafter.Build-Clang.o ^
.\build\Crafter.Build-Test.o ^
.\build\Crafter.Build-Lint.o ^
.\build\Crafter.Build-Progress.o ^
.\build\Crafter.Build-Asset.o ^
.\build\Crafter.Build.o ^
.\build\Crafter.Build-Shader_impl.o ^
.\build\Crafter.Build-Platform_impl.o ^
.\build\Crafter.Build-Interface_impl.o ^
.\build\Crafter.Build-Implementation_impl.o ^
.\build\Crafter.Build-External_impl.o ^
.\build\Crafter.Build-Clang_impl.o ^
.\build\Crafter.Build-Test_impl.o ^
.\build\Crafter.Build-Lint_impl.o ^
.\build\Crafter.Build-Progress_impl.o ^
.\build\Crafter.Build-Asset_impl.o ^
-o .\bin\crafter-build.dll
REM Step 2: link the launcher exe against crafter-build.lib
clang++ %useLibcLinker% -std=c++26 -O3 -march=%CRAFTER_BUILD_MARCH% -mtune=%CRAFTER_BUILD_MTUNE% -L.\bin -fuse-ld=lld ^
.\build\main.o ^
.\bin\crafter-build.lib ^
-o .\bin\crafter-build.exe
REM Step 3: bundle the same .o set as a static archive for downstream consumers
llvm-lib.exe /OUT:.\lib\crafter-build-static.lib ^
.\build\Crafter.Build-Shader.o ^
.\build\Crafter.Build-Platform.o ^
.\build\Crafter.Build-Interface.o ^
.\build\Crafter.Build-Implementation.o ^
.\build\Crafter.Build-External.o ^
.\build\Crafter.Build-Clang.o ^
.\build\Crafter.Build-Test.o ^
.\build\Crafter.Build-Lint.o ^
.\build\Crafter.Build-Progress.o ^
.\build\Crafter.Build-Asset.o ^
.\build\Crafter.Build.o ^
.\build\Crafter.Build-Shader_impl.o ^
.\build\Crafter.Build-Platform_impl.o ^
.\build\Crafter.Build-Interface_impl.o ^
.\build\Crafter.Build-Implementation_impl.o ^
.\build\Crafter.Build-External_impl.o ^
.\build\Crafter.Build-Clang_impl.o ^
.\build\Crafter.Build-Test_impl.o ^
.\build\Crafter.Build-Lint_impl.o ^
.\build\Crafter.Build-Progress_impl.o ^
.\build\Crafter.Build-Asset_impl.o
copy /Y "%LIBCXX_DIR%\lib\c++.dll" ".\bin\c++.dll"

View file

@ -21,6 +21,7 @@ cp interfaces/Crafter.Build-Test.cppm share/crafter-build/
cp interfaces/Crafter.Build-Lint.cppm share/crafter-build/ cp interfaces/Crafter.Build-Lint.cppm share/crafter-build/
cp interfaces/Crafter.Build-Progress.cppm share/crafter-build/ cp interfaces/Crafter.Build-Progress.cppm share/crafter-build/
cp interfaces/Crafter.Build-Asset.cppm share/crafter-build/ cp interfaces/Crafter.Build-Asset.cppm share/crafter-build/
cp interfaces/Crafter.Build-Api.h share/crafter-build/
git clone https://github.com/KhronosGroup/glslang.git ./build/glslang git clone https://github.com/KhronosGroup/glslang.git ./build/glslang

View file

@ -2,9 +2,17 @@
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
module; module;
#if defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc) || defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32)
#include <winsock2.h>
#include <ws2tcpip.h>
// <rpc.h> (pulled in transitively) defines `interface` as a macro for `struct`,
// which collides with local variables named `interface` in this TU.
#undef interface
#else
#include <sys/socket.h> #include <sys/socket.h>
#include <netinet/in.h> #include <netinet/in.h>
#include <unistd.h> #include <unistd.h>
#endif
export module Crafter.Build:Clang_impl; export module Crafter.Build:Clang_impl;
import std; import std;
import :Clang; import :Clang;
@ -259,19 +267,17 @@ fs::path Crafter::StdPcmDir(const Configuration& config) {
fs::path Crafter::ExpectedOutput(const Configuration& config) { fs::path Crafter::ExpectedOutput(const Configuration& config) {
const fs::path dir = config.BinDir(); const fs::path dir = config.BinDir();
const bool mingwTarget = config.target == "x86_64-w64-mingw32"; const bool windowsTarget = config.target == "x86_64-w64-mingw32" || config.target == "x86_64-pc-windows-msvc";
if (config.type == ConfigurationType::Executable) { if (config.type == ConfigurationType::Executable) {
if (config.target.starts_with("wasm32")) return dir / std::format("{}.wasm", config.outputName); if (config.target.starts_with("wasm32")) return dir / std::format("{}.wasm", config.outputName);
return mingwTarget ? dir / std::format("{}.exe", config.outputName) : dir / config.outputName; return config.target == "x86_64-w64-mingw32" ? dir / std::format("{}.exe", config.outputName) : dir / config.outputName;
} }
if (config.type == ConfigurationType::LibraryStatic) { if (config.type == ConfigurationType::LibraryStatic) {
// The archive step writes lib<name>.a for every target, mingw included: return windowsTarget ? dir / std::format("{}.lib", config.outputName) : dir / std::format("lib{}.a", config.outputName);
// llvm-ar does not care about the object format.
return dir / std::format("lib{}.a", config.outputName);
} }
// LibraryDynamic — the .dll on the mingw target so an mtime check sees the // LibraryDynamic — the .dll on Windows targets so an mtime check sees the
// freshly linked library; on Unix the .so is that file. // freshly linked library; on Unix the .so is that file.
return mingwTarget ? dir / std::format("{}.dll", config.outputName) : dir / std::format("lib{}.so", config.outputName); return windowsTarget ? dir / std::format("{}.dll", config.outputName) : dir / std::format("lib{}.so", config.outputName);
} }
CompileCommand Crafter::GetCompileCommand(const Configuration& config) { CompileCommand Crafter::GetCompileCommand(const Configuration& config) {
@ -316,9 +322,22 @@ CompileCommand Crafter::GetCompileCommand(const Configuration& config) {
} }
if(config.type == ConfigurationType::LibraryDynamic) { if(config.type == ConfigurationType::LibraryDynamic) {
#ifdef __linux__
out.command += " -fPIC -D CRAFTER_BUILD_CONFIGURATION_TYPE_SHARED_LIBRARY"; out.command += " -fPIC -D CRAFTER_BUILD_CONFIGURATION_TYPE_SHARED_LIBRARY";
#endif
#if defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc) || defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32)
out.command += " -D CRAFTER_BUILD_CONFIGURATION_TYPE_SHARED_LIBRARY";
#endif
} else if(config.type == ConfigurationType::Executable) { } else if(config.type == ConfigurationType::Executable) {
out.command += " -D CRAFTER_BUILD_CONFIGURATION_TYPE_EXECUTABLE"; out.command += " -D CRAFTER_BUILD_CONFIGURATION_TYPE_EXECUTABLE";
// On Windows targets the API uses __declspec(dllimport) when consuming
// a DLL. Set the macro for executables so CRAFTER_API resolves to
// dllimport in their PCM cache (separate from the lib's PCM cache,
// which gets dllexport). Harmless if the exe doesn't actually link a
// crafter DLL — CRAFTER_API only matters at API call sites.
if (config.target == "x86_64-w64-mingw32" || config.target == "x86_64-pc-windows-msvc") {
out.command += " -D CRAFTER_BUILD_DLL_IMPORT";
}
} else { } else {
out.command += " -D CRAFTER_BUILD_CONFIGURATION_TYPE_LIBRARY"; out.command += " -D CRAFTER_BUILD_CONFIGURATION_TYPE_LIBRARY";
} }
@ -445,12 +464,15 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
config.ResolvePendingImports(); config.ResolvePendingImports();
// Auto-detect the WASI sysroot before any compile step runs so BuildStdPcm // Auto-detect the WASI sysroot before any compile step runs so BuildStdPcm
// and the main compile command see the same value. Covers all wasm32-* // and the main compile command see the same value. Linux-only — Windows
// triples (wasi, wasip1, wasip2, ...); the sysroot's per-triple subdirs // users supply cfg.sysroot pointing at their wasi-sdk install. Covers all
// handle the differences. // wasm32-* triples (wasi, wasip1, wasip2, ...); the sysroot's per-triple
// subdirs handle the differences.
#ifdef __linux__
if (config.sysroot.empty() && config.target.starts_with("wasm32")) { if (config.sysroot.empty() && config.target.starts_with("wasm32")) {
config.sysroot = "/usr/share/wasi-sysroot"; config.sysroot = "/usr/share/wasi-sysroot";
} }
#endif
fs::path buildDir = config.BuildDir(); fs::path buildDir = config.BuildDir();
fs::path outputDir = config.BinDir(); fs::path outputDir = config.BinDir();
@ -683,6 +705,7 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
const std::string& ltoCompileFlags = compile.ltoCompileFlags; const std::string& ltoCompileFlags = compile.ltoCompileFlags;
const std::string& ltoLinkFlags = compile.ltoLinkFlags; const std::string& ltoLinkFlags = compile.ltoLinkFlags;
const bool isWasm = config.target.starts_with("wasm32"); const bool isWasm = config.target.starts_with("wasm32");
const bool useLto = compile.useLto;
std::string files; std::string files;
std::unordered_set<std::string> libSet; std::unordered_set<std::string> libSet;
@ -1102,6 +1125,7 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
if(buildResult.repack) { if(buildResult.repack) {
Progress::Task task(std::format("Linking {}", config.outputName)); Progress::Task task(std::format("Linking {}", config.outputName));
if(config.type == ConfigurationType::Executable) { if(config.type == ConfigurationType::Executable) {
#ifdef __linux__
if(config.target == "x86_64-w64-mingw32") { if(config.target == "x86_64-w64-mingw32") {
try { try {
// Copy any LibraryDynamic dependency DLLs alongside // Copy any LibraryDynamic dependency DLLs alongside
@ -1112,12 +1136,15 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
std::function<void(Configuration*)> copyDepDlls = [&](Configuration* dep) { std::function<void(Configuration*)> copyDepDlls = [&](Configuration* dep) {
if (!dllSeen.insert(dep).second) return; if (!dllSeen.insert(dep).second) return;
if (dep->type == ConfigurationType::LibraryDynamic && dep->target == "x86_64-w64-mingw32") { if (dep->type == ConfigurationType::LibraryDynamic && dep->target == "x86_64-w64-mingw32") {
// Only the DLL: Windows resolves it from the exe's fs::path depDir = dep->BinDir();
// directory at load time. The import lib stays in // The DLL itself (Windows resolves it from the
// the dependency's own bin dir, which is already // exe's directory at load time) and the mingw
// on the link line through buildResult.libs. // import lib (so a downstream `crafter-build.exe`
fs::path src = dep->BinDir() / std::format("{}.dll", dep->outputName); // can link a fresh project.dll against it without
if (fs::exists(src)) { // hunting through sibling output dirs).
for (auto fname : {std::format("{}.dll", dep->outputName), std::format("lib{}.dll.a", dep->outputName)}) {
fs::path src = depDir / fname;
if (!fs::exists(src)) continue;
fs::path dest = outputDir / src.filename(); fs::path dest = outputDir / src.filename();
if (!fs::exists(dest) || fs::last_write_time(src) > fs::last_write_time(dest)) { if (!fs::exists(dest) || fs::last_write_time(src) > fs::last_write_time(dest)) {
fs::copy(src, dest, fs::copy_options::overwrite_existing); fs::copy(src, dest, fs::copy_options::overwrite_existing);
@ -1136,22 +1163,65 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
} else { } else {
buildResult.result = RunBuildCommand(std::format("{}{} -o {} -fuse-ld=lld{}", command, files, (outputDir/config.outputName).string(), linkExtras)); buildResult.result = RunBuildCommand(std::format("{}{} -o {} -fuse-ld=lld{}", command, files, (outputDir/config.outputName).string(), linkExtras));
} }
#endif
#if defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc) || defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32)
if (config.target == "x86_64-w64-mingw32") {
// Windows host, mingw target: same shape as the Linux→mingw
// path (no LIBCXX_DIR / -lc++ / -nostdlib++ — those are MSVC
// libc++ flags). Copy LibraryDynamic dep DLLs + import libs
// alongside the launcher exe so Windows resolves them from
// the exe's own directory at load time. Runtime DLLs (libstdc++,
// libgcc, libwinpthread) come from msys2 on PATH.
std::unordered_set<Configuration*> dllSeen;
std::function<void(Configuration*)> copyDepDlls = [&](Configuration* dep) {
if (!dllSeen.insert(dep).second) return;
if (dep->type == ConfigurationType::LibraryDynamic && dep->target == "x86_64-w64-mingw32") {
fs::path depDir = dep->BinDir();
for (auto fname : {std::format("{}.dll", dep->outputName), std::format("lib{}.dll.a", dep->outputName)}) {
fs::path src = depDir / fname;
if (!fs::exists(src)) continue;
fs::path dest = outputDir / src.filename();
if (!fs::exists(dest) || fs::last_write_time(src) > fs::last_write_time(dest)) {
fs::copy(src, dest, fs::copy_options::overwrite_existing);
}
}
}
for (Configuration* sub : dep->dependencies) copyDepDlls(sub);
};
for (Configuration* dep : config.dependencies) copyDepDlls(dep);
buildResult.result = RunBuildCommand(std::format("{}{} -o {} -fuse-ld=lld{}", command, files, (outputDir/config.outputName).string(), linkExtras));
} else {
std::system(std::format("copy \"%LIBCXX_DIR%\\lib\\c++.dll\" \"{}\\c++.dll\"", outputDir.string()).c_str());
buildResult.result = RunBuildCommand(std::format("{}{} -o {}.exe -fuse-ld=lld -L %LIBCXX_DIR%\\lib -lc++ -nostdinc++ -nostdlib++{}", command, files, (outputDir/config.outputName).string(), linkExtras));
}
#endif
} else if(config.type == ConfigurationType::LibraryStatic) { } else if(config.type == ConfigurationType::LibraryStatic) {
#ifdef __linux__
// ThinLTO emits LLVM bitcode objects; plain `ar` writes an archive // ThinLTO emits LLVM bitcode objects; plain `ar` writes an archive
// index that omits their symbols, so a consumer's lld link can't // index that omits their symbols, so a consumer's lld link can't
// pull the needed members. llvm-ar writes a bitcode-aware index // pull the needed members. llvm-ar writes a bitcode-aware index.
// and handles plain ELF and COFF objects just as well, so every buildResult.result = RunBuildCommand(std::format("{} rcs {}.a {}", useLto ? "llvm-ar" : "ar", (outputDir/fs::path(std::string("lib")+config.outputName)).string(), files));
// build uses it. #endif
buildResult.result = RunBuildCommand(std::format("llvm-ar rcs {}.a {}", (outputDir/fs::path(std::string("lib")+config.outputName)).string(), files));
#if defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc) || defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32)
buildResult.result = RunBuildCommand(std::format("llvm-lib.exe {} /OUT:{}.lib", files, (outputDir/fs::path(config.outputName)).string()));
#endif
} else { } else {
// LibraryDynamic. Output names follow each target's convention so // LibraryDynamic. Output names follow each target's convention so
// consumers can link via the standard linker search: // consumers can link via the standard linker search:
// mingw: <name>.dll + lib<name>.dll.a (lld --out-implib) // mingw: <name>.dll + lib<name>.dll.a (lld --out-implib)
// msvc: <name>.dll + <name>.lib (lld /IMPLIB)
// unix: lib<name>.so (rpath $ORIGIN) // unix: lib<name>.so (rpath $ORIGIN)
if (config.target == "x86_64-w64-mingw32") { if (config.target == "x86_64-w64-mingw32") {
fs::path dll = outputDir / std::format("{}.dll", config.outputName); fs::path dll = outputDir / std::format("{}.dll", config.outputName);
fs::path implib = outputDir / std::format("lib{}.dll.a", 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)); buildResult.result = RunBuildCommand(std::format("{}{} -shared -o {} -Wl,--out-implib,{} -fuse-ld=lld{}", command, files, dll.string(), implib.string(), linkExtras));
} else if (config.target == "x86_64-pc-windows-msvc") {
fs::path dll = outputDir / std::format("{}.dll", config.outputName);
fs::path implib = outputDir / std::format("{}.lib", config.outputName);
buildResult.result = RunBuildCommand(std::format("{}{} -shared -o {} -Wl,/IMPLIB:{} -fuse-ld=lld{}", command, files, dll.string(), implib.string(), linkExtras));
} else { } 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)); buildResult.result = RunBuildCommand(std::format("{}{} -shared -o {}.so -Wl,-rpath,'$ORIGIN' -fuse-ld=lld{}", command, files, (outputDir/(std::string("lib")+config.outputName)).string(), linkExtras));
} }
@ -1474,9 +1544,11 @@ ArgQuery Crafter::ApplyStandardArgs(Configuration& cfg, std::span<const std::str
// includes it. (Build() also runs this once more for callers that bypassed // includes it. (Build() also runs this once more for callers that bypassed
// ApplyStandardArgs, but doing it here makes dep PcmDirs consistent // ApplyStandardArgs, but doing it here makes dep PcmDirs consistent
// between the consumer's command-construction and the dep's own build.) // between the consumer's command-construction and the dep's own build.)
#ifdef __linux__
if (cfg.sysroot.empty() && cfg.target.starts_with("wasm32")) { if (cfg.sysroot.empty() && cfg.target.starts_with("wasm32")) {
cfg.sysroot = "/usr/share/wasi-sysroot"; cfg.sysroot = "/usr/share/wasi-sysroot";
} }
#endif
return ArgQuery{args}; return ArgQuery{args};
} }
@ -1572,6 +1644,7 @@ Environment:
Replace '-' and '.' with '_' in the Replace '-' and '.' with '_' in the
triple. CLI --runner= overrides this. triple. CLI --runner= overrides this.
CRAFTER_MINGW_DIR Override mingw-w64 sysroot auto-detect. CRAFTER_MINGW_DIR Override mingw-w64 sysroot auto-detect.
LIBCXX_DIR Windows libc++ install (MSVC ABI builds).
Exit status: Exit status:
0 success / all non-skipped tests passed / lint clean / format applied 0 success / all non-skipped tests passed / lint clean / format applied
@ -1716,7 +1789,11 @@ std::int32_t Crafter::Run(std::int32_t argc, char** argv) {
if (config.target.starts_with("wasm32")) { if (config.target.starts_with("wasm32")) {
const bool browserBuild = fs::exists(absDir / "index.html"); const bool browserBuild = fs::exists(absDir / "index.html");
auto have = [](std::string_view exe) { auto have = [](std::string_view exe) {
#ifdef _WIN32
std::string probe = std::format("where {} > NUL 2>&1", exe);
#else
std::string probe = std::format("command -v {} > /dev/null 2>&1", exe); std::string probe = std::format("command -v {} > /dev/null 2>&1", exe);
#endif
return std::system(probe.c_str()) == 0; return std::system(probe.c_str()) == 0;
}; };
if (browserBuild) { if (browserBuild) {
@ -1724,9 +1801,17 @@ std::int32_t Crafter::Run(std::int32_t argc, char** argv) {
// user has another dev server running we shift up rather // user has another dev server running we shift up rather
// than letting caddy/python exit with EADDRINUSE. // than letting caddy/python exit with EADDRINUSE.
auto findFreePort = [](std::int32_t basePort, std::int32_t span) -> std::int32_t { auto findFreePort = [](std::int32_t basePort, std::int32_t span) -> std::int32_t {
#if defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc) || defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32)
static const bool WsaInit = []{ WSADATA d; return WSAStartup(MAKEWORD(2, 2), &d) == 0; }();
if (!WsaInit) return basePort;
using Sock = SOCKET;
const Sock invalid = INVALID_SOCKET;
auto closesock = [](Sock s){ closesocket(s); };
#else
using Sock = std::int32_t; using Sock = std::int32_t;
const Sock invalid = -1; const Sock invalid = -1;
auto closesock = [](Sock s){ ::close(s); }; auto closesock = [](Sock s){ ::close(s); };
#endif
for (std::int32_t p = basePort; p < basePort + span; ++p) { for (std::int32_t p = basePort; p < basePort + span; ++p) {
Sock s = ::socket(AF_INET, SOCK_STREAM, 0); Sock s = ::socket(AF_INET, SOCK_STREAM, 0);
if (s == invalid) return basePort; if (s == invalid) return basePort;
@ -1849,7 +1934,11 @@ std::int32_t Crafter::Run(std::int32_t argc, char** argv) {
// Silence the backend's own banner/request logs so the // Silence the backend's own banner/request logs so the
// experience is identical regardless of which server is // experience is identical regardless of which server is
// picked — the line above is the only output. // picked — the line above is the only output.
#ifdef _WIN32
cmd += " > NUL 2>&1";
#else
cmd += " > /dev/null 2>&1"; cmd += " > /dev/null 2>&1";
#endif
return std::system(cmd.c_str()) == 0 ? 0 : 1; return std::system(cmd.c_str()) == 0 ? 0 : 1;
} }
// wasi-cli wasm — needs a standalone runtime. // wasi-cli wasm — needs a standalone runtime.
@ -1863,6 +1952,10 @@ std::int32_t Crafter::Run(std::int32_t argc, char** argv) {
return 1; return 1;
} }
// Resolve to absolute — cmd.exe on Windows mishandles a leading
// "./" by trying to interpret it as a command. system() invokes
// through cmd /c, so the relative-prefixed path makes cmd error
// with "'.' is not recognized as an internal or external command".
// Run from the artifact's own directory so relative file opens // Run from the artifact's own directory so relative file opens
// (shaders, assets copied alongside the exe via cfg.files) resolve // (shaders, assets copied alongside the exe via cfg.files) resolve
// against the bin dir rather than the user's cwd. We exit the // against the bin dir rather than the user's cwd. We exit the

View file

@ -21,6 +21,18 @@ std::string DeriveName(const GitSource& source) {
} }
std::string ShellQuote(std::string_view s) { std::string ShellQuote(std::string_view s) {
// Windows cmd treats single quotes as literal characters — only double
// quotes work. Paths don't contain " in practice, so a simple wrap
// suffices; backslashes inside the path stay backslashes.
#ifdef _WIN32
std::string out = "\"";
for (char c : s) {
if (c == '"') out += "\\\"";
else out += c;
}
out += "\"";
return out;
#else
std::string out = "'"; std::string out = "'";
for (char c : s) { for (char c : s) {
if (c == '\'') out += "'\\''"; if (c == '\'') out += "'\\''";
@ -28,6 +40,7 @@ std::string ShellQuote(std::string_view s) {
} }
out += "'"; out += "'";
return out; return out;
#endif
} }
std::string JoinOptions(std::span<const std::string> options) { std::string JoinOptions(std::span<const std::string> options) {
@ -120,14 +133,17 @@ std::string BuildInjectedCMakeFlags(const fs::path& cmakeBuildDir, std::string_v
out += " -DCMAKE_BUILD_TYPE=Release"; out += " -DCMAKE_BUILD_TYPE=Release";
out += " -DCMAKE_C_COMPILER=clang"; out += " -DCMAKE_C_COMPILER=clang";
out += " -DCMAKE_CXX_COMPILER=clang++"; out += " -DCMAKE_CXX_COMPILER=clang++";
// Stdlib choice: libc++ for Linux targets (matches how crafter-build // Stdlib choice: libc++ for the Linux host build (matches how crafter-build
// itself is built with -stdlib=libc++); the mingw cross-compile uses the // itself is built with -stdlib=libc++); mingw cross-compile uses libstdc++
// libstdc++ shipped by the mingw-w64 toolchain, so it keeps the default. // shipped by the mingw-w64 toolchain so we leave the default; Windows
// native (msvc target) similarly uses libc++ via LIBCXX_DIR.
#ifdef __linux__
if (target.ends_with("-linux-gnu") || target.ends_with("-linux-musl")) { if (target.ends_with("-linux-gnu") || target.ends_with("-linux-musl")) {
out += " -DCMAKE_CXX_FLAGS=-stdlib=libc++"; out += " -DCMAKE_CXX_FLAGS=-stdlib=libc++";
out += " -DCMAKE_EXE_LINKER_FLAGS=-stdlib=libc++"; out += " -DCMAKE_EXE_LINKER_FLAGS=-stdlib=libc++";
out += " -DCMAKE_SHARED_LINKER_FLAGS=-stdlib=libc++"; out += " -DCMAKE_SHARED_LINKER_FLAGS=-stdlib=libc++";
} }
#endif
if (target == "x86_64-w64-mingw32") { if (target == "x86_64-w64-mingw32") {
out += " -DCMAKE_SYSTEM_NAME=Windows"; out += " -DCMAKE_SYSTEM_NAME=Windows";
out += std::format(" -DCMAKE_C_COMPILER_TARGET={}", target); out += std::format(" -DCMAKE_C_COMPILER_TARGET={}", target);
@ -261,9 +277,9 @@ ExternalBuildResult Crafter::BuildExternal(const ExternalDependency& dep, std::s
// (runtime-loader). The rpath stays embedded in the produced // (runtime-loader). The rpath stays embedded in the produced
// binary so it picks up shared deps from the cache without any // binary so it picks up shared deps from the cache without any
// LD_LIBRARY_PATH gymnastics. Static deps (.a) ignore the rpath // LD_LIBRARY_PATH gymnastics. Static deps (.a) ignore the rpath
// harmlessly. The PE/COFF target (mingw) resolves DLLs via PATH or // harmlessly. PE/COFF targets (mingw, msvc) resolve DLLs via PATH
// adjacency rather than rpath, so lld warns if we pass it. // or adjacency rather than rpath, so lld warns if we pass it.
const bool isPe = target == "x86_64-w64-mingw32"; const bool isPe = target == "x86_64-w64-mingw32" || target == "x86_64-pc-windows-msvc";
std::string buildDirAbs = fs::absolute(cmakeBuildDir).string(); std::string buildDirAbs = fs::absolute(cmakeBuildDir).string();
result.linkFlags.push_back(std::format("-L{}", buildDirAbs)); result.linkFlags.push_back(std::format("-L{}", buildDirAbs));
if (!isPe) result.linkFlags.push_back(std::format("-Wl,-rpath,{}", buildDirAbs)); if (!isPe) result.linkFlags.push_back(std::format("-Wl,-rpath,{}", buildDirAbs));

View file

@ -3,7 +3,11 @@
module; module;
#include <clang-c/Index.h> #include <clang-c/Index.h>
#if defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc) || defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32)
#include <windows.h>
#else
#include <dlfcn.h> #include <dlfcn.h>
#endif
export module Crafter.Build:Lint_impl; export module Crafter.Build:Lint_impl;
import std; import std;
import :Lint; import :Lint;
@ -16,13 +20,20 @@ using namespace Crafter;
namespace { namespace {
// ---------------- libclang ---------------- // ---------------- libclang ----------------
// //
// libclang is loaded at runtime rather than linked, so a plain build never // libclang is loaded at runtime rather than linked. Linking -lclang would
// needs it and the binary carries no libclang.so.NN loader dependency; the // break the mingw and MSVC cross-builds at link time and would put a
// clang-c header is used for its declarations only, and every call goes // libclang.so.NN runtime dependency into the otherwise self-contained
// through a pointer resolved here. Failure to load is // release tarballs; the clang-c header is used for its declarations only,
// and every call goes through a pointer resolved here. Failure to load is
// a hard error surfaced once by RunLint — there is deliberately no second, // a hard error surfaced once by RunLint — there is deliberately no second,
// weaker lexer to fall back to, because two engines disagreeing about what // weaker lexer to fall back to, because two engines disagreeing about what
// is a comment is a worse failure than not running. // is a comment is a worse failure than not running.
#if defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc) || defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32)
using LibHandle = HMODULE;
LibHandle OpenLibrary(const std::string& name) { return LoadLibraryA(name.c_str()); }
void* LibrarySymbol(LibHandle handle, const std::string& name) { return reinterpret_cast<void*>(GetProcAddress(handle, name.c_str())); }
constexpr std::string_view LibClangNames[] = {"libclang.dll", "clang.dll"};
#else
using LibHandle = void*; using LibHandle = void*;
LibHandle OpenLibrary(const std::string& name) { return dlopen(name.c_str(), RTLD_NOW | RTLD_LOCAL); } LibHandle OpenLibrary(const std::string& name) { return dlopen(name.c_str(), RTLD_NOW | RTLD_LOCAL); }
void* LibrarySymbol(LibHandle handle, const std::string& name) { return dlsym(handle, name.c_str()); } void* LibrarySymbol(LibHandle handle, const std::string& name) { return dlsym(handle, name.c_str()); }
@ -30,6 +41,7 @@ namespace {
"libclang.so", "libclang.so.22.1", "libclang.so.21.1", "libclang.so.20.1", "libclang.so", "libclang.so.22.1", "libclang.so.21.1", "libclang.so.20.1",
"libclang.so.1", "libclang.dylib", "libclang.so.1", "libclang.dylib",
}; };
#endif
// Signatures come from decltype on the header's declarations, so they can // Signatures come from decltype on the header's declarations, so they can
// never drift from the real API. decltype is unevaluated, so naming the // never drift from the real API. decltype is unevaluated, so naming the
@ -92,8 +104,8 @@ namespace {
LibClang LoadLibClang() { LibClang LoadLibClang() {
LibClang lib; LibClang lib;
std::vector<std::string> tried; std::vector<std::string> tried;
// CRAFTER_BUILD_LIBCLANG pins an exact path, mirroring the // CRAFTER_BUILD_LIBCLANG pins an exact path, mirroring the LIBCXX_DIR /
// CRAFTER_MINGW_DIR override used elsewhere. It is exclusive: pointing // CRAFTER_MINGW_DIR overrides used elsewhere. It is exclusive: pointing
// it at a broken path must fail loudly rather than quietly succeed with // it at a broken path must fail loudly rather than quietly succeed with
// some other libclang, or the override is useless for diagnosing which // some other libclang, or the override is useless for diagnosing which
// library is actually in play. // library is actually in play.
@ -365,8 +377,8 @@ namespace {
} }
if (!args.empty()) args.erase(args.begin()); // argv[0] is the compiler if (!args.empty()) args.erase(args.begin()); // argv[0] is the compiler
for (std::string& arg : args) { for (std::string& arg : args) {
// Only \" unescapes; a backslash anywhere else is part of the // Only \" unescapes. Erasing every backslash would destroy the
// argument. // Windows include paths GetBaseCommand puts on the command line.
std::string unescaped; std::string unescaped;
unescaped.reserve(arg.size()); unescaped.reserve(arg.size());
for (std::size_t i = 0; i < arg.size(); ++i) { for (std::size_t i = 0; i < arg.size(); ++i) {

View file

@ -2,15 +2,20 @@
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
module; module;
#if defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc) || defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32)
#include <windows.h>
#endif
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <limits.h> #include <limits.h>
#ifdef __linux__
#include <unistd.h> #include <unistd.h>
#include <dlfcn.h> #include <dlfcn.h>
#include <sys/wait.h> #include <sys/wait.h>
#include <sys/file.h> #include <sys/file.h>
#include <fcntl.h> #include <fcntl.h>
#include <errno.h> #include <errno.h>
#endif
module Crafter.Build:Platform_impl; module Crafter.Build:Platform_impl;
import std; import std;
import :Platform; import :Platform;
@ -38,6 +43,7 @@ namespace {
// descriptor is close-on-exec / non-inheritable so spawned clang processes // descriptor is close-on-exec / non-inheritable so spawned clang processes
// don't keep the lock alive past our own release. // don't keep the lock alive past our own release.
class CacheLock { class CacheLock {
#ifdef __linux__
std::int32_t fd_ = -1; std::int32_t fd_ = -1;
public: public:
explicit CacheLock(const fs::path& cacheDir) { explicit CacheLock(const fs::path& cacheDir) {
@ -51,6 +57,22 @@ namespace {
::close(fd_); // closing the descriptor drops the flock ::close(fd_); // closing the descriptor drops the flock
} }
} }
#else
HANDLE handle_ = INVALID_HANDLE_VALUE;
public:
explicit CacheLock(const fs::path& cacheDir) {
handle_ = CreateFileW((cacheDir / ".lock").wstring().c_str(), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
if (handle_ != INVALID_HANDLE_VALUE) {
OVERLAPPED ov{};
LockFileEx(handle_, LOCKFILE_EXCLUSIVE_LOCK, 0, MAXDWORD, MAXDWORD, &ov);
}
}
~CacheLock() {
if (handle_ != INVALID_HANDLE_VALUE) {
CloseHandle(handle_); // closing the handle drops the lock
}
}
#endif
CacheLock(const CacheLock&) = delete; CacheLock(const CacheLock&) = delete;
CacheLock& operator=(const CacheLock&) = delete; CacheLock& operator=(const CacheLock&) = delete;
}; };
@ -82,6 +104,9 @@ namespace {
for (std::string_view name : moduleNames) { for (std::string_view name : moduleNames) {
append(fs::path(std::format("{}.cppm", name))); append(fs::path(std::format("{}.cppm", name)));
} }
// Exported through `module;` preambles, so its contents land in the PCMs
// too.
append("Crafter.Build-Api.h");
return std::format("{:016x}", std::hash<std::string>{}(all)); return std::format("{:016x}", std::hash<std::string>{}(all));
} }
@ -105,7 +130,16 @@ fs::path Crafter::GetCrafterBuildHome() {
if (const char* envHome = std::getenv("CRAFTER_BUILD_HOME")) { if (const char* envHome = std::getenv("CRAFTER_BUILD_HOME")) {
return fs::path(envHome); return fs::path(envHome);
} }
#if defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc) || defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32)
char buf[MAX_PATH];
DWORD len = GetModuleFileNameA(nullptr, buf, MAX_PATH);
if (len == 0 || len == MAX_PATH) {
throw std::runtime_error("GetModuleFileName failed");
}
fs::path hostExe(std::string(buf, len));
#else
fs::path hostExe = fs::read_symlink("/proc/self/exe"); fs::path hostExe = fs::read_symlink("/proc/self/exe");
#endif
// Walk up from the exe's directory looking for share/crafter-build with // Walk up from the exe's directory looking for share/crafter-build with
// a known module source present as a sentinel. Handles: // a known module source present as a sentinel. Handles:
@ -526,6 +560,499 @@ bool Crafter::IsTopLevelProject() {
return LoadingFrames.size() <= 1; return LoadingFrames.size() <= 1;
} }
#if defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc) || defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32)
CommandResult Crafter::RunCommandChecked(std::string_view cmd) {
std::array<char, 128> buffer;
CommandResult result{};
std::string with = std::format("cmd /C \"{} 2>&1\"", std::string(cmd));
FILE* pipe = _popen(with.c_str(), "r");
if (!pipe) {
throw std::runtime_error("_popen() failed!");
}
while (fgets(buffer.data(), static_cast<std::int32_t>(buffer.size()), pipe) != nullptr) {
result.output += buffer.data();
}
result.exitCode = _pclose(pipe);
return result;
}
CommandResult Crafter::RunCommandWithTimeout(std::string_view cmd, std::chrono::seconds timeout) {
CommandResult result{};
SECURITY_ATTRIBUTES sa{ sizeof(sa), nullptr, TRUE };
HANDLE readEnd = nullptr;
HANDLE writeEnd = nullptr;
if (!CreatePipe(&readEnd, &writeEnd, &sa, 0)) {
throw std::runtime_error("CreatePipe failed");
}
SetHandleInformation(readEnd, HANDLE_FLAG_INHERIT, 0);
// KILL_ON_JOB_CLOSE so the cmd.exe wrapper plus whatever it spawned
// (the test binary, ssh, etc.) all die when the job handle goes away —
// that's how we enforce the timeout reliably across the whole tree.
HANDLE job = CreateJobObjectA(nullptr, nullptr);
if (!job) {
CloseHandle(readEnd);
CloseHandle(writeEnd);
throw std::runtime_error("CreateJobObject failed");
}
JOBOBJECT_EXTENDED_LIMIT_INFORMATION jeli{};
jeli.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
SetInformationJobObject(job, JobObjectExtendedLimitInformation, &jeli, sizeof(jeli));
STARTUPINFOA si{};
si.cb = sizeof(si);
si.dwFlags = STARTF_USESTDHANDLES;
si.hStdOutput = writeEnd;
si.hStdError = writeEnd;
si.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
PROCESS_INFORMATION pi{};
// cmd /C "...": gives users the same shell-syntax surface (redirects,
// &&, ||) as popen("/bin/sh -c …") does on Linux. cmd's special-case
// /C parsing strips the outer quote pair when the inner text contains
// additional quotes, which is the common case here.
std::string wrapped = std::format("cmd /C \"{}\"", std::string(cmd));
std::vector<char> cmdBuf(wrapped.begin(), wrapped.end());
cmdBuf.push_back('\0');
BOOL ok = CreateProcessA(nullptr, cmdBuf.data(), nullptr, nullptr, TRUE, CREATE_NO_WINDOW | CREATE_SUSPENDED, nullptr, nullptr, &si, &pi);
if (!ok) {
CloseHandle(readEnd);
CloseHandle(writeEnd);
CloseHandle(job);
throw std::runtime_error("CreateProcess failed");
}
AssignProcessToJobObject(job, pi.hProcess);
ResumeThread(pi.hThread);
// Drop our writer ref so the read end sees EOF once the child (its
// own dup of writeEnd) exits.
CloseHandle(writeEnd);
std::string output;
std::jthread reader([&output, readEnd]() {
char buf[4096];
DWORD n = 0;
while (ReadFile(readEnd, buf, sizeof(buf), &n, nullptr) && n > 0) {
output.append(buf, n);
}
});
DWORD waitMs = static_cast<DWORD>(std::min<std::int64_t>(static_cast<std::int64_t>(timeout.count()) * 1000LL, static_cast<std::int64_t>(INFINITE) - 1));
DWORD waitResult = WaitForSingleObject(pi.hProcess, waitMs);
if (waitResult == WAIT_TIMEOUT) {
TerminateJobObject(job, 1);
WaitForSingleObject(pi.hProcess, INFINITE);
result.timedOut = true;
result.exitCode = 124;
} else {
DWORD exit = 0;
GetExitCodeProcess(pi.hProcess, &exit);
// NTSTATUS error severity (top two bits set) means the process
// terminated by exception — STATUS_ACCESS_VIOLATION 0xC0000005,
// STATUS_STACK_OVERFLOW 0xC00000FD, etc. Surface those as crashes
// so the runner shows 💥 instead of a numeric exit code.
if ((exit & 0xC0000000) == 0xC0000000) {
result.crashed = true;
result.signal = static_cast<std::int32_t>(exit);
}
result.exitCode = static_cast<std::int32_t>(exit);
}
reader.join();
CloseHandle(readEnd);
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
CloseHandle(job);
result.output = std::move(output);
return result;
}
fs::path Crafter::GetCacheDir() {
if (const char* local = std::getenv("LOCALAPPDATA")) {
return fs::path(local) / "crafter.build";
}
throw std::runtime_error("LOCALAPPDATA not set");
}
namespace {
constexpr std::array<std::string_view, 11> CrafterBuildModules = {
"Crafter.Build-Shader",
"Crafter.Build-Platform",
"Crafter.Build-Interface",
"Crafter.Build-Implementation",
"Crafter.Build-External",
"Crafter.Build-Clang",
"Crafter.Build-Test",
"Crafter.Build-Lint",
"Crafter.Build-Progress",
"Crafter.Build-Asset",
"Crafter.Build",
};
}
#endif
#ifdef CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc
// Native Windows build via build.cmd: MSVC ABI + libc++ from %LIBCXX_DIR%.
std::string Crafter::BuildStdPcm(const Configuration& config, fs::path stdPcm) {
std::string libcxx = std::getenv("LIBCXX_DIR");
std::string stdcppm = std::format("{}\\modules\\c++\\v1\\std.cppm", libcxx);
CacheLock lock(stdPcm.parent_path());
if(!fs::exists(stdPcm) || fs::last_write_time(stdPcm) < fs::last_write_time(stdcppm)) {
return RunBuildCommand(std::format("clang++ --target={} -march={} -mtune={} -isystem %LIBCXX_DIR%\\include\\c++\\v1 -nostdinc++ -nostdlib++ -std=c++26 -Wno-reserved-identifier -Wno-reserved-module-identifier --precompile %LIBCXX_DIR%\\modules\\c++\\v1\\std.cppm -o {}", config.target, config.march, config.mtune, stdPcm.string()));
}
return "";
}
std::string Crafter::GetBaseCommand(const Configuration& config) {
return std::format("clang++ -nostdinc++ -nostdlib++ -isystem %LIBCXX_DIR%\\include\\c++\\v1");
}
namespace {
void EnsureCrafterBuildPcms(const fs::path& sourceDir, const fs::path& cacheDir) {
CacheLock lock(cacheDir);
// Re-derived under the lock: another builder may have refreshed the
// cache from its own sources while we waited.
std::string stamp = CrafterBuildSourceStamp(sourceDir, CrafterBuildModules);
bool upToDate = ReadCacheStamp(cacheDir) == stamp;
for (std::string_view name : CrafterBuildModules) {
fs::path cppmPath = sourceDir / std::format("{}.cppm", name);
fs::path pcmPath = cacheDir / std::format("{}.pcm", name);
if (!fs::exists(cppmPath)) {
throw std::runtime_error(std::format("module source {} not found in {} (set CRAFTER_BUILD_HOME)", name, sourceDir.string()));
}
if (upToDate && fs::exists(pcmPath)) {
continue;
}
std::string cmd = std::format(
"clang++ --target=x86_64-pc-windows-msvc -march=native -mtune=native "
"-std=c++26 -O3 -D CRAFTER_BUILD_DLL_IMPORT "
"-nostdinc++ -nostdlib++ -isystem %LIBCXX_DIR%\\include\\c++\\v1 "
"-Wno-reserved-identifier -Wno-reserved-module-identifier "
"-fprebuilt-module-path={} "
"--precompile {} -o {}",
cacheDir.string(), cppmPath.string(), pcmPath.string());
CommandResult r = Crafter::RunCommandChecked(cmd);
if (r.exitCode != 0) {
throw std::runtime_error(std::format("Failed to precompile {} (exit {}): {}", name, r.exitCode, r.output));
}
}
// Last, so an interrupted rebuild leaves the stamp disagreeing with the
// sources and the next run starts over rather than trusting a half-built
// set of PCMs.
if (!upToDate) WriteCacheStamp(cacheDir, stamp);
}
}
Configuration Crafter::LoadProject(const fs::path& projectFile, std::span<const std::string_view> args) {
fs::path absProject = fs::canonical(projectFile);
ProjectFileGuard guard(absProject);
fs::path buildDir = absProject.parent_path() / "build";
if (!fs::exists(buildDir)) {
fs::create_directories(buildDir);
}
fs::path dllPath = buildDir / std::format("{}.dll", absProject.stem().string());
char hostExeBuf[MAX_PATH];
DWORD hostExeLen = GetModuleFileNameA(nullptr, hostExeBuf, MAX_PATH);
if (hostExeLen == 0 || hostExeLen == MAX_PATH) {
throw std::runtime_error("GetModuleFileName failed");
}
fs::path hostExe(std::string(hostExeBuf, hostExeLen));
fs::path sourceDir = Crafter::GetCrafterBuildHome();
Configuration hostConfig;
hostConfig.target = "x86_64-pc-windows-msvc";
hostConfig.march = "native";
hostConfig.mtune = "native";
fs::path cacheDir = GetCacheDir() / std::format("{}-{}", hostConfig.target, hostConfig.march);
if (!fs::exists(cacheDir)) fs::create_directories(cacheDir);
std::string stdResult = BuildStdPcm(hostConfig, cacheDir / "std.pcm");
if (!stdResult.empty()) {
throw std::runtime_error(std::format("Failed to build std.pcm: {}", stdResult));
}
EnsureCrafterBuildPcms(sourceDir, cacheDir);
// See the Linux LoadProject for why this sits here and runs every time.
ProjectFileDependencies preDeps = ResolveProjectFileDependencies(absProject, hostConfig);
bool stale = ProjectFileStale(dllPath, absProject, hostExe, preDeps);
if (stale) {
fs::path crafterBuildLib = hostExe.parent_path() / "crafter-build.lib";
std::string compileCmd = std::format(
"clang++ --target=x86_64-pc-windows-msvc -march=native -mtune=native "
"-std=c++26 -shared -O3 -Wno-return-type-c-linkage -fuse-ld=lld "
"-D CRAFTER_BUILD_DLL_IMPORT "
"-nostdinc++ -nostdlib++ -isystem %LIBCXX_DIR%\\include\\c++\\v1 "
"-fprebuilt-module-path={}{} "
"-Wl,/EXPORT:CrafterBuildProject "
"{} {}{} -o {} -L %LIBCXX_DIR%\\lib -lc++",
cacheDir.string(), preDeps.compileFlags,
absProject.string(), crafterBuildLib.string(), preDeps.linkFlags, dllPath.string());
std::string result = RunBuildCommand(compileCmd);
if (!result.empty()) {
throw std::runtime_error(std::format("Failed to compile project {}: {}", absProject.string(), result));
}
}
// project.dll links libc++ dynamically (-lc++) and the dll typically lives
// under %LIBCXX_DIR%\lib. Add it to the loader's search path so the user
// doesn't have to put it on PATH manually.
if (const char* libcxx = std::getenv("LIBCXX_DIR"); libcxx && *libcxx) {
fs::path libcxxLib = fs::path(libcxx) / "lib";
SetDllDirectoryA(libcxxLib.string().c_str());
}
HMODULE handle = LoadLibraryA(dllPath.string().c_str());
if (!handle) {
throw std::runtime_error(std::format("Failed to load project {}: error {}", dllPath.string(), GetLastError()));
}
using ProjectFn = Configuration (*)(std::span<const std::string_view>);
auto fn = reinterpret_cast<ProjectFn>(GetProcAddress(handle, "CrafterBuildProject"));
if (!fn) {
throw std::runtime_error(std::format("CrafterBuildProject not found in {}: error {}", dllPath.string(), GetLastError()));
}
guard.Frame().dependencies = std::move(preDeps.configs);
return fn(args);
}
#endif
#ifdef CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32
// Linux→mingw cross-compile run on Windows: mingw ABI + libstdc++ from msys2.
// Resolves the mingw sysroot at C:\msys64\ucrt64 by default (UCRT runtime,
// matching the Arch cross-compile that produced this binary). User can
// override with CRAFTER_MINGW_DIR if they have mingw-w64 installed elsewhere
// or want the legacy MSVCRT mingw at C:\msys64\mingw64. clang itself is the
// user's existing standalone clang on PATH.
namespace {
fs::path MingwPrefix() {
if (const char* env = std::getenv("CRAFTER_MINGW_DIR"); env && *env) {
return fs::path(env);
}
return "C:\\msys64\\ucrt64";
}
std::string MingwGccVersion() {
fs::path includeRoot = MingwPrefix() / "include" / "c++";
if (!fs::exists(includeRoot)) {
throw std::runtime_error(std::format("mingw-w64 not found at {} (install msys2 mingw-w64-x86_64-toolchain or set CRAFTER_MINGW_DIR)", MingwPrefix().string()));
}
std::vector<std::string> versions;
for (const auto& entry : fs::directory_iterator(includeRoot)) {
if (entry.is_directory()) versions.push_back(entry.path().filename().string());
}
if (versions.empty()) {
throw std::runtime_error(std::format("no C++ versions under {}", includeRoot.string()));
}
std::sort(versions.begin(), versions.end(), std::greater<>());
return versions.front();
}
}
std::string Crafter::BuildStdPcm(const Configuration& config, fs::path stdPcm) {
CacheLock lock(stdPcm.parent_path());
if (config.target == "x86_64-pc-windows-msvc") {
// MSVC target on mingw host: same MSVC libc++ logic as the
// native-MSVC LoadProject path. User must have LIBCXX_DIR pointing
// at a Windows libc++ install.
const char* libcxxEnv = std::getenv("LIBCXX_DIR");
if (!libcxxEnv || !*libcxxEnv) {
return "MSVC target requires LIBCXX_DIR pointing at a Windows libc++ install";
}
std::string stdcppm = std::format("{}\\modules\\c++\\v1\\std.cppm", libcxxEnv);
if (fs::exists(stdPcm) && fs::last_write_time(stdPcm) >= fs::last_write_time(stdcppm)) {
return "";
}
return RunBuildCommand(std::format(
"clang++ --target={} -march={} -mtune={} -isystem %LIBCXX_DIR%\\include\\c++\\v1 "
"-nostdinc++ -nostdlib++ -std=c++26 -Wno-reserved-identifier -Wno-reserved-module-identifier "
"--precompile %LIBCXX_DIR%\\modules\\c++\\v1\\std.cppm -o {}",
config.target, config.march, config.mtune, stdPcm.string()));
}
// Default: mingw target. Look up mingw-w64 libstdc++ via the msys2 prefix.
fs::path prefix = MingwPrefix();
fs::path stdCc = prefix / "include" / "c++" / MingwGccVersion() / "bits" / "std.cc";
if (!fs::exists(stdCc)) {
return std::format("std.cc not found at {}", stdCc.string());
}
if (fs::exists(stdPcm) && fs::last_write_time(stdPcm) >= fs::last_write_time(stdCc)) {
return "";
}
// Copy std.cc → std.cppm in C++ rather than via cmd's `copy /Y`: keeping
// it in-process means one less shell round-trip and a std::error_code we
// can report directly. Held under the cache lock, so a plain filename is
// safe — no other builder writes this path concurrently.
fs::path stdCppm = stdPcm.parent_path() / "std.cppm";
std::error_code ec;
fs::copy_file(stdCc, stdCppm, fs::copy_options::overwrite_existing, ec);
if (ec) {
return std::format("copy {} -> {}: {}", stdCc.string(), stdCppm.string(), ec.message());
}
return RunBuildCommand(std::format(
"clang++ --target={} -march={} -mtune={} "
"--sysroot=\"{}\" -femulated-tls "
"-O3 -std=c++26 -Wno-reserved-identifier -Wno-reserved-module-identifier "
"--precompile \"{}\" -o {}",
config.target, config.march, config.mtune,
prefix.string(),
stdCppm.string(),
stdPcm.string()));
}
std::string Crafter::GetBaseCommand(const Configuration& config) {
if (config.target == "x86_64-pc-windows-msvc") {
return std::format("clang++ -nostdinc++ -nostdlib++ -isystem %LIBCXX_DIR%\\include\\c++\\v1");
}
return std::format("clang++ --sysroot=\"{}\" -femulated-tls", MingwPrefix().string());
}
namespace {
void EnsureCrafterBuildPcms(const fs::path& sourceDir, const fs::path& cacheDir) {
CacheLock lock(cacheDir);
// Re-derived under the lock: another builder may have refreshed the
// cache from its own sources while we waited.
std::string stamp = CrafterBuildSourceStamp(sourceDir, CrafterBuildModules);
bool upToDate = ReadCacheStamp(cacheDir) == stamp;
fs::path prefix = MingwPrefix();
for (std::string_view name : CrafterBuildModules) {
fs::path cppmPath = sourceDir / std::format("{}.cppm", name);
fs::path pcmPath = cacheDir / std::format("{}.pcm", name);
if (!fs::exists(cppmPath)) {
throw std::runtime_error(std::format("module source {} not found in {} (set CRAFTER_BUILD_HOME)", name, sourceDir.string()));
}
if (upToDate && fs::exists(pcmPath)) {
continue;
}
std::string cmd = std::format(
"clang++ --target=x86_64-w64-mingw32 -march=native -mtune=native "
"--sysroot=\"{}\" -femulated-tls "
"-std=c++26 -O3 -D CRAFTER_BUILD_DLL_IMPORT "
"-Wno-reserved-identifier -Wno-reserved-module-identifier "
"-fprebuilt-module-path={} "
"--precompile {} -o {}",
prefix.string(), cacheDir.string(), cppmPath.string(), pcmPath.string());
CommandResult r = Crafter::RunCommandChecked(cmd);
if (r.exitCode != 0) {
throw std::runtime_error(std::format("Failed to precompile {} (exit {}): {}", name, r.exitCode, r.output));
}
}
// Last, so an interrupted rebuild leaves the stamp disagreeing with the
// sources and the next run starts over rather than trusting a half-built
// set of PCMs.
if (!upToDate) WriteCacheStamp(cacheDir, stamp);
}
}
Configuration Crafter::LoadProject(const fs::path& projectFile, std::span<const std::string_view> args) {
fs::path absProject = fs::canonical(projectFile);
ProjectFileGuard guard(absProject);
fs::path buildDir = absProject.parent_path() / "build";
if (!fs::exists(buildDir)) fs::create_directories(buildDir);
fs::path dllPath = buildDir / std::format("{}.dll", absProject.stem().string());
char hostExeBuf[MAX_PATH];
DWORD hostExeLen = GetModuleFileNameA(nullptr, hostExeBuf, MAX_PATH);
if (hostExeLen == 0 || hostExeLen == MAX_PATH) {
throw std::runtime_error("GetModuleFileName failed");
}
fs::path hostExe(std::string(hostExeBuf, hostExeLen));
fs::path sourceDir = Crafter::GetCrafterBuildHome();
Configuration hostConfig;
hostConfig.target = "x86_64-w64-mingw32";
hostConfig.march = "native";
hostConfig.mtune = "native";
fs::path cacheDir = GetCacheDir() / std::format("{}-{}", hostConfig.target, hostConfig.march);
if (!fs::exists(cacheDir)) fs::create_directories(cacheDir);
std::string stdResult = BuildStdPcm(hostConfig, cacheDir / "std.pcm");
if (!stdResult.empty()) {
throw std::runtime_error(std::format("Failed to build std.pcm: {}", stdResult));
}
EnsureCrafterBuildPcms(sourceDir, cacheDir);
// See the Linux LoadProject for why this sits here and runs every time.
ProjectFileDependencies preDeps = ResolveProjectFileDependencies(absProject, hostConfig);
bool stale = ProjectFileStale(dllPath, absProject, hostExe, preDeps);
if (stale) {
// The import lib lives next to the launcher exe (Build()'s mingw
// copy step puts libcrafter-build.dll.a there) so -L<exe-dir>
// -lcrafter-build resolves it.
fs::path prefix = MingwPrefix();
std::string compileCmd = std::format(
"clang++ --target=x86_64-w64-mingw32 -march=native -mtune=native "
"--sysroot=\"{}\" -femulated-tls "
"-std=c++26 -shared -O3 -Wno-return-type-c-linkage -fuse-ld=lld "
"-D CRAFTER_BUILD_DLL_IMPORT "
"-fprebuilt-module-path={}{} "
// mingw-lld doesn't accept /EXPORT:NAME — use the catch-all so
// CrafterBuildProject is reachable via GetProcAddress without
// forcing project.cpp templates to add __declspec(dllexport).
"-Wl,--export-all-symbols "
// Dependency archives go before -lcrafter-build: they reference
// Crafter.Build symbols, and ld.lld resolves archives in order.
"{} -o {}{} -L\"{}\" -lcrafter-build -lstdc++exp -lpthread",
prefix.string(),
cacheDir.string(), preDeps.compileFlags,
absProject.string(), dllPath.string(), preDeps.linkFlags,
hostExe.parent_path().string());
std::string result = RunBuildCommand(compileCmd);
if (!result.empty()) {
throw std::runtime_error(std::format("Failed to compile project {}: {}", absProject.string(), result));
}
}
// project.dll is a mingw DLL linking libstdc++-6.dll / libgcc_s_seh-1.dll /
// libwinpthread-1.dll dynamically (LoadProject's compile uses -lstdc++exp
// -lpthread, not -static). We already know where they live from
// MingwPrefix(), so add bin/ to the loader's search path here instead of
// requiring the user to prepend it to PATH manually.
fs::path mingwBin = MingwPrefix() / "bin";
SetDllDirectoryA(mingwBin.string().c_str());
HMODULE handle = LoadLibraryA(dllPath.string().c_str());
if (!handle) {
throw std::runtime_error(std::format("Failed to load project {}: error {}", dllPath.string(), GetLastError()));
}
using ProjectFn = Configuration (*)(std::span<const std::string_view>);
auto fn = reinterpret_cast<ProjectFn>(GetProcAddress(handle, "CrafterBuildProject"));
if (!fn) {
throw std::runtime_error(std::format("CrafterBuildProject not found in {}: error {}", dllPath.string(), GetLastError()));
}
guard.Frame().dependencies = std::move(preDeps.configs);
return fn(args);
}
#endif
#ifdef __linux__
CommandResult Crafter::RunCommandChecked(std::string_view cmd) { CommandResult Crafter::RunCommandChecked(std::string_view cmd) {
std::array<char, 128> buffer; std::array<char, 128> buffer;
CommandResult result{}; CommandResult result{};
@ -664,7 +1191,7 @@ std::string Crafter::BuildStdPcm(const Configuration& config, fs::path stdPcm) {
// non-wasm cross builds: the driver ranks the host toolchain's own // non-wasm cross builds: the driver ranks the host toolchain's own
// libc++ headers (<install>/../include/c++/v1) above --sysroot, so // libc++ headers (<install>/../include/c++/v1) above --sysroot, so
// the PCM would mix host libc++ with the foreign libc — force the // the PCM would mix host libc++ with the foreign libc — force the
// sysroot's copy. // sysroot's copy (same trick as the Windows LIBCXX_DIR path)
if (!isWasm && !config.sysroot.empty()) { if (!isWasm && !config.sysroot.empty()) {
archFlags += std::format(" -nostdinc++ -isystem {}/usr/include/c++/v1", config.sysroot); archFlags += std::format(" -nostdinc++ -isystem {}/usr/include/c++/v1", config.sysroot);
} }
@ -821,3 +1348,5 @@ Configuration Crafter::LoadProject(const fs::path& projectFile, std::span<const
guard.Frame().dependencies = std::move(preDeps.configs); guard.Frame().dependencies = std::move(preDeps.configs);
return fn(args); return fn(args);
} }
#endif

View file

@ -4,8 +4,18 @@
module; module;
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <unistd.h> #if defined(_WIN32)
#include <sys/ioctl.h> #include <io.h>
#include <windows.h>
#ifndef STDOUT_FILENO
#define STDOUT_FILENO _fileno(stdout)
#endif
#define CRAFTER_PROGRESS_ISATTY _isatty
#else
#include <unistd.h>
#include <sys/ioctl.h>
#define CRAFTER_PROGRESS_ISATTY isatty
#endif
export module Crafter.Build:Progress_impl; export module Crafter.Build:Progress_impl;
import std; import std;
@ -22,10 +32,19 @@ namespace {
std::chrono::steady_clock::time_point StartTime = std::chrono::steady_clock::now(); std::chrono::steady_clock::time_point StartTime = std::chrono::steady_clock::now();
std::int32_t TerminalWidth() { std::int32_t TerminalWidth() {
#if defined(_WIN32)
HANDLE h = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_SCREEN_BUFFER_INFO info;
if (h != INVALID_HANDLE_VALUE && GetConsoleScreenBufferInfo(h, &info)) {
std::int32_t w = info.srWindow.Right - info.srWindow.Left + 1;
if (w > 0) return w;
}
#else
winsize ws{}; winsize ws{};
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col > 0) { if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col > 0) {
return ws.ws_col; return ws.ws_col;
} }
#endif
if (const char* col = std::getenv("COLUMNS")) { if (const char* col = std::getenv("COLUMNS")) {
try { std::int32_t c = std::stoi(col); if (c > 0) return c; } catch (...) {} try { std::int32_t c = std::stoi(col); if (c > 0) return c; } catch (...) {}
} }
@ -67,7 +86,7 @@ namespace Crafter::Progress {
void SetVerbosity(Verbosity v) { void SetVerbosity(Verbosity v) {
std::lock_guard lock(StateMutex); std::lock_guard lock(StateMutex);
ActiveVerbosity = v; ActiveVerbosity = v;
IsTty = isatty(STDOUT_FILENO) != 0; IsTty = CRAFTER_PROGRESS_ISATTY(STDOUT_FILENO) != 0;
StartTime = std::chrono::steady_clock::now(); StartTime = std::chrono::steady_clock::now();
Total.store(0); Total.store(0);
Done.store(0); Done.store(0);

View file

@ -35,11 +35,38 @@ namespace {
return out; return out;
} }
// cmd.exe doesn't recognize '...' as quoting (it would pass the single
// quotes through to the executable). Wrap in "..." for cmd; embedded "
// is rare in paths but escape it to be safe. Backslash sequences before
// " don't need the MS-CRT doubling rules because we go through
// `cmd /C "..."`, which uses cmd's parser, not the CRT's argv splitter.
std::string ShellQuoteCmd(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;
}
// Host-shell quoting: sh on Linux, cmd on Windows. For args/paths that
// hit the local shell (Local runner exec, Cmd-prefix runner exec).
std::string ShellQuoteHost(std::string_view s) {
#ifdef _WIN32
return ShellQuoteCmd(s);
#else
return ShellQuoteSh(s);
#endif
}
std::string JoinAndQuoteArgs(std::span<const std::string> args) { std::string JoinAndQuoteArgs(std::span<const std::string> args) {
std::string out; std::string out;
for (const auto& a : args) { for (const auto& a : args) {
if (!out.empty()) out.push_back(' '); if (!out.empty()) out.push_back(' ');
out += ShellQuoteSh(a); out += ShellQuoteHost(a);
} }
return out; return out;
} }
@ -164,7 +191,11 @@ TestRunner TestRunner::Cmd(std::string command) {
TestRunner r; TestRunner r;
r.name = std::format("cmd:{}", command); r.name = std::format("cmd:{}", command);
r.exec = std::format("{} {{bin}} {{args}}", command); r.exec = std::format("{} {{bin}} {{args}}", command);
#ifdef _WIN32
r.probe = std::format("where {}", command);
#else
r.probe = std::format("which {}", command); r.probe = std::format("which {}", command);
#endif
return r; return r;
} }
@ -172,7 +203,11 @@ TestRunner TestRunner::Wine() {
TestRunner r; TestRunner r;
r.name = "wine"; r.name = "wine";
r.exec = "wine {bin} {args}"; r.exec = "wine {bin} {args}";
#ifdef _WIN32
r.probe = "where wine";
#else
r.probe = "which wine"; r.probe = "which wine";
#endif
return r; return r;
} }
@ -183,8 +218,11 @@ TestRunner TestRunner::ForTarget(const Configuration& cfg) {
// case (cfg.target defaulted to HostTarget()) without any wrapper. // case (cfg.target defaulted to HostTarget()) without any wrapper.
if (target == HostTarget()) return Local(); if (target == HostTarget()) return Local();
// Windows targets run under Wine; there is no Windows host. // Windows targets: native on a Windows host, Wine on Linux. We don't
if (TargetIsWindows(target)) return Wine(); // distinguish mingw vs msvc here — the produced .exe runs the same way.
if (TargetIsWindows(target)) {
return TargetIsWindows(HostTarget()) ? Local() : Wine();
}
// WASI: a .wasm file isn't directly executable; wasmtime is the canonical // WASI: a .wasm file isn't directly executable; wasmtime is the canonical
// runtime. wasi-cli also works but the upstream Bytecode Alliance name is // runtime. wasi-cli also works but the upstream Bytecode Alliance name is
@ -273,7 +311,7 @@ TestResult Crafter::RunSingleTest(const Test& test, const fs::path& binary, std:
if (test.runner.exec.empty()) { if (test.runner.exec.empty()) {
// Local runner: spawn the binary directly through the host shell. // Local runner: spawn the binary directly through the host shell.
std::string cmd = std::format("{} {}", ShellQuoteSh(binary.string()), ph["{args}"]); std::string cmd = std::format("{} {}", ShellQuoteHost(binary.string()), ph["{args}"]);
r = RunCommandWithTimeout(cmd, timeout); r = RunCommandWithTimeout(cmd, timeout);
} else { } else {
// Prefix runner (qemu-user, wasmtime, wine, ...): templated exec // Prefix runner (qemu-user, wasmtime, wine, ...): templated exec
@ -299,7 +337,11 @@ TestResult Crafter::RunSingleTest(const Test& test, const fs::path& binary, std:
namespace { namespace {
bool ToolOnPath(std::string_view name) { bool ToolOnPath(std::string_view name) {
#ifdef _WIN32
std::string cmd = std::format("where {} > nul 2>&1", name);
#else
std::string cmd = std::format("which {} > /dev/null 2>&1", name); std::string cmd = std::format("which {} > /dev/null 2>&1", name);
#endif
return std::system(cmd.c_str()) == 0; return std::system(cmd.c_str()) == 0;
} }

View file

@ -2,7 +2,16 @@
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
#include <cstdio> #include <cstdio>
#include <unistd.h> #if defined(_WIN32)
#include <io.h>
#ifndef STDOUT_FILENO
#define STDOUT_FILENO _fileno(stdout)
#endif
#define CRAFTER_MAIN_ISATTY _isatty
#else
#include <unistd.h>
#define CRAFTER_MAIN_ISATTY isatty
#endif
import std; import std;
import Crafter.Build; import Crafter.Build;
@ -17,7 +26,7 @@ int main(int argc, char** argv) {
// process blocks in its serve loop and never returns to flush. A TTY is // process blocks in its serve loop and never returns to flush. A TTY is
// already line-buffered and its in-place redraw path flushes explicitly, so // already line-buffered and its in-place redraw path flushes explicitly, so
// only adjust when redirected. // only adjust when redirected.
if (isatty(STDOUT_FILENO) == 0) { if (CRAFTER_MAIN_ISATTY(STDOUT_FILENO) == 0) {
std::setvbuf(stdout, nullptr, _IOLBF, 0); std::setvbuf(stdout, nullptr, _IOLBF, 0);
} }
return Crafter::Run(argc, argv); return Crafter::Run(argc, argv);

View file

@ -0,0 +1,14 @@
// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
#pragma once
#if defined(_WIN32)
#if defined(CRAFTER_BUILD_DLL_IMPORT)
#define CRAFTER_API __declspec(dllimport)
#else
#define CRAFTER_API __declspec(dllexport)
#endif
#else
#define CRAFTER_API
#endif

View file

@ -1,6 +1,8 @@
// SPDX-License-Identifier: LGPL-3.0-only // SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
module;
#include "Crafter.Build-Api.h"
export module Crafter.Build:Asset; export module Crafter.Build:Asset;
import std; import std;
namespace fs = std::filesystem; namespace fs = std::filesystem;
@ -17,7 +19,7 @@ namespace Crafter {
// that errors out at runtime if cfg.assets is non-empty — full // that errors out at runtime if cfg.assets is non-empty — full
// support requires a self-host rebuild via crafter-build itself, // support requires a self-host rebuild via crafter-build itself,
// which resolves the Crafter.Asset dep through project.cpp. // which resolves the Crafter.Asset dep through project.cpp.
export std::string CompressAsset(const fs::path& input, const fs::path& output); export CRAFTER_API std::string CompressAsset(const fs::path& input, const fs::path& output);
// Split a single .obj + .mtl file by material group into per-material // Split a single .obj + .mtl file by material group into per-material
// .cmesh files, per-unique-albedo .ctex files, and a scene.txt manifest // .cmesh files, per-unique-albedo .ctex files, and a scene.txt manifest
@ -34,7 +36,7 @@ namespace Crafter {
// Idempotent: returns "" immediately if outDir/scene.txt already exists, // Idempotent: returns "" immediately if outDir/scene.txt already exists,
// so repeated builds skip the expensive split+resize step. // so repeated builds skip the expensive split+resize step.
// Returns "" on success, an error string on failure. // Returns "" on success, an error string on failure.
export std::string BuildOBJBundle( export CRAFTER_API std::string BuildOBJBundle(
const fs::path& objPath, const fs::path& objPath,
const fs::path& mtlPath, const fs::path& mtlPath,
const fs::path& outDir, const fs::path& outDir,

View file

@ -1,6 +1,8 @@
// SPDX-License-Identifier: LGPL-3.0-only // SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
module;
#include "Crafter.Build-Api.h"
export module Crafter.Build:Clang; export module Crafter.Build:Clang;
import std; import std;
import :Shader; import :Shader;
@ -63,28 +65,30 @@ export namespace Crafter {
bool IsLocal() const { return exec.empty(); } bool IsLocal() const { return exec.empty(); }
static TestRunner Local(); static CRAFTER_API TestRunner Local();
// Prefix runner: wraps the local binary in `<command> {bin} {args}`. // Prefix runner: wraps the local binary in `<command> {bin} {args}`.
// Used for qemu-user, wasmtime, and similar single-binary wrappers. // Used for qemu-user, wasmtime, and similar single-binary wrappers.
static TestRunner Cmd(std::string command); static CRAFTER_API TestRunner Cmd(std::string command);
// Run a Windows .exe through Wine. Probes `wine` on PATH. // Run a Windows .exe through Wine. Probes `wine` on PATH; on a Windows
static TestRunner Wine(); // host the wine wrapper is pointless, so callers should route to Local
// before reaching here.
static CRAFTER_API TestRunner Wine();
// Parse a `<kind>[:<arg>]` spec used by CRAFTER_BUILD_RUNNER_<target> // Parse a `<kind>[:<arg>]` spec used by CRAFTER_BUILD_RUNNER_<target>
// and --runner=. Supported: "local", "cmd:<binary>". Returns nullopt // and --runner=. Supported: "local", "cmd:<binary>". Returns nullopt
// for an empty string; throws on a non-empty unrecognized spec. // for an empty string; throws on a non-empty unrecognized spec.
static std::optional<TestRunner> FromSpec(std::string_view spec); static CRAFTER_API std::optional<TestRunner> FromSpec(std::string_view spec);
// Honor CRAFTER_BUILD_RUNNER_<target> (power-user override). Triple // Honor CRAFTER_BUILD_RUNNER_<target> (power-user override). Triple
// dashes/dots become underscores so they're valid in env-var names. // dashes/dots become underscores so they're valid in env-var names.
// Returns `fallback` when the env var is unset. // Returns `fallback` when the env var is unset.
static TestRunner FromEnv(std::string_view target, TestRunner fallback = Local()); static CRAFTER_API TestRunner FromEnv(std::string_view target, TestRunner fallback = Local());
// Derive a runner from a Configuration's target triple + sysroot. // Derive a runner from a Configuration's target triple + sysroot.
// Returns Local() when target equals the host, Wine() for Windows // Returns Local() when target equals the host, Wine() for Windows
// targets, `qemu-<arch>` (with QEMU_LD_PREFIX // targets on a non-Windows host, `qemu-<arch>` (with QEMU_LD_PREFIX
// set when cfg.sysroot is non-empty) for non-host -linux- triples, // set when cfg.sysroot is non-empty) for non-host -linux- triples,
// `wasmtime` for wasm32-wasi/wasm64-wasi, and Local() as a last // `wasmtime` for wasm32-wasi/wasm64-wasi, and Local() as a last
// resort. CRAFTER_BUILD_RUNNER_<target> still wins as an override // resort. CRAFTER_BUILD_RUNNER_<target> still wins as an override
// upstream of this — see FromEnv. // upstream of this — see FromEnv.
static TestRunner ForTarget(const struct Configuration& cfg); static CRAFTER_API TestRunner ForTarget(const struct Configuration& cfg);
}; };
enum class TestOutcome { Pass, Fail, Crash, Timeout, Skipped }; enum class TestOutcome { Pass, Fail, Crash, Timeout, Skipped };
@ -229,18 +233,17 @@ export namespace Crafter {
}; };
// Per-file view handed to each LintRule's check callback. Every member // Per-file view handed to each LintRule's check callback. Every member
// function is out-of-line (defined in Crafter.Build:Lint's implementation // function is out-of-line and CRAFTER_API (defined in Crafter.Build:Lint's
// unit) because rule lambdas execute from the user's project.so and // implementation unit) because rule lambdas execute from the user's
// resolve against the running executable: clang does not emit // project DLL on Windows — clang does not emit module-attached in-class
// module-attached in-class inline bodies into consumers (see ArgQuery // inline bodies into consumers (see ArgQuery below).
// below).
struct LintContext { struct LintContext {
fs::path file; // absolute path of the file under lint fs::path file; // absolute path of the file under lint
std::string content; // whole file as read from disk std::string content; // whole file as read from disk
std::vector<std::string_view> lines; // views into `content`, one per line, no '\n' std::vector<std::string_view> lines; // views into `content`, one per line, no '\n'
std::string Extension() const; // ".cppm", ".cpp", ".h", ... CRAFTER_API std::string Extension() const; // ".cppm", ".cpp", ".h", ...
std::string_view Line(std::size_t n) const; // 1-based; empty if out of range CRAFTER_API std::string_view Line(std::size_t n) const; // 1-based; empty if out of range
// `content` with comments and string/char literal bodies blanked to // `content` with comments and string/char literal bodies blanked to
// spaces, newlines preserved — offsets and line numbers stay valid. // spaces, newlines preserved — offsets and line numbers stay valid.
// Built from Tokens() on first call, cached per file, so raw strings, // Built from Tokens() on first call, cached per file, so raw strings,
@ -251,9 +254,9 @@ export namespace Crafter {
// Convenient for a quick scan, but Tokens() is the better tool for // Convenient for a quick scan, but Tokens() is the better tool for
// anything structural: this view cannot tell an identifier from a // anything structural: this view cannot tell an identifier from a
// keyword, and it has already thrown away where the literals were. // keyword, and it has already thrown away where the literals were.
const std::string& CommentStripped(); CRAFTER_API const std::string& CommentStripped();
// Record a finding at `line` (1-based; pass 0 for a whole-file finding). // Record a finding at `line` (1-based; pass 0 for a whole-file finding).
void Report(std::size_t line, std::string message); CRAFTER_API void Report(std::size_t line, std::string message);
// Replace the file's content. Makes this rule a *transform*: // Replace the file's content. Makes this rule a *transform*:
// `crafter-build format` writes the result back to disk; `lint` // `crafter-build format` writes the result back to disk; `lint`
// derives would-reformat findings from it (dry — never writes). // derives would-reformat findings from it (dry — never writes).
@ -262,7 +265,7 @@ export namespace Crafter {
// pattern: build the new string, call SetContent once at the end. Do // pattern: build the new string, call SetContent once at the end. Do
// not assign `content` directly — that bypasses the re-split. May be // not assign `content` directly — that bypasses the re-split. May be
// combined with Report() in the same rule. // combined with Report() in the same rule.
void SetContent(std::string newContent); CRAFTER_API void SetContent(std::string newContent);
// True when `rule` is suppressed at `line` (1-based; 0 = whole-file, // True when `rule` is suppressed at `line` (1-based; 0 = whole-file,
// which only file-level directives cover) by a suppression comment: // which only file-level directives cover) by a suppression comment:
// // lint-disable-next-line [rules...] applies to the following line // // lint-disable-next-line [rules...] applies to the following line
@ -274,7 +277,7 @@ export namespace Crafter {
// itself for every line its edit touches (the driver cannot map lines // itself for every line its edit touches (the driver cannot map lines
// across a count-changing rewrite). Parsed lazily from the raw lines; // across a count-changing rewrite). Parsed lazily from the raw lines;
// re-parsed after SetContent. // re-parsed after SetContent.
bool Suppressed(std::string_view rule, std::size_t line); CRAFTER_API bool Suppressed(std::string_view rule, std::size_t line);
// The file lexed by clang, in source order. Built on first call, cached // The file lexed by clang, in source order. Built on first call, cached
// per file, re-lexed after SetContent. Empty for extensions that are // per file, re-lexed after SetContent. Empty for extensions that are
@ -284,16 +287,16 @@ export namespace Crafter {
// raw strings, line splices, digraphs and nested quoting right, and // raw strings, line splices, digraphs and nested quoting right, and
// offsets index straight into `content`, so a transform can find in // offsets index straight into `content`, so a transform can find in
// the token stream and edit in place. // the token stream and edit in place.
std::span<const LintToken> Tokens(); CRAFTER_API std::span<const LintToken> Tokens();
// The token's own bytes: content.substr(tok.offset, tok.length). // The token's own bytes: content.substr(tok.offset, tok.length).
std::string_view TokenText(const LintToken& token) const; CRAFTER_API std::string_view TokenText(const LintToken& token) const;
// Tokens whose FIRST byte is on `line` (1-based). A token that starts // Tokens whose FIRST byte is on `line` (1-based). A token that starts
// earlier and spans into `line` — a raw string, a block comment — is // earlier and spans into `line` — a raw string, a block comment — is
// not included; ask Tokens() directly when that matters. // not included; ask Tokens() directly when that matters.
std::span<const LintToken> TokensOnLine(std::size_t line); CRAFTER_API std::span<const LintToken> TokensOnLine(std::size_t line);
// True when a comment token starts on `line`. Replaces `Line(n).contains("//")`, // True when a comment token starts on `line`. Replaces `Line(n).contains("//")`,
// which false-positives on a `//` inside a string literal. // which false-positives on a `//` inside a string literal.
bool LineHasComment(std::size_t line); CRAFTER_API bool LineHasComment(std::size_t line);
// True when `line` (1-based) is touched by a token that spans more // True when `line` (1-based) is touched by a token that spans more
// than one line — a raw string or a block comment. A transform that // than one line — a raw string or a block comment. A transform that
// joins, splits or rewrites such a line changes what is inside that // joins, splits or rewrites such a line changes what is inside that
@ -301,7 +304,7 @@ export namespace Crafter {
// Replaces `Line(n).contains("R\"")`, which both misses raw strings // Replaces `Line(n).contains("R\"")`, which both misses raw strings
// opened on an earlier line and fires on the characters R" appearing // opened on an earlier line and fires on the characters R" appearing
// inside an ordinary literal. // inside an ordinary literal.
bool LineHasMultiLineToken(std::size_t line); CRAFTER_API bool LineHasMultiLineToken(std::size_t line);
// Declarations written in this file, flattened depth-first with // Declarations written in this file, flattened depth-first with
// parents before children (see LintDecl). Empty when the AST is not // parents before children (see LintDecl). Empty when the AST is not
@ -318,11 +321,11 @@ export namespace Crafter {
// slice. Declarations inside a preprocessor branch that is inactive // slice. Declarations inside a preprocessor branch that is inactive
// for the host are absent here, though Tokens() still sees them. Rules // for the host are absent here, though Tokens() still sees them. Rules
// that must cover every platform belong on tokens. // that must cover every platform belong on tokens.
std::span<const LintDecl> Decls(); CRAFTER_API std::span<const LintDecl> Decls();
bool AstAvailable(); CRAFTER_API bool AstAvailable();
// Why AstAvailable() is false — a missing PCM, a parse error, a file // Why AstAvailable() is false — a missing PCM, a parse error, a file
// that is not a translation unit. Empty when the AST is available. // that is not a translation unit. Empty when the AST is available.
std::string_view AstUnavailableReason(); CRAFTER_API std::string_view AstUnavailableReason();
// Driver wiring — set by RunLint before each check call. Not for rules. // Driver wiring — set by RunLint before each check call. Not for rules.
std::string activeRule; std::string activeRule;
@ -360,7 +363,7 @@ export namespace Crafter {
// Configuration::target so projects don't have to hardcode it for the // Configuration::target so projects don't have to hardcode it for the
// no-cross-compile case. Returns "" if clang isn't on PATH or doesn't // no-cross-compile case. Returns "" if clang isn't on PATH or doesn't
// print a triple — those projects still need an explicit cfg.target. // print a triple — those projects still need an explicit cfg.target.
std::string HostTarget(); CRAFTER_API std::string HostTarget();
struct Configuration { struct Configuration {
fs::path path; fs::path path;
@ -436,7 +439,7 @@ export namespace Crafter {
std::vector<Test> tests; std::vector<Test> tests;
// Lint rules for `crafter-build lint`. Populate via AddLintRule. // Lint rules for `crafter-build lint`. Populate via AddLintRule.
std::vector<LintRule> lintRules; std::vector<LintRule> lintRules;
void GetInterfacesAndImplementations(std::span<fs::path> interfaces, std::span<fs::path> implementations); CRAFTER_API void GetInterfacesAndImplementations(std::span<fs::path> interfaces, std::span<fs::path> implementations);
// Retry the `import X;` names GetInterfacesAndImplementations could not // Retry the `import X;` names GetInterfacesAndImplementations could not
// place, against the dependency DAG as it stands now. Sources are // place, against the dependency DAG as it stands now. Sources are
// scanned when they're declared, but `dependencies` is often assigned // scanned when they're declared, but `dependencies` is often assigned
@ -445,30 +448,30 @@ export namespace Crafter {
// change and this Configuration's objects would be silently reused // change and this Configuration's objects would be silently reused
// against the new layout. Build() calls this before checking mtimes; // against the new layout. Build() calls this before checking mtimes;
// calling it again is harmless. // calling it again is harmless.
void ResolvePendingImports(); CRAFTER_API void ResolvePendingImports();
// Declare a test. Sources default to `tests/<name>/main.cpp` resolved // Declare a test. Sources default to `tests/<name>/main.cpp` resolved
// against this Configuration's path; target/march/mtune/sysroot/debug // against this Configuration's path; target/march/mtune/sysroot/debug
// are inherited from this Configuration so cross-arch projects don't // are inherited from this Configuration so cross-arch projects don't
// have to re-specify. Returns a builder for chaining defines, deps, // have to re-specify. Returns a builder for chaining defines, deps,
// etc. Defined in Crafter.Build:Test. // etc. Defined in Crafter.Build:Test.
struct TestBuilder AddTest(std::string_view name); CRAFTER_API struct TestBuilder AddTest(std::string_view name);
// Same as AddTest, but compiles the parent's `interfaces` directly // Same as AddTest, but compiles the parent's `interfaces` directly
// into this test's Configuration (rather than going through a dep). // into this test's Configuration (rather than going through a dep).
// Use when the test must rebuild those interfaces with its own // Use when the test must rebuild those interfaces with its own
// compile flags — typically per-march SIMD codegen. // compile flags — typically per-march SIMD codegen.
struct TestBuilder AddTest(std::string_view name, std::span<fs::path> interfaces); CRAFTER_API struct TestBuilder AddTest(std::string_view name, std::span<fs::path> interfaces);
// Math-style fan-out: one Test per MarchTier, all sharing the same // Math-style fan-out: one Test per MarchTier, all sharing the same
// `tests/<name>/main.cpp` source and the same interface set, each // `tests/<name>/main.cpp` source and the same interface set, each
// compiled with the tier's `-march`/`-mtune`. Test names are // compiled with the tier's `-march`/`-mtune`. Test names are
// `<name>-<march>`. // `<name>-<march>`.
void AddMarchVariants(std::string_view name, std::span<fs::path> interfaces, std::span<const struct MarchTier> tiers); CRAFTER_API void AddMarchVariants(std::string_view name, std::span<fs::path> interfaces, std::span<const struct MarchTier> tiers);
// Register a lint rule for `crafter-build lint` / `format`. Rules // Register a lint rule for `crafter-build lint` / `format`. Rules
// registered on any Configuration whose path lies inside the project // registered on any Configuration whose path lies inside the project
// root are collected (deduplicated by name, root-first) — attach them // root are collected (deduplicated by name, root-first) — attach them
// to the lib or the exe config, either works. Rules that call // to the lib or the exe config, either works. Rules that call
// ctx.SetContent are transforms (see LintContext::SetContent). // ctx.SetContent are transforms (see LintContext::SetContent).
// Defined in Crafter.Build:Lint. // Defined in Crafter.Build:Lint.
void AddLintRule(std::string name, std::function<void(LintContext&)> check); CRAFTER_API void AddLintRule(std::string name, std::function<void(LintContext&)> check);
// Same, for a rule that reads LintContext::Decls(). Declared // Same, for a rule that reads LintContext::Decls(). Declared
// separately rather than as a flag on AddLintRule so existing // separately rather than as a flag on AddLintRule so existing
// registrations keep compiling. Such a rule makes the run build the // registrations keep compiling. Such a rule makes the run build the
@ -476,7 +479,7 @@ export namespace Crafter {
// produced becomes an error instead of a silent pass. Prefer // produced becomes an error instead of a silent pass. Prefer
// report-only: a transform running after the first one forces a // report-only: a transform running after the first one forces a
// re-parse of everything it changed. // re-parse of everything it changed.
void AddAstLintRule(std::string name, std::function<void(LintContext&)> check); CRAFTER_API void AddAstLintRule(std::string name, std::function<void(LintContext&)> check);
// Suffix that uniquely identifies this Configuration's compile state. // Suffix that uniquely identifies this Configuration's compile state.
// target+march+mtune are spelled out for readability; the rest // target+march+mtune are spelled out for readability; the rest
// (type, debug, sysroot, defines, compileFlags) collapse into a short // (type, debug, sysroot, defines, compileFlags) collapse into a short
@ -541,14 +544,14 @@ export namespace Crafter {
// Directory holding the std module PCM for this Configuration. Keyed on // Directory holding the std module PCM for this Configuration. Keyed on
// target+march because a BMI is only loadable by a TU with matching target // target+march because a BMI is only loadable by a TU with matching target
// features, and suffixed with the wasm variant flags for the same reason. // features, and suffixed with the wasm variant flags for the same reason.
fs::path StdPcmDir(const Configuration& config); CRAFTER_API fs::path StdPcmDir(const Configuration& config);
// Where Build leaves this Configuration's linked artifact: the executable // Where Build leaves this Configuration's linked artifact: the executable
// (with the target's suffix), `lib<name>.a` / `<name>.lib`, or the shared // (with the target's suffix), `lib<name>.a` / `<name>.lib`, or the shared
// library / DLL. The one rule every "is the consumer older than its // library / DLL. The one rule every "is the consumer older than its
// dependency" check reads, so a staleness test and the link step can't // dependency" check reads, so a staleness test and the link step can't
// disagree about the file name. // disagree about the file name.
fs::path ExpectedOutput(const Configuration& config); CRAFTER_API fs::path ExpectedOutput(const Configuration& config);
// The clang invocation every C++ translation unit in this Configuration is // The clang invocation every C++ translation unit in this Configuration is
// compiled with, before anything that depends on work having happened // compiled with, before anything that depends on work having happened
@ -583,14 +586,14 @@ export namespace Crafter {
fs::path stdPcmDir; fs::path stdPcmDir;
fs::path pcmDir; fs::path pcmDir;
}; };
CompileCommand GetCompileCommand(const Configuration& config); CRAFTER_API CompileCommand GetCompileCommand(const Configuration& config);
BuildResult Build(Configuration& config, std::unordered_map<fs::path, std::shared_future<BuildResult>>& depResults, std::mutex& depMutex); CRAFTER_API BuildResult Build(Configuration& config, std::unordered_map<fs::path, std::shared_future<BuildResult>>& depResults, std::mutex& depMutex);
// Takes main's argv verbatim so the CLI entry point is a one-liner; // Takes main's argv verbatim so the CLI entry point is a one-liner;
// the shape is inherited from the language, not chosen here. // the shape is inherited from the language, not chosen here.
// lint-disable-next-line no-char-pointer // lint-disable-next-line no-char-pointer
std::int32_t Run(std::int32_t argc, char** argv); CRAFTER_API std::int32_t Run(std::int32_t argc, char** argv);
// Delete the bin/ and build/ trees beside `projectFile`, returning the paths // Delete the bin/ and build/ trees beside `projectFile`, returning the paths
// that existed and were removed. Backs `crafter-build clean`. // that existed and were removed. Backs `crafter-build clean`.
@ -600,7 +603,7 @@ export namespace Crafter {
// to compile (and its git dependencies to be present) is useless exactly // to compile (and its git dependencies to be present) is useless exactly
// then. That means variant directories are not enumerated — the whole tree // then. That means variant directories are not enumerated — the whole tree
// goes, which is what the by-hand `rm -rf bin build` did anyway. // goes, which is what the by-hand `rm -rf bin build` did anyway.
std::vector<fs::path> CleanProject(const fs::path& projectFile); CRAFTER_API std::vector<fs::path> CleanProject(const fs::path& projectFile);
// Add a small index.html + runtime.js pair next to the .wasm output so the // Add a small index.html + runtime.js pair next to the .wasm output so the
// build can be loaded directly in a browser (just `serve` the bin dir and // build can be loaded directly in a browser (just `serve` the bin dir and
@ -608,7 +611,7 @@ export namespace Crafter {
// this and shouldn't carry the extra files. Call from project.cpp after // this and shouldn't carry the extra files. Call from project.cpp after
// outputName is set; index.html is generated against the current // outputName is set; index.html is generated against the current
// outputName so renaming the binary later requires another call. // outputName so renaming the binary later requires another call.
void EnableWasiBrowserRuntime(Configuration& cfg); CRAFTER_API void EnableWasiBrowserRuntime(Configuration& cfg);
// Register the relaxed-SIMD codegen variant on cfg. Builds an additional // Register the relaxed-SIMD codegen variant on cfg. Builds an additional
// outputName.relaxed-simd.wasm compiled with -mrelaxed-simd (FMA, // outputName.relaxed-simd.wasm compiled with -mrelaxed-simd (FMA,
@ -619,17 +622,17 @@ export namespace Crafter {
// 2026). Idempotent. Call before EnableWasiBrowserRuntime so the emitted // 2026). Idempotent. Call before EnableWasiBrowserRuntime so the emitted
// variants.json manifest includes it. A no-op for non-wasm targets at build // variants.json manifest includes it. A no-op for non-wasm targets at build
// time (no wasm is produced), but harmless to declare unconditionally. // time (no wasm is produced), but harmless to declare unconditionally.
void EnableWasiRelaxedSimdVariant(Configuration& cfg); CRAFTER_API void EnableWasiRelaxedSimdVariant(Configuration& cfg);
// View over the project's args with simple query helpers. Has(flag) for // View over the project's args with simple query helpers. Has(flag) for
// boolean switches; Get(prefix) for valued options (e.g. Get("--prefix=") // boolean switches; Get(prefix) for valued options (e.g. Get("--prefix=")
// returns the substring after the equals). Definitions are out-of-line so // returns the substring after the equals). Definitions are out-of-line
// project.so resolves them from the executable: clang does not emit // and CRAFTER_API so they cross the Windows DLL boundary cleanly — clang
// module-attached in-class inline bodies into consumers. // does not emit module-attached in-class inline bodies into consumers.
struct ArgQuery { struct ArgQuery {
std::span<const std::string_view> args; std::span<const std::string_view> args;
bool Has(std::string_view flag) const; CRAFTER_API bool Has(std::string_view flag) const;
std::optional<std::string> Get(std::string_view prefix) const; CRAFTER_API std::optional<std::string> Get(std::string_view prefix) const;
}; };
// Apply the framework's standard CLI args + env vars onto cfg: // Apply the framework's standard CLI args + env vars onto cfg:
@ -648,5 +651,5 @@ export namespace Crafter {
// pre-set defaults. Returns an ArgQuery over the same span so projects // pre-set defaults. Returns an ArgQuery over the same span so projects
// can query their own flags (`--timing`, ...) without re-rolling the // can query their own flags (`--timing`, ...) without re-rolling the
// for-arg-in-args loop. // for-arg-in-args loop.
ArgQuery ApplyStandardArgs(Configuration& cfg, std::span<const std::string_view> args); CRAFTER_API ArgQuery ApplyStandardArgs(Configuration& cfg, std::span<const std::string_view> args);
} }

View file

@ -1,6 +1,8 @@
// SPDX-License-Identifier: LGPL-3.0-only // SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
module;
#include "Crafter.Build-Api.h"
export module Crafter.Build:External; export module Crafter.Build:External;
import std; import std;
namespace fs = std::filesystem; namespace fs = std::filesystem;
@ -46,13 +48,13 @@ export namespace Crafter {
// declaration and target — BuildExternal derives its own working directory // declaration and target — BuildExternal derives its own working directory
// through this, so a caller that only needs to know where the headers ended // through this, so a caller that only needs to know where the headers ended
// up cannot drift from where they actually are. // up cannot drift from where they actually are.
fs::path ExternalCloneDir(const ExternalDependency& dep, std::string_view target); CRAFTER_API fs::path ExternalCloneDir(const ExternalDependency& dep, std::string_view target);
// The -I flags this dependency contributes, from its declared includeDirs. // The -I flags this dependency contributes, from its declared includeDirs.
// Also pure, which is what lets the linter's AST layer parse a source that // Also pure, which is what lets the linter's AST layer parse a source that
// includes an external's headers without running a build to find out where // includes an external's headers without running a build to find out where
// they are. The paths only resolve once something has fetched the clone. // they are. The paths only resolve once something has fetched the clone.
std::vector<std::string> ExternalIncludeFlags(const ExternalDependency& dep, std::string_view target); CRAFTER_API std::vector<std::string> ExternalIncludeFlags(const ExternalDependency& dep, std::string_view target);
ExternalBuildResult BuildExternal(const ExternalDependency& dep, std::string_view target, std::atomic<bool>& cancelled); CRAFTER_API ExternalBuildResult BuildExternal(const ExternalDependency& dep, std::string_view target, std::atomic<bool>& cancelled);
// Specification for a sibling crafter-build project to fetch and depend on. // Specification for a sibling crafter-build project to fetch and depend on.
// GitSource picks the revision: leave branch + commit empty for the // GitSource picks the revision: leave branch + commit empty for the
@ -71,7 +73,7 @@ export namespace Crafter {
// remote's project.cpp, and returns a stable Configuration* you can drop // remote's project.cpp, and returns a stable Configuration* you can drop
// into cfg.dependencies. The Configuration is owned by an internal cache // into cfg.dependencies. The Configuration is owned by an internal cache
// for the lifetime of the build; identical specs share one Configuration. // for the lifetime of the build; identical specs share one Configuration.
Configuration* GitProject(const GitProjectSpec& spec); CRAFTER_API Configuration* GitProject(const GitProjectSpec& spec);
// Specification for a local sibling crafter-build project (e.g. an // Specification for a local sibling crafter-build project (e.g. an
// example folder depending on its parent project, or a workspace where // example folder depending on its parent project, or a workspace where
@ -85,7 +87,7 @@ export namespace Crafter {
// cache copy. Resolves projectFile to its canonical absolute path and // cache copy. Resolves projectFile to its canonical absolute path and
// recursively LoadProject's it. Returns a stable Configuration* owned // recursively LoadProject's it. Returns a stable Configuration* owned
// by the same internal cache as GitProject. // by the same internal cache as GitProject.
Configuration* LocalProject(const LocalProjectSpec& spec); CRAFTER_API Configuration* LocalProject(const LocalProjectSpec& spec);
// Clone (or update) the given git source into the shared external // Clone (or update) the given git source into the shared external
// cache and return the path to the cloned working tree. Intended for // cache and return the path to the cloned working tree. Intended for
@ -95,5 +97,5 @@ export namespace Crafter {
// ExternalDependency / GitProject. Caching key = url + branch + commit; // ExternalDependency / GitProject. Caching key = url + branch + commit;
// identical calls reuse the existing clone. Pin `source.commit` to a // identical calls reuse the existing clone. Pin `source.commit` to a
// SHA for reproducible, offline-after-first-fetch builds. // SHA for reproducible, offline-after-first-fetch builds.
fs::path GitFetch(const GitSource& source); CRAFTER_API fs::path GitFetch(const GitSource& source);
} }

View file

@ -1,6 +1,8 @@
// SPDX-License-Identifier: LGPL-3.0-only // SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
module;
#include "Crafter.Build-Api.h"
export module Crafter.Build:Implementation; export module Crafter.Build:Implementation;
import std; import std;
namespace fs = std::filesystem; namespace fs = std::filesystem;
@ -17,8 +19,8 @@ namespace Crafter {
// no reachable Configuration provided when the source was scanned. // no reachable Configuration provided when the source was scanned.
std::vector<std::string> pendingImports; std::vector<std::string> pendingImports;
fs::path path; fs::path path;
Implementation(fs::path&& path); CRAFTER_API Implementation(fs::path&& path);
bool Check(const fs::path& buildDir, const fs::path& pcmDir, fs::file_time_type sourceFloor = fs::file_time_type::min()) const; CRAFTER_API bool Check(const fs::path& buildDir, const fs::path& pcmDir, fs::file_time_type sourceFloor = fs::file_time_type::min()) const;
void Compile(const std::string_view clang, const fs::path& buildDir, std::atomic<bool>& buildCancelled, std::string& buildError) const; CRAFTER_API void Compile(const std::string_view clang, const fs::path& buildDir, std::atomic<bool>& buildCancelled, std::string& buildError) const;
}; };
} }

View file

@ -1,6 +1,8 @@
// SPDX-License-Identifier: LGPL-3.0-only // SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
module;
#include "Crafter.Build-Api.h"
export module Crafter.Build:Interface; export module Crafter.Build:Interface;
import std; import std;
namespace fs = std::filesystem; namespace fs = std::filesystem;
@ -24,9 +26,9 @@ namespace Crafter {
bool checked = false; bool checked = false;
std::string name; std::string name;
fs::path path; fs::path path;
ModulePartition(std::string&& name, fs::path&& path); CRAFTER_API ModulePartition(std::string&& name, fs::path&& path);
bool Check(const fs::path& pcmDir, fs::file_time_type sourceFloor = fs::file_time_type::min()); CRAFTER_API bool Check(const fs::path& pcmDir, fs::file_time_type sourceFloor = fs::file_time_type::min());
void Compile(const std::string_view clang, const fs::path& pcmDir, const fs::path& buildDir, std::atomic<bool>& buildCancelled, std::string& buildError); CRAFTER_API void Compile(const std::string_view clang, const fs::path& pcmDir, const fs::path& buildDir, std::atomic<bool>& buildCancelled, std::string& buildError);
}; };
export class Module { export class Module {
@ -46,8 +48,8 @@ namespace Crafter {
std::vector<std::unique_ptr<ModulePartition>> partitions; std::vector<std::unique_ptr<ModulePartition>> partitions;
std::string name; std::string name;
fs::path path; fs::path path;
Module(std::string&& name, fs::path&& path); CRAFTER_API Module(std::string&& name, fs::path&& path);
bool Check(const fs::path& pcmDir, fs::file_time_type sourceFloor = fs::file_time_type::min()); CRAFTER_API bool Check(const fs::path& pcmDir, fs::file_time_type sourceFloor = fs::file_time_type::min());
void Compile(const std::string_view clang, const fs::path& pcmDir, const fs::path& buildDir, std::atomic<bool>& buildCancelled, std::string& buildError); CRAFTER_API void Compile(const std::string_view clang, const fs::path& pcmDir, const fs::path& buildDir, std::atomic<bool>& buildCancelled, std::string& buildError);
}; };
} }

View file

@ -1,6 +1,8 @@
// SPDX-License-Identifier: LGPL-3.0-only // SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
module;
#include "Crafter.Build-Api.h"
export module Crafter.Build:Lint; export module Crafter.Build:Lint;
import std; import std;
import :Clang; import :Clang;
@ -61,5 +63,5 @@ export namespace Crafter {
// final content differs from the original. Prints per-mode output: // final content differs from the original. Prints per-mode output:
// findings compiler-style (Report), would-change paths (Check), or // findings compiler-style (Report), would-change paths (Check), or
// formatted paths (Apply), plus a summary line. // formatted paths (Apply), plus a summary line.
LintSummary RunLint(Configuration& projectCfg, const RunLintOptions& opts); CRAFTER_API LintSummary RunLint(Configuration& projectCfg, const RunLintOptions& opts);
} }

View file

@ -1,6 +1,8 @@
// SPDX-License-Identifier: LGPL-3.0-only // SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
module;
#include "Crafter.Build-Api.h"
export module Crafter.Build:Platform; export module Crafter.Build:Platform;
import std; import std;
namespace fs = std::filesystem; namespace fs = std::filesystem;
@ -28,9 +30,9 @@ namespace Crafter {
// Progress::Diagnostic), otherwise the compiler's diagnostics. Callers // Progress::Diagnostic), otherwise the compiler's diagnostics. Callers
// therefore keep the `if (!result.empty())` shape they already had. // therefore keep the `if (!result.empty())` shape they already had.
std::string RunBuildCommand(std::string_view command); std::string RunBuildCommand(std::string_view command);
export CommandResult RunCommandWithTimeout(std::string_view command, std::chrono::seconds timeout); export CRAFTER_API CommandResult RunCommandWithTimeout(std::string_view command, std::chrono::seconds timeout);
std::string GetBaseCommand(const Configuration& config); std::string GetBaseCommand(const Configuration& config);
export Configuration LoadProject(const fs::path& projectFile, std::span<const std::string_view> args); export CRAFTER_API Configuration LoadProject(const fs::path& projectFile, std::span<const std::string_view> args);
// A dependency the project file currently running its CrafterBuildProject // A dependency the project file currently running its CrafterBuildProject
// declared with `#pragma crafter dependency`, so it can also be wired // declared with `#pragma crafter dependency`, so it can also be wired
// into the build graph (a test that links the same module, say) without // into the build graph (a test that links the same module, say) without
@ -38,7 +40,7 @@ namespace Crafter {
// name or any module it exports — whatever the project file `import`s. // name or any module it exports — whatever the project file `import`s.
// Throws when no such dependency was declared, or when called from // Throws when no such dependency was declared, or when called from
// anywhere other than inside LoadProject. // anywhere other than inside LoadProject.
export Configuration* ProjectDependency(std::string_view name); export CRAFTER_API Configuration* ProjectDependency(std::string_view name);
// True while the project file crafter-build was pointed at runs its // True while the project file crafter-build was pointed at runs its
// CrafterBuildProject, and when no project file is running at all; false // CrafterBuildProject, and when no project file is running at all; false
// inside a dependency's project file (a `#pragma crafter dependency`, // inside a dependency's project file (a `#pragma crafter dependency`,
@ -47,11 +49,11 @@ namespace Crafter {
// library's test links crafter-build itself, so when crafter-build loads // library's test links crafter-build itself, so when crafter-build loads
// that library as a dependency, the test's GitProject loads crafter-build's // that library as a dependency, the test's GitProject loads crafter-build's
// project file in turn and the two recurse until the cycle guard throws. // project file in turn and the two recurse until the cycle guard throws.
export bool IsTopLevelProject(); export CRAFTER_API bool IsTopLevelProject();
// Resolves the directory holding distributed runtime assets (Crafter.Build // Resolves the directory holding distributed runtime assets (Crafter.Build
// module sources, wasi-runtime/, etc). Honors CRAFTER_BUILD_HOME; otherwise // module sources, wasi-runtime/, etc). Honors CRAFTER_BUILD_HOME; otherwise
// derives <prefix>/share/crafter-build from the running executable's path. // derives <prefix>/share/crafter-build from the running executable's path.
export fs::path GetCrafterBuildHome(); export CRAFTER_API fs::path GetCrafterBuildHome();
// Newest mtime among the prerequisites a compiler recorded in `depFile` // Newest mtime among the prerequisites a compiler recorded in `depFile`
// (clang's `-MD -MF <artifact>.d`). This is how a build step learns which // (clang's `-MD -MF <artifact>.d`). This is how a build step learns which
// headers its source pulled in: the module scanner only reads `import` // headers its source pulled in: the module scanner only reads `import`

View file

@ -1,20 +1,22 @@
// SPDX-License-Identifier: LGPL-3.0-only // SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
module;
#include "Crafter.Build-Api.h"
export module Crafter.Build:Progress; export module Crafter.Build:Progress;
import std; import std;
export namespace Crafter::Progress { export namespace Crafter::Progress {
enum class Verbosity { Quiet, Default, Verbose }; enum class Verbosity { Quiet, Default, Verbose };
void SetVerbosity(Verbosity v); CRAFTER_API void SetVerbosity(Verbosity v);
Verbosity GetVerbosity(); CRAFTER_API Verbosity GetVerbosity();
// RAII work-unit marker. Construction inflates the running total; destruction // RAII work-unit marker. Construction inflates the running total; destruction
// increments completed and (on Default + TTY) redraws the status line. Move // increments completed and (on Default + TTY) redraws the status line. Move
// semantics are disabled — keep these as plain stack locals inside the worker // semantics are disabled — keep these as plain stack locals inside the worker
// lambda so the destructor fires when that thread's work ends. // lambda so the destructor fires when that thread's work ends.
class Task { class CRAFTER_API Task {
public: public:
explicit Task(std::string label); explicit Task(std::string label);
~Task(); ~Task();
@ -27,18 +29,18 @@ export namespace Crafter::Progress {
}; };
// Verbose-mode command echo. No-op outside Verbose. // Verbose-mode command echo. No-op outside Verbose.
void EchoCommand(std::string_view command); CRAFTER_API void EchoCommand(std::string_view command);
// Non-fatal output from a build step that succeeded — compiler and linker // Non-fatal output from a build step that succeeded — compiler and linker
// warnings. Erases the status line first so the text doesn't land on top of // warnings. Erases the status line first so the text doesn't land on top of
// it, then writes to stderr. No-op on Quiet. // it, then writes to stderr. No-op on Quiet.
void Diagnostic(std::string_view text); CRAFTER_API void Diagnostic(std::string_view text);
// Erase the in-place status line so subsequent stderr writes (errors, // Erase the in-place status line so subsequent stderr writes (errors,
// banners) don't collide with it. No-op when not in TTY-redraw mode. // banners) don't collide with it. No-op when not in TTY-redraw mode.
void Clear(); CRAFTER_API void Clear();
// Print "Built N targets in Xms" (or equivalent) and clear in-place state. // Print "Built N targets in Xms" (or equivalent) and clear in-place state.
// Safe to call multiple times; subsequent calls are no-ops. // Safe to call multiple times; subsequent calls are no-ops.
void Finalize(); CRAFTER_API void Finalize();
} }

View file

@ -1,6 +1,8 @@
// SPDX-License-Identifier: LGPL-3.0-only // SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
module;
#include "Crafter.Build-Api.h"
export module Crafter.Build:Shader; export module Crafter.Build:Shader;
import std; import std;
namespace fs = std::filesystem; namespace fs = std::filesystem;
@ -28,8 +30,8 @@ namespace Crafter {
fs::path path; fs::path path;
std::string entrypoint; std::string entrypoint;
ShaderType type; ShaderType type;
Shader(fs::path&& path, std::string&& entrypoint, ShaderType type); CRAFTER_API Shader(fs::path&& path, std::string&& entrypoint, ShaderType type);
bool Check(const fs::path& outputDir) const; CRAFTER_API bool Check(const fs::path& outputDir) const;
std::string Compile(const fs::path& outputDir, std::span<const fs::path> includeDirs) const; CRAFTER_API std::string Compile(const fs::path& outputDir, std::span<const fs::path> includeDirs) const;
}; };
} }

View file

@ -1,6 +1,8 @@
// SPDX-License-Identifier: LGPL-3.0-only // SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
module;
#include "Crafter.Build-Api.h"
export module Crafter.Build:Test; export module Crafter.Build:Test;
import std; import std;
import :Clang; import :Clang;
@ -26,7 +28,7 @@ export namespace Crafter {
// avoid the double-`GetInterfacesAndImplementations` footgun (calling // avoid the double-`GetInterfacesAndImplementations` footgun (calling
// it twice would re-parse the same .cppm files into duplicate Module // it twice would re-parse the same .cppm files into duplicate Module
// entries). // entries).
struct TestBuilder { struct CRAFTER_API TestBuilder {
Configuration* parent; Configuration* parent;
std::size_t index; std::size_t index;
@ -83,8 +85,8 @@ export namespace Crafter {
bool AllPassed() const { return failed == 0 && crashed == 0 && timedOut == 0; } bool AllPassed() const { return failed == 0 && crashed == 0 && timedOut == 0; }
}; };
TestSummary RunTests(Configuration& projectCfg, const RunTestsOptions& opts, std::span<const std::string_view> projectArgs = {}); CRAFTER_API TestSummary RunTests(Configuration& projectCfg, const RunTestsOptions& opts, std::span<const std::string_view> projectArgs = {});
TestResult RunSingleTest(const Test& test, const std::filesystem::path& binary, std::chrono::seconds timeout); CRAFTER_API TestResult RunSingleTest(const Test& test, const std::filesystem::path& binary, std::chrono::seconds timeout);
// Parent-project access for test fixtures. Run() sets the pointer to the // Parent-project access for test fixtures. Run() sets the pointer to the
// root project's Configuration before discovery; fixtures call ParentLib() // root project's Configuration before discovery; fixtures call ParentLib()
@ -92,6 +94,6 @@ export namespace Crafter {
// parent itself) so they can declare it as a dependency without rebuilding // parent itself) so they can declare it as a dependency without rebuilding
// the lib config from scratch. Lookup is by Configuration::name and // the lib config from scratch. Lookup is by Configuration::name and
// recurses through the parent's dependency graph. // recurses through the parent's dependency graph.
void SetParentProject(Configuration* parent); CRAFTER_API void SetParentProject(Configuration* parent);
Configuration* ParentLib(std::string_view name); CRAFTER_API Configuration* ParentLib(std::string_view name);
} }

View file

@ -24,9 +24,12 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
CrafterBuildLib->name = "crafter.build-lib"; CrafterBuildLib->name = "crafter.build-lib";
CrafterBuildLib->outputName = "crafter-build"; CrafterBuildLib->outputName = "crafter-build";
ApplyStandardArgs(*CrafterBuildLib, args); ApplyStandardArgs(*CrafterBuildLib, args);
// Monolithic: project.so resolves Crafter:: symbols from the running // Windows builds (native msvc via build.cmd or cross-compiled mingw from
// executable, so the library is only ever linked statically into it. // Linux) need a DLL + import lib + launcher exe so LoadProject can
CrafterBuildLib->type = ConfigurationType::LibraryStatic; // compile project.cpp against a stable ABI boundary. Linux is monolithic.
CrafterBuildLib->type = (CrafterBuildLib->target == "x86_64-w64-mingw32" || CrafterBuildLib->target == "x86_64-pc-windows-msvc")
? ConfigurationType::LibraryDynamic
: ConfigurationType::LibraryStatic;
CrafterBuildLib->dependencies = { math, asset }; CrafterBuildLib->dependencies = { math, asset };
CrafterBuildLib->defines.push_back({"CRAFTER_BUILD_HAS_ASSET", ""}); CrafterBuildLib->defines.push_back({"CRAFTER_BUILD_HAS_ASSET", ""});
@ -76,6 +79,12 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
glslang.source.branch = "main"; glslang.source.branch = "main";
glslang.builder = ExternalBuilder::CMake; glslang.builder = ExternalBuilder::CMake;
glslang.options = { "-DENABLE_OPT=OFF" }; glslang.options = { "-DENABLE_OPT=OFF" };
// mingw cross-build: skip the standalone executable. We only consume the
// libraries, and glslang.exe pulls in libgcc_eh which needs pthread that
// mingw-w64 doesn't link by default.
if (CrafterBuildLib->target == "x86_64-w64-mingw32") {
glslang.options.push_back("-DENABLE_GLSLANG_BINARIES=OFF");
}
glslang.includeDirs = { "" }; glslang.includeDirs = { "" };
glslang.libs = { "SPIRV", "GenericCodeGen", "glslang", "OSDependent", "MachineIndependent", "glslang-default-resource-limits" }; glslang.libs = { "SPIRV", "GenericCodeGen", "glslang", "OSDependent", "MachineIndependent", "glslang-default-resource-limits" };
@ -97,6 +106,10 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
cfg.linkFlags.push_back("-Wl,--export-dynamic"); cfg.linkFlags.push_back("-Wl,--export-dynamic");
cfg.linkFlags.push_back("-ldl"); cfg.linkFlags.push_back("-ldl");
} }
if (cfg.target == "x86_64-w64-mingw32" || cfg.target == "x86_64-pc-windows-msvc") {
// winsock for the -r wasm port probe (bind/WSAStartup).
CrafterBuildLib->linkFlags.push_back("-lws2_32");
}
// Self-tests link the local crafter-build library and exercise it in // Self-tests link the local crafter-build library and exercise it in
// process. The harness (whichever crafter-build invokes `test`) compiles // process. The harness (whichever crafter-build invokes `test`) compiles

View file

@ -71,7 +71,11 @@ namespace {
app.dependencies = { lib }; app.dependencies = { lib };
// The link step reads a command's output the same way a compile does, // The link step reads a command's output the same way a compile does,
// so cover it too: an unknown -z value makes ld.lld warn and exit 0. // so cover it too: an unknown -z value makes ld.lld warn and exit 0.
// GNU-style only — lld-link spells its options differently — so the
// link case rides along on hosts that use it and is skipped elsewhere.
if (!app.target.contains("windows")) {
app.linkFlags = { "-Wl,-z,crafter-build-nonexistent-z-value" }; app.linkFlags = { "-Wl,-z,crafter-build-nonexistent-z-value" };
}
return app; return app;
} }

View file

@ -6,7 +6,9 @@ import Crafter.Build;
using namespace Crafter; using namespace Crafter;
// POSIX env helpers — libc++ exports only std::getenv via `import std`, so // POSIX env helpers — libc++ exports only std::getenv via `import std`, so
// forward-declare the setters we need against the C library directly. // forward-declare the setters we need against the C library directly. The
// test only runs on POSIX hosts (the project.cpp gates new tests by
// x86_64-pc-linux-gnu) so we don't need a Windows alternative here.
extern "C" int setenv(const char* name, const char* value, int overwrite); extern "C" int setenv(const char* name, const char* value, int overwrite);
extern "C" int unsetenv(const char* name); extern "C" int unsetenv(const char* name);
@ -85,11 +87,13 @@ int main() {
Check(r.exec.find("QEMU_LD_PREFIX=/opt/alarm-sysroot") != std::string::npos, "ForTarget propagates sysroot to QEMU_LD_PREFIX"); Check(r.exec.find("QEMU_LD_PREFIX=/opt/alarm-sysroot") != std::string::npos, "ForTarget propagates sysroot to QEMU_LD_PREFIX");
} }
{ {
// The only way to run x86_64-w64-mingw32 output is wine. // From a Linux host the only sensible way to run x86_64-w64-mingw32 is wine.
Configuration cfg; Configuration cfg;
cfg.target = "x86_64-w64-mingw32"; cfg.target = "x86_64-w64-mingw32";
TestRunner r = TestRunner::ForTarget(cfg); TestRunner r = TestRunner::ForTarget(cfg);
Check(r.name == "wine", "ForTarget(mingw) uses wine"); if (HostTarget().find("linux") != std::string::npos) {
Check(r.name == "wine", "ForTarget(mingw) on Linux host uses wine");
}
} }
// FromEnv: when the env var is set, it wins over the fallback. // FromEnv: when the env var is set, it wins over the fallback.