diff --git a/.forgejo/workflows/ci.yaml b/.forgejo/workflows/ci.yaml index 0c5d79a..2feeb1d 100644 --- a/.forgejo/workflows/ci.yaml +++ b/.forgejo/workflows/ci.yaml @@ -1,205 +1,261 @@ -name: Flatpak +name: CI -# One job, three outcomes: -# pull request -> build the Flatpak and run the suite inside it -# push master -> also publish the result as the `beta` branch of the -# Catcrafts repo and refresh the rolling `latest` release -# tag X.Y.Z -> publish as `stable` and create release X.Y.Z -# -# The `flatpak` runner label is the privileged runner on the Hetzner box -# (bubblewrap needs it) with four volumes allowed: the Sdk install, the -# flatpak-builder state (ccache and the LLVM module, so only crafter-build -# itself rebuilds on a normal run), the published repo's web root, and the -# repo signing key, which lives on the runner host and never in the forge. on: pull_request: branches: [master] push: branches: [master] - # Bare semver tags. Spelled with globs so it means the same whether the - # forge reads it as a workflow pattern or as a plain glob; `latest`, the - # rolling tag this workflow moves itself, does not start with a digit. - tags: ['[0-9]*.[0-9]*.[0-9]*'] workflow_dispatch: -env: - APP_ID: net.catcrafts.CrafterBuild - MANIFEST: packaging/flatpak/net.catcrafts.CrafterBuild.yaml - CI_MANIFEST: packaging/flatpak/ci-manifest.yaml - RUNTIME: org.freedesktop.Sdk//26.08 - REPO_DIR: /srv/flatpak/repo - WEB_DIR: /srv/flatpak - jobs: - flatpak: - runs-on: flatpak - container: - image: archlinux:latest - options: >- - -v crafter-flatpak-system:/var/lib/flatpak - -v crafter-flatpak-builder:/root/.cache/flatpak-builder - -v /var/www/flatpak:/srv/flatpak - -v /etc/crafter-flatpak:/run/crafter-flatpak:ro + build-test-release: + runs-on: arch-latest + env: + # x86-64-v2 = SSE4.2 baseline; runs on the CI SBC (Intel N5105 / Tremont, + # no AVX) and on essentially every x86_64 CPU since ~2011. The bootstrap + # MUST stay v2 — anything higher would emit instructions the SBC can't + # execute and the bootstrap binary itself would crash before it could + # rebuild for higher levels. v3/v4 variants are produced from this v2 + # bootstrap by re-running it with CRAFTER_BUILD_MARCH overridden. + CRAFTER_BUILD_MARCH: x86-64-v2 + CRAFTER_BUILD_MTUNE: generic steps: - - name: Install tools + - name: Install build dependencies run: | + # The slim archlinux:latest image arrives without a populated + # pacman keyring AND without a local pacman master key, so: + # 1. --init generates the local signing key (needed when pacman + # itself rewrites the keyring during package upgrades) + # 2. --populate archlinux imports the upstream master keys so + # currently-shipping signatures verify + # After that we can refresh archlinux-keyring to pick up keys for + # packagers added after the image's snapshot, then -Syu the rest. pacman-key --init pacman-key --populate archlinux pacman -Sy --noconfirm --needed archlinux-keyring - pacman -Syu --noconfirm --needed flatpak flatpak-builder ccache git nodejs jq gnupg go-yq ostree python + pacman -Syu --noconfirm --needed \ + base-devel git zip tar jq \ + clang llvm lld libc++ cmake \ + mingw-w64-gcc \ + wasi-libc wasi-libc++ wasi-libc++abi wasi-compiler-rt \ + nodejs + # The container runs as root and the workspace may be owned by a + # different uid; tell git not to refuse operations on it. git config --global --add safe.directory '*' - - name: Sdk - # A no-op once the volume is warm; `update` follows point releases. - run: | - flatpak remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo - flatpak install -y --noninteractive flathub "$RUNTIME" - flatpak update -y --noninteractive - - name: Checkout uses: actions/checkout@v4 with: - fetch-depth: 0 + # Persist the auth token so the 'Update rolling latest tag' step + # below can push the tag back via the implicit GITHUB_TOKEN. persist-credentials: true - - name: Version and channel - id: meta - run: | - set -eu - if [ "${GITHUB_REF_TYPE}" = tag ]; then - version="$GITHUB_REF_NAME" - branch=stable - # The metainfo's newest release is the release notes and must be - # the version being tagged. - top=$(grep -m1 -oP '&2 - exit 1 - fi - else - # A master build names itself after the release it follows, the - # way a local checkout does: 2.0.0-3-g21746b1 is three commits - # past 2.0.0. Before the first tag there is nothing to describe - # from and the bare hash gets a placeholder in front. - described=$(git describe --tags --always --exclude=latest) - case "$described" in - [0-9]*.[0-9]*.[0-9]*) version="$described" ;; - *) version="0.0.0-dev+$described" ;; - esac - branch=beta - fi - echo "version=$version" >> "$GITHUB_OUTPUT" - echo "branch=$branch" >> "$GITHUB_OUTPUT" - echo "version $version, flatpak branch $branch" - - - name: Build - # The manifest is rewritten beside the original so its relative - # source paths still resolve. The suite runs inside this build. - env: - VERSION: ${{ steps.meta.outputs.version }} - run: | - set -eu - yq '(.modules[] | select(.name == "crafter-build") | ."build-options".env.CRAFTER_BUILD_VERSION) = strenv(VERSION)' "$MANIFEST" > "$CI_MANIFEST" - grep -n 'CRAFTER_BUILD_VERSION' "$CI_MANIFEST" - flatpak-builder --force-clean --ccache --disable-rofiles-fuse \ - --state-dir=/root/.cache/flatpak-builder \ - --repo=repo --default-branch="${{ steps.meta.outputs.branch }}" \ - build-dir "$CI_MANIFEST" - - - name: Smoke test - run: | - flatpak-builder --run build-dir "$CI_MANIFEST" crafter-build --version - flatpak-builder --run build-dir "$CI_MANIFEST" sh -c 'cd examples/hello-world && crafter-build && ls bin/*/hello*' - - # Everything below publishes and only runs on master and on tags. - - name: Bundle - if: github.event_name != 'pull_request' - run: | - mkdir -p dist - flatpak build-bundle repo "dist/crafter-build-${{ steps.meta.outputs.version }}.flatpak" "$APP_ID" "${{ steps.meta.outputs.branch }}" - cp packaging/flatpak/net.catcrafts.CrafterBuild.flatpakref dist/ - (cd dist && sha256sum * > SHA256SUMS) - ls -la dist - - - name: Upload bundle - if: github.event_name != 'pull_request' - uses: actions/upload-artifact@v3 + - 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. + uses: actions/cache@v4 with: - name: crafter-build-${{ steps.meta.outputs.version }} - path: dist/*.flatpak - if-no-files-found: error + path: | + ~/.cache/crafter.build/external + key: glslang-${{ runner.os }}-v2 - - name: Publish to the Catcrafts repo - if: github.event_name != 'pull_request' + - name: Bootstrap (build.sh) + run: ./build.sh + + # Full style gate: Report() findings (naming, enum-class, char*, …) AND + # would-reformat findings from the transform rules. A dirty tree means + # someone skipped `crafter-build format` — fail fast, before the + # (much slower) test suite. + # - name: Lint + # run: CRAFTER_BUILD_HOME=$PWD/share/crafter-build ./bin/crafter-build lint + + - name: Run tests + run: CRAFTER_BUILD_HOME=$PWD/share/crafter-build ./bin/crafter-build test + + # Everything below produces release artifacts and only runs on push to + # master. PRs stop after the test step above — the bootstrap binary is + # enough to validate the change; the per-march variant builds and the + # archive uploads are wasted work on a PR. + - name: Build Linux x86-64-v2 (matches bootstrap) + 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 + + - name: Build Linux x86-64-v3 + 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 + + - name: Build Linux x86-64-v4 + 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 -eu - # The signing key is generated once on the runner host - # (/etc/crafter-flatpak: signing-key.asc and key-id) and mounted - # read-only; it is imported into a throwaway keyring per run. - if [ ! -s /run/crafter-flatpak/signing-key.asc ] || [ ! -s /run/crafter-flatpak/key-id ]; then - echo "no signing key under /etc/crafter-flatpak on the runner host; generate it as described in AGENTS.md (CI and publishing)" >&2 - exit 1 - fi - export GNUPGHOME=$(mktemp -d) - gpg --batch --quiet --import /run/crafter-flatpak/signing-key.asc - FLATPAK_GPG_KEY_ID=$(cat /run/crafter-flatpak/key-id) - # The live repo is the canonical copy; builds are promoted into it, - # never built into it. - # Keyed on the repo's config file, not the directory: the web root - # may exist as an empty directory before the first publish. - [ -f "$REPO_DIR/config" ] || ostree init --repo="$REPO_DIR" --mode=archive - flatpak build-commit-from --src-repo=repo \ - --gpg-sign="$FLATPAK_GPG_KEY_ID" --gpg-homedir="$GNUPGHOME" \ - "$REPO_DIR" "app/$APP_ID/x86_64/${{ steps.meta.outputs.branch }}" - flatpak build-update-repo --gpg-sign="$FLATPAK_GPG_KEY_ID" --gpg-homedir="$GNUPGHOME" \ - --generate-static-deltas --prune "$REPO_DIR" - # The repo description and the app ref carry the public key. - key=$(gpg --export "$FLATPAK_GPG_KEY_ID" | base64 --wrap=0) - sed "s|GPGKey=.*|GPGKey=$key|" packaging/flatpak/catcrafts.flatpakrepo > "$WEB_DIR/catcrafts.flatpakrepo" - sed "s|GPGKey=.*|GPGKey=$key|" packaging/flatpak/net.catcrafts.CrafterBuild.flatpakref > "$WEB_DIR/net.catcrafts.CrafterBuild.flatpakref" + set -eux + mkdir -p dist - - name: Rolling 'latest' release - if: github.event_name != 'pull_request' && github.ref_type != 'tag' + # 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. + 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. + short=${march##*-} + + # VariantId is "----"; CI pins + # mtune=generic so each (target, march) resolves to a single subdir + # but the trailing hash isn't known to the workflow, hence the glob. + stage_lin=$(mktemp -d) + mkdir -p "$stage_lin/bin" "$stage_lin/lib" + cp bin/crafter.build-exe-x86_64-pc-linux-gnu-$march-*/crafter-build "$stage_lin/bin/" + 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 + # 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' + uses: actions/upload-artifact@v3 + with: { name: crafter-build-linux-x86_64-v2, path: dist/crafter-build-linux-x86_64-v2.tar.gz, if-no-files-found: error } + - name: Upload Linux 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-linux-x86_64-v3, path: dist/crafter-build-linux-x86_64-v3.tar.gz, if-no-files-found: error } + - name: Upload Linux 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-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' run: | git config user.email "ci@catcrafts.net" git config user.name "Crafter Build CI" git tag -f latest git push origin latest --force + - name: Publish rolling 'latest' release - if: github.event_name != 'pull_request' && github.ref_type != 'tag' + # forgejo-release uploads each file in release-dir as a separate + # release asset, so the Releases page shows six 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 with: direction: upload url: ${{ github.server_url }} repo: ${{ github.repository }} tag: latest - title: Latest master build + title: Latest main build prerelease: true override: true release-dir: dist token: ${{ secrets.GITHUB_TOKEN }} - - name: Release notes - if: github.ref_type == 'tag' + # musl build for Alpine / postmarketOS hosts: the glibc launcher above cannot + # run there (musl has no glibc loader and gcompat lacks libgcc_s + the + # __isoc23_* symbols). Bootstrap natively on alpine:edge and ship the + # bootstrap output — the same thing the Arch PKGBUILD installs. Dynamic + # against musl libc++ (crafter-build dlopens project.so, so it cannot be + # fully static); consumers need `apk add clang lld libc++-dev llvm-runtimes`. + release-musl: + needs: build-test-release + if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/master' + runs-on: arch-latest + container: + image: alpine:edge + env: + CRAFTER_BUILD_MARCH: x86-64-v2 + CRAFTER_BUILD_MTUNE: generic + steps: + - name: Install build dependencies + # nodejs: actions/checkout is a Node action. build-base: gcc's + # libgcc_s + crt objects, which clang's musl driver links against, plus + # make/binutils for the glslang cmake build (python3: its generators). + # clang-dev + llvm-dev: the libclang header Crafter.Build-Lint includes + # lives under llvm-config --includedir here, not /usr/include. llvm: + # llvm-ar for the LTO static archives the self-host/test builds write. run: | - # The newest block's description, as plain text. - python3 - > release-notes.md <<'EOF' - import re, html - text = open("packaging/flatpak/net.catcrafts.CrafterBuild.metainfo.xml").read() - block = re.search(r"]*>(.*?)", text, re.S).group(1) - paras = re.findall(r"

(.*?)

", block, re.S) - print("\n\n".join(html.unescape(" ".join(p.split())) for p in paras)) - print("\nInstall:\n\n flatpak remote-add --if-not-exists catcrafts https://flatpak.catcrafts.net/catcrafts.flatpakrepo\n flatpak install catcrafts net.catcrafts.CrafterBuild\n") - EOF - - name: Publish release - if: github.ref_type == 'tag' - uses: https://code.forgejo.org/actions/forgejo-release@v2 - with: - direction: upload - url: ${{ github.server_url }} - repo: ${{ github.repository }} - tag: ${{ github.ref_name }} - title: crafter-build ${{ github.ref_name }} - release-notes-file: release-notes.md - release-dir: dist - token: ${{ secrets.GITHUB_TOKEN }} + apk add -q nodejs git curl jq tar clang clang-dev lld libc++-dev \ + llvm-libunwind-dev llvm-runtimes llvm llvm-dev cmake build-base python3 + git config --global --add safe.directory '*' + + - name: Checkout + uses: actions/checkout@v4 + + - name: Bootstrap (build.sh) + run: ./build.sh + + - name: Run tests + run: CRAFTER_BUILD_HOME=$PWD/share/crafter-build ./bin/crafter-build test + + - name: Package artifact + run: | + set -eux + file bin/crafter-build || true + mkdir -p dist + stage=$(mktemp -d) + mkdir -p "$stage/bin" "$stage/lib" + cp bin/crafter-build "$stage/bin/" + cp lib/libcrafter-build.a "$stage/lib/" + cp -r share "$stage/" + tar czf dist/crafter-build-linux-x86_64-musl-v2.tar.gz -C "$stage" . + ls -la dist/ + + - name: Upload musl artifact + uses: actions/upload-artifact@v3 + with: { name: crafter-build-linux-x86_64-musl-v2, path: dist/crafter-build-linux-x86_64-musl-v2.tar.gz, if-no-files-found: error } + + # Add the asset to the rolling 'latest' release the first job just + # (re)published, via the API rather than forgejo-release: that action's + # override mode would drop the other job's assets. + - name: Attach to rolling 'latest' release + env: + TOKEN: ${{ secrets.GITHUB_TOKEN }} + API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }} + run: | + set -eu + asset=crafter-build-linux-x86_64-musl-v2.tar.gz + rel=$(curl -fsS -H "Authorization: token $TOKEN" "$API/releases/tags/latest") + id=$(printf '%s' "$rel" | jq -r .id) + for old in $(printf '%s' "$rel" | jq -r --arg n "$asset" '.assets[] | select(.name==$n) | .id'); do + curl -fsS -X DELETE -H "Authorization: token $TOKEN" "$API/releases/$id/assets/$old" + done + curl -fsS -H "Authorization: token $TOKEN" -F "attachment=@dist/$asset" \ + "$API/releases/$id/assets?name=$asset" > /dev/null + echo "attached $asset to release $id" diff --git a/.gitignore b/.gitignore index 67d2a68..a47aaf6 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,4 @@ build/ bin/ lib/*.a share/crafter-build/ -.claude -# written by CI next to the manifest so relative source paths still resolve -packaging/flatpak/ci-manifest.yaml +.claude \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index ff93b96..f0eff75 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,34 +8,30 @@ crafter-build is a C++26 modules build system. A project is described by a `proj | Path | What lives there | |---|---| -| `interfaces/Crafter.Build-.cppm` | Module interface partitions. `Crafter.Build.cppm` is the umbrella that re-exports them. | +| `interfaces/Crafter.Build-.cppm` | Module interface partitions. `Crafter.Build.cppm` is the umbrella that re-exports them. `Crafter.Build-Api.h` defines `CRAFTER_API`. | | `implementations/Crafter.Build-.cpp` | Implementation units, one per partition, named `Crafter.Build:_impl`. `main.cpp` is a thin wrapper around `Crafter::Run`. | -| `project.cpp` | The self-host build description: a `crafter.build-lib` library, a `crafter.build-exe` executable, the test declarations, the lint rules and the `flatpak` and `install` scripts. | +| `project.cpp` | The self-host build description: a `crafter.build-lib` library, a `crafter.build-exe` executable, the test declarations and the lint rules. | | `tests//main.cpp` | One self-test per directory; `tests//fixture/` holds the sample project it builds. | | `examples/` | Standalone user-facing projects, MIT licensed. | -| `build.sh` | Bootstrap script. It compiles everything by hand, without crafter-build. | +| `build.sh`, `build.cmd` | Bootstrap scripts for Linux and Windows. They compile everything by hand, without crafter-build. | | `wasi-runtime/` | `runtime.js` and `index.html.in`, shipped for browser targets. | | `lib/` | Vendored third-party headers (nlohmann/json, glslang's DirStackFileIncluder). Do not edit. | | `share/crafter-build/`, `bin/`, `build/`, `lib/*.a` | Generated and gitignored. | -| `packaging/flatpak/`, `.forgejo/workflows/ci.yaml` | The Flatpak manifest with its metainfo, repo and ref files, and the Forgejo Actions CI that builds and publishes it. | +| `PKGBUILD`, `.forgejo/workflows/ci.yaml` | Arch package and the Forgejo Actions CI. | Partitions and what they own: -- `:Toolchain` resolves the compiler once per process (`GetToolchain`): clang++, clang, llvm-ar, libclang, libc++'s std module source and the default sysroots, from `CRAFTER_BUILD_LLVM_DIR`, else a toolchain installed beside crafter-build, else PATH. It also holds the version baked in at build time. `SysrootStdModule` picks the std module source for a sysroot build by the sysroot's libc++ release. -- `:Clang` holds `Configuration`, `Build()`, `VariantId()`, `ApplyStandardArgs`, `HostTarget`, the CLI entry point `Crafter::Run`, and the lint, test and script data types. +- `:Clang` holds `Configuration`, `Build()`, `VariantId()`, `ApplyStandardArgs`, `HostTarget`, the CLI entry point `Crafter::Run`, and the lint and test data types. - `:Platform` runs commands, implements `LoadProject` (compile and load `project.cpp`), the host PCM cache, `#pragma crafter dependency`, and all OS-specific code. - `:Interface` and `:Implementation` model module units, their staleness checks and their compiles. - `:External` handles git + cmake dependencies, `GitProject` and `LocalProject`. - `:Test` holds `AddTest`, `RunTests` and `TestRunner`. - `:Lint` is the lint/format driver plus the libclang token and AST layer. -- `:Script` is the driver behind `crafter-build run`: it looks a name up in `cfg.scripts`, calls the callback and turns what it returns into the exit code. - `:Progress` is console output. `:Shader` compiles shaders through glslang. `:Asset` is the hook into Crafter.Asset. ## Dev environment -The shipped artifact is the Flatpak (see "CI and publishing"); the loop below is the native development loop. - -Prerequisites on Linux: `clang` (with `libclang`, which the AST lint rules load at runtime), `lld`, `libc++` with its std module source (`share/libc++/v1/std.cppm`), `cmake`, `git`. `crafter-build --version` prints which compiler was resolved; `CRAFTER_BUILD_LLVM_DIR` points it at another LLVM prefix, and `build.sh` honours the same variable. Network access to `forgejo.catcrafts.net` and `github.com` is needed: loading `project.cpp` fetches Crafter.Math, Crafter.Asset and Crafter.LintRules into `~/.cache/crafter.build/external/`, and the bootstrap clones glslang. Those sources are unpinned, so every invocation runs `git pull` on them. +Prerequisites on Linux: `clang` (with `libclang`, which the AST lint rules load at runtime), `lld`, `libc++` with `/usr/share/libc++/v1/std.cppm`, `cmake`, `git`. Network access to `forgejo.catcrafts.net` and `github.com` is needed: loading `project.cpp` fetches Crafter.Math, Crafter.Asset and Crafter.LintRules into `~/.cache/crafter.build/external/`, and the bootstrap clones glslang. Those sources are unpinned, so every invocation runs `git pull` on them. ```bash ./build.sh # bootstrap: bin/crafter-build, lib/libcrafter-build.a, share/crafter-build/ @@ -46,9 +42,11 @@ 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-/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 exported symbol that a `project.cpp` reaches on its default path also needs `./build.sh`: the old binary does not export it, and `LoadProject` fails with an undefined symbol. +- A new `CRAFTER_API` symbol that a `project.cpp` reaches on its default path also needs `./build.sh`: the old binary does not export it, and `LoadProject` fails with an undefined symbol. -`./build.sh` clones and builds glslang on first run and takes several minutes; later runs are faster. `./bin/crafter-build clean` deletes `bin/` and `build/` without loading `project.cpp`. That takes `bin/crafter-build` itself with it, and `build/flatpak`, the state of the local Flatpak build (see "CI and publishing"): about 2 GB, and the next `run flatpak` rebuilds LLVM. Set `CRAFTER_BUILD_HOME` only when running a binary from outside this tree; `CRAFTER_BUILD_MARCH` and `CRAFTER_BUILD_MTUNE` default to `native`. +`./build.sh` clones and builds glslang on first run and takes several minutes; later runs are faster. `./bin/crafter-build clean` deletes `bin/` and `build/` without loading `project.cpp`. Set `CRAFTER_BUILD_HOME` only when running a binary from outside this tree; `CRAFTER_BUILD_MARCH` and `CRAFTER_BUILD_MTUNE` default to `native`. + +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 @@ -61,8 +59,6 @@ Three things to know about this loop: ./bin/crafter-build lint # dry style gate, exit 1 on any finding ./bin/crafter-build format # apply the transform rules in place ./bin/crafter-build format --check # CI-style dry run -./bin/crafter-build run # list the scripts project.cpp defines -./bin/crafter-build run [args...] # run one; everything after the name is the script's ./bin/crafter-build clean ``` @@ -108,12 +104,13 @@ Suppress one finding with `// lint-disable-next-line ` 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`) go in the global module fragment after `module;`. +- `import std;` everywhere; standard headers are never `#include`d. C headers (`clang-c/Index.h`, `dlfcn.h`, `windows.h`) go in the global module fragment after `module;`. +- Mark every function or method that `project.cpp` or a downstream consumer calls with `CRAFTER_API` in the interface. It is the Windows DLL boundary. - Comments explain why: the failure mode, the constraint, the rejected alternative. The codebase carries long comments on purpose. Do not strip them, and write them for new non-obvious code. Commit bodies follow the same rule. -- There is no host-platform preprocessor split: the host is Linux. Target differences (mingw, wasm) branch on `cfg.target` strings at runtime, in the library as well as in `project.cpp` and tests. +- Platform splits live in `:Platform`, guarded by `CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc` and `_x86_64_w64_mingw32`. In `project.cpp` and tests, branch on `cfg.target` strings instead of preprocessor macros. - A build step fails when its command exits nonzero, never because it printed. Use `RunBuildCommand` or `RunCommandChecked`; warnings go to `Progress::Diagnostic`. - 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, uninterpreted project args and the toolchain (`Toolchain::Key()`). +- 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. ## Design rules @@ -121,8 +118,8 @@ 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 `/build` and `/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.** The Flatpak installs `*.cppm` under `/app/share/crafter-build/`. Nothing target-baked ships except the executable and the toolchain beside it, and the executable is built for a portable `-march`. -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. +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. 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. @@ -136,53 +133,25 @@ These were decided deliberately. Do not undo them without the maintainer's say-s - Subject line: `type(scope): imperative summary`, with `feat`, `fix`, `refactor`, `docs` or `test`; the scope (`lint`, `incremental`, ...) is optional. Body: what was wrong, why, and why the fix has this shape, wrapped at about 75 columns. Read `git log` for the voice. - Separate commits for the fix, its test and the README change when each stands on its own. - Update the README in the same PR when user-visible behaviour changes. -- `format`, `lint` and `test` green before pushing. CI builds the Flatpak and runs the suite inside it on PRs, publishes the `beta` branch of the Catcrafts repo on every push to `master`, and the `stable` branch plus a release on an `X.Y.Z` tag. +- `format`, `lint` and `test` green before pushing. CI runs the bootstrap and the test suite on PRs and publishes release archives on `master`. - Never commit `bin/`, `build/`, `share/crafter-build/`, `lib/*.a` or fixture build outputs (all gitignored), and never edit the vendored files in `lib/`. -- Keep `CRAFTER_BUILD_MARCH=x86-64-v2` in the manifest for the shipped binary: it has to run on any x86_64 machine. User projects still default to `native`. +- Keep CI's `CRAFTER_BUILD_MARCH=x86-64-v2` for the bootstrap: the runner's CPU cannot execute v3 code, and the v3 and v4 artifacts are produced from the v2 binary. ## Security considerations - `project.cpp` is compiled and loaded into the crafter-build process, and every `GitProject`, `#pragma crafter dependency` and `ExternalDependency` clones a repository and runs its build on the host. Treat a dependency URL as code you are choosing to run. This repo's own dependencies are sibling Catcrafts repos tracking their default branch on purpose; pin with `commit` when adding anything else, and whenever reproducibility matters. - Unpinned dependencies `git pull` on every invocation, so builds and tests need network. A missing or private repo surfaces as a clone failure before anything is built. - `--runner=cmd:` executes the named wrapper around every test binary. Wrappers come from `--runner=` or `CRAFTER_BUILD_RUNNER_*`; do not hardcode hosts, wrappers or credentials in `project.cpp` or tests. -- `crafter-build run ` executes whatever the script does, with your permissions. Only the scripts of the project crafter-build was pointed at can be run, never a dependency's; keep it that way. - The linter loads `libclang` at runtime by name. `CRAFTER_BUILD_LIBCLANG` pins an exact path and must fail loudly when wrong, never fall back to another library. -- CI force-pushes the rolling `latest` tag and publishes a prerelease on every push to `master` with the workflow's own token, and signs the Flatpak repo with the key mounted from the runner host. Do not add secrets to the workflow or echo the environment in it. +- CI force-pushes the rolling `latest` tag and publishes a prerelease on every push to `master` with the workflow's own token. Do not add secrets to the workflow or echo the environment in it. - Tests write only under `fs::temp_directory_path()` and the fixture's own `bin/` and `build/`. Keep it that way. -## CI and publishing - -`.forgejo/workflows/ci.yaml` runs on the `flatpak` runner label: a dedicated privileged forgejo-runner on the Hetzner box (`forgejo-runner-flatpak.service`, config `/etc/forgejo-runner/config-flatpak.yaml`), registered to this repository only. It is privileged because bubblewrap inside the job container needs it, and separate from the other runners so that privilege is not shared with any other repository. Its config allows four volumes, which the workflow mounts: `crafter-flatpak-system` (the Sdk install at `/var/lib/flatpak`), `crafter-flatpak-builder` (flatpak-builder state and ccache, so the LLVM and WASI modules are rebuilt only when their sources change; a cold run takes about 80 minutes on 8 cores, a warm one a fraction of that), `/var/www/flatpak`, the web root Caddy serves as `https://flatpak.catcrafts.net/`, and `/etc/crafter-flatpak`, which holds the repo signing key (`signing-key.asc`, no passphrase, and `key-id`), generated once on the host and mounted read-only into the job; nothing is stored in the forge. The live repo at `/var/www/flatpak/repo` is canonical; builds are promoted into it with `flatpak build-commit-from`, never built into it. - -The key is made once, as root on the runner host, and the step fails with a message pointing here when it is missing: - -```bash -install -d -m 700 /etc/crafter-flatpak/gnupg && export GNUPGHOME=/etc/crafter-flatpak/gnupg -gpg --batch --pinentry-mode loopback --passphrase '' --quick-generate-key 'Catcrafts Flatpak Repository ' ed25519 sign never -KEYID=$(gpg --list-secret-keys --with-colons | awk -F: '/^sec/{print $5; exit}') -gpg --batch --pinentry-mode loopback --passphrase '' --export-secret-keys --armor "$KEYID" > /etc/crafter-flatpak/signing-key.asc -printf '%s\n' "$KEYID" > /etc/crafter-flatpak/key-id && chmod 600 /etc/crafter-flatpak/signing-key.asc /etc/crafter-flatpak/key-id -``` - -To release: add a `` to `packaging/flatpak/net.catcrafts.CrafterBuild.metainfo.xml` (its description becomes the release notes), merge, and push the tag `X.Y.Z`. CI refuses a tag that does not match the newest entry. -Local builds are two scripts in `project.cpp`. Run them with the native binary: the Flatpak build of crafter-build has no `flatpak` to drive. - -```bash -./bin/crafter-build run flatpak [--jobs=] # build the Flatpak, then print its version from inside the sandbox -./bin/crafter-build run install # install that build as branch `local` and make it current -``` - -`run flatpak` needs `flatpak-builder` or the `org.flatpak.Builder` app, and the Sdk (`flatpak install flathub org.freedesktop.Sdk//26.08`). It builds the working tree, uncommitted changes included, runs the whole suite inside the sandbox and reports version `0.0.0-dev`. Its state is `build/flatpak/` (`state/`, `repo/`, `build-dir/`). The first run builds LLVM and the WASI sysroot, about 25 minutes on a 56-core machine; later runs rebuild only the crafter-build module, a few minutes, because flatpak-builder never caches a `type: dir` source. The checkout must not be under `/tmp`, which `org.flatpak.Builder` cannot see. Use it for anything toolchain-sensitive: the native loop compiles with the distro's clang and the Flatpak with its bundled LLVM, and the two can be a major version apart. - -`run install` does not build; it installs what `run flatpak` last exported. It points the user remote `crafter-build-local` at `build/flatpak/repo`, installs `net.catcrafts.CrafterBuild//local` from it next to whichever published branch is installed, and makes it the branch `flatpak run net.catcrafts.CrafterBuild` starts. `flatpak make-current --user net.catcrafts.CrafterBuild ` switches back. Before `crafter-build clean`, or before deleting the checkout, run `flatpak uninstall --user net.catcrafts.CrafterBuild//local` and `flatpak remote-delete --user crafter-build-local`; otherwise the remote is left pointing at a repo that is gone. - ## Gotchas -- A new partition is listed in four places, in import order: `PARTITIONS` in `build.sh`, `CrafterBuildModules` in `Crafter.Build-Platform.cpp`, both arrays in `project.cpp`, and the umbrella `Crafter.Build.cppm`. Then `./build.sh`. - A `#pragma crafter dependency` target is rebuilt on every load on purpose: clang crashes rather than erroring when it imports a BMI whose own imported PCM was rebuilt underneath it, and the depfile check is what catches a regenerated host cache. Do not optimise the rebuild away. - A host-tooling library such as Crafter.LintRules may `import Crafter.Build;` without declaring it as a dependency, because `LoadProject` fills the cache it resolves from. Listing Crafter.Build in its `cfg.dependencies` would clone and build all of crafter-build for every consumer. Its test does link crafter-build, so that test is wired behind `IsTopLevelProject()`: without the guard, crafter-build loading the library as a dependency would load its own `project.cpp` again through the test's `GitProject` and hit the cycle guard. - `crafter-build clean` leaves a pragma dependency's build alone; it lives with the clone in the cache. - The bootstrap binary is compiled without `CRAFTER_BUILD_HAS_ASSET`, so `cfg.assets` only works in the self-hosted binary. Changing asset extensions means updating two maps in `Crafter.Build-Clang.cpp`: `assetOutput` and the transitive copy loop. - A warning is printed only when its unit actually recompiles. A build with nothing to do is silent, which is not evidence that a warning is gone. -- A std module source only compiles against the headers of its own libc++ release, and a sysroot does not always ship one (Alpine). The Flatpak therefore carries the sources of the release next to the bundled one under `share/crafter-build/libc++-modules//`, one `libcxx--modules` manifest module each. When the bundled LLVM is bumped, keep the release it leaves behind as such a module: sysroots on the stable distro stay on it for months. The native loop has no such directory; stage one by hand under `share/crafter-build/` to test against a foreign sysroot. -- `VariantId()` names the output directory, so CI globs `bin/crafter.build-exe---*/` rather than predicting the hash. The hash includes the compiler, so after a toolchain update the previous variant's directories stay in `bin/` and `build/` until `clean`, and such a glob matches both. +- `VariantId()` names the output directory, so CI globs `bin/crafter.build-exe---*/` 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 `&&`. diff --git a/PKGBUILD b/PKGBUILD new file mode 100644 index 0000000..d7372ac --- /dev/null +++ b/PKGBUILD @@ -0,0 +1,36 @@ +#SPDX-License-Identifier: LGPL-3.0-only +#SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® + +pkgname=crafter-build +pkgver=0.1.0 +pkgrel=1 +pkgdesc='C++26 modules build system' +arch=('x86_64') +url='https://forgejo.catcrafts.net/Catcrafts/Crafter.Build' +license=('LGPL-3.0-only') +depends=('clang' 'libc++' 'lld') +optdepends=('wasi-libc: WASI/WebAssembly target support' + 'wasi-libc++: C++ standard library for WASI' + 'wasi-libc++abi: C++ ABI support for WASI') +makedepends=('cmake' 'git') +source=() +sha256sums=() +options=('!strip' '!debug' '!lto') + +build() { + cd "$startdir" + rm -rf bin build share/crafter-build + CRAFTER_BUILD_MARCH=x86-64-v3 CRAFTER_BUILD_MTUNE=generic ./build.sh +} + +package() { + cd "$startdir" + install -Dm755 bin/crafter-build "$pkgdir/usr/bin/crafter-build" + 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/" +} diff --git a/README.md b/README.md index b7e0d46..301f355 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,23 @@ # Crafter Build -A C++26 modules build system. Project descriptions are written in C++ with no JSON, no Lua, no embedded DSL. You write a `project.cpp` that constructs a `Configuration` and returns it. Crafter Build compiles, loads, and executes it. +A C++26 modules build system. Project descriptions are written in C++ with no JSON, no Lua, no embedded DSL. You write a `project.cpp` that constructs a `Configuration` and returns it; Crafter Build compiles, loads, and executes it. -## Install +## Quick start (Linux) -crafter-build ships as a Flatpak that carries its own clang, lld, libc++ and WASI sysroot, so every machine builds with the same compiler: +Bootstrap requires `clang`, `cmake`, `git`, `lld`, and `libc++`. ```bash -flatpak remote-add --if-not-exists catcrafts https://flatpak.catcrafts.net/catcrafts.flatpakrepo -flatpak install catcrafts net.catcrafts.CrafterBuild -alias crafter-build='flatpak run net.catcrafts.CrafterBuild' +./build.sh # produces bin/crafter-build +./bin/crafter-build # rebuild via project.cpp ``` -Pre-release builds of `master` are the `beta` branch (`flatpak install catcrafts net.catcrafts.CrafterBuild//beta`), and every release also attaches a single-file bundle on the Forgejo releases page (`flatpak install --user crafter-build-.flatpak`). The sandbox sees the whole host filesystem, so projects can live anywhere. Binaries built for the host link the bundled `libc++.so.1` through an rpath into the Flatpak: they run inside it (`-r`, the test runner), and are not meant to be copied out onto the host. +The bootstrap targets the host toolchain's own triple (`clang++ -print-target-triple`): `x86_64-pc-linux-gnu` on glibc distros, `x86_64-alpine-linux-musl` on Alpine/postmarketOS (`apk add clang clang-dev lld libc++-dev llvm-libunwind-dev llvm-runtimes llvm llvm-dev cmake build-base python3 git`). Override with `CRAFTER_BUILD_TARGET=`. CI ships both: `crafter-build-linux-x86_64-v{2,3,4}.tar.gz` (glibc) and `crafter-build-linux-x86_64-musl-v2.tar.gz` (musl, needs `libc++` at runtime — the launcher dlopens `project.so`, so it cannot be static). + +To build the system as a distro package on Arch: + +```bash +makepkg -si # uses CRAFTER_BUILD_MARCH=x86-64-v3 by default for portability +``` ## Writing a project.cpp @@ -29,6 +34,7 @@ extern "C" Configuration CrafterBuildProject(std::span a cfg.path = "./"; cfg.name = "myapp"; cfg.outputName = "myapp"; + cfg.target = "x86_64-pc-linux-gnu"; cfg.type = ConfigurationType::Executable; std::array ifaces = { "interfaces/Hello" }; @@ -39,7 +45,7 @@ extern "C" Configuration CrafterBuildProject(std::span a } ``` -Run `crafter-build` in that directory, outputs land at `bin/myapp----/myapp` and intermediates at `build/myapp----/`. The hash covers everything that changes what is compiled (build type, defines, flags, sysroot, the compiler itself), so two such builds never share a directory. After a compiler update, a new crafter-build release included, the first build starts a fresh directory and recompiles. `crafter-build clean` removes the ones left behind. Add `-r` to run the produced executable straight after a successful build. +Run `crafter-build` in that directory; outputs land at `bin/myapp--/myapp` and intermediates at `build/myapp--/`. Add `-r` to run the produced executable straight after a successful build. ## Dependencies @@ -60,7 +66,7 @@ glslang.includeDirs = { "" }; glslang.libs = { "SPIRV", "glslang", "OSDependent" }; ``` -Clones live at `~/.cache/crafter.build/external/-/`, keyed by `(url, branch, commit, options)` so different projects sharing the same dep with the same spec reuse the clone and cmake build, while projects with diverging specs each get their own cache entry. The clone is resilient to partial-clone recovery and CMake reconfigure is triggered automatically on `options` changes. Submodules are checked out at the revisions the fetched commit records, after every clone, switch, checkout and pull, so a pinned commit builds against its own submodules and not the default branch's. +Clones live at `~/.cache/crafter.build/external/-/`, keyed by `(url, branch, commit, options)` so different projects sharing the same dep with the same spec reuse the clone and cmake build, while projects with diverging specs each get their own cache entry. The clone is resilient to partial-clone recovery and CMake reconfigure is triggered automatically on `options` changes. **Header-only git deps** — same as cmake deps but `builder = ExternalBuilder::None`. Just clones and propagates `-I` flags. @@ -75,7 +81,7 @@ Configuration* sibling = LocalProject({ .projectFile = "../Crafter.Math/project. cfg.dependencies = { math }; ``` -Clones share the external cache and identical specs share one `Configuration`. An unpinned source is `git pull`ed on every invocation, set `.commit` for reproducible, offline-after-first-fetch builds. +Clones share the external cache and identical specs share one `Configuration`. An unpinned source is `git pull`ed on every invocation; set `.commit` for reproducible, offline-after-first-fetch builds. A project loaded this way runs its `CrafterBuildProject` as a dependency, and `IsTopLevelProject()` is `false` while it does (`true` in the project crafter-build was pointed at, and outside any project file). Wire test-only dependencies behind it: a library whose test links crafter-build itself would otherwise load crafter-build's `project.cpp` from inside crafter-build and recurse. @@ -94,6 +100,15 @@ import Crafter.LintRules; #pragma crafter dependency "" [branch "" | commit ""] [project ""] ``` +## Install layout + +``` +/bin/crafter-build # the executable +/lib/libcrafter-build.a # static archive for downstream consumers +/share/crafter-build/*.cppm # module sources (rebuilt on user machine on first run) +~/.cache/crafter.build/-/ # std.pcm + Crafter.Build*.pcm, built locally +``` + ## Tests Tests live under `tests//`. The simplest case is a single C++ file: @@ -112,7 +127,7 @@ Test conventions: - **Exit codes**: `0` = ✅ pass, nonzero = ❌ fail, **`77`** = ⏭ skipped (autoconf convention; use it for runtime opt-outs like "tool not on PATH"). - Each test runs in its own subprocess — a segfault doesn't take the runner down. -- Default timeout is 60 s per test, runs in parallel up to `hardware_concurrency()`. +- Default timeout is 60 s per test; runs in parallel up to `hardware_concurrency()`. - Stdout+stderr is captured and printed atomically on completion. Failed/crashed/timed-out tests also write to `build/test-logs/.log`. - A broken fixture `project.cpp` is reported as a single failed test, not a fatal error — other tests still run. @@ -130,7 +145,7 @@ crafter-build test --timeout=5 # override per-t Outcomes: ✅ pass, ❌ fail, 💥 crash (with signal name), ⏱ timeout, ⏭ skipped. The runner exits 0 only if every non-skipped test passed. -`--target=` filters the run to tests whose `cfg.target` matches, default is the host triple. Per-target tests parse `--target=...` from their own args span and build for it, tests with hardcoded targets (e.g. host-only meta-tests) are filtered out under non-matching `--target=`. +`--target=` filters the run to tests whose `cfg.target` matches; default is the host triple. Per-target tests parse `--target=...` from their own args span and build for it; tests with hardcoded targets (e.g. host-only meta-tests) are filtered out under non-matching `--target=`. ### Cross-target test runners @@ -139,15 +154,18 @@ 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 ` ` prefix runner — covers wine, qemu-user, valgrind, gdbserver, anything with that shape. Probes via `which `. +`Cmd(cmd)` is a generic ` ` prefix runner — covers wine, qemu-user, valgrind, gdbserver, anything with that shape. Probes via `which ` (or `where` on Windows hosts). The `Ssh` variants `scp -r` the test's whole bin dir, run the binary remotely, then clean up; each invocation uses a unique remote subdir so parallel runs don't collide. `Wsl()` mirrors the SSH model but copies the bundle into WSL's native filesystem (faster than executing in-place from `/mnt/c`) — useful when a Windows host wants to exercise Linux test binaries. For elaborate setups (rsync, ssh ControlMaster, env vars on the spawn, `QEMU_LD_PREFIX` for sysroots), construct `TestRunner` directly — `copy`/`exec`/`cleanup`/`probe` are public string fields with `{bundle}`, `{remote_bundle}`, `{bin}`, `{bin_name}`, `{args}` placeholders. -For 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=` is passed to clang, and the std module is precompiled against the sysroot's libc++ headers. Ordinary compiles are pointed at the same headers (`-nostdinc++ -isystem /usr/include/c++/v1`), so a unit that `#include`s a standard header gets the sysroot's libc++ and not the host toolchain's. The module source has to come from the same libc++ release as those headers, so it is taken from `/usr/share/libc++/v1/std.cppm` when the sysroot ships it, else from the toolchain when both are the same major release, else from the sources crafter-build carries for other releases (`share/crafter-build/libc++-modules//`, `crafter-build --version` lists them). Alpine is the common case for the last two: its `libc++-dev` has the headers and no module source. When nothing matches, the build stops with one line naming both releases, installing that release's `share/libc++/v1` directory into the sysroot resolves it. The cheapest aarch64 sysroot is an extracted [Arch Linux ARM rootfs](http://os.archlinuxarm.org/) plus `libc++` + `libc++abi` + `gcc` installed into it via `pacman --root --config --dbpath /var/lib/pacman -Sy …` (point pacman at ALARM's mirrors via explicit config to avoid pulling host-arch packages). +For cross-arch builds, set `cfg.sysroot` on the `Configuration` to point at a sysroot for the target. `BuildStdPcm` and the compile/link commands both consume it: `--sysroot=` is passed to clang, and `/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 --config --dbpath /var/lib/pacman -Sy …` (point pacman at ALARM's mirrors via explicit config to avoid pulling host-arch packages). See `tests/CrossArchAarch64/inner/` for a working sysroot+qemu example. -For `--target=x86_64-w64-mingw32` (Windows from Linux), the pipeline cross-compiles as long as `mingw-w64` is installed, with libstdc++ 15 or newer for its std module. clang finds it by the `x86_64-w64-mingw32-gcc` on `PATH`; `CRAFTER_MINGW_DIR` names another prefix (the directory holding `x86_64-w64-mingw32/` and `lib/gcc/`), and inside the Flatpak the host's `/usr` is used when it has mingw-w64. Without one the build stops with a message saying where it looked. Pair it with `--runner=cmd:wine` to run the result. Only the MinGW ABI is supported, there is no MSVC target and no Windows host. `-fuse-ld=lld` is auto-injected on every link, and `-lstdc++exp` is auto-linked for the mingw target so user projects don't need to declare those flags. +For `--target=x86_64-w64-mingw32` (Windows from Linux), V2's pipeline cross-compiles as long as `mingw-w64` is installed. Pair it with `--runner=cmd:wine` (no VM needed) or `--runner=sshwin:winhost` (real Windows). Full MSVC ABI (`x86_64-pc-windows-msvc`) cross-compile from Linux is not yet wired; use MinGW for now. `-fuse-ld=lld` is auto-injected on every link, and `-lstdc++exp` is auto-linked for the mingw target so user projects don't need to declare those flags. ### Configuring runners @@ -174,9 +192,12 @@ Spec grammar: ``` local cmd: # cmd:wine, cmd:qemu-aarch64, cmd:valgrind, ... +ssh:[:] +sshwin:[:] # cmd.exe-shell variant for Windows remotes +wsl[:] # Windows host -> Linux binaries via WSL2 ``` -Each runner carries a `probe` command (e.g. `which wine`), if it fails, every test using that runner is reported as ⏭ skipped and the rest of the suite continues. Skipped tests count toward the summary but don't fail the suite (`AllPassed()` returns true on skip-only runs). +Each runner carries a `probe` command (e.g. `which wine`, `ssh -BatchMode=yes true`); if it fails, every test using that runner is reported as ⏭ skipped and the rest of the suite continues. Skipped tests count toward the summary but don't fail the suite (`AllPassed()` returns true on skip-only runs). ### Linking the parent project's library @@ -189,6 +210,7 @@ extern "C" Configuration CrafterBuildProject(std::span) cfg.path = "tests/UnitFoo/"; cfg.name = "UnitFoo"; cfg.outputName = "UnitFoo"; + cfg.target = "x86_64-pc-linux-gnu"; cfg.type = ConfigurationType::Executable; cfg.dependencies = { ParentLib("MyLib") }; // <-- the magic line @@ -203,7 +225,7 @@ extern "C" Configuration CrafterBuildProject(std::span) ## Linting & formatting -Lint rules are C++ in `project.cpp` with no `.lintrc`, no YAML, and no built-in rules. A rule is a named callback that runs once per source file, register it on the `Configuration` before returning: +Lint rules are C++ in `project.cpp` with no `.lintrc`, no YAML, and no built-in rules. A rule is a named callback that runs once per source file; register it on the `Configuration` before returning: ```cpp cfg.AddLintRule("no-tabs", [](Crafter::LintContext& ctx) { @@ -244,7 +266,7 @@ Lint findings print compiler-style — `file:line: warning: message [rule]` — | `Extension()` | file extension, e.g. `".cppm"` | | `CommentStripped()` | `content` with comments and string/char literal bodies blanked to spaces — newlines kept, so line numbers stay valid (raw strings not recognized yet) | | `Report(line, msg)` | record a finding (`line == 0` for whole-file) | -| `SetContent(str)` | replace the file's content — makes the rule a transform, `lines` re-splits, `CommentStripped()` re-derives. Build the new string, call once at the end, don't assign `content` directly | +| `SetContent(str)` | replace the file's content — makes the rule a transform; `lines` re-splits, `CommentStripped()` re-derives. Build the new string, call once at the end; don't assign `content` directly | Suppression is comment-based : @@ -256,50 +278,6 @@ Suppression is comment-based : // lint-disable-file — all rules, whole file ``` -## Scripts - -A project has chores that are not part of building it: packaging, installing, publishing, generating documentation. A script gives one a name. Scripts are C++ in `project.cpp` like everything else, a name and a callback pushed onto `cfg.scripts` before returning the `Configuration`: - -```cpp -cfg.scripts.push_back({"docs", [](Crafter::ScriptContext& ctx) { - return ctx.Run("python3 tools/gen-docs.py"); -}}); -``` - -```bash -crafter-build run # list the project's scripts -crafter-build run docs # run one -crafter-build run docs --open api # everything after the name is handed to the script -``` - -The callback returns the exit status, and `crafter-build run` exits with it. `ctx.Run(command)` runs a shell command with the terminal attached, so its output appears as it is produced and it can ask questions, and returns the command's exit code (128 plus the signal number when it was killed). A script with several steps stops at the first failure by returning that code: - -```cpp -cfg.scripts.push_back({"package", [](Crafter::ScriptContext& ctx) { - const std::string dist = Crafter::ShellQuote((ctx.projectDir / "dist").string()); - if (const std::int32_t code = ctx.Run(std::format("mkdir -p {}", dist)); code != 0) return code; - return ctx.Run(std::format("tar -czf {}/myapp.tar.gz -C {} .", dist, Crafter::ShellQuote(ctx.project.BinDir().string()))); -}}); -``` - -`ScriptContext` gives each script: - -| Member | Meaning | -|---|---| -| `project` | the `Configuration` that `project.cpp` returned. Read the project through this; a `cfg` captured by reference is gone by the time a script runs | -| `projectDir` | absolute directory of the project file | -| `args` | everything typed after the script's name, untouched | -| `Run(command)` | run a shell command with the terminal attached and return its exit code | - -Things to know: - -- Everything after the script's name belongs to the script, including what looks like an option of crafter-build (`--help`, `-v`). Options and project args for crafter-build go before the name: `crafter-build --debug run package`. -- `run` loads `project.cpp` but does not build the project. A script that needs the build output runs after `crafter-build`. -- Commands run in the directory crafter-build was started from. Build paths from `ctx.projectDir`, and quote anything that is not a literal with `Crafter::ShellQuote`. -- Only the scripts of the project you run crafter-build in are available. A dependency's scripts are not. -- Inside the Flatpak, commands run in its sandbox: the host's files are visible, but the tools on `PATH` are the ones the Flatpak ships (`git`, `cmake`, `python3`, `tar`, ...), not everything installed on the host. -- A return value outside 0 to 255 is not an exit status and becomes 1. So does a script that throws, with the exception's message printed. - ## Examples [examples/](examples/) contains progressively larger self-contained projects: @@ -319,7 +297,7 @@ This project is licensed under the GNU Lesser General Public License, version 3 The example code under [examples/](examples/) is licensed under the MIT license (see [examples/LICENSE](examples/LICENSE)), so it can be freely copied into your own projects. -The [lib/](lib/) directory contains vendored third-party code (nlohmann/json, glslang's DirStackFileIncluder) that remains under its own permissive licenses, see the headers of those files. +The [lib/](lib/) directory contains vendored third-party code (nlohmann/json, glslang's DirStackFileIncluder) that remains under its own permissive licenses; see the headers of those files. ## Copyright diff --git a/build.cmd b/build.cmd new file mode 100644 index 0000000..a2f2fc5 --- /dev/null +++ b/build.cmd @@ -0,0 +1,137 @@ +@REM SPDX-License-Identifier: LGPL-3.0-only +@REM SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® +@echo OFF +mkdir build 2>nul +mkdir bin 2>nul +mkdir lib 2>nul +mkdir share\crafter-build 2>nul +mkdir share\crafter-build\wasi-runtime 2>nul + +copy /Y wasi-runtime\runtime.js share\crafter-build\wasi-runtime\ +copy /Y wasi-runtime\index.html.in share\crafter-build\wasi-runtime\ + +copy /Y interfaces\Crafter.Build.cppm share\crafter-build\ +copy /Y interfaces\Crafter.Build-Shader.cppm share\crafter-build\ +copy /Y interfaces\Crafter.Build-Platform.cppm share\crafter-build\ +copy /Y interfaces\Crafter.Build-Interface.cppm share\crafter-build\ +copy /Y interfaces\Crafter.Build-Implementation.cppm share\crafter-build\ +copy /Y interfaces\Crafter.Build-External.cppm share\crafter-build\ +copy /Y interfaces\Crafter.Build-Clang.cppm share\crafter-build\ +copy /Y interfaces\Crafter.Build-Test.cppm share\crafter-build\ +copy /Y interfaces\Crafter.Build-Lint.cppm share\crafter-build\ +copy /Y interfaces\Crafter.Build-Progress.cppm share\crafter-build\ +copy /Y interfaces\Crafter.Build-Asset.cppm share\crafter-build\ +copy /Y interfaces\Crafter.Build-Api.h share\crafter-build\ + +if not exist .\build\glslang\NUL git clone https://github.com/KhronosGroup/glslang.git .\build\glslang + +set useLibcCommon=-nostdinc++ -nostdlib++ +set useLibcLinker=-L %LIBCXX_DIR%\lib -lc++ %useLibcCommon% +set useLibcSource=-isystem %LIBCXX_DIR%\include\c++\v1 %useLibcCommon% +set buildDir=%CD%\build + +if "%CRAFTER_BUILD_MARCH%"=="" set CRAFTER_BUILD_MARCH=native +if "%CRAFTER_BUILD_MTUNE%"=="" set CRAFTER_BUILD_MTUNE=native + +cd .\build\glslang +cmake -G Ninja -B build ^ + -DCMAKE_C_COMPILER=clang ^ + -DCMAKE_CXX_COMPILER=clang++ ^ + -DCMAKE_CXX_FLAGS="%useLibcSource%" ^ + -DCMAKE_EXE_LINKER_FLAGS="%useLibcLinker%" ^ + -DCMAKE_SHARED_LINKER_FLAGS="%useLibcLinker%" ^ + -DCMAKE_BUILD_TYPE=Release ^ + -DCMAKE_ARCHIVE_OUTPUT_DIRECTORY="%buildDir%" ^ + -DCMAKE_LIBRARY_OUTPUT_DIRECTORY="%buildDir%" ^ + -DENABLE_OPT=OFF +cmake --build build --config Release +cd ..\..\ + +set common_options=-I.\build\glslang -std=c++26 -O3 -march=%CRAFTER_BUILD_MARCH% -mtune=%CRAFTER_BUILD_MTUNE% -fprebuilt-module-path=.\build -D CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc -D CRAFTER_BUILD_CONFIGURATION_TYPE_EXECUTABLE -D _CRT_SECURE_NO_WARNINGS %useLibcSource% -c + +clang++ %useLibcSource% -std=c++26 -march=%CRAFTER_BUILD_MARCH% -mtune=%CRAFTER_BUILD_MTUNE% -O3 -Wno-reserved-identifier -Wno-reserved-module-identifier --precompile %LIBCXX_DIR%\modules\c++\v1\std.cppm -o .\build\std.pcm + +clang++ %common_options% -fmodule-output interfaces\Crafter.Build-Shader.cppm -o .\build\Crafter.Build-Shader.o +clang++ %common_options% -fmodule-output interfaces\Crafter.Build-Platform.cppm -o .\build\Crafter.Build-Platform.o +clang++ %common_options% -fmodule-output interfaces\Crafter.Build-Interface.cppm -o .\build\Crafter.Build-Interface.o +clang++ %common_options% -fmodule-output interfaces\Crafter.Build-Implementation.cppm -o .\build\Crafter.Build-Implementation.o +clang++ %common_options% -fmodule-output interfaces\Crafter.Build-External.cppm -o .\build\Crafter.Build-External.o +clang++ %common_options% -fmodule-output interfaces\Crafter.Build-Clang.cppm -o .\build\Crafter.Build-Clang.o +clang++ %common_options% -fmodule-output interfaces\Crafter.Build-Test.cppm -o .\build\Crafter.Build-Test.o +clang++ %common_options% -fmodule-output interfaces\Crafter.Build-Lint.cppm -o .\build\Crafter.Build-Lint.o +clang++ %common_options% -fmodule-output interfaces\Crafter.Build-Progress.cppm -o .\build\Crafter.Build-Progress.o +REM Asset partition: bootstrap compiles it WITHOUT CRAFTER_BUILD_HAS_ASSET so +REM CompressAsset takes the stub branch (no Crafter.Asset PCM exists yet). +clang++ %common_options% -fmodule-output interfaces\Crafter.Build-Asset.cppm -o .\build\Crafter.Build-Asset.o +clang++ %common_options% -fmodule-output interfaces\Crafter.Build.cppm -o .\build\Crafter.Build.o + +clang++ %common_options% .\implementations\Crafter.Build-Shader.cpp -o .\build\Crafter.Build-Shader_impl.o +clang++ %common_options% .\implementations\Crafter.Build-Platform.cpp -o .\build\Crafter.Build-Platform_impl.o +clang++ %common_options% .\implementations\Crafter.Build-Interface.cpp -o .\build\Crafter.Build-Interface_impl.o +clang++ %common_options% .\implementations\Crafter.Build-Implementation.cpp -o .\build\Crafter.Build-Implementation_impl.o +clang++ %common_options% .\implementations\Crafter.Build-External.cpp -o .\build\Crafter.Build-External_impl.o +clang++ %common_options% .\implementations\Crafter.Build-Clang.cpp -o .\build\Crafter.Build-Clang_impl.o +clang++ %common_options% .\implementations\Crafter.Build-Test.cpp -o .\build\Crafter.Build-Test_impl.o +clang++ %common_options% .\implementations\Crafter.Build-Lint.cpp -o .\build\Crafter.Build-Lint_impl.o +clang++ %common_options% .\implementations\Crafter.Build-Progress.cpp -o .\build\Crafter.Build-Progress_impl.o +clang++ %common_options% .\implementations\Crafter.Build-Asset.cpp -o .\build\Crafter.Build-Asset_impl.o +clang++ %common_options% .\implementations\main.cpp -o .\build\main.o + +REM Step 1: link all impl .o files into crafter-build.dll, generating crafter-build.lib import lib +clang++ %useLibcLinker% -shared -std=c++26 -O3 -march=%CRAFTER_BUILD_MARCH% -mtune=%CRAFTER_BUILD_MTUNE% -L.\build -fuse-ld=lld ^ + -Wl,/IMPLIB:.\bin\crafter-build.lib ^ + -lSPIRV -lGenericCodeGen -lglslang -lOSDependent -lMachineIndependent -lglslang-default-resource-limits ^ + .\build\Crafter.Build-Shader.o ^ + .\build\Crafter.Build-Platform.o ^ + .\build\Crafter.Build-Interface.o ^ + .\build\Crafter.Build-Implementation.o ^ + .\build\Crafter.Build-External.o ^ + .\build\Crafter.Build-Clang.o ^ + .\build\Crafter.Build-Test.o ^ + .\build\Crafter.Build-Lint.o ^ + .\build\Crafter.Build-Progress.o ^ + .\build\Crafter.Build-Asset.o ^ + .\build\Crafter.Build.o ^ + .\build\Crafter.Build-Shader_impl.o ^ + .\build\Crafter.Build-Platform_impl.o ^ + .\build\Crafter.Build-Interface_impl.o ^ + .\build\Crafter.Build-Implementation_impl.o ^ + .\build\Crafter.Build-External_impl.o ^ + .\build\Crafter.Build-Clang_impl.o ^ + .\build\Crafter.Build-Test_impl.o ^ + .\build\Crafter.Build-Lint_impl.o ^ + .\build\Crafter.Build-Progress_impl.o ^ + .\build\Crafter.Build-Asset_impl.o ^ + -o .\bin\crafter-build.dll + +REM Step 2: link the launcher exe against crafter-build.lib +clang++ %useLibcLinker% -std=c++26 -O3 -march=%CRAFTER_BUILD_MARCH% -mtune=%CRAFTER_BUILD_MTUNE% -L.\bin -fuse-ld=lld ^ + .\build\main.o ^ + .\bin\crafter-build.lib ^ + -o .\bin\crafter-build.exe + +REM Step 3: bundle the same .o set as a static archive for downstream consumers +llvm-lib.exe /OUT:.\lib\crafter-build-static.lib ^ + .\build\Crafter.Build-Shader.o ^ + .\build\Crafter.Build-Platform.o ^ + .\build\Crafter.Build-Interface.o ^ + .\build\Crafter.Build-Implementation.o ^ + .\build\Crafter.Build-External.o ^ + .\build\Crafter.Build-Clang.o ^ + .\build\Crafter.Build-Test.o ^ + .\build\Crafter.Build-Lint.o ^ + .\build\Crafter.Build-Progress.o ^ + .\build\Crafter.Build-Asset.o ^ + .\build\Crafter.Build.o ^ + .\build\Crafter.Build-Shader_impl.o ^ + .\build\Crafter.Build-Platform_impl.o ^ + .\build\Crafter.Build-Interface_impl.o ^ + .\build\Crafter.Build-Implementation_impl.o ^ + .\build\Crafter.Build-External_impl.o ^ + .\build\Crafter.Build-Clang_impl.o ^ + .\build\Crafter.Build-Test_impl.o ^ + .\build\Crafter.Build-Lint_impl.o ^ + .\build\Crafter.Build-Progress_impl.o ^ + .\build\Crafter.Build-Asset_impl.o + +copy /Y "%LIBCXX_DIR%\lib\c++.dll" ".\bin\c++.dll" diff --git a/build.sh b/build.sh index 312abde..5ff6fb8 100755 --- a/build.sh +++ b/build.sh @@ -1,127 +1,143 @@ -#!/bin/sh #SPDX-License-Identifier: LGPL-3.0-only #SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® -# Bootstrap: compile crafter-build by hand, without crafter-build. The -# self-host build through project.cpp follows (`./bin/crafter-build`). -set -eu - -# The toolchain. CRAFTER_BUILD_LLVM_DIR names an LLVM prefix (the Flatpak -# sets /app); otherwise the tools come from PATH. The same rule the built -# binary applies at run time (Crafter.Build:Toolchain). -if [ -n "${CRAFTER_BUILD_LLVM_DIR:-}" ]; then - CXX="$CRAFTER_BUILD_LLVM_DIR/bin/clang++" - CC="$CRAFTER_BUILD_LLVM_DIR/bin/clang" - AR="$CRAFTER_BUILD_LLVM_DIR/bin/llvm-ar" - LLVM_CONFIG="$CRAFTER_BUILD_LLVM_DIR/bin/llvm-config" - # A prefix other than the system one keeps its libc++ off both the - # linker's search path (clang looks in lib/ and the system dirs, - # not /lib) and the loader's; -L and an rpath cover the two. - if [ "$CRAFTER_BUILD_LLVM_DIR" != /usr ]; then - LIBCXX_LINK_FLAGS="-L$CRAFTER_BUILD_LLVM_DIR/lib -Wl,-rpath,$CRAFTER_BUILD_LLVM_DIR/lib" - else - LIBCXX_LINK_FLAGS="" - fi - if [ -f "$CRAFTER_BUILD_LLVM_DIR/share/libc++/v1/std.cppm" ]; then - STD_CPPM="$CRAFTER_BUILD_LLVM_DIR/share/libc++/v1/std.cppm" - else - STD_CPPM=/usr/share/libc++/v1/std.cppm - fi -else - CXX=clang++ - CC=clang - AR=llvm-ar - LLVM_CONFIG=llvm-config - LIBCXX_LINK_FLAGS="" - STD_CPPM=/usr/share/libc++/v1/std.cppm -fi - -# The version baked into the binary: the environment (CI sets it from the -# tag), else the nearest tag, else a development marker. `latest` is the -# rolling CI tag and never a version. -VERSION="${CRAFTER_BUILD_VERSION:-$(git describe --tags --always --dirty --exclude=latest 2>/dev/null || echo 0.0.0-dev)}" - -mkdir -p build bin lib share/crafter-build share/crafter-build/wasi-runtime +mkdir -p build +mkdir -p bin +mkdir -p lib +mkdir -p share/crafter-build +mkdir -p share/crafter-build/wasi-runtime cp wasi-runtime/runtime.js share/crafter-build/wasi-runtime/ cp wasi-runtime/index.html.in share/crafter-build/wasi-runtime/ -# Partition order is the import order: every unit is listed before the -# units that import it, and the umbrella last. -PARTITIONS="Toolchain Shader Platform Interface Implementation External Clang Test Lint Script Progress Asset" - cp interfaces/Crafter.Build.cppm share/crafter-build/ -for p in $PARTITIONS; do - cp "interfaces/Crafter.Build-$p.cppm" share/crafter-build/ -done +cp interfaces/Crafter.Build-Shader.cppm share/crafter-build/ +cp interfaces/Crafter.Build-Platform.cppm share/crafter-build/ +cp interfaces/Crafter.Build-Interface.cppm share/crafter-build/ +cp interfaces/Crafter.Build-Implementation.cppm share/crafter-build/ +cp interfaces/Crafter.Build-External.cppm share/crafter-build/ +cp interfaces/Crafter.Build-Clang.cppm share/crafter-build/ +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/ -if [ ! -d build/glslang/.git ]; then - git clone --depth 1 https://github.com/KhronosGroup/glslang.git build/glslang -fi +git clone https://github.com/KhronosGroup/glslang.git ./build/glslang current_dir=$(pwd) -cd build/glslang +cd ./build/glslang cmake -B build \ - -DCMAKE_C_COMPILER="$CC" \ - -DCMAKE_CXX_COMPILER="$CXX" \ + -DCMAKE_C_COMPILER=clang \ + -DCMAKE_CXX_COMPILER=clang++ \ -DCMAKE_CXX_FLAGS="-stdlib=libc++" \ - -DCMAKE_EXE_LINKER_FLAGS="-stdlib=libc++ $LIBCXX_LINK_FLAGS" \ - -DCMAKE_SHARED_LINKER_FLAGS="-stdlib=libc++ $LIBCXX_LINK_FLAGS" \ + -DCMAKE_EXE_LINKER_FLAGS="-stdlib=libc++" \ + -DCMAKE_SHARED_LINKER_FLAGS="-stdlib=libc++" \ -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_ARCHIVE_OUTPUT_DIRECTORY="$current_dir/build" \ - -DCMAKE_LIBRARY_OUTPUT_DIRECTORY="$current_dir/build" \ + -DCMAKE_ARCHIVE_OUTPUT_DIRECTORY=$current_dir/build \ + -DCMAKE_LIBRARY_OUTPUT_DIRECTORY=$current_dir/build \ -DENABLE_OPT=OFF + cmake --build build --config Release cd ../../ MARCH="${CRAFTER_BUILD_MARCH:-native}" MTUNE="${CRAFTER_BUILD_MTUNE:-native}" -# The bootstrap targets the toolchain's own triple: x86_64-pc-linux-gnu on -# glibc distros, x86_64-unknown-linux-gnu for the bundled LLVM, -# x86_64-alpine-linux-musl on Alpine. Override with CRAFTER_BUILD_TARGET. -TARGET="${CRAFTER_BUILD_TARGET:-$("$CXX" -print-target-triple)}" +# The bootstrap targets the host toolchain's own triple: x86_64-pc-linux-gnu +# on glibc distros, x86_64-alpine-linux-musl on Alpine. Override with +# CRAFTER_BUILD_TARGET. The per-target define (triple with '-' -> '_') is the +# same one the self-hosted build emits, so the host-gated code paths match. +TARGET="${CRAFTER_BUILD_TARGET:-$(clang++ -print-target-triple)}" TARGET_DEFINE="CRAFTER_BUILD_CONFIGURATION_TARGET_$(printf '%s' "$TARGET" | tr - _)" # Crafter.Build-Lint includes libclang's clang-c/Index.h. glibc distros keep -# it in /usr/include; other prefixes keep it under their own include dir, so -# ask llvm-config and add the dir when it is not already on the default path +# it in /usr/include; Alpine keeps LLVM under /usr/lib/llvmNN, so ask +# llvm-config and add the dir when it is not already on the default path # (never add /usr/include itself: that breaks libc++'s #include_next). -LLVM_INCDIR="$("$LLVM_CONFIG" --includedir 2>/dev/null || true)" +LLVM_INCDIR="$(llvm-config --includedir 2>/dev/null || true)" case "$LLVM_INCDIR" in ""|/usr/include) LLVM_INCFLAG="" ;; *) LLVM_INCFLAG="-I$LLVM_INCDIR" ;; esac -common_options="--target=$TARGET -stdlib=libc++ -I./build/glslang $LLVM_INCFLAG -std=c++26 -O3 -march=$MARCH -mtune=$MTUNE -fprebuilt-module-path=./build -D $TARGET_DEFINE -D CRAFTER_BUILD_CONFIGURATION_TYPE_EXECUTABLE -D CRAFTER_BUILD_VERSION=\"$VERSION\" -c" +common_options="--target=$TARGET -stdlib=libc++ -I./build/glslang $LLVM_INCFLAG -std=c++26 -O3 -march=$MARCH -mtune=$MTUNE -fprebuilt-module-path=./build -D $TARGET_DEFINE -D CRAFTER_BUILD_CONFIGURATION_TYPE_EXECUTABLE -c" -"$CXX" --target="$TARGET" -std=c++26 -stdlib=libc++ -O3 -march="$MARCH" -mtune="$MTUNE" -Wno-reserved-identifier -Wno-reserved-module-identifier --precompile "$STD_CPPM" -o ./build/std.pcm +clang++ --target=$TARGET -std=c++26 -stdlib=libc++ -O3 -march=$MARCH -mtune=$MTUNE -Wno-reserved-identifier -Wno-reserved-module-identifier --precompile /usr/share/libc++/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 # Asset partition: bootstrap compiles it WITHOUT CRAFTER_BUILD_HAS_ASSET so # CompressAsset takes the stub branch and the impl unit doesn't try to # `import Crafter.Asset` (no Crafter.Asset PCM exists at this stage — the # self-host pass that follows wires in the real dep). -OBJECTS="" -for p in $PARTITIONS; do - "$CXX" $common_options -fmodule-output "interfaces/Crafter.Build-$p.cppm" -o "./build/Crafter.Build-$p.o" - OBJECTS="$OBJECTS ./build/Crafter.Build-$p.o" -done -"$CXX" $common_options -fmodule-output interfaces/Crafter.Build.cppm -o ./build/Crafter.Build.o -OBJECTS="$OBJECTS ./build/Crafter.Build.o" +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 -for p in $PARTITIONS; do - "$CXX" $common_options "./implementations/Crafter.Build-$p.cpp" -o "./build/Crafter.Build-${p}_impl.o" - OBJECTS="$OBJECTS ./build/Crafter.Build-${p}_impl.o" -done -"$CXX" $common_options ./implementations/main.cpp -o ./build/main.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 -# A fresh archive each time: `ar rcs` into an existing one keeps members a -# previous layout left behind. -rm -f ./lib/libcrafter-build.a -"$AR" rcs ./lib/libcrafter-build.a $OBJECTS +ar rcs ./lib/libcrafter-build.a \ + ./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 -"$CXX" --target="$TARGET" -std=c++26 -stdlib=libc++ -O3 -march="$MARCH" -mtune="$MTUNE" -fuse-ld=lld \ - -Wl,--export-dynamic $LIBCXX_LINK_FLAGS \ +clang++ --target=$TARGET -std=c++26 -stdlib=libc++ -O3 -march=$MARCH -mtune=$MTUNE -fuse-ld=lld \ + -Wl,--export-dynamic \ -L./build \ - $OBJECTS \ + ./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 \ ./build/main.o \ -lSPIRV -lGenericCodeGen -lglslang -lOSDependent -lMachineIndependent -lglslang-default-resource-limits \ -ldl \ - -o ./bin/crafter-build + -o ./bin/crafter-build \ No newline at end of file diff --git a/examples/hello-world/project.cpp b/examples/hello-world/project.cpp index b2dfa0a..d9dfc51 100644 --- a/examples/hello-world/project.cpp +++ b/examples/hello-world/project.cpp @@ -11,6 +11,7 @@ extern "C" Configuration CrafterBuildProject(std::span) cfg.path = "./"; cfg.name = "hello"; cfg.outputName = "hello"; + cfg.target = "x86_64-pc-linux-gnu"; cfg.type = ConfigurationType::Executable; std::array ifaces = {}; diff --git a/examples/library/project.cpp b/examples/library/project.cpp index f7159e7..24f7a61 100644 --- a/examples/library/project.cpp +++ b/examples/library/project.cpp @@ -15,6 +15,7 @@ extern "C" Configuration CrafterBuildProject(std::span) lib->path = "./mylib/"; lib->name = "MyMath"; lib->outputName = "MyMath"; + lib->target = "x86_64-pc-linux-gnu"; lib->type = ConfigurationType::LibraryStatic; { std::array ifaces = { "MyMath" }; @@ -28,6 +29,7 @@ extern "C" Configuration CrafterBuildProject(std::span) cfg.path = "./"; cfg.name = "math-app"; cfg.outputName = "math-app"; + cfg.target = "x86_64-pc-linux-gnu"; cfg.type = ConfigurationType::Executable; cfg.dependencies = { lib.get() }; diff --git a/examples/tests/project.cpp b/examples/tests/project.cpp index 85f8468..9a026cb 100644 --- a/examples/tests/project.cpp +++ b/examples/tests/project.cpp @@ -14,6 +14,7 @@ extern "C" Configuration CrafterBuildProject(std::span) cfg.path = "./mylib/"; cfg.name = "MyMath"; cfg.outputName = "MyMath"; + cfg.target = "x86_64-pc-linux-gnu"; cfg.type = ConfigurationType::LibraryStatic; std::array ifaces = { "MyMath" }; diff --git a/examples/with-module/project.cpp b/examples/with-module/project.cpp index c7b68cb..f885fba 100644 --- a/examples/with-module/project.cpp +++ b/examples/with-module/project.cpp @@ -11,6 +11,7 @@ extern "C" Configuration CrafterBuildProject(std::span) cfg.path = "./"; cfg.name = "greeter-app"; cfg.outputName = "greeter-app"; + cfg.target = "x86_64-pc-linux-gnu"; cfg.type = ConfigurationType::Executable; std::array ifaces = { "interfaces/Greeter" }; diff --git a/implementations/Crafter.Build-Clang.cpp b/implementations/Crafter.Build-Clang.cpp index 416856e..8e34387 100644 --- a/implementations/Crafter.Build-Clang.cpp +++ b/implementations/Crafter.Build-Clang.cpp @@ -2,17 +2,23 @@ // 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 +#include +// (pulled in transitively) defines `interface` as a macro for `struct`, +// which collides with local variables named `interface` in this TU. +#undef interface +#else #include #include #include +#endif export module Crafter.Build:Clang_impl; import std; import :Clang; import :Platform; -import :Toolchain; import :Test; import :Lint; -import :Script; import :Progress; import :Asset; namespace fs = std::filesystem; @@ -247,14 +253,11 @@ void Configuration::GetInterfacesAndImplementations(std::span interfac } fs::path Crafter::StdPcmDir(const Configuration& config) { - // The std PCM is cached per target+march+toolchain: a PCM is only valid - // for the exact clang that wrote it, so a compiler bump starts a cold - // directory instead of handing the new clang a PCM it will crash on. A - // wasm variant pass compiles it with extra codegen flags (e.g. - // -mrelaxed-simd) — its BMI must not be shared with the baseline's, or - // the consuming TUs see a target-feature mismatch. Suffix the cache dir - // with the variant flags when present. - std::string stdPcmKey = std::format("{}-{}-{}", config.target, config.march, GetToolchain().Key()); + // The std PCM is cached per target+march, but a wasm variant pass compiles + // it with extra codegen flags (e.g. -mrelaxed-simd) — its BMI must not be + // shared with the baseline's, or the consuming TUs see a target-feature + // mismatch. Suffix the cache dir with the variant flags when present. + std::string stdPcmKey = std::format("{}-{}", config.target, config.march); for (const std::string& f : config.wasmVariantFlags) { stdPcmKey += "+"; for (char c : f) stdPcmKey += (c == '/' || c == '\\') ? '_' : c; @@ -264,19 +267,17 @@ fs::path Crafter::StdPcmDir(const Configuration& config) { fs::path Crafter::ExpectedOutput(const Configuration& config) { const fs::path dir = config.BinDir(); - const bool mingwTarget = config.target == "x86_64-w64-mingw32"; + const bool windowsTarget = config.target == "x86_64-w64-mingw32" || config.target == "x86_64-pc-windows-msvc"; if (config.type == ConfigurationType::Executable) { if (config.target.starts_with("wasm32")) return dir / std::format("{}.wasm", config.outputName); - return mingwTarget ? dir / std::format("{}.exe", config.outputName) : dir / config.outputName; + return config.target == "x86_64-w64-mingw32" ? dir / std::format("{}.exe", config.outputName) : dir / config.outputName; } if (config.type == ConfigurationType::LibraryStatic) { - // The archive step writes lib.a for every target, mingw included: - // llvm-ar does not care about the object format. - return dir / std::format("lib{}.a", config.outputName); + return windowsTarget ? dir / std::format("{}.lib", config.outputName) : dir / std::format("lib{}.a", config.outputName); } - // LibraryDynamic — the .dll on the mingw target so an mtime check sees the + // LibraryDynamic — the .dll on Windows targets so an mtime check sees the // freshly linked library; on Unix the .so is that file. - return mingwTarget ? dir / std::format("{}.dll", config.outputName) : dir / std::format("lib{}.so", config.outputName); + return windowsTarget ? dir / std::format("{}.dll", config.outputName) : dir / std::format("lib{}.so", config.outputName); } CompileCommand Crafter::GetCompileCommand(const Configuration& config) { @@ -297,21 +298,6 @@ CompileCommand Crafter::GetCompileCommand(const Configuration& config) { if (!config.sysroot.empty()) { out.command += std::format(" --sysroot={}", config.sysroot); - // Same reason as in BuildStdPcm: the driver ranks the host - // toolchain's own libc++ headers (/../include/c++/v1) above - // --sysroot. A unit that only does `import std;` never notices, but - // one that includes a standard header textually (vendored C++ such as - // Crafter.Asset's gdeflate) would get host libc++ configured for the - // host libc, which fails on a musl sysroot with "unknown rune table - // for this platform". Force the sysroot's copy. -nostdinc++ leaves - // -stdlib=libc++ unused on every compile ("argument unused during - // compilation"), hence the silencer; the price is that it also hides - // that diagnostic for a user's own misplaced flag (a -l or -Wl, - // option in compileFlags) on sysroot builds. Not for mingw, which - // uses libstdc++ and has no libc++ in its sysroot. - if (!isWasm && config.target != "x86_64-w64-mingw32") { - out.command += std::format(" -nostdinc++ -isystem {}/usr/include/c++/v1 -Wno-unused-command-line-argument", config.sysroot); - } } if (isWasm) { // -mllvm is consumed by codegen but not the link driver, which is the @@ -336,9 +322,22 @@ 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"; } @@ -424,11 +423,6 @@ CompileCommand Crafter::GetCompileCommand(const Configuration& config) { } BuildResult Crafter::Build(Configuration& config, std::unordered_map>& depResults, std::mutex& depMutex) { - // Before anything is spawned: one message naming the missing compiler - // beats a clang++-not-found per translation unit. - if (const Toolchain& tc = GetToolchain(); !tc.error.empty()) { - return {tc.error, false, {}}; - } // Reset per-build cached state on every Module/ModulePartition so that // successive Build() calls on the same Configuration re-evaluate mtimes // (incremental-rebuild test scenarios). @@ -470,12 +464,15 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map buildCancelled{false}; - // Two entries with the same output name would compile in parallel into - // one file: whichever finished last won, the build reported success, and - // Check() then called both up to date. The usual way in is one source - // registered twice with different defines and no variant; a.vert next to - // a.frag is another. - { - std::unordered_map outputs; - for (const Shader& shader : config.shaders) { - const auto [existing, inserted] = outputs.emplace(shader.OutputName().string(), &shader); - if (inserted) continue; - buildError = std::format("shaders {} and {} both compile to {}: give one of them a distinct Shader::variant", existing->second->path.string(), shader.path.string(), existing->first); - buildCancelled.store(true); - break; - } - } for (const Shader& shader : config.shaders) { - if (buildCancelled.load(std::memory_order_relaxed)) break; - if (shader.Check(outputDir, buildDir, shaderIncludeDirs)) continue; - threads.emplace_back([&shader, &outputDir, &buildDir, &shaderIncludeDirs, &buildError, &buildCancelled]() { - // A source registered more than once shows up once per variant; - // name the variant so the lines can be told apart. - Progress::Task task(shader.variant.empty() ? std::format("Compiling shader {}", shader.path.filename().string()) : std::format("Compiling shader {} ({})", shader.path.filename().string(), shader.variant)); + if (shader.Check(outputDir)) continue; + threads.emplace_back([&shader, &outputDir, &shaderIncludeDirs, &buildError, &buildCancelled]() { + Progress::Task task(std::format("Compiling shader {}", shader.path.filename().string())); if (buildCancelled.load(std::memory_order_relaxed)) return; - std::string result = shader.Compile(outputDir, buildDir, shaderIncludeDirs); + std::string result = shader.Compile(outputDir, shaderIncludeDirs); if (result.empty()) return; bool expected = false; @@ -699,13 +678,7 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map libSet; - std::set publicFlagSet; + std::unordered_set libSet; + std::unordered_set publicFlagSet; std::mutex fileMutex; std::vector depThreads; depThreads.reserve(config.dependencies.size()); @@ -810,9 +784,6 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_mapBinDir(); for (const Shader& shader : dep->shaders) { - fs::path src = depBinDir / shader.OutputName(); + fs::path src = depBinDir / shader.path.filename().replace_extension("spv"); if (!fs::exists(src)) continue; copyTree(src, outputDir / src.filename()); } @@ -1057,13 +1028,6 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map.dll + lib.dll.a (lld --out-implib) - // unix: lib.so (rpath $ORIGIN) - fs::path dll = outputDir / std::format("{}.dll", config.outputName); - fs::path implib = outputDir / std::format("lib{}.dll.a", config.outputName); - linkCommand = std::format("{}{} -shared -o {} -Wl,--out-implib,{} -fuse-ld=lld{}", command, files, dll.string(), implib.string(), linkExtras); - } else { - linkCommand = std::format("{}{} -shared -o {}.so -Wl,-rpath,'$ORIGIN' -fuse-ld=lld{}", command, files, (outputDir/(std::string("lib")+config.outputName)).string(), linkExtras); - } - const fs::path linkRecord = buildDir / std::format("{}.link", config.outputName); - if (!buildResult.repack) { - std::ifstream recorded(linkRecord, std::ios::binary); - const std::string previous((std::istreambuf_iterator(recorded)), std::istreambuf_iterator()); - if (previous != linkCommand) buildResult.repack = true; - } - if(buildResult.repack) { Progress::Task task(std::format("Linking {}", config.outputName)); - // The record goes first, so a link that fails leaves it stale. - std::error_code removed; - fs::remove(linkRecord, removed); if(config.type == ConfigurationType::Executable) { + #ifdef __linux__ if(config.target == "x86_64-w64-mingw32") { try { // Copy any LibraryDynamic dependency DLLs alongside @@ -1211,12 +1136,15 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map copyDepDlls = [&](Configuration* dep) { if (!dllSeen.insert(dep).second) return; if (dep->type == ConfigurationType::LibraryDynamic && dep->target == "x86_64-w64-mingw32") { - // 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 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; 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); @@ -1230,10 +1158,73 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map dllSeen; + std::function 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 + } else { + // LibraryDynamic. Output names follow each target's convention so + // consumers can link via the standard linker search: + // mingw: .dll + lib.dll.a (lld --out-implib) + // msvc: .dll + .lib (lld /IMPLIB) + // unix: lib.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)); + } } } @@ -1499,32 +1490,14 @@ void Crafter::EnableWasiRelaxedSimdVariant(Configuration& cfg) { } std::string Crafter::HostTarget() { - return GetToolchain().hostTriple; -} - -std::string Crafter::NormalizeTarget(std::string_view triple) { - // ---: drop the vendor. A three-part triple only - // carries one when its middle part is a vendor name rather than an OS - // (x86_64-unknown-wasip1 yes, x86_64-linux-gnu no). - std::vector parts; - for (std::size_t begin = 0;;) { - std::size_t dash = triple.find('-', begin); - parts.push_back(dash == std::string_view::npos ? triple.substr(begin) : triple.substr(begin, dash - begin)); - if (dash == std::string_view::npos) break; - begin = dash + 1; - } - const bool hasVendor = parts.size() == 4 || (parts.size() == 3 && (parts[1] == "unknown" || parts[1] == "pc")); - if (hasVendor) parts.erase(parts.begin() + 1); - std::string out; - for (std::string_view part : parts) { - if (!out.empty()) out += '-'; - out += part; - } - return out; -} - -bool Crafter::SameTarget(std::string_view a, std::string_view b) { - return a == b || NormalizeTarget(a) == NormalizeTarget(b); + static const std::string Cached = []() -> std::string { + CommandResult r = RunCommandChecked("clang++ -print-target-triple"); + if (r.exitCode != 0) return {}; + std::string out = std::move(r.output); + while (!out.empty() && (out.back() == '\n' || out.back() == '\r')) out.pop_back(); + return out; + }(); + return Cached; } bool Crafter::ArgQuery::Has(std::string_view flag) const { @@ -1571,9 +1544,11 @@ ArgQuery Crafter::ApplyStandardArgs(Configuration& cfg, std::span Crafter::CleanProject(const fs::path& projectFile) { return removed; } -static void PrintVersion() { - std::println("crafter-build {}", CrafterBuildVersion()); - const Toolchain& tc = GetToolchain(); - if (!tc.error.empty()) { - std::println("compiler: {}", tc.error); - return; - } - std::println("compiler {} ({})", tc.clangVersion, tc.clangxx.string()); - if (tc.libcxxVersion != 0) { - // A sysroot on another libc++ release builds only when its std - // module source is in the sysroot or among these; printed here - // because that is the first question when such a build fails. - std::vector shipped; - std::error_code ec; - for (const fs::directory_entry& entry : fs::directory_iterator(tc.stdModulesDir, ec)) { - if (fs::exists(entry.path() / "std.cppm")) shipped.push_back(entry.path().filename().string()); - } - std::ranges::sort(shipped); - std::string also; - for (const std::string& release : shipped) also += std::format(", {}", release); - std::println("libc++ {}{}", LibcxxVersionString(tc.libcxxVersion), also.empty() ? std::string() : std::format(" (std module sources for sysroots also for{})", also.substr(1))); - } - std::println("host {}", tc.hostTriple); -} - static void PrintHelp(std::string_view argv0) { std::println( R"(Usage: @@ -1624,10 +1574,8 @@ R"(Usage: {0} test [test-options] [globs...] Build and run the project's tests {0} lint [lint-options] [globs...] Run the project's lint rules over its sources {0} format [format-options] [globs...] Apply the project's transform rules (rewrites files) - {0} run [script [args...]] Run a script the project defines, or list them {0} clean Delete the project's bin/ and build/ trees {0} help | -h | --help Show this help - {0} --version Print the version and the compiler in use Loads ./project.cpp (override with --project=), compiles it to a shared object, and invokes its CrafterBuildProject() to obtain a Configuration that @@ -1677,23 +1625,6 @@ Format options (after the `format` subcommand): transform. `format` rewrites changed files in place; `lint` reports the same transforms as would-reformat findings without writing. -Run (after the `run` subcommand): -