Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
68e9b6bfa5 |
|||
|
46082a3117 |
27 changed files with 154 additions and 1111 deletions
|
|
@ -51,9 +51,8 @@ jobs:
|
|||
persist-credentials: true
|
||||
|
||||
- name: Cache glslang clone+build
|
||||
# Cache covers crafter-build's per-(target, march) external dep cache.
|
||||
# First run builds glslang up to 6 times (3 marches × 2 targets); the
|
||||
# cache makes subsequent runs reuse those builds.
|
||||
# Cache covers crafter-build's external dep cache. The first run builds
|
||||
# glslang; the cache makes subsequent runs reuse that build.
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
|
|
@ -89,27 +88,14 @@ jobs:
|
|||
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
|
||||
|
||||
# - 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
|
||||
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/master'
|
||||
run: |
|
||||
set -eux
|
||||
mkdir -p dist
|
||||
|
||||
# 6 archives — 3 marches × 2 OSes. Each is self-contained: the
|
||||
# Linux launcher is statically linked; the Windows launcher has
|
||||
# crafter-build.dll + libcrafter-build.dll.a alongside the exe.
|
||||
# 3 archives, one per march level. Each carries the executable, the
|
||||
# static library and share/; libc++ comes from the host.
|
||||
for march in x86-64-v2 x86-64-v3 x86-64-v4; do
|
||||
# Short suffix (v2/v3/v4) for archive filenames; the upload-artifact
|
||||
# steps below reference these short names.
|
||||
|
|
@ -124,18 +110,12 @@ jobs:
|
|||
cp bin/crafter.build-lib-x86_64-pc-linux-gnu-$march-*/libcrafter-build.a "$stage_lin/lib/"
|
||||
cp -r share "$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
|
||||
|
||||
ls -la dist/
|
||||
|
||||
# Upload each archive as its own workflow artifact (one upload per file)
|
||||
# so users browsing the PR / run page get six small, individually-named
|
||||
# so users browsing the PR / run page get small, individually-named
|
||||
# downloads instead of one wrapper zip containing the lot.
|
||||
- name: Upload Linux v2 artifact
|
||||
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/master'
|
||||
|
|
@ -149,18 +129,6 @@ jobs:
|
|||
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/master'
|
||||
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 }
|
||||
# - 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
|
||||
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/master'
|
||||
|
|
@ -172,7 +140,7 @@ jobs:
|
|||
|
||||
- name: Publish rolling 'latest' release
|
||||
# forgejo-release uploads each file in release-dir as a separate
|
||||
# release asset, so the Releases page shows six individually
|
||||
# release asset, so the Releases page shows individually
|
||||
# downloadable archives — pick the v-level matching your CPU.
|
||||
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/master'
|
||||
uses: https://code.forgejo.org/actions/forgejo-release@v2
|
||||
|
|
|
|||
16
AGENTS.md
16
AGENTS.md
|
|
@ -8,12 +8,12 @@ crafter-build is a C++26 modules build system. A project is described by a `proj
|
|||
|
||||
| Path | What lives there |
|
||||
|---|---|
|
||||
| `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`. |
|
||||
| `interfaces/Crafter.Build-<Partition>.cppm` | Module interface partitions. `Crafter.Build.cppm` is the umbrella that re-exports them. |
|
||||
| `implementations/Crafter.Build-<Partition>.cpp` | Implementation units, one per partition, named `Crafter.Build:<Partition>_impl`. `main.cpp` is a thin wrapper around `Crafter::Run`. |
|
||||
| `project.cpp` | The self-host build description: a `crafter.build-lib` library, a `crafter.build-exe` executable, the test declarations and the lint rules. |
|
||||
| `tests/<Name>/main.cpp` | One self-test per directory; `tests/<Name>/fixture/` holds the sample project it builds. |
|
||||
| `examples/` | Standalone user-facing projects, MIT licensed. |
|
||||
| `build.sh`, `build.cmd` | Bootstrap scripts for Linux and Windows. They compile everything by hand, without crafter-build. |
|
||||
| `build.sh` | Bootstrap script. It compiles everything by hand, without crafter-build. |
|
||||
| `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. |
|
||||
| `share/crafter-build/`, `bin/`, `build/`, `lib/*.a` | Generated and gitignored. |
|
||||
|
|
@ -42,12 +42,10 @@ 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.
|
||||
- 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 `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.
|
||||
- A new exported symbol that a `project.cpp` reaches on its default path also needs `./build.sh`: the old binary does not export it, and `LoadProject` fails with an undefined symbol.
|
||||
|
||||
`./build.sh` clones and builds glslang on first run and takes several minutes; later runs are faster. `./bin/crafter-build clean` deletes `bin/` and `build/` without loading `project.cpp`. 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
|
||||
|
||||
```bash
|
||||
|
|
@ -104,10 +102,9 @@ Suppress one finding with `// lint-disable-next-line <rule>` or a whole file wit
|
|||
|
||||
Beyond the rules:
|
||||
|
||||
- `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.
|
||||
- `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;`.
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
|
|
@ -119,7 +116,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.
|
||||
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.
|
||||
4. **Linux is a monolithic executable with `--export-dynamic`; Windows is a DLL plus a launcher.** The asymmetry is intentional.
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
|
|
@ -154,4 +151,3 @@ 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.
|
||||
- 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.
|
||||
- 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 `&&`.
|
||||
|
|
|
|||
1
PKGBUILD
1
PKGBUILD
|
|
@ -29,7 +29,6 @@ package() {
|
|||
install -Dm644 lib/libcrafter-build.a "$pkgdir/usr/lib/libcrafter-build.a"
|
||||
install -dm755 "$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 -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/"
|
||||
|
|
|
|||
14
README.md
14
README.md
|
|
@ -154,18 +154,15 @@ Outcomes: ✅ pass, ❌ fail, 💥 crash (with signal name), ⏱ timeout, ⏭ sk
|
|||
```cpp
|
||||
TestRunner::Local() // run on this machine (default)
|
||||
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>` (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.
|
||||
`Cmd(cmd)` is a generic `<cmd> <bin> <args>` prefix runner — covers wine, qemu-user, valgrind, gdbserver, anything with that shape. Probes via `which <cmd>`.
|
||||
|
||||
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). See `tests/CrossArchAarch64/inner/` for a working sysroot+qemu example.
|
||||
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 `--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.
|
||||
For `--target=x86_64-w64-mingw32` (Windows from Linux), the pipeline cross-compiles as long as `mingw-w64` is installed. Pair it with `--runner=cmd:wine` to run the result. Only the MinGW ABI is supported; there is no MSVC target and no Windows host. `-fuse-ld=lld` is auto-injected on every link, and `-lstdc++exp` is auto-linked for the mingw target so user projects don't need to declare those flags.
|
||||
|
||||
### Configuring runners
|
||||
|
||||
|
|
@ -192,12 +189,9 @@ Spec grammar:
|
|||
```
|
||||
local
|
||||
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`, `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).
|
||||
Each runner carries a `probe` command (e.g. `which wine`); if it fails, every test using that runner is reported as ⏭ skipped and the rest of the suite continues. Skipped tests count toward the summary but don't fail the suite (`AllPassed()` returns true on skip-only runs).
|
||||
|
||||
### Linking the parent project's library
|
||||
|
||||
|
|
|
|||
137
build.cmd
137
build.cmd
|
|
@ -1,137 +0,0 @@
|
|||
@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"
|
||||
1
build.sh
1
build.sh
|
|
@ -21,7 +21,6 @@ cp interfaces/Crafter.Build-Test.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-Asset.cppm share/crafter-build/
|
||||
cp interfaces/Crafter.Build-Api.h share/crafter-build/
|
||||
|
||||
git clone https://github.com/KhronosGroup/glslang.git ./build/glslang
|
||||
|
||||
|
|
|
|||
|
|
@ -2,17 +2,9 @@
|
|||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
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 <netinet/in.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
export module Crafter.Build:Clang_impl;
|
||||
import std;
|
||||
import :Clang;
|
||||
|
|
@ -267,17 +259,19 @@ fs::path Crafter::StdPcmDir(const Configuration& config) {
|
|||
|
||||
fs::path Crafter::ExpectedOutput(const Configuration& config) {
|
||||
const fs::path dir = config.BinDir();
|
||||
const bool windowsTarget = config.target == "x86_64-w64-mingw32" || config.target == "x86_64-pc-windows-msvc";
|
||||
const bool mingwTarget = config.target == "x86_64-w64-mingw32";
|
||||
if (config.type == ConfigurationType::Executable) {
|
||||
if (config.target.starts_with("wasm32")) return dir / std::format("{}.wasm", config.outputName);
|
||||
return config.target == "x86_64-w64-mingw32" ? dir / std::format("{}.exe", config.outputName) : dir / config.outputName;
|
||||
return mingwTarget ? dir / std::format("{}.exe", config.outputName) : dir / config.outputName;
|
||||
}
|
||||
if (config.type == ConfigurationType::LibraryStatic) {
|
||||
return windowsTarget ? dir / std::format("{}.lib", config.outputName) : dir / std::format("lib{}.a", config.outputName);
|
||||
// The archive step writes lib<name>.a for every target, mingw included:
|
||||
// llvm-ar does not care about the object format.
|
||||
return dir / std::format("lib{}.a", config.outputName);
|
||||
}
|
||||
// LibraryDynamic — the .dll on Windows targets so an mtime check sees the
|
||||
// LibraryDynamic — the .dll on the mingw target so an mtime check sees the
|
||||
// freshly linked library; on Unix the .so is that file.
|
||||
return windowsTarget ? dir / std::format("{}.dll", config.outputName) : dir / std::format("lib{}.so", config.outputName);
|
||||
return mingwTarget ? dir / std::format("{}.dll", config.outputName) : dir / std::format("lib{}.so", config.outputName);
|
||||
}
|
||||
|
||||
CompileCommand Crafter::GetCompileCommand(const Configuration& config) {
|
||||
|
|
@ -322,22 +316,9 @@ CompileCommand Crafter::GetCompileCommand(const Configuration& config) {
|
|||
}
|
||||
|
||||
if(config.type == ConfigurationType::LibraryDynamic) {
|
||||
#ifdef __linux__
|
||||
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) {
|
||||
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 {
|
||||
out.command += " -D CRAFTER_BUILD_CONFIGURATION_TYPE_LIBRARY";
|
||||
}
|
||||
|
|
@ -464,15 +445,12 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
|||
config.ResolvePendingImports();
|
||||
|
||||
// Auto-detect the WASI sysroot before any compile step runs so BuildStdPcm
|
||||
// and the main compile command see the same value. Linux-only — Windows
|
||||
// users supply cfg.sysroot pointing at their wasi-sdk install. Covers all
|
||||
// wasm32-* triples (wasi, wasip1, wasip2, ...); the sysroot's per-triple
|
||||
// subdirs handle the differences.
|
||||
#ifdef __linux__
|
||||
// and the main compile command see the same value. Covers all wasm32-*
|
||||
// triples (wasi, wasip1, wasip2, ...); the sysroot's per-triple subdirs
|
||||
// handle the differences.
|
||||
if (config.sysroot.empty() && config.target.starts_with("wasm32")) {
|
||||
config.sysroot = "/usr/share/wasi-sysroot";
|
||||
}
|
||||
#endif
|
||||
|
||||
fs::path buildDir = config.BuildDir();
|
||||
fs::path outputDir = config.BinDir();
|
||||
|
|
@ -705,7 +683,6 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
|||
const std::string& ltoCompileFlags = compile.ltoCompileFlags;
|
||||
const std::string& ltoLinkFlags = compile.ltoLinkFlags;
|
||||
const bool isWasm = config.target.starts_with("wasm32");
|
||||
const bool useLto = compile.useLto;
|
||||
|
||||
std::string files;
|
||||
std::unordered_set<std::string> libSet;
|
||||
|
|
@ -1125,7 +1102,6 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
|||
if(buildResult.repack) {
|
||||
Progress::Task task(std::format("Linking {}", config.outputName));
|
||||
if(config.type == ConfigurationType::Executable) {
|
||||
#ifdef __linux__
|
||||
if(config.target == "x86_64-w64-mingw32") {
|
||||
try {
|
||||
// Copy any LibraryDynamic dependency DLLs alongside
|
||||
|
|
@ -1136,15 +1112,12 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
|||
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();
|
||||
// The DLL itself (Windows resolves it from the
|
||||
// exe's directory at load time) and the mingw
|
||||
// import lib (so a downstream `crafter-build.exe`
|
||||
// can link a fresh project.dll against it without
|
||||
// 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;
|
||||
// Only the DLL: Windows resolves it from the exe's
|
||||
// directory at load time. The import lib stays in
|
||||
// the dependency's own bin dir, which is already
|
||||
// on the link line through buildResult.libs.
|
||||
fs::path src = dep->BinDir() / std::format("{}.dll", dep->outputName);
|
||||
if (fs::exists(src)) {
|
||||
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);
|
||||
|
|
@ -1163,65 +1136,22 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
|||
} else {
|
||||
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) {
|
||||
#ifdef __linux__
|
||||
// ThinLTO emits LLVM bitcode objects; plain `ar` writes an archive
|
||||
// index that omits their symbols, so a consumer's lld link can't
|
||||
// pull the needed members. llvm-ar writes a bitcode-aware index.
|
||||
buildResult.result = RunBuildCommand(std::format("{} rcs {}.a {}", useLto ? "llvm-ar" : "ar", (outputDir/fs::path(std::string("lib")+config.outputName)).string(), files));
|
||||
#endif
|
||||
|
||||
#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
|
||||
// pull the needed members. llvm-ar writes a bitcode-aware index
|
||||
// and handles plain ELF and COFF objects just as well, so every
|
||||
// build uses it.
|
||||
buildResult.result = RunBuildCommand(std::format("llvm-ar rcs {}.a {}", (outputDir/fs::path(std::string("lib")+config.outputName)).string(), files));
|
||||
} else {
|
||||
// LibraryDynamic. Output names follow each target's convention so
|
||||
// consumers can link via the standard linker search:
|
||||
// mingw: <name>.dll + lib<name>.dll.a (lld --out-implib)
|
||||
// msvc: <name>.dll + <name>.lib (lld /IMPLIB)
|
||||
// unix: lib<name>.so (rpath $ORIGIN)
|
||||
if (config.target == "x86_64-w64-mingw32") {
|
||||
fs::path dll = outputDir / std::format("{}.dll", config.outputName);
|
||||
fs::path implib = outputDir / std::format("lib{}.dll.a", config.outputName);
|
||||
buildResult.result = RunBuildCommand(std::format("{}{} -shared -o {} -Wl,--out-implib,{} -fuse-ld=lld{}", command, files, dll.string(), implib.string(), linkExtras));
|
||||
} else 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 {
|
||||
buildResult.result = RunBuildCommand(std::format("{}{} -shared -o {}.so -Wl,-rpath,'$ORIGIN' -fuse-ld=lld{}", command, files, (outputDir/(std::string("lib")+config.outputName)).string(), linkExtras));
|
||||
}
|
||||
|
|
@ -1544,11 +1474,9 @@ ArgQuery Crafter::ApplyStandardArgs(Configuration& cfg, std::span<const std::str
|
|||
// includes it. (Build() also runs this once more for callers that bypassed
|
||||
// ApplyStandardArgs, but doing it here makes dep PcmDirs consistent
|
||||
// between the consumer's command-construction and the dep's own build.)
|
||||
#ifdef __linux__
|
||||
if (cfg.sysroot.empty() && cfg.target.starts_with("wasm32")) {
|
||||
cfg.sysroot = "/usr/share/wasi-sysroot";
|
||||
}
|
||||
#endif
|
||||
return ArgQuery{args};
|
||||
}
|
||||
|
||||
|
|
@ -1644,7 +1572,6 @@ Environment:
|
|||
Replace '-' and '.' with '_' in the
|
||||
triple. CLI --runner= overrides this.
|
||||
CRAFTER_MINGW_DIR Override mingw-w64 sysroot auto-detect.
|
||||
LIBCXX_DIR Windows libc++ install (MSVC ABI builds).
|
||||
|
||||
Exit status:
|
||||
0 success / all non-skipped tests passed / lint clean / format applied
|
||||
|
|
@ -1789,11 +1716,7 @@ std::int32_t Crafter::Run(std::int32_t argc, char** argv) {
|
|||
if (config.target.starts_with("wasm32")) {
|
||||
const bool browserBuild = fs::exists(absDir / "index.html");
|
||||
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);
|
||||
#endif
|
||||
return std::system(probe.c_str()) == 0;
|
||||
};
|
||||
if (browserBuild) {
|
||||
|
|
@ -1801,17 +1724,9 @@ std::int32_t Crafter::Run(std::int32_t argc, char** argv) {
|
|||
// user has another dev server running we shift up rather
|
||||
// than letting caddy/python exit with EADDRINUSE.
|
||||
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;
|
||||
const Sock invalid = -1;
|
||||
auto closesock = [](Sock s){ ::close(s); };
|
||||
#endif
|
||||
for (std::int32_t p = basePort; p < basePort + span; ++p) {
|
||||
Sock s = ::socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (s == invalid) return basePort;
|
||||
|
|
@ -1934,11 +1849,7 @@ std::int32_t Crafter::Run(std::int32_t argc, char** argv) {
|
|||
// Silence the backend's own banner/request logs so the
|
||||
// experience is identical regardless of which server is
|
||||
// picked — the line above is the only output.
|
||||
#ifdef _WIN32
|
||||
cmd += " > NUL 2>&1";
|
||||
#else
|
||||
cmd += " > /dev/null 2>&1";
|
||||
#endif
|
||||
return std::system(cmd.c_str()) == 0 ? 0 : 1;
|
||||
}
|
||||
// wasi-cli wasm — needs a standalone runtime.
|
||||
|
|
@ -1952,10 +1863,6 @@ std::int32_t Crafter::Run(std::int32_t argc, char** argv) {
|
|||
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
|
||||
// (shaders, assets copied alongside the exe via cfg.files) resolve
|
||||
// against the bin dir rather than the user's cwd. We exit the
|
||||
|
|
|
|||
|
|
@ -21,18 +21,6 @@ std::string DeriveName(const GitSource& source) {
|
|||
}
|
||||
|
||||
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 = "'";
|
||||
for (char c : s) {
|
||||
if (c == '\'') out += "'\\''";
|
||||
|
|
@ -40,7 +28,6 @@ std::string ShellQuote(std::string_view s) {
|
|||
}
|
||||
out += "'";
|
||||
return out;
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string JoinOptions(std::span<const std::string> options) {
|
||||
|
|
@ -133,17 +120,14 @@ std::string BuildInjectedCMakeFlags(const fs::path& cmakeBuildDir, std::string_v
|
|||
out += " -DCMAKE_BUILD_TYPE=Release";
|
||||
out += " -DCMAKE_C_COMPILER=clang";
|
||||
out += " -DCMAKE_CXX_COMPILER=clang++";
|
||||
// Stdlib choice: libc++ for the Linux host build (matches how crafter-build
|
||||
// itself is built with -stdlib=libc++); mingw cross-compile uses libstdc++
|
||||
// shipped by the mingw-w64 toolchain so we leave the default; Windows
|
||||
// native (msvc target) similarly uses libc++ via LIBCXX_DIR.
|
||||
#ifdef __linux__
|
||||
// Stdlib choice: libc++ for Linux targets (matches how crafter-build
|
||||
// itself is built with -stdlib=libc++); the mingw cross-compile uses the
|
||||
// libstdc++ shipped by the mingw-w64 toolchain, so it keeps the default.
|
||||
if (target.ends_with("-linux-gnu") || target.ends_with("-linux-musl")) {
|
||||
out += " -DCMAKE_CXX_FLAGS=-stdlib=libc++";
|
||||
out += " -DCMAKE_EXE_LINKER_FLAGS=-stdlib=libc++";
|
||||
out += " -DCMAKE_SHARED_LINKER_FLAGS=-stdlib=libc++";
|
||||
}
|
||||
#endif
|
||||
if (target == "x86_64-w64-mingw32") {
|
||||
out += " -DCMAKE_SYSTEM_NAME=Windows";
|
||||
out += std::format(" -DCMAKE_C_COMPILER_TARGET={}", target);
|
||||
|
|
@ -277,9 +261,9 @@ ExternalBuildResult Crafter::BuildExternal(const ExternalDependency& dep, std::s
|
|||
// (runtime-loader). The rpath stays embedded in the produced
|
||||
// binary so it picks up shared deps from the cache without any
|
||||
// LD_LIBRARY_PATH gymnastics. Static deps (.a) ignore the rpath
|
||||
// harmlessly. PE/COFF targets (mingw, msvc) resolve DLLs via PATH
|
||||
// or adjacency rather than rpath, so lld warns if we pass it.
|
||||
const bool isPe = target == "x86_64-w64-mingw32" || target == "x86_64-pc-windows-msvc";
|
||||
// harmlessly. The PE/COFF target (mingw) resolves DLLs via PATH or
|
||||
// adjacency rather than rpath, so lld warns if we pass it.
|
||||
const bool isPe = target == "x86_64-w64-mingw32";
|
||||
std::string buildDirAbs = fs::absolute(cmakeBuildDir).string();
|
||||
result.linkFlags.push_back(std::format("-L{}", buildDirAbs));
|
||||
if (!isPe) result.linkFlags.push_back(std::format("-Wl,-rpath,{}", buildDirAbs));
|
||||
|
|
|
|||
|
|
@ -3,11 +3,7 @@
|
|||
|
||||
module;
|
||||
#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>
|
||||
#endif
|
||||
export module Crafter.Build:Lint_impl;
|
||||
import std;
|
||||
import :Lint;
|
||||
|
|
@ -20,20 +16,13 @@ using namespace Crafter;
|
|||
namespace {
|
||||
// ---------------- libclang ----------------
|
||||
//
|
||||
// libclang is loaded at runtime rather than linked. Linking -lclang would
|
||||
// break the mingw and MSVC cross-builds at link time and would put a
|
||||
// libclang.so.NN runtime dependency into the otherwise self-contained
|
||||
// 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
|
||||
// libclang is loaded at runtime rather than linked, so a plain build never
|
||||
// needs it and the binary carries no libclang.so.NN loader dependency; 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,
|
||||
// weaker lexer to fall back to, because two engines disagreeing about what
|
||||
// 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*;
|
||||
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()); }
|
||||
|
|
@ -41,7 +30,6 @@ namespace {
|
|||
"libclang.so", "libclang.so.22.1", "libclang.so.21.1", "libclang.so.20.1",
|
||||
"libclang.so.1", "libclang.dylib",
|
||||
};
|
||||
#endif
|
||||
|
||||
// Signatures come from decltype on the header's declarations, so they can
|
||||
// never drift from the real API. decltype is unevaluated, so naming the
|
||||
|
|
@ -104,8 +92,8 @@ namespace {
|
|||
LibClang LoadLibClang() {
|
||||
LibClang lib;
|
||||
std::vector<std::string> tried;
|
||||
// CRAFTER_BUILD_LIBCLANG pins an exact path, mirroring the LIBCXX_DIR /
|
||||
// CRAFTER_MINGW_DIR overrides used elsewhere. It is exclusive: pointing
|
||||
// CRAFTER_BUILD_LIBCLANG pins an exact path, mirroring the
|
||||
// CRAFTER_MINGW_DIR override used elsewhere. It is exclusive: pointing
|
||||
// it at a broken path must fail loudly rather than quietly succeed with
|
||||
// some other libclang, or the override is useless for diagnosing which
|
||||
// library is actually in play.
|
||||
|
|
@ -377,8 +365,8 @@ namespace {
|
|||
}
|
||||
if (!args.empty()) args.erase(args.begin()); // argv[0] is the compiler
|
||||
for (std::string& arg : args) {
|
||||
// Only \" unescapes. Erasing every backslash would destroy the
|
||||
// Windows include paths GetBaseCommand puts on the command line.
|
||||
// Only \" unescapes; a backslash anywhere else is part of the
|
||||
// argument.
|
||||
std::string unescaped;
|
||||
unescaped.reserve(arg.size());
|
||||
for (std::size_t i = 0; i < arg.size(); ++i) {
|
||||
|
|
|
|||
|
|
@ -2,20 +2,15 @@
|
|||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
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 <stdlib.h>
|
||||
#include <limits.h>
|
||||
#ifdef __linux__
|
||||
#include <unistd.h>
|
||||
#include <dlfcn.h>
|
||||
#include <sys/wait.h>
|
||||
#include <sys/file.h>
|
||||
#include <fcntl.h>
|
||||
#include <errno.h>
|
||||
#endif
|
||||
module Crafter.Build:Platform_impl;
|
||||
import std;
|
||||
import :Platform;
|
||||
|
|
@ -43,7 +38,6 @@ namespace {
|
|||
// descriptor is close-on-exec / non-inheritable so spawned clang processes
|
||||
// don't keep the lock alive past our own release.
|
||||
class CacheLock {
|
||||
#ifdef __linux__
|
||||
std::int32_t fd_ = -1;
|
||||
public:
|
||||
explicit CacheLock(const fs::path& cacheDir) {
|
||||
|
|
@ -57,22 +51,6 @@ namespace {
|
|||
::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& operator=(const CacheLock&) = delete;
|
||||
};
|
||||
|
|
@ -104,9 +82,6 @@ namespace {
|
|||
for (std::string_view name : moduleNames) {
|
||||
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));
|
||||
}
|
||||
|
||||
|
|
@ -130,16 +105,7 @@ fs::path Crafter::GetCrafterBuildHome() {
|
|||
if (const char* envHome = std::getenv("CRAFTER_BUILD_HOME")) {
|
||||
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");
|
||||
#endif
|
||||
|
||||
// Walk up from the exe's directory looking for share/crafter-build with
|
||||
// a known module source present as a sentinel. Handles:
|
||||
|
|
@ -560,499 +526,6 @@ bool Crafter::IsTopLevelProject() {
|
|||
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) {
|
||||
std::array<char, 128> buffer;
|
||||
CommandResult result{};
|
||||
|
|
@ -1191,7 +664,7 @@ std::string Crafter::BuildStdPcm(const Configuration& config, fs::path stdPcm) {
|
|||
// non-wasm cross builds: the driver ranks the host toolchain's own
|
||||
// libc++ headers (<install>/../include/c++/v1) above --sysroot, so
|
||||
// the PCM would mix host libc++ with the foreign libc — force the
|
||||
// sysroot's copy (same trick as the Windows LIBCXX_DIR path)
|
||||
// sysroot's copy.
|
||||
if (!isWasm && !config.sysroot.empty()) {
|
||||
archFlags += std::format(" -nostdinc++ -isystem {}/usr/include/c++/v1", config.sysroot);
|
||||
}
|
||||
|
|
@ -1348,5 +821,3 @@ Configuration Crafter::LoadProject(const fs::path& projectFile, std::span<const
|
|||
guard.Frame().dependencies = std::move(preDeps.configs);
|
||||
return fn(args);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -4,18 +4,8 @@
|
|||
module;
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#if defined(_WIN32)
|
||||
#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
|
||||
#include <unistd.h>
|
||||
#include <sys/ioctl.h>
|
||||
|
||||
export module Crafter.Build:Progress_impl;
|
||||
import std;
|
||||
|
|
@ -32,19 +22,10 @@ namespace {
|
|||
std::chrono::steady_clock::time_point StartTime = std::chrono::steady_clock::now();
|
||||
|
||||
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{};
|
||||
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col > 0) {
|
||||
return ws.ws_col;
|
||||
}
|
||||
#endif
|
||||
if (const char* col = std::getenv("COLUMNS")) {
|
||||
try { std::int32_t c = std::stoi(col); if (c > 0) return c; } catch (...) {}
|
||||
}
|
||||
|
|
@ -86,7 +67,7 @@ namespace Crafter::Progress {
|
|||
void SetVerbosity(Verbosity v) {
|
||||
std::lock_guard lock(StateMutex);
|
||||
ActiveVerbosity = v;
|
||||
IsTty = CRAFTER_PROGRESS_ISATTY(STDOUT_FILENO) != 0;
|
||||
IsTty = isatty(STDOUT_FILENO) != 0;
|
||||
StartTime = std::chrono::steady_clock::now();
|
||||
Total.store(0);
|
||||
Done.store(0);
|
||||
|
|
|
|||
|
|
@ -35,38 +35,11 @@ namespace {
|
|||
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 out;
|
||||
for (const auto& a : args) {
|
||||
if (!out.empty()) out.push_back(' ');
|
||||
out += ShellQuoteHost(a);
|
||||
out += ShellQuoteSh(a);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
|
@ -191,11 +164,7 @@ TestRunner TestRunner::Cmd(std::string command) {
|
|||
TestRunner r;
|
||||
r.name = std::format("cmd:{}", command);
|
||||
r.exec = std::format("{} {{bin}} {{args}}", command);
|
||||
#ifdef _WIN32
|
||||
r.probe = std::format("where {}", command);
|
||||
#else
|
||||
r.probe = std::format("which {}", command);
|
||||
#endif
|
||||
return r;
|
||||
}
|
||||
|
||||
|
|
@ -203,11 +172,7 @@ TestRunner TestRunner::Wine() {
|
|||
TestRunner r;
|
||||
r.name = "wine";
|
||||
r.exec = "wine {bin} {args}";
|
||||
#ifdef _WIN32
|
||||
r.probe = "where wine";
|
||||
#else
|
||||
r.probe = "which wine";
|
||||
#endif
|
||||
return r;
|
||||
}
|
||||
|
||||
|
|
@ -218,11 +183,8 @@ TestRunner TestRunner::ForTarget(const Configuration& cfg) {
|
|||
// case (cfg.target defaulted to HostTarget()) without any wrapper.
|
||||
if (target == HostTarget()) return Local();
|
||||
|
||||
// Windows targets: native on a Windows host, Wine on Linux. We don't
|
||||
// distinguish mingw vs msvc here — the produced .exe runs the same way.
|
||||
if (TargetIsWindows(target)) {
|
||||
return TargetIsWindows(HostTarget()) ? Local() : Wine();
|
||||
}
|
||||
// Windows targets run under Wine; there is no Windows host.
|
||||
if (TargetIsWindows(target)) return Wine();
|
||||
|
||||
// WASI: a .wasm file isn't directly executable; wasmtime is the canonical
|
||||
// runtime. wasi-cli also works but the upstream Bytecode Alliance name is
|
||||
|
|
@ -311,7 +273,7 @@ TestResult Crafter::RunSingleTest(const Test& test, const fs::path& binary, std:
|
|||
|
||||
if (test.runner.exec.empty()) {
|
||||
// Local runner: spawn the binary directly through the host shell.
|
||||
std::string cmd = std::format("{} {}", ShellQuoteHost(binary.string()), ph["{args}"]);
|
||||
std::string cmd = std::format("{} {}", ShellQuoteSh(binary.string()), ph["{args}"]);
|
||||
r = RunCommandWithTimeout(cmd, timeout);
|
||||
} else {
|
||||
// Prefix runner (qemu-user, wasmtime, wine, ...): templated exec
|
||||
|
|
@ -337,11 +299,7 @@ TestResult Crafter::RunSingleTest(const Test& test, const fs::path& binary, std:
|
|||
|
||||
namespace {
|
||||
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);
|
||||
#endif
|
||||
return std::system(cmd.c_str()) == 0;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,16 +2,7 @@
|
|||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
#include <cstdio>
|
||||
#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
|
||||
#include <unistd.h>
|
||||
|
||||
import std;
|
||||
import Crafter.Build;
|
||||
|
|
@ -26,7 +17,7 @@ int main(int argc, char** argv) {
|
|||
// 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
|
||||
// only adjust when redirected.
|
||||
if (CRAFTER_MAIN_ISATTY(STDOUT_FILENO) == 0) {
|
||||
if (isatty(STDOUT_FILENO) == 0) {
|
||||
std::setvbuf(stdout, nullptr, _IOLBF, 0);
|
||||
}
|
||||
return Crafter::Run(argc, argv);
|
||||
|
|
|
|||
|
|
@ -1,14 +0,0 @@
|
|||
// 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
|
||||
|
|
@ -1,8 +1,6 @@
|
|||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
module;
|
||||
#include "Crafter.Build-Api.h"
|
||||
export module Crafter.Build:Asset;
|
||||
import std;
|
||||
namespace fs = std::filesystem;
|
||||
|
|
@ -19,7 +17,7 @@ namespace Crafter {
|
|||
// that errors out at runtime if cfg.assets is non-empty — full
|
||||
// support requires a self-host rebuild via crafter-build itself,
|
||||
// which resolves the Crafter.Asset dep through project.cpp.
|
||||
export CRAFTER_API std::string CompressAsset(const fs::path& input, const fs::path& output);
|
||||
export std::string CompressAsset(const fs::path& input, const fs::path& output);
|
||||
|
||||
// Split a single .obj + .mtl file by material group into per-material
|
||||
// .cmesh files, per-unique-albedo .ctex files, and a scene.txt manifest
|
||||
|
|
@ -36,7 +34,7 @@ namespace Crafter {
|
|||
// Idempotent: returns "" immediately if outDir/scene.txt already exists,
|
||||
// so repeated builds skip the expensive split+resize step.
|
||||
// Returns "" on success, an error string on failure.
|
||||
export CRAFTER_API std::string BuildOBJBundle(
|
||||
export std::string BuildOBJBundle(
|
||||
const fs::path& objPath,
|
||||
const fs::path& mtlPath,
|
||||
const fs::path& outDir,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
module;
|
||||
#include "Crafter.Build-Api.h"
|
||||
export module Crafter.Build:Clang;
|
||||
import std;
|
||||
import :Shader;
|
||||
|
|
@ -65,30 +63,28 @@ export namespace Crafter {
|
|||
|
||||
bool IsLocal() const { return exec.empty(); }
|
||||
|
||||
static CRAFTER_API TestRunner Local();
|
||||
static TestRunner Local();
|
||||
// Prefix runner: wraps the local binary in `<command> {bin} {args}`.
|
||||
// Used for qemu-user, wasmtime, and similar single-binary wrappers.
|
||||
static CRAFTER_API TestRunner Cmd(std::string command);
|
||||
// Run a Windows .exe through Wine. Probes `wine` on PATH; on a Windows
|
||||
// host the wine wrapper is pointless, so callers should route to Local
|
||||
// before reaching here.
|
||||
static CRAFTER_API TestRunner Wine();
|
||||
static TestRunner Cmd(std::string command);
|
||||
// Run a Windows .exe through Wine. Probes `wine` on PATH.
|
||||
static TestRunner Wine();
|
||||
// Parse a `<kind>[:<arg>]` spec used by CRAFTER_BUILD_RUNNER_<target>
|
||||
// and --runner=. Supported: "local", "cmd:<binary>". Returns nullopt
|
||||
// for an empty string; throws on a non-empty unrecognized spec.
|
||||
static CRAFTER_API std::optional<TestRunner> FromSpec(std::string_view spec);
|
||||
static std::optional<TestRunner> FromSpec(std::string_view spec);
|
||||
// Honor CRAFTER_BUILD_RUNNER_<target> (power-user override). Triple
|
||||
// dashes/dots become underscores so they're valid in env-var names.
|
||||
// Returns `fallback` when the env var is unset.
|
||||
static CRAFTER_API TestRunner FromEnv(std::string_view target, TestRunner fallback = Local());
|
||||
static TestRunner FromEnv(std::string_view target, TestRunner fallback = Local());
|
||||
// Derive a runner from a Configuration's target triple + sysroot.
|
||||
// Returns Local() when target equals the host, Wine() for Windows
|
||||
// targets on a non-Windows host, `qemu-<arch>` (with QEMU_LD_PREFIX
|
||||
// targets, `qemu-<arch>` (with QEMU_LD_PREFIX
|
||||
// set when cfg.sysroot is non-empty) for non-host -linux- triples,
|
||||
// `wasmtime` for wasm32-wasi/wasm64-wasi, and Local() as a last
|
||||
// resort. CRAFTER_BUILD_RUNNER_<target> still wins as an override
|
||||
// upstream of this — see FromEnv.
|
||||
static CRAFTER_API TestRunner ForTarget(const struct Configuration& cfg);
|
||||
static TestRunner ForTarget(const struct Configuration& cfg);
|
||||
};
|
||||
|
||||
enum class TestOutcome { Pass, Fail, Crash, Timeout, Skipped };
|
||||
|
|
@ -233,17 +229,18 @@ export namespace Crafter {
|
|||
};
|
||||
|
||||
// Per-file view handed to each LintRule's check callback. Every member
|
||||
// function is out-of-line and CRAFTER_API (defined in Crafter.Build:Lint's
|
||||
// implementation unit) because rule lambdas execute from the user's
|
||||
// project DLL on Windows — clang does not emit module-attached in-class
|
||||
// inline bodies into consumers (see ArgQuery below).
|
||||
// function is out-of-line (defined in Crafter.Build:Lint's implementation
|
||||
// unit) because rule lambdas execute from the user's project.so and
|
||||
// resolve against the running executable: clang does not emit
|
||||
// module-attached in-class inline bodies into consumers (see ArgQuery
|
||||
// below).
|
||||
struct LintContext {
|
||||
fs::path file; // absolute path of the file under lint
|
||||
std::string content; // whole file as read from disk
|
||||
std::vector<std::string_view> lines; // views into `content`, one per line, no '\n'
|
||||
|
||||
CRAFTER_API std::string Extension() const; // ".cppm", ".cpp", ".h", ...
|
||||
CRAFTER_API std::string_view Line(std::size_t n) const; // 1-based; empty if out of range
|
||||
std::string Extension() const; // ".cppm", ".cpp", ".h", ...
|
||||
std::string_view Line(std::size_t n) const; // 1-based; empty if out of range
|
||||
// `content` with comments and string/char literal bodies blanked to
|
||||
// spaces, newlines preserved — offsets and line numbers stay valid.
|
||||
// Built from Tokens() on first call, cached per file, so raw strings,
|
||||
|
|
@ -254,9 +251,9 @@ export namespace Crafter {
|
|||
// Convenient for a quick scan, but Tokens() is the better tool for
|
||||
// anything structural: this view cannot tell an identifier from a
|
||||
// keyword, and it has already thrown away where the literals were.
|
||||
CRAFTER_API const std::string& CommentStripped();
|
||||
const std::string& CommentStripped();
|
||||
// Record a finding at `line` (1-based; pass 0 for a whole-file finding).
|
||||
CRAFTER_API void Report(std::size_t line, std::string message);
|
||||
void Report(std::size_t line, std::string message);
|
||||
// Replace the file's content. Makes this rule a *transform*:
|
||||
// `crafter-build format` writes the result back to disk; `lint`
|
||||
// derives would-reformat findings from it (dry — never writes).
|
||||
|
|
@ -265,7 +262,7 @@ export namespace Crafter {
|
|||
// pattern: build the new string, call SetContent once at the end. Do
|
||||
// not assign `content` directly — that bypasses the re-split. May be
|
||||
// combined with Report() in the same rule.
|
||||
CRAFTER_API void SetContent(std::string newContent);
|
||||
void SetContent(std::string newContent);
|
||||
// True when `rule` is suppressed at `line` (1-based; 0 = whole-file,
|
||||
// which only file-level directives cover) by a suppression comment:
|
||||
// // lint-disable-next-line [rules...] applies to the following line
|
||||
|
|
@ -277,7 +274,7 @@ export namespace Crafter {
|
|||
// itself for every line its edit touches (the driver cannot map lines
|
||||
// across a count-changing rewrite). Parsed lazily from the raw lines;
|
||||
// re-parsed after SetContent.
|
||||
CRAFTER_API bool Suppressed(std::string_view rule, std::size_t line);
|
||||
bool Suppressed(std::string_view rule, std::size_t line);
|
||||
|
||||
// The file lexed by clang, in source order. Built on first call, cached
|
||||
// per file, re-lexed after SetContent. Empty for extensions that are
|
||||
|
|
@ -287,16 +284,16 @@ export namespace Crafter {
|
|||
// raw strings, line splices, digraphs and nested quoting right, and
|
||||
// offsets index straight into `content`, so a transform can find in
|
||||
// the token stream and edit in place.
|
||||
CRAFTER_API std::span<const LintToken> Tokens();
|
||||
std::span<const LintToken> Tokens();
|
||||
// The token's own bytes: content.substr(tok.offset, tok.length).
|
||||
CRAFTER_API std::string_view TokenText(const LintToken& token) const;
|
||||
std::string_view TokenText(const LintToken& token) const;
|
||||
// Tokens whose FIRST byte is on `line` (1-based). A token that starts
|
||||
// earlier and spans into `line` — a raw string, a block comment — is
|
||||
// not included; ask Tokens() directly when that matters.
|
||||
CRAFTER_API std::span<const LintToken> TokensOnLine(std::size_t line);
|
||||
std::span<const LintToken> TokensOnLine(std::size_t line);
|
||||
// True when a comment token starts on `line`. Replaces `Line(n).contains("//")`,
|
||||
// which false-positives on a `//` inside a string literal.
|
||||
CRAFTER_API bool LineHasComment(std::size_t line);
|
||||
bool LineHasComment(std::size_t line);
|
||||
// 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
|
||||
// joins, splits or rewrites such a line changes what is inside that
|
||||
|
|
@ -304,7 +301,7 @@ export namespace Crafter {
|
|||
// Replaces `Line(n).contains("R\"")`, which both misses raw strings
|
||||
// opened on an earlier line and fires on the characters R" appearing
|
||||
// inside an ordinary literal.
|
||||
CRAFTER_API bool LineHasMultiLineToken(std::size_t line);
|
||||
bool LineHasMultiLineToken(std::size_t line);
|
||||
|
||||
// Declarations written in this file, flattened depth-first with
|
||||
// parents before children (see LintDecl). Empty when the AST is not
|
||||
|
|
@ -321,11 +318,11 @@ export namespace Crafter {
|
|||
// slice. Declarations inside a preprocessor branch that is inactive
|
||||
// for the host are absent here, though Tokens() still sees them. Rules
|
||||
// that must cover every platform belong on tokens.
|
||||
CRAFTER_API std::span<const LintDecl> Decls();
|
||||
CRAFTER_API bool AstAvailable();
|
||||
std::span<const LintDecl> Decls();
|
||||
bool AstAvailable();
|
||||
// Why AstAvailable() is false — a missing PCM, a parse error, a file
|
||||
// that is not a translation unit. Empty when the AST is available.
|
||||
CRAFTER_API std::string_view AstUnavailableReason();
|
||||
std::string_view AstUnavailableReason();
|
||||
|
||||
// Driver wiring — set by RunLint before each check call. Not for rules.
|
||||
std::string activeRule;
|
||||
|
|
@ -363,7 +360,7 @@ export namespace Crafter {
|
|||
// Configuration::target so projects don't have to hardcode it for the
|
||||
// no-cross-compile case. Returns "" if clang isn't on PATH or doesn't
|
||||
// print a triple — those projects still need an explicit cfg.target.
|
||||
CRAFTER_API std::string HostTarget();
|
||||
std::string HostTarget();
|
||||
|
||||
struct Configuration {
|
||||
fs::path path;
|
||||
|
|
@ -439,7 +436,7 @@ export namespace Crafter {
|
|||
std::vector<Test> tests;
|
||||
// Lint rules for `crafter-build lint`. Populate via AddLintRule.
|
||||
std::vector<LintRule> lintRules;
|
||||
CRAFTER_API void GetInterfacesAndImplementations(std::span<fs::path> interfaces, std::span<fs::path> implementations);
|
||||
void GetInterfacesAndImplementations(std::span<fs::path> interfaces, std::span<fs::path> implementations);
|
||||
// Retry the `import X;` names GetInterfacesAndImplementations could not
|
||||
// place, against the dependency DAG as it stands now. Sources are
|
||||
// scanned when they're declared, but `dependencies` is often assigned
|
||||
|
|
@ -448,30 +445,30 @@ export namespace Crafter {
|
|||
// change and this Configuration's objects would be silently reused
|
||||
// against the new layout. Build() calls this before checking mtimes;
|
||||
// calling it again is harmless.
|
||||
CRAFTER_API void ResolvePendingImports();
|
||||
void ResolvePendingImports();
|
||||
// Declare a test. Sources default to `tests/<name>/main.cpp` resolved
|
||||
// against this Configuration's path; target/march/mtune/sysroot/debug
|
||||
// are inherited from this Configuration so cross-arch projects don't
|
||||
// have to re-specify. Returns a builder for chaining defines, deps,
|
||||
// etc. Defined in Crafter.Build:Test.
|
||||
CRAFTER_API struct TestBuilder AddTest(std::string_view name);
|
||||
struct TestBuilder AddTest(std::string_view name);
|
||||
// Same as AddTest, but compiles the parent's `interfaces` directly
|
||||
// into this test's Configuration (rather than going through a dep).
|
||||
// Use when the test must rebuild those interfaces with its own
|
||||
// compile flags — typically per-march SIMD codegen.
|
||||
CRAFTER_API struct TestBuilder AddTest(std::string_view name, std::span<fs::path> interfaces);
|
||||
struct TestBuilder AddTest(std::string_view name, std::span<fs::path> interfaces);
|
||||
// Math-style fan-out: one Test per MarchTier, all sharing the same
|
||||
// `tests/<name>/main.cpp` source and the same interface set, each
|
||||
// compiled with the tier's `-march`/`-mtune`. Test names are
|
||||
// `<name>-<march>`.
|
||||
CRAFTER_API void AddMarchVariants(std::string_view name, std::span<fs::path> interfaces, std::span<const struct MarchTier> tiers);
|
||||
void AddMarchVariants(std::string_view name, std::span<fs::path> interfaces, std::span<const struct MarchTier> tiers);
|
||||
// Register a lint rule for `crafter-build lint` / `format`. Rules
|
||||
// registered on any Configuration whose path lies inside the project
|
||||
// root are collected (deduplicated by name, root-first) — attach them
|
||||
// to the lib or the exe config, either works. Rules that call
|
||||
// ctx.SetContent are transforms (see LintContext::SetContent).
|
||||
// Defined in Crafter.Build:Lint.
|
||||
CRAFTER_API void AddLintRule(std::string name, std::function<void(LintContext&)> check);
|
||||
void AddLintRule(std::string name, std::function<void(LintContext&)> check);
|
||||
// Same, for a rule that reads LintContext::Decls(). Declared
|
||||
// separately rather than as a flag on AddLintRule so existing
|
||||
// registrations keep compiling. Such a rule makes the run build the
|
||||
|
|
@ -479,7 +476,7 @@ export namespace Crafter {
|
|||
// produced becomes an error instead of a silent pass. Prefer
|
||||
// report-only: a transform running after the first one forces a
|
||||
// re-parse of everything it changed.
|
||||
CRAFTER_API void AddAstLintRule(std::string name, std::function<void(LintContext&)> check);
|
||||
void AddAstLintRule(std::string name, std::function<void(LintContext&)> check);
|
||||
// Suffix that uniquely identifies this Configuration's compile state.
|
||||
// target+march+mtune are spelled out for readability; the rest
|
||||
// (type, debug, sysroot, defines, compileFlags) collapse into a short
|
||||
|
|
@ -544,14 +541,14 @@ export namespace Crafter {
|
|||
// 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
|
||||
// features, and suffixed with the wasm variant flags for the same reason.
|
||||
CRAFTER_API fs::path StdPcmDir(const Configuration& config);
|
||||
fs::path StdPcmDir(const Configuration& config);
|
||||
|
||||
// Where Build leaves this Configuration's linked artifact: the executable
|
||||
// (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
|
||||
// dependency" check reads, so a staleness test and the link step can't
|
||||
// disagree about the file name.
|
||||
CRAFTER_API fs::path ExpectedOutput(const Configuration& config);
|
||||
fs::path ExpectedOutput(const Configuration& config);
|
||||
|
||||
// The clang invocation every C++ translation unit in this Configuration is
|
||||
// compiled with, before anything that depends on work having happened
|
||||
|
|
@ -586,14 +583,14 @@ export namespace Crafter {
|
|||
fs::path stdPcmDir;
|
||||
fs::path pcmDir;
|
||||
};
|
||||
CRAFTER_API CompileCommand GetCompileCommand(const Configuration& config);
|
||||
CompileCommand GetCompileCommand(const Configuration& config);
|
||||
|
||||
CRAFTER_API BuildResult Build(Configuration& config, std::unordered_map<fs::path, std::shared_future<BuildResult>>& depResults, std::mutex& depMutex);
|
||||
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;
|
||||
// the shape is inherited from the language, not chosen here.
|
||||
// lint-disable-next-line no-char-pointer
|
||||
CRAFTER_API std::int32_t Run(std::int32_t argc, char** argv);
|
||||
std::int32_t Run(std::int32_t argc, char** argv);
|
||||
|
||||
// Delete the bin/ and build/ trees beside `projectFile`, returning the paths
|
||||
// that existed and were removed. Backs `crafter-build clean`.
|
||||
|
|
@ -603,7 +600,7 @@ export namespace Crafter {
|
|||
// to compile (and its git dependencies to be present) is useless exactly
|
||||
// then. That means variant directories are not enumerated — the whole tree
|
||||
// goes, which is what the by-hand `rm -rf bin build` did anyway.
|
||||
CRAFTER_API std::vector<fs::path> CleanProject(const fs::path& projectFile);
|
||||
std::vector<fs::path> CleanProject(const fs::path& projectFile);
|
||||
|
||||
// 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
|
||||
|
|
@ -611,7 +608,7 @@ export namespace Crafter {
|
|||
// this and shouldn't carry the extra files. Call from project.cpp after
|
||||
// outputName is set; index.html is generated against the current
|
||||
// outputName so renaming the binary later requires another call.
|
||||
CRAFTER_API void EnableWasiBrowserRuntime(Configuration& cfg);
|
||||
void EnableWasiBrowserRuntime(Configuration& cfg);
|
||||
|
||||
// Register the relaxed-SIMD codegen variant on cfg. Builds an additional
|
||||
// outputName.relaxed-simd.wasm compiled with -mrelaxed-simd (FMA,
|
||||
|
|
@ -622,17 +619,17 @@ export namespace Crafter {
|
|||
// 2026). Idempotent. Call before EnableWasiBrowserRuntime so the emitted
|
||||
// variants.json manifest includes it. A no-op for non-wasm targets at build
|
||||
// time (no wasm is produced), but harmless to declare unconditionally.
|
||||
CRAFTER_API void EnableWasiRelaxedSimdVariant(Configuration& cfg);
|
||||
void EnableWasiRelaxedSimdVariant(Configuration& cfg);
|
||||
|
||||
// View over the project's args with simple query helpers. Has(flag) for
|
||||
// boolean switches; Get(prefix) for valued options (e.g. Get("--prefix=")
|
||||
// returns the substring after the equals). Definitions are out-of-line
|
||||
// and CRAFTER_API so they cross the Windows DLL boundary cleanly — clang
|
||||
// does not emit module-attached in-class inline bodies into consumers.
|
||||
// returns the substring after the equals). Definitions are out-of-line so
|
||||
// project.so resolves them from the executable: clang does not emit
|
||||
// module-attached in-class inline bodies into consumers.
|
||||
struct ArgQuery {
|
||||
std::span<const std::string_view> args;
|
||||
CRAFTER_API bool Has(std::string_view flag) const;
|
||||
CRAFTER_API std::optional<std::string> Get(std::string_view prefix) const;
|
||||
bool Has(std::string_view flag) const;
|
||||
std::optional<std::string> Get(std::string_view prefix) const;
|
||||
};
|
||||
|
||||
// Apply the framework's standard CLI args + env vars onto cfg:
|
||||
|
|
@ -651,5 +648,5 @@ export namespace Crafter {
|
|||
// pre-set defaults. Returns an ArgQuery over the same span so projects
|
||||
// can query their own flags (`--timing`, ...) without re-rolling the
|
||||
// for-arg-in-args loop.
|
||||
CRAFTER_API ArgQuery ApplyStandardArgs(Configuration& cfg, std::span<const std::string_view> args);
|
||||
ArgQuery ApplyStandardArgs(Configuration& cfg, std::span<const std::string_view> args);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
module;
|
||||
#include "Crafter.Build-Api.h"
|
||||
export module Crafter.Build:External;
|
||||
import std;
|
||||
namespace fs = std::filesystem;
|
||||
|
|
@ -48,13 +46,13 @@ export namespace Crafter {
|
|||
// declaration and target — BuildExternal derives its own working directory
|
||||
// through this, so a caller that only needs to know where the headers ended
|
||||
// up cannot drift from where they actually are.
|
||||
CRAFTER_API fs::path ExternalCloneDir(const ExternalDependency& dep, std::string_view target);
|
||||
fs::path ExternalCloneDir(const ExternalDependency& dep, std::string_view target);
|
||||
// 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
|
||||
// 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.
|
||||
CRAFTER_API std::vector<std::string> ExternalIncludeFlags(const ExternalDependency& dep, std::string_view target);
|
||||
CRAFTER_API ExternalBuildResult BuildExternal(const ExternalDependency& dep, std::string_view target, std::atomic<bool>& cancelled);
|
||||
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);
|
||||
|
||||
// Specification for a sibling crafter-build project to fetch and depend on.
|
||||
// GitSource picks the revision: leave branch + commit empty for the
|
||||
|
|
@ -73,7 +71,7 @@ export namespace Crafter {
|
|||
// remote's project.cpp, and returns a stable Configuration* you can drop
|
||||
// into cfg.dependencies. The Configuration is owned by an internal cache
|
||||
// for the lifetime of the build; identical specs share one Configuration.
|
||||
CRAFTER_API Configuration* GitProject(const GitProjectSpec& spec);
|
||||
Configuration* GitProject(const GitProjectSpec& spec);
|
||||
|
||||
// Specification for a local sibling crafter-build project (e.g. an
|
||||
// example folder depending on its parent project, or a workspace where
|
||||
|
|
@ -87,7 +85,7 @@ export namespace Crafter {
|
|||
// cache copy. Resolves projectFile to its canonical absolute path and
|
||||
// recursively LoadProject's it. Returns a stable Configuration* owned
|
||||
// by the same internal cache as GitProject.
|
||||
CRAFTER_API Configuration* LocalProject(const LocalProjectSpec& spec);
|
||||
Configuration* LocalProject(const LocalProjectSpec& spec);
|
||||
|
||||
// Clone (or update) the given git source into the shared external
|
||||
// cache and return the path to the cloned working tree. Intended for
|
||||
|
|
@ -97,5 +95,5 @@ export namespace Crafter {
|
|||
// ExternalDependency / GitProject. Caching key = url + branch + commit;
|
||||
// identical calls reuse the existing clone. Pin `source.commit` to a
|
||||
// SHA for reproducible, offline-after-first-fetch builds.
|
||||
CRAFTER_API fs::path GitFetch(const GitSource& source);
|
||||
fs::path GitFetch(const GitSource& source);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
module;
|
||||
#include "Crafter.Build-Api.h"
|
||||
export module Crafter.Build:Implementation;
|
||||
import std;
|
||||
namespace fs = std::filesystem;
|
||||
|
|
@ -19,8 +17,8 @@ namespace Crafter {
|
|||
// no reachable Configuration provided when the source was scanned.
|
||||
std::vector<std::string> pendingImports;
|
||||
fs::path path;
|
||||
CRAFTER_API Implementation(fs::path&& path);
|
||||
CRAFTER_API bool Check(const fs::path& buildDir, const fs::path& pcmDir, fs::file_time_type sourceFloor = fs::file_time_type::min()) const;
|
||||
CRAFTER_API void Compile(const std::string_view clang, const fs::path& buildDir, std::atomic<bool>& buildCancelled, std::string& buildError) const;
|
||||
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;
|
||||
void Compile(const std::string_view clang, const fs::path& buildDir, std::atomic<bool>& buildCancelled, std::string& buildError) const;
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
module;
|
||||
#include "Crafter.Build-Api.h"
|
||||
export module Crafter.Build:Interface;
|
||||
import std;
|
||||
namespace fs = std::filesystem;
|
||||
|
|
@ -26,9 +24,9 @@ namespace Crafter {
|
|||
bool checked = false;
|
||||
std::string name;
|
||||
fs::path path;
|
||||
CRAFTER_API ModulePartition(std::string&& name, fs::path&& path);
|
||||
CRAFTER_API bool Check(const fs::path& pcmDir, fs::file_time_type sourceFloor = fs::file_time_type::min());
|
||||
CRAFTER_API void Compile(const std::string_view clang, const fs::path& pcmDir, const fs::path& buildDir, std::atomic<bool>& buildCancelled, std::string& buildError);
|
||||
ModulePartition(std::string&& name, fs::path&& path);
|
||||
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);
|
||||
};
|
||||
|
||||
export class Module {
|
||||
|
|
@ -48,8 +46,8 @@ namespace Crafter {
|
|||
std::vector<std::unique_ptr<ModulePartition>> partitions;
|
||||
std::string name;
|
||||
fs::path path;
|
||||
CRAFTER_API Module(std::string&& name, fs::path&& path);
|
||||
CRAFTER_API bool Check(const fs::path& pcmDir, fs::file_time_type sourceFloor = fs::file_time_type::min());
|
||||
CRAFTER_API void Compile(const std::string_view clang, const fs::path& pcmDir, const fs::path& buildDir, std::atomic<bool>& buildCancelled, std::string& buildError);
|
||||
Module(std::string&& name, fs::path&& path);
|
||||
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);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
module;
|
||||
#include "Crafter.Build-Api.h"
|
||||
export module Crafter.Build:Lint;
|
||||
import std;
|
||||
import :Clang;
|
||||
|
|
@ -63,5 +61,5 @@ export namespace Crafter {
|
|||
// final content differs from the original. Prints per-mode output:
|
||||
// findings compiler-style (Report), would-change paths (Check), or
|
||||
// formatted paths (Apply), plus a summary line.
|
||||
CRAFTER_API LintSummary RunLint(Configuration& projectCfg, const RunLintOptions& opts);
|
||||
LintSummary RunLint(Configuration& projectCfg, const RunLintOptions& opts);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
module;
|
||||
#include "Crafter.Build-Api.h"
|
||||
export module Crafter.Build:Platform;
|
||||
import std;
|
||||
namespace fs = std::filesystem;
|
||||
|
|
@ -30,9 +28,9 @@ namespace Crafter {
|
|||
// Progress::Diagnostic), otherwise the compiler's diagnostics. Callers
|
||||
// therefore keep the `if (!result.empty())` shape they already had.
|
||||
std::string RunBuildCommand(std::string_view command);
|
||||
export CRAFTER_API CommandResult RunCommandWithTimeout(std::string_view command, std::chrono::seconds timeout);
|
||||
export CommandResult RunCommandWithTimeout(std::string_view command, std::chrono::seconds timeout);
|
||||
std::string GetBaseCommand(const Configuration& config);
|
||||
export CRAFTER_API Configuration LoadProject(const fs::path& projectFile, std::span<const std::string_view> args);
|
||||
export Configuration LoadProject(const fs::path& projectFile, std::span<const std::string_view> args);
|
||||
// A dependency the project file currently running its CrafterBuildProject
|
||||
// declared with `#pragma crafter dependency`, so it can also be wired
|
||||
// into the build graph (a test that links the same module, say) without
|
||||
|
|
@ -40,7 +38,7 @@ namespace Crafter {
|
|||
// name or any module it exports — whatever the project file `import`s.
|
||||
// Throws when no such dependency was declared, or when called from
|
||||
// anywhere other than inside LoadProject.
|
||||
export CRAFTER_API Configuration* ProjectDependency(std::string_view name);
|
||||
export Configuration* ProjectDependency(std::string_view name);
|
||||
// True while the project file crafter-build was pointed at runs its
|
||||
// CrafterBuildProject, and when no project file is running at all; false
|
||||
// inside a dependency's project file (a `#pragma crafter dependency`,
|
||||
|
|
@ -49,11 +47,11 @@ namespace Crafter {
|
|||
// 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
|
||||
// project file in turn and the two recurse until the cycle guard throws.
|
||||
export CRAFTER_API bool IsTopLevelProject();
|
||||
export bool IsTopLevelProject();
|
||||
// Resolves the directory holding distributed runtime assets (Crafter.Build
|
||||
// module sources, wasi-runtime/, etc). Honors CRAFTER_BUILD_HOME; otherwise
|
||||
// derives <prefix>/share/crafter-build from the running executable's path.
|
||||
export CRAFTER_API fs::path GetCrafterBuildHome();
|
||||
export fs::path GetCrafterBuildHome();
|
||||
// Newest mtime among the prerequisites a compiler recorded in `depFile`
|
||||
// (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`
|
||||
|
|
|
|||
|
|
@ -1,22 +1,20 @@
|
|||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
module;
|
||||
#include "Crafter.Build-Api.h"
|
||||
export module Crafter.Build:Progress;
|
||||
import std;
|
||||
|
||||
export namespace Crafter::Progress {
|
||||
enum class Verbosity { Quiet, Default, Verbose };
|
||||
|
||||
CRAFTER_API void SetVerbosity(Verbosity v);
|
||||
CRAFTER_API Verbosity GetVerbosity();
|
||||
void SetVerbosity(Verbosity v);
|
||||
Verbosity GetVerbosity();
|
||||
|
||||
// RAII work-unit marker. Construction inflates the running total; destruction
|
||||
// increments completed and (on Default + TTY) redraws the status line. Move
|
||||
// semantics are disabled — keep these as plain stack locals inside the worker
|
||||
// lambda so the destructor fires when that thread's work ends.
|
||||
class CRAFTER_API Task {
|
||||
class Task {
|
||||
public:
|
||||
explicit Task(std::string label);
|
||||
~Task();
|
||||
|
|
@ -29,18 +27,18 @@ export namespace Crafter::Progress {
|
|||
};
|
||||
|
||||
// Verbose-mode command echo. No-op outside Verbose.
|
||||
CRAFTER_API void EchoCommand(std::string_view command);
|
||||
void EchoCommand(std::string_view command);
|
||||
|
||||
// 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
|
||||
// it, then writes to stderr. No-op on Quiet.
|
||||
CRAFTER_API void Diagnostic(std::string_view text);
|
||||
void Diagnostic(std::string_view text);
|
||||
|
||||
// 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.
|
||||
CRAFTER_API void Clear();
|
||||
void Clear();
|
||||
|
||||
// Print "Built N targets in Xms" (or equivalent) and clear in-place state.
|
||||
// Safe to call multiple times; subsequent calls are no-ops.
|
||||
CRAFTER_API void Finalize();
|
||||
void Finalize();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
module;
|
||||
#include "Crafter.Build-Api.h"
|
||||
export module Crafter.Build:Shader;
|
||||
import std;
|
||||
namespace fs = std::filesystem;
|
||||
|
|
@ -30,8 +28,8 @@ namespace Crafter {
|
|||
fs::path path;
|
||||
std::string entrypoint;
|
||||
ShaderType type;
|
||||
CRAFTER_API Shader(fs::path&& path, std::string&& entrypoint, ShaderType type);
|
||||
CRAFTER_API bool Check(const fs::path& outputDir) const;
|
||||
CRAFTER_API std::string Compile(const fs::path& outputDir, std::span<const fs::path> includeDirs) const;
|
||||
Shader(fs::path&& path, std::string&& entrypoint, ShaderType type);
|
||||
bool Check(const fs::path& outputDir) const;
|
||||
std::string Compile(const fs::path& outputDir, std::span<const fs::path> includeDirs) const;
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
module;
|
||||
#include "Crafter.Build-Api.h"
|
||||
export module Crafter.Build:Test;
|
||||
import std;
|
||||
import :Clang;
|
||||
|
|
@ -28,7 +26,7 @@ export namespace Crafter {
|
|||
// avoid the double-`GetInterfacesAndImplementations` footgun (calling
|
||||
// it twice would re-parse the same .cppm files into duplicate Module
|
||||
// entries).
|
||||
struct CRAFTER_API TestBuilder {
|
||||
struct TestBuilder {
|
||||
Configuration* parent;
|
||||
std::size_t index;
|
||||
|
||||
|
|
@ -85,8 +83,8 @@ export namespace Crafter {
|
|||
bool AllPassed() const { return failed == 0 && crashed == 0 && timedOut == 0; }
|
||||
};
|
||||
|
||||
CRAFTER_API TestSummary RunTests(Configuration& projectCfg, const RunTestsOptions& opts, std::span<const std::string_view> projectArgs = {});
|
||||
CRAFTER_API TestResult RunSingleTest(const Test& test, const std::filesystem::path& binary, std::chrono::seconds timeout);
|
||||
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);
|
||||
|
||||
// Parent-project access for test fixtures. Run() sets the pointer to the
|
||||
// root project's Configuration before discovery; fixtures call ParentLib()
|
||||
|
|
@ -94,6 +92,6 @@ export namespace Crafter {
|
|||
// parent itself) so they can declare it as a dependency without rebuilding
|
||||
// the lib config from scratch. Lookup is by Configuration::name and
|
||||
// recurses through the parent's dependency graph.
|
||||
CRAFTER_API void SetParentProject(Configuration* parent);
|
||||
CRAFTER_API Configuration* ParentLib(std::string_view name);
|
||||
void SetParentProject(Configuration* parent);
|
||||
Configuration* ParentLib(std::string_view name);
|
||||
}
|
||||
|
|
|
|||
19
project.cpp
19
project.cpp
|
|
@ -24,12 +24,9 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
|||
CrafterBuildLib->name = "crafter.build-lib";
|
||||
CrafterBuildLib->outputName = "crafter-build";
|
||||
ApplyStandardArgs(*CrafterBuildLib, args);
|
||||
// Windows builds (native msvc via build.cmd or cross-compiled mingw from
|
||||
// Linux) need a DLL + import lib + launcher exe so LoadProject can
|
||||
// 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;
|
||||
// Monolithic: project.so resolves Crafter:: symbols from the running
|
||||
// executable, so the library is only ever linked statically into it.
|
||||
CrafterBuildLib->type = ConfigurationType::LibraryStatic;
|
||||
|
||||
CrafterBuildLib->dependencies = { math, asset };
|
||||
CrafterBuildLib->defines.push_back({"CRAFTER_BUILD_HAS_ASSET", ""});
|
||||
|
|
@ -79,12 +76,6 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
|||
glslang.source.branch = "main";
|
||||
glslang.builder = ExternalBuilder::CMake;
|
||||
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.libs = { "SPIRV", "GenericCodeGen", "glslang", "OSDependent", "MachineIndependent", "glslang-default-resource-limits" };
|
||||
|
||||
|
|
@ -106,10 +97,6 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
|||
cfg.linkFlags.push_back("-Wl,--export-dynamic");
|
||||
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
|
||||
// process. The harness (whichever crafter-build invokes `test`) compiles
|
||||
|
|
|
|||
|
|
@ -71,11 +71,7 @@ namespace {
|
|||
app.dependencies = { lib };
|
||||
// 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.
|
||||
// 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" };
|
||||
}
|
||||
return app;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,9 +6,7 @@ import Crafter.Build;
|
|||
using namespace Crafter;
|
||||
|
||||
// POSIX env helpers — libc++ exports only std::getenv via `import std`, so
|
||||
// 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.
|
||||
// forward-declare the setters we need against the C library directly.
|
||||
extern "C" int setenv(const char* name, const char* value, int overwrite);
|
||||
extern "C" int unsetenv(const char* name);
|
||||
|
||||
|
|
@ -87,13 +85,11 @@ int main() {
|
|||
Check(r.exec.find("QEMU_LD_PREFIX=/opt/alarm-sysroot") != std::string::npos, "ForTarget propagates sysroot to QEMU_LD_PREFIX");
|
||||
}
|
||||
{
|
||||
// From a Linux host the only sensible way to run x86_64-w64-mingw32 is wine.
|
||||
// The only way to run x86_64-w64-mingw32 output is wine.
|
||||
Configuration cfg;
|
||||
cfg.target = "x86_64-w64-mingw32";
|
||||
TestRunner r = TestRunner::ForTarget(cfg);
|
||||
if (HostTarget().find("linux") != std::string::npos) {
|
||||
Check(r.name == "wine", "ForTarget(mingw) on Linux host uses wine");
|
||||
}
|
||||
Check(r.name == "wine", "ForTarget(mingw) uses wine");
|
||||
}
|
||||
|
||||
// FromEnv: when the env var is set, it wins over the fallback.
|
||||
|
|
|
|||
Loading…
Reference in a new issue