Compare commits
74 changed files with 27095 additions and 4356 deletions
|
|
@ -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:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [master]
|
branches: [master]
|
||||||
push:
|
push:
|
||||||
branches: [master]
|
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:
|
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:
|
jobs:
|
||||||
flatpak:
|
build-test-release:
|
||||||
runs-on: flatpak
|
runs-on: arch-latest
|
||||||
container:
|
env:
|
||||||
image: archlinux:latest
|
# x86-64-v2 = SSE4.2 baseline; runs on the CI SBC (Intel N5105 / Tremont,
|
||||||
options: >-
|
# no AVX) and on essentially every x86_64 CPU since ~2011. The bootstrap
|
||||||
-v crafter-flatpak-system:/var/lib/flatpak
|
# MUST stay v2 — anything higher would emit instructions the SBC can't
|
||||||
-v crafter-flatpak-builder:/root/.cache/flatpak-builder
|
# execute and the bootstrap binary itself would crash before it could
|
||||||
-v /var/www/flatpak:/srv/flatpak
|
# rebuild for higher levels. v3/v4 variants are produced from this v2
|
||||||
-v /etc/crafter-flatpak:/run/crafter-flatpak:ro
|
# bootstrap by re-running it with CRAFTER_BUILD_MARCH overridden.
|
||||||
|
CRAFTER_BUILD_MARCH: x86-64-v2
|
||||||
|
CRAFTER_BUILD_MTUNE: generic
|
||||||
steps:
|
steps:
|
||||||
- name: Install tools
|
- name: Install build dependencies
|
||||||
run: |
|
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 --init
|
||||||
pacman-key --populate archlinux
|
pacman-key --populate archlinux
|
||||||
pacman -Sy --noconfirm --needed archlinux-keyring
|
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 '*'
|
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
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
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
|
persist-credentials: true
|
||||||
|
|
||||||
- name: Version and channel
|
- name: Cache glslang clone+build
|
||||||
id: meta
|
# Cache covers crafter-build's per-(target, march) external dep cache.
|
||||||
run: |
|
# First run builds glslang up to 6 times (3 marches × 2 targets); the
|
||||||
set -eu
|
# cache makes subsequent runs reuse those builds.
|
||||||
if [ "${GITHUB_REF_TYPE}" = tag ]; then
|
uses: actions/cache@v4
|
||||||
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 '<release version="\K[^"]+' packaging/flatpak/net.catcrafts.CrafterBuild.metainfo.xml)
|
|
||||||
if [ "$top" != "$version" ]; then
|
|
||||||
echo "metainfo lists $top as the newest release, but the tag is $version" >&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
|
|
||||||
with:
|
with:
|
||||||
name: crafter-build-${{ steps.meta.outputs.version }}
|
path: |
|
||||||
path: dist/*.flatpak
|
~/.cache/crafter.build/external
|
||||||
if-no-files-found: error
|
key: glslang-${{ runner.os }}-v2
|
||||||
|
|
||||||
- name: Publish to the Catcrafts repo
|
- name: Bootstrap (build.sh)
|
||||||
if: github.event_name != 'pull_request'
|
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: |
|
run: |
|
||||||
set -eu
|
set -eux
|
||||||
# The signing key is generated once on the runner host
|
mkdir -p dist
|
||||||
# (/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"
|
|
||||||
|
|
||||||
- name: Rolling 'latest' release
|
# 6 archives — 3 marches × 2 OSes. Each is self-contained: the
|
||||||
if: github.event_name != 'pull_request' && github.ref_type != 'tag'
|
# 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 "<name>-<target>-<march>-<mtune>-<hash>"; 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: |
|
run: |
|
||||||
git config user.email "ci@catcrafts.net"
|
git config user.email "ci@catcrafts.net"
|
||||||
git config user.name "Crafter Build CI"
|
git config user.name "Crafter Build CI"
|
||||||
git tag -f latest
|
git tag -f latest
|
||||||
git push origin latest --force
|
git push origin latest --force
|
||||||
|
|
||||||
- name: Publish rolling 'latest' release
|
- 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
|
uses: https://code.forgejo.org/actions/forgejo-release@v2
|
||||||
with:
|
with:
|
||||||
direction: upload
|
direction: upload
|
||||||
url: ${{ github.server_url }}
|
url: ${{ github.server_url }}
|
||||||
repo: ${{ github.repository }}
|
repo: ${{ github.repository }}
|
||||||
tag: latest
|
tag: latest
|
||||||
title: Latest master build
|
title: Latest main build
|
||||||
prerelease: true
|
prerelease: true
|
||||||
override: true
|
override: true
|
||||||
release-dir: dist
|
release-dir: dist
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Release notes
|
# musl build for Alpine / postmarketOS hosts: the glibc launcher above cannot
|
||||||
if: github.ref_type == 'tag'
|
# 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: |
|
run: |
|
||||||
# The newest <release> block's description, as plain text.
|
apk add -q nodejs git curl jq tar clang clang-dev lld libc++-dev \
|
||||||
python3 - > release-notes.md <<'EOF'
|
llvm-libunwind-dev llvm-runtimes llvm llvm-dev cmake build-base python3
|
||||||
import re, html
|
git config --global --add safe.directory '*'
|
||||||
text = open("packaging/flatpak/net.catcrafts.CrafterBuild.metainfo.xml").read()
|
|
||||||
block = re.search(r"<release [^>]*>(.*?)</release>", text, re.S).group(1)
|
- name: Checkout
|
||||||
paras = re.findall(r"<p>(.*?)</p>", block, re.S)
|
uses: actions/checkout@v4
|
||||||
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")
|
- name: Bootstrap (build.sh)
|
||||||
EOF
|
run: ./build.sh
|
||||||
- name: Publish release
|
|
||||||
if: github.ref_type == 'tag'
|
- name: Run tests
|
||||||
uses: https://code.forgejo.org/actions/forgejo-release@v2
|
run: CRAFTER_BUILD_HOME=$PWD/share/crafter-build ./bin/crafter-build test
|
||||||
with:
|
|
||||||
direction: upload
|
- name: Package artifact
|
||||||
url: ${{ github.server_url }}
|
run: |
|
||||||
repo: ${{ github.repository }}
|
set -eux
|
||||||
tag: ${{ github.ref_name }}
|
file bin/crafter-build || true
|
||||||
title: crafter-build ${{ github.ref_name }}
|
mkdir -p dist
|
||||||
release-notes-file: release-notes.md
|
stage=$(mktemp -d)
|
||||||
release-dir: dist
|
mkdir -p "$stage/bin" "$stage/lib"
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
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"
|
||||||
|
|
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -2,6 +2,4 @@ build/
|
||||||
bin/
|
bin/
|
||||||
lib/*.a
|
lib/*.a
|
||||||
share/crafter-build/
|
share/crafter-build/
|
||||||
.claude
|
.claude
|
||||||
# written by CI next to the manifest so relative source paths still resolve
|
|
||||||
packaging/flatpak/ci-manifest.yaml
|
|
||||||
73
AGENTS.md
73
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 |
|
| Path | What lives there |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `interfaces/Crafter.Build-<Partition>.cppm` | Module interface partitions. `Crafter.Build.cppm` is the umbrella that re-exports them. |
|
| `interfaces/Crafter.Build-<Partition>.cppm` | Module interface partitions. `Crafter.Build.cppm` is the umbrella that re-exports them. `Crafter.Build-Api.h` defines `CRAFTER_API`. |
|
||||||
| `implementations/Crafter.Build-<Partition>.cpp` | Implementation units, one per partition, named `Crafter.Build:<Partition>_impl`. `main.cpp` is a thin wrapper around `Crafter::Run`. |
|
| `implementations/Crafter.Build-<Partition>.cpp` | Implementation units, one per partition, named `Crafter.Build:<Partition>_impl`. `main.cpp` is a thin wrapper around `Crafter::Run`. |
|
||||||
| `project.cpp` | The self-host build description: a `crafter.build-lib` library, a `crafter.build-exe` executable, the test declarations, the lint rules and the `flatpak` and `install` scripts. |
|
| `project.cpp` | The self-host build description: a `crafter.build-lib` library, a `crafter.build-exe` executable, the test declarations and the lint rules. |
|
||||||
| `tests/<Name>/main.cpp` | One self-test per directory; `tests/<Name>/fixture/` holds the sample project it builds. |
|
| `tests/<Name>/main.cpp` | One self-test per directory; `tests/<Name>/fixture/` holds the sample project it builds. |
|
||||||
| `examples/` | Standalone user-facing projects, MIT licensed. |
|
| `examples/` | Standalone user-facing projects, MIT licensed. |
|
||||||
| `build.sh` | Bootstrap script. It compiles everything by hand, without crafter-build. |
|
| `build.sh`, `build.cmd` | Bootstrap scripts for Linux and Windows. They compile everything by hand, without crafter-build. |
|
||||||
| `wasi-runtime/` | `runtime.js` and `index.html.in`, shipped for browser targets. |
|
| `wasi-runtime/` | `runtime.js` and `index.html.in`, shipped for browser targets. |
|
||||||
| `lib/` | Vendored third-party headers (nlohmann/json, glslang's DirStackFileIncluder). Do not edit. |
|
| `lib/` | Vendored third-party headers (nlohmann/json, glslang's DirStackFileIncluder). Do not edit. |
|
||||||
| `share/crafter-build/`, `bin/`, `build/`, `lib/*.a` | Generated and gitignored. |
|
| `share/crafter-build/`, `bin/`, `build/`, `lib/*.a` | Generated and gitignored. |
|
||||||
| `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:
|
Partitions and what they own:
|
||||||
|
|
||||||
- `:Toolchain` resolves the compiler once per process (`GetToolchain`): clang++, clang, llvm-ar, libclang, libc++'s std module source and the default sysroots, from `CRAFTER_BUILD_LLVM_DIR`, else a toolchain installed beside crafter-build, else PATH. It also holds the version baked in at build time. `SysrootStdModule` picks the std module source for a sysroot build by the sysroot's libc++ release.
|
- `:Clang` holds `Configuration`, `Build()`, `VariantId()`, `ApplyStandardArgs`, `HostTarget`, the CLI entry point `Crafter::Run`, and the lint and test data types.
|
||||||
- `:Clang` holds `Configuration`, `Build()`, `VariantId()`, `ApplyStandardArgs`, `HostTarget`, the CLI entry point `Crafter::Run`, and the lint, test and script data types.
|
|
||||||
- `:Platform` runs commands, implements `LoadProject` (compile and load `project.cpp`), the host PCM cache, `#pragma crafter dependency`, and all OS-specific code.
|
- `: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.
|
- `:Interface` and `:Implementation` model module units, their staleness checks and their compiles.
|
||||||
- `:External` handles git + cmake dependencies, `GitProject` and `LocalProject`.
|
- `:External` handles git + cmake dependencies, `GitProject` and `LocalProject`.
|
||||||
- `:Test` holds `AddTest`, `RunTests` and `TestRunner`.
|
- `:Test` holds `AddTest`, `RunTests` and `TestRunner`.
|
||||||
- `:Lint` is the lint/format driver plus the libclang token and AST layer.
|
- `: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.
|
- `:Progress` is console output. `:Shader` compiles shaders through glslang. `:Asset` is the hook into Crafter.Asset.
|
||||||
|
|
||||||
## Dev environment
|
## 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 `/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.
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./build.sh # bootstrap: bin/crafter-build, lib/libcrafter-build.a, share/crafter-build/
|
./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-<variant>/crafter-build` and does not replace it.
|
- `bin/crafter-build` is the bootstrap binary and the harness for every command below. The self-host build writes to `bin/crafter.build-exe-<variant>/crafter-build` and does not replace it.
|
||||||
- Changes under `implementations/` are picked up by the self-host build and by the tests, which link the local library. Changes under `interfaces/` are not: the harness compiles against the copies in `share/crafter-build/` and the PCM cache built from them, so a new or changed declaration is invisible until you run `./build.sh` again. The symptom is `use of undeclared identifier` for the symbol you just added.
|
- Changes under `implementations/` are picked up by the self-host build and by the tests, which link the local library. Changes under `interfaces/` are not: the harness compiles against the copies in `share/crafter-build/` and the PCM cache built from them, so a new or changed declaration is invisible until you run `./build.sh` again. The symptom is `use of undeclared identifier` for the symbol you just added.
|
||||||
- A new exported symbol that a `project.cpp` reaches on its default path also needs `./build.sh`: the old binary does not export it, and `LoadProject` fails with an undefined symbol.
|
- A new `CRAFTER_API` symbol that a `project.cpp` reaches on its default path also needs `./build.sh`: the old binary does not export it, and `LoadProject` fails with an undefined symbol.
|
||||||
|
|
||||||
`./build.sh` clones and builds glslang on first run and takes several minutes; later runs are faster. `./bin/crafter-build clean` deletes `bin/` and `build/` without loading `project.cpp`. 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
|
## 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 lint # dry style gate, exit 1 on any finding
|
||||||
./bin/crafter-build format # apply the transform rules in place
|
./bin/crafter-build format # apply the transform rules in place
|
||||||
./bin/crafter-build format --check # CI-style dry run
|
./bin/crafter-build format --check # CI-style dry run
|
||||||
./bin/crafter-build run # list the scripts project.cpp defines
|
|
||||||
./bin/crafter-build run <name> [args...] # run one; everything after the name is the script's
|
|
||||||
./bin/crafter-build clean
|
./bin/crafter-build clean
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -108,12 +104,13 @@ Suppress one finding with `// lint-disable-next-line <rule>` or a whole file wit
|
||||||
|
|
||||||
Beyond the rules:
|
Beyond the rules:
|
||||||
|
|
||||||
- `import std;` everywhere; standard headers are never `#include`d. C headers (`clang-c/Index.h`, `dlfcn.h`) go in the global module fragment after `module;`.
|
- `import std;` everywhere; standard headers are never `#include`d. C headers (`clang-c/Index.h`, `dlfcn.h`, `windows.h`) go in the global module fragment after `module;`.
|
||||||
|
- Mark every function or method that `project.cpp` or a downstream consumer calls with `CRAFTER_API` in the interface. It is the Windows DLL boundary.
|
||||||
- Comments explain why: the failure mode, the constraint, the rejected alternative. The codebase carries long comments on purpose. Do not strip them, and write them for new non-obvious code. Commit bodies follow the same rule.
|
- Comments explain why: the failure mode, the constraint, the rejected alternative. The codebase carries long comments on purpose. Do not strip them, and write them for new non-obvious code. Commit bodies follow the same rule.
|
||||||
- There is no host-platform preprocessor split: the host is Linux. Target differences (mingw, wasm) branch on `cfg.target` strings at runtime, in the library as well as in `project.cpp` and tests.
|
- Platform splits live in `:Platform`, guarded by `CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc` and `_x86_64_w64_mingw32`. In `project.cpp` and tests, branch on `cfg.target` strings instead of preprocessor macros.
|
||||||
- A build step fails when its command exits nonzero, never because it printed. Use `RunBuildCommand` or `RunCommandChecked`; warnings go to `Progress::Diagnostic`.
|
- A build step fails when its command exits nonzero, never because it printed. Use `RunBuildCommand` or `RunCommandChecked`; warnings go to `Progress::Diagnostic`.
|
||||||
- Every output path goes through `Configuration::BinDir()`, `BuildDir()` and `PcmDir()`. Hand-rolled `path / "bin" / ...` clobbered variants before; do not add new ones.
|
- Every output path goes through `Configuration::BinDir()`, `BuildDir()` and `PcmDir()`. Hand-rolled `path / "bin" / ...` clobbered variants before; do not add new ones.
|
||||||
- Anything that changes what gets compiled or bundled belongs in `VariantId()`. It already hashes type, debug, sysroot, defines, compile flags, wasm variant flags, 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
|
## 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.
|
1. **Configuration is C++.** No JSON, YAML, TOML or DSL for project, runner or lint configuration. Lint rules are C++ callbacks; there is no clang-format or clang-tidy integration and no separate "format rule" type. A rule that calls `ctx.SetContent` is a transform, and that is the whole mechanism.
|
||||||
2. **Per-project artifacts go in `<project>/build` and `<project>/bin`. Only cross-project things go in `~/.cache/crafter.build/`:** the std PCM and Crafter.Build PCMs per target and march, and external clones keyed by their spec hash.
|
2. **Per-project artifacts go in `<project>/build` and `<project>/bin`. Only cross-project things go in `~/.cache/crafter.build/`:** the std PCM and Crafter.Build PCMs per target and march, and external clones keyed by their spec hash.
|
||||||
3. **Sources ship, PCMs build locally.** 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`.
|
3. **Sources ship, PCMs build locally.** Packages install `*.cppm` under `share/crafter-build/`. Nothing target-baked ships except the executable itself, and that defaults to a portable `-march` in `PKGBUILD` and CI.
|
||||||
4. **crafter-build is a monolithic executable with `--export-dynamic`.** `project.so` resolves `Crafter::` symbols from the running binary at dlopen time. There is no Windows host; `x86_64-w64-mingw32` is a cross-compile target for user projects only.
|
4. **Linux is a monolithic executable with `--export-dynamic`; Windows is a DLL plus a launcher.** The asymmetry is intentional.
|
||||||
5. **Convention over configuration.** Lead with the zero-config path. Env var, then CLI flag, then full `project.cpp` are the escalation, not the entry point.
|
5. **Convention over configuration.** Lead with the zero-config path. Env var, then CLI flag, then full `project.cpp` are the escalation, not the entry point.
|
||||||
6. **One skip mechanism** (runner probe plus exit 77) and **one `TestRunner` shape** (template fields with `{bin}`, `{bundle}` and similar placeholders; `Cmd(...)` covers any wrapper). Do not add a special-case runner class or a hardcoded "skip on this OS" branch.
|
6. **One skip mechanism** (runner probe plus exit 77) and **one `TestRunner` shape** (template fields with `{bin}`, `{bundle}` and similar placeholders; `Cmd(...)` covers any wrapper). Do not add a special-case runner class or a hardcoded "skip on this OS" branch.
|
||||||
7. **The house-style rules stay in their own repo.** They are not library API of crafter-build and are not forced on downstream projects.
|
7. **The house-style rules stay in their own repo.** They are not library API of crafter-build and are not forced on downstream projects.
|
||||||
|
|
@ -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.
|
- 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.
|
- 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.
|
- 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/`.
|
- 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
|
## 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.
|
- `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.
|
- Unpinned dependencies `git pull` on every invocation, so builds and tests need network. A missing or private repo surfaces as a clone failure before anything is built.
|
||||||
- `--runner=cmd:<command>` executes the named wrapper around every test binary. Wrappers come from `--runner=` or `CRAFTER_BUILD_RUNNER_*`; do not hardcode hosts, wrappers or credentials in `project.cpp` or tests.
|
- `--runner=cmd:<command>` executes the named wrapper around every test binary. Wrappers come from `--runner=` or `CRAFTER_BUILD_RUNNER_*`; do not hardcode hosts, wrappers or credentials in `project.cpp` or tests.
|
||||||
- `crafter-build run <name>` executes whatever the script does, with your permissions. Only the scripts of the project crafter-build was pointed at can be run, never a dependency's; keep it that way.
|
|
||||||
- The linter loads `libclang` at runtime by name. `CRAFTER_BUILD_LIBCLANG` pins an exact path and must fail loudly when wrong, never fall back to another library.
|
- 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.
|
- 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 <flatpak@catcrafts.net>' 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 `<release>` to `packaging/flatpak/net.catcrafts.CrafterBuild.metainfo.xml` (its description becomes the release notes), merge, and push the tag `X.Y.Z`. CI refuses a tag that does not match the newest entry.
|
|
||||||
Local builds are two scripts in `project.cpp`. Run them with the native binary: the Flatpak build of crafter-build has no `flatpak` to drive.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./bin/crafter-build run flatpak [--jobs=<N>] # build the Flatpak, then print its version from inside the sandbox
|
|
||||||
./bin/crafter-build run install # install that build as branch `local` and make it current
|
|
||||||
```
|
|
||||||
|
|
||||||
`run flatpak` needs `flatpak-builder` or the `org.flatpak.Builder` app, and the Sdk (`flatpak install flathub org.freedesktop.Sdk//26.08`). It builds the working tree, uncommitted changes included, runs the whole suite inside the sandbox and reports version `0.0.0-dev`. Its state is `build/flatpak/` (`state/`, `repo/`, `build-dir/`). The first run builds LLVM and the WASI sysroot, about 25 minutes on a 56-core machine; later runs rebuild only the crafter-build module, a few minutes, because flatpak-builder never caches a `type: dir` source. The checkout must not be under `/tmp`, which `org.flatpak.Builder` cannot see. Use it for anything toolchain-sensitive: the native loop compiles with the distro's clang and the Flatpak with its bundled LLVM, and the two can be a major version apart.
|
|
||||||
|
|
||||||
`run install` does not build; it installs what `run flatpak` last exported. It points the user remote `crafter-build-local` at `build/flatpak/repo`, installs `net.catcrafts.CrafterBuild//local` from it next to whichever published branch is installed, and makes it the branch `flatpak run net.catcrafts.CrafterBuild` starts. `flatpak make-current --user net.catcrafts.CrafterBuild <branch>` switches back. Before `crafter-build clean`, or before deleting the checkout, run `flatpak uninstall --user net.catcrafts.CrafterBuild//local` and `flatpak remote-delete --user crafter-build-local`; otherwise the remote is left pointing at a repo that is gone.
|
|
||||||
|
|
||||||
## Gotchas
|
## 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 `#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.
|
- 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.
|
- `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.
|
- The bootstrap binary is compiled without `CRAFTER_BUILD_HAS_ASSET`, so `cfg.assets` only works in the self-hosted binary. Changing asset extensions means updating two maps in `Crafter.Build-Clang.cpp`: `assetOutput` and the transitive copy loop.
|
||||||
- A warning is printed only when its unit actually recompiles. A build with nothing to do is silent, which is not evidence that a warning is gone.
|
- A warning is printed only when its unit actually recompiles. A build with nothing to do is silent, which is not evidence that a warning is gone.
|
||||||
- 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/<major>/`, one `libcxx-<major>-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-<target>-<march>-*/` rather than predicting the hash.
|
||||||
- `VariantId()` names the output directory, so CI globs `bin/crafter.build-exe-<target>-<march>-*/` rather than predicting the hash. 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.
|
- 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 `&&`.
|
||||||
|
|
|
||||||
36
PKGBUILD
Normal file
36
PKGBUILD
Normal file
|
|
@ -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/"
|
||||||
|
}
|
||||||
104
README.md
104
README.md
|
|
@ -1,18 +1,23 @@
|
||||||
# Crafter Build
|
# 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
|
```bash
|
||||||
flatpak remote-add --if-not-exists catcrafts https://flatpak.catcrafts.net/catcrafts.flatpakrepo
|
./build.sh # produces bin/crafter-build
|
||||||
flatpak install catcrafts net.catcrafts.CrafterBuild
|
./bin/crafter-build # rebuild via project.cpp
|
||||||
alias crafter-build='flatpak run net.catcrafts.CrafterBuild'
|
|
||||||
```
|
```
|
||||||
|
|
||||||
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-<version>.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
|
## Writing a project.cpp
|
||||||
|
|
||||||
|
|
@ -29,6 +34,7 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
cfg.path = "./";
|
cfg.path = "./";
|
||||||
cfg.name = "myapp";
|
cfg.name = "myapp";
|
||||||
cfg.outputName = "myapp";
|
cfg.outputName = "myapp";
|
||||||
|
cfg.target = "x86_64-pc-linux-gnu";
|
||||||
cfg.type = ConfigurationType::Executable;
|
cfg.type = ConfigurationType::Executable;
|
||||||
|
|
||||||
std::array<fs::path, 1> ifaces = { "interfaces/Hello" };
|
std::array<fs::path, 1> ifaces = { "interfaces/Hello" };
|
||||||
|
|
@ -39,7 +45,7 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Run `crafter-build` in that directory, outputs land at `bin/myapp-<target>-<march>-<mtune>-<hash>/myapp` and intermediates at `build/myapp-<target>-<march>-<mtune>-<hash>/`. 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-<target>-<march>/myapp` and intermediates at `build/myapp-<target>-<march>/`. Add `-r` to run the produced executable straight after a successful build.
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
|
|
@ -60,7 +66,7 @@ glslang.includeDirs = { "" };
|
||||||
glslang.libs = { "SPIRV", "glslang", "OSDependent" };
|
glslang.libs = { "SPIRV", "glslang", "OSDependent" };
|
||||||
```
|
```
|
||||||
|
|
||||||
Clones live at `~/.cache/crafter.build/external/<name>-<hash>/`, keyed by `(url, branch, commit, options)` so different projects sharing the same dep with the same spec reuse the clone and cmake build, while projects with diverging specs each get their own cache entry. The clone is resilient to partial-clone recovery and CMake reconfigure is triggered automatically on `options` changes. Submodules are checked out at the revisions the fetched commit records, after every clone, switch, checkout and pull, so a pinned commit builds against its own submodules and not the default branch's.
|
Clones live at `~/.cache/crafter.build/external/<name>-<hash>/`, keyed by `(url, branch, commit, options)` so different projects sharing the same dep with the same spec reuse the clone and cmake build, while projects with diverging specs each get their own cache entry. The clone is resilient to partial-clone recovery and CMake reconfigure is triggered automatically on `options` changes.
|
||||||
|
|
||||||
**Header-only git deps** — same as cmake deps but `builder = ExternalBuilder::None`. Just clones and propagates `-I` flags.
|
**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 };
|
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.
|
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 "<source>" [branch "<name>" | commit "<sha>"] [project "<file>"]
|
#pragma crafter dependency "<source>" [branch "<name>" | commit "<sha>"] [project "<file>"]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Install layout
|
||||||
|
|
||||||
|
```
|
||||||
|
<prefix>/bin/crafter-build # the executable
|
||||||
|
<prefix>/lib/libcrafter-build.a # static archive for downstream consumers
|
||||||
|
<prefix>/share/crafter-build/*.cppm # module sources (rebuilt on user machine on first run)
|
||||||
|
~/.cache/crafter.build/<target>-<march>/ # std.pcm + Crafter.Build*.pcm, built locally
|
||||||
|
```
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
Tests live under `tests/<Name>/`. The simplest case is a single C++ file:
|
Tests live under `tests/<Name>/`. 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").
|
- **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.
|
- 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/<name>.log`.
|
- Stdout+stderr is captured and printed atomically on completion. Failed/crashed/timed-out tests also write to `build/test-logs/<name>.log`.
|
||||||
- A broken fixture `project.cpp` is reported as a single failed test, not a fatal error — other tests still run.
|
- 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.
|
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
|
### Cross-target test runners
|
||||||
|
|
||||||
|
|
@ -139,15 +154,18 @@ Outcomes: ✅ pass, ❌ fail, 💥 crash (with signal name), ⏱ timeout, ⏭ sk
|
||||||
```cpp
|
```cpp
|
||||||
TestRunner::Local() // run on this machine (default)
|
TestRunner::Local() // run on this machine (default)
|
||||||
TestRunner::Cmd("wine") // local exec via prefix command
|
TestRunner::Cmd("wine") // local exec via prefix command
|
||||||
|
TestRunner::Ssh("hostname") // remote linux via scp + ssh
|
||||||
|
TestRunner::SshWin("winhost") // remote windows via scp + ssh-cmd.exe
|
||||||
|
TestRunner::Wsl() // Linux binaries via WSL2 (Windows host)
|
||||||
```
|
```
|
||||||
|
|
||||||
`Cmd(cmd)` is a generic `<cmd> <bin> <args>` prefix runner — covers wine, qemu-user, valgrind, gdbserver, anything with that shape. Probes via `which <cmd>`.
|
`Cmd(cmd)` is a generic `<cmd> <bin> <args>` prefix runner — covers wine, qemu-user, valgrind, gdbserver, anything with that shape. Probes via `which <cmd>` (or `where` on Windows hosts). The `Ssh` variants `scp -r` the test's whole bin dir, run the binary remotely, then clean up; each invocation uses a unique remote subdir so parallel runs don't collide. `Wsl()` mirrors the SSH model but copies the bundle into WSL's native filesystem (faster than executing in-place from `/mnt/c`) — useful when a Windows host wants to exercise Linux test binaries.
|
||||||
|
|
||||||
For elaborate setups (rsync, ssh ControlMaster, env vars on the spawn, `QEMU_LD_PREFIX` for sysroots), construct `TestRunner` directly — `copy`/`exec`/`cleanup`/`probe` are public string fields with `{bundle}`, `{remote_bundle}`, `{bin}`, `{bin_name}`, `{args}` placeholders.
|
For elaborate setups (rsync, ssh ControlMaster, env vars on the spawn, `QEMU_LD_PREFIX` for sysroots), construct `TestRunner` directly — `copy`/`exec`/`cleanup`/`probe` are public string fields with `{bundle}`, `{remote_bundle}`, `{bin}`, `{bin_name}`, `{args}` placeholders.
|
||||||
|
|
||||||
For cross-arch builds, set `cfg.sysroot` on the `Configuration` to point at a sysroot for the target. `BuildStdPcm` and the compile/link commands both consume it: `--sysroot=<path>` is passed to clang, and the std module is precompiled against the sysroot's libc++ headers. Ordinary compiles are pointed at the same headers (`-nostdinc++ -isystem <sysroot>/usr/include/c++/v1`), so a unit that `#include`s a standard header gets the sysroot's libc++ and not the host toolchain's. The module source has to come from the same libc++ release as those headers, so it is taken from `<sysroot>/usr/share/libc++/v1/std.cppm` when the sysroot ships it, else from the toolchain when both are the same major release, else from the sources crafter-build carries for other releases (`share/crafter-build/libc++-modules/<major>/`, `crafter-build --version` lists them). Alpine is the common case for the last two: its `libc++-dev` has the headers and no module source. When nothing matches, the build stops with one line naming both releases, installing that release's `share/libc++/v1` directory into the sysroot resolves it. The cheapest aarch64 sysroot is an extracted [Arch Linux ARM rootfs](http://os.archlinuxarm.org/) plus `libc++` + `libc++abi` + `gcc` installed into it via `pacman --root <rootfs> --config <alarm-pacman.conf> --dbpath <rootfs>/var/lib/pacman -Sy …` (point pacman at ALARM's mirrors via explicit config to avoid pulling host-arch packages).
|
For cross-arch builds, set `cfg.sysroot` on the `Configuration` to point at a sysroot for the target. `BuildStdPcm` and the compile/link commands both consume it: `--sysroot=<path>` is passed to clang, and `<sysroot>/usr/share/libc++/v1/std.cppm` is precompiled instead of the host's. The cheapest aarch64 sysroot is an extracted [Arch Linux ARM rootfs](http://os.archlinuxarm.org/) plus `libc++` + `libc++abi` + `gcc` installed into it via `pacman --root <rootfs> --config <alarm-pacman.conf> --dbpath <rootfs>/var/lib/pacman -Sy …` (point pacman at ALARM's mirrors via explicit config to avoid pulling host-arch packages). See `tests/CrossArchAarch64/inner/` for a working sysroot+qemu example.
|
||||||
|
|
||||||
For `--target=x86_64-w64-mingw32` (Windows from Linux), the pipeline cross-compiles as long as `mingw-w64` is installed, 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
|
### Configuring runners
|
||||||
|
|
||||||
|
|
@ -174,9 +192,12 @@ Spec grammar:
|
||||||
```
|
```
|
||||||
local
|
local
|
||||||
cmd:<command> # cmd:wine, cmd:qemu-aarch64, cmd:valgrind, ...
|
cmd:<command> # cmd:wine, cmd:qemu-aarch64, cmd:valgrind, ...
|
||||||
|
ssh:<host>[:<remoteDir>]
|
||||||
|
sshwin:<host>[:<remoteDir>] # cmd.exe-shell variant for Windows remotes
|
||||||
|
wsl[:<remoteDir>] # Windows host -> Linux binaries via WSL2
|
||||||
```
|
```
|
||||||
|
|
||||||
Each runner carries a `probe` command (e.g. `which wine`), if it fails, every test using that runner is reported as ⏭ skipped and the rest of the suite continues. Skipped tests count toward the summary but don't fail the suite (`AllPassed()` returns true on skip-only runs).
|
Each runner carries a `probe` command (e.g. `which wine`, `ssh -BatchMode=yes <host> true`); if it fails, every test using that runner is reported as ⏭ skipped and the rest of the suite continues. Skipped tests count toward the summary but don't fail the suite (`AllPassed()` returns true on skip-only runs).
|
||||||
|
|
||||||
### Linking the parent project's library
|
### Linking the parent project's library
|
||||||
|
|
||||||
|
|
@ -189,6 +210,7 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view>)
|
||||||
cfg.path = "tests/UnitFoo/";
|
cfg.path = "tests/UnitFoo/";
|
||||||
cfg.name = "UnitFoo";
|
cfg.name = "UnitFoo";
|
||||||
cfg.outputName = "UnitFoo";
|
cfg.outputName = "UnitFoo";
|
||||||
|
cfg.target = "x86_64-pc-linux-gnu";
|
||||||
cfg.type = ConfigurationType::Executable;
|
cfg.type = ConfigurationType::Executable;
|
||||||
cfg.dependencies = { ParentLib("MyLib") }; // <-- the magic line
|
cfg.dependencies = { ParentLib("MyLib") }; // <-- the magic line
|
||||||
|
|
||||||
|
|
@ -203,7 +225,7 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view>)
|
||||||
|
|
||||||
## Linting & formatting
|
## 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
|
```cpp
|
||||||
cfg.AddLintRule("no-tabs", [](Crafter::LintContext& ctx) {
|
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"` |
|
| `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) |
|
| `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) |
|
| `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 :
|
Suppression is comment-based :
|
||||||
|
|
||||||
|
|
@ -256,50 +278,6 @@ Suppression is comment-based :
|
||||||
// lint-disable-file — all rules, whole file
|
// 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/](examples/) contains progressively larger self-contained projects:
|
[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 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
|
## Copyright
|
||||||
|
|
||||||
|
|
|
||||||
137
build.cmd
Normal file
137
build.cmd
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
@REM SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
@REM SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||||
|
@echo OFF
|
||||||
|
mkdir build 2>nul
|
||||||
|
mkdir bin 2>nul
|
||||||
|
mkdir lib 2>nul
|
||||||
|
mkdir share\crafter-build 2>nul
|
||||||
|
mkdir share\crafter-build\wasi-runtime 2>nul
|
||||||
|
|
||||||
|
copy /Y wasi-runtime\runtime.js share\crafter-build\wasi-runtime\
|
||||||
|
copy /Y wasi-runtime\index.html.in share\crafter-build\wasi-runtime\
|
||||||
|
|
||||||
|
copy /Y interfaces\Crafter.Build.cppm share\crafter-build\
|
||||||
|
copy /Y interfaces\Crafter.Build-Shader.cppm share\crafter-build\
|
||||||
|
copy /Y interfaces\Crafter.Build-Platform.cppm share\crafter-build\
|
||||||
|
copy /Y interfaces\Crafter.Build-Interface.cppm share\crafter-build\
|
||||||
|
copy /Y interfaces\Crafter.Build-Implementation.cppm share\crafter-build\
|
||||||
|
copy /Y interfaces\Crafter.Build-External.cppm share\crafter-build\
|
||||||
|
copy /Y interfaces\Crafter.Build-Clang.cppm share\crafter-build\
|
||||||
|
copy /Y interfaces\Crafter.Build-Test.cppm share\crafter-build\
|
||||||
|
copy /Y interfaces\Crafter.Build-Lint.cppm share\crafter-build\
|
||||||
|
copy /Y interfaces\Crafter.Build-Progress.cppm share\crafter-build\
|
||||||
|
copy /Y interfaces\Crafter.Build-Asset.cppm share\crafter-build\
|
||||||
|
copy /Y interfaces\Crafter.Build-Api.h share\crafter-build\
|
||||||
|
|
||||||
|
if not exist .\build\glslang\NUL git clone https://github.com/KhronosGroup/glslang.git .\build\glslang
|
||||||
|
|
||||||
|
set useLibcCommon=-nostdinc++ -nostdlib++
|
||||||
|
set useLibcLinker=-L %LIBCXX_DIR%\lib -lc++ %useLibcCommon%
|
||||||
|
set useLibcSource=-isystem %LIBCXX_DIR%\include\c++\v1 %useLibcCommon%
|
||||||
|
set buildDir=%CD%\build
|
||||||
|
|
||||||
|
if "%CRAFTER_BUILD_MARCH%"=="" set CRAFTER_BUILD_MARCH=native
|
||||||
|
if "%CRAFTER_BUILD_MTUNE%"=="" set CRAFTER_BUILD_MTUNE=native
|
||||||
|
|
||||||
|
cd .\build\glslang
|
||||||
|
cmake -G Ninja -B build ^
|
||||||
|
-DCMAKE_C_COMPILER=clang ^
|
||||||
|
-DCMAKE_CXX_COMPILER=clang++ ^
|
||||||
|
-DCMAKE_CXX_FLAGS="%useLibcSource%" ^
|
||||||
|
-DCMAKE_EXE_LINKER_FLAGS="%useLibcLinker%" ^
|
||||||
|
-DCMAKE_SHARED_LINKER_FLAGS="%useLibcLinker%" ^
|
||||||
|
-DCMAKE_BUILD_TYPE=Release ^
|
||||||
|
-DCMAKE_ARCHIVE_OUTPUT_DIRECTORY="%buildDir%" ^
|
||||||
|
-DCMAKE_LIBRARY_OUTPUT_DIRECTORY="%buildDir%" ^
|
||||||
|
-DENABLE_OPT=OFF
|
||||||
|
cmake --build build --config Release
|
||||||
|
cd ..\..\
|
||||||
|
|
||||||
|
set common_options=-I.\build\glslang -std=c++26 -O3 -march=%CRAFTER_BUILD_MARCH% -mtune=%CRAFTER_BUILD_MTUNE% -fprebuilt-module-path=.\build -D CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc -D CRAFTER_BUILD_CONFIGURATION_TYPE_EXECUTABLE -D _CRT_SECURE_NO_WARNINGS %useLibcSource% -c
|
||||||
|
|
||||||
|
clang++ %useLibcSource% -std=c++26 -march=%CRAFTER_BUILD_MARCH% -mtune=%CRAFTER_BUILD_MTUNE% -O3 -Wno-reserved-identifier -Wno-reserved-module-identifier --precompile %LIBCXX_DIR%\modules\c++\v1\std.cppm -o .\build\std.pcm
|
||||||
|
|
||||||
|
clang++ %common_options% -fmodule-output interfaces\Crafter.Build-Shader.cppm -o .\build\Crafter.Build-Shader.o
|
||||||
|
clang++ %common_options% -fmodule-output interfaces\Crafter.Build-Platform.cppm -o .\build\Crafter.Build-Platform.o
|
||||||
|
clang++ %common_options% -fmodule-output interfaces\Crafter.Build-Interface.cppm -o .\build\Crafter.Build-Interface.o
|
||||||
|
clang++ %common_options% -fmodule-output interfaces\Crafter.Build-Implementation.cppm -o .\build\Crafter.Build-Implementation.o
|
||||||
|
clang++ %common_options% -fmodule-output interfaces\Crafter.Build-External.cppm -o .\build\Crafter.Build-External.o
|
||||||
|
clang++ %common_options% -fmodule-output interfaces\Crafter.Build-Clang.cppm -o .\build\Crafter.Build-Clang.o
|
||||||
|
clang++ %common_options% -fmodule-output interfaces\Crafter.Build-Test.cppm -o .\build\Crafter.Build-Test.o
|
||||||
|
clang++ %common_options% -fmodule-output interfaces\Crafter.Build-Lint.cppm -o .\build\Crafter.Build-Lint.o
|
||||||
|
clang++ %common_options% -fmodule-output interfaces\Crafter.Build-Progress.cppm -o .\build\Crafter.Build-Progress.o
|
||||||
|
REM Asset partition: bootstrap compiles it WITHOUT CRAFTER_BUILD_HAS_ASSET so
|
||||||
|
REM CompressAsset takes the stub branch (no Crafter.Asset PCM exists yet).
|
||||||
|
clang++ %common_options% -fmodule-output interfaces\Crafter.Build-Asset.cppm -o .\build\Crafter.Build-Asset.o
|
||||||
|
clang++ %common_options% -fmodule-output interfaces\Crafter.Build.cppm -o .\build\Crafter.Build.o
|
||||||
|
|
||||||
|
clang++ %common_options% .\implementations\Crafter.Build-Shader.cpp -o .\build\Crafter.Build-Shader_impl.o
|
||||||
|
clang++ %common_options% .\implementations\Crafter.Build-Platform.cpp -o .\build\Crafter.Build-Platform_impl.o
|
||||||
|
clang++ %common_options% .\implementations\Crafter.Build-Interface.cpp -o .\build\Crafter.Build-Interface_impl.o
|
||||||
|
clang++ %common_options% .\implementations\Crafter.Build-Implementation.cpp -o .\build\Crafter.Build-Implementation_impl.o
|
||||||
|
clang++ %common_options% .\implementations\Crafter.Build-External.cpp -o .\build\Crafter.Build-External_impl.o
|
||||||
|
clang++ %common_options% .\implementations\Crafter.Build-Clang.cpp -o .\build\Crafter.Build-Clang_impl.o
|
||||||
|
clang++ %common_options% .\implementations\Crafter.Build-Test.cpp -o .\build\Crafter.Build-Test_impl.o
|
||||||
|
clang++ %common_options% .\implementations\Crafter.Build-Lint.cpp -o .\build\Crafter.Build-Lint_impl.o
|
||||||
|
clang++ %common_options% .\implementations\Crafter.Build-Progress.cpp -o .\build\Crafter.Build-Progress_impl.o
|
||||||
|
clang++ %common_options% .\implementations\Crafter.Build-Asset.cpp -o .\build\Crafter.Build-Asset_impl.o
|
||||||
|
clang++ %common_options% .\implementations\main.cpp -o .\build\main.o
|
||||||
|
|
||||||
|
REM Step 1: link all impl .o files into crafter-build.dll, generating crafter-build.lib import lib
|
||||||
|
clang++ %useLibcLinker% -shared -std=c++26 -O3 -march=%CRAFTER_BUILD_MARCH% -mtune=%CRAFTER_BUILD_MTUNE% -L.\build -fuse-ld=lld ^
|
||||||
|
-Wl,/IMPLIB:.\bin\crafter-build.lib ^
|
||||||
|
-lSPIRV -lGenericCodeGen -lglslang -lOSDependent -lMachineIndependent -lglslang-default-resource-limits ^
|
||||||
|
.\build\Crafter.Build-Shader.o ^
|
||||||
|
.\build\Crafter.Build-Platform.o ^
|
||||||
|
.\build\Crafter.Build-Interface.o ^
|
||||||
|
.\build\Crafter.Build-Implementation.o ^
|
||||||
|
.\build\Crafter.Build-External.o ^
|
||||||
|
.\build\Crafter.Build-Clang.o ^
|
||||||
|
.\build\Crafter.Build-Test.o ^
|
||||||
|
.\build\Crafter.Build-Lint.o ^
|
||||||
|
.\build\Crafter.Build-Progress.o ^
|
||||||
|
.\build\Crafter.Build-Asset.o ^
|
||||||
|
.\build\Crafter.Build.o ^
|
||||||
|
.\build\Crafter.Build-Shader_impl.o ^
|
||||||
|
.\build\Crafter.Build-Platform_impl.o ^
|
||||||
|
.\build\Crafter.Build-Interface_impl.o ^
|
||||||
|
.\build\Crafter.Build-Implementation_impl.o ^
|
||||||
|
.\build\Crafter.Build-External_impl.o ^
|
||||||
|
.\build\Crafter.Build-Clang_impl.o ^
|
||||||
|
.\build\Crafter.Build-Test_impl.o ^
|
||||||
|
.\build\Crafter.Build-Lint_impl.o ^
|
||||||
|
.\build\Crafter.Build-Progress_impl.o ^
|
||||||
|
.\build\Crafter.Build-Asset_impl.o ^
|
||||||
|
-o .\bin\crafter-build.dll
|
||||||
|
|
||||||
|
REM Step 2: link the launcher exe against crafter-build.lib
|
||||||
|
clang++ %useLibcLinker% -std=c++26 -O3 -march=%CRAFTER_BUILD_MARCH% -mtune=%CRAFTER_BUILD_MTUNE% -L.\bin -fuse-ld=lld ^
|
||||||
|
.\build\main.o ^
|
||||||
|
.\bin\crafter-build.lib ^
|
||||||
|
-o .\bin\crafter-build.exe
|
||||||
|
|
||||||
|
REM Step 3: bundle the same .o set as a static archive for downstream consumers
|
||||||
|
llvm-lib.exe /OUT:.\lib\crafter-build-static.lib ^
|
||||||
|
.\build\Crafter.Build-Shader.o ^
|
||||||
|
.\build\Crafter.Build-Platform.o ^
|
||||||
|
.\build\Crafter.Build-Interface.o ^
|
||||||
|
.\build\Crafter.Build-Implementation.o ^
|
||||||
|
.\build\Crafter.Build-External.o ^
|
||||||
|
.\build\Crafter.Build-Clang.o ^
|
||||||
|
.\build\Crafter.Build-Test.o ^
|
||||||
|
.\build\Crafter.Build-Lint.o ^
|
||||||
|
.\build\Crafter.Build-Progress.o ^
|
||||||
|
.\build\Crafter.Build-Asset.o ^
|
||||||
|
.\build\Crafter.Build.o ^
|
||||||
|
.\build\Crafter.Build-Shader_impl.o ^
|
||||||
|
.\build\Crafter.Build-Platform_impl.o ^
|
||||||
|
.\build\Crafter.Build-Interface_impl.o ^
|
||||||
|
.\build\Crafter.Build-Implementation_impl.o ^
|
||||||
|
.\build\Crafter.Build-External_impl.o ^
|
||||||
|
.\build\Crafter.Build-Clang_impl.o ^
|
||||||
|
.\build\Crafter.Build-Test_impl.o ^
|
||||||
|
.\build\Crafter.Build-Lint_impl.o ^
|
||||||
|
.\build\Crafter.Build-Progress_impl.o ^
|
||||||
|
.\build\Crafter.Build-Asset_impl.o
|
||||||
|
|
||||||
|
copy /Y "%LIBCXX_DIR%\lib\c++.dll" ".\bin\c++.dll"
|
||||||
190
build.sh
190
build.sh
|
|
@ -1,127 +1,143 @@
|
||||||
#!/bin/sh
|
|
||||||
#SPDX-License-Identifier: LGPL-3.0-only
|
#SPDX-License-Identifier: LGPL-3.0-only
|
||||||
#SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
#SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||||
|
|
||||||
# Bootstrap: compile crafter-build by hand, without crafter-build. The
|
mkdir -p build
|
||||||
# self-host build through project.cpp follows (`./bin/crafter-build`).
|
mkdir -p bin
|
||||||
set -eu
|
mkdir -p lib
|
||||||
|
mkdir -p share/crafter-build
|
||||||
# The toolchain. CRAFTER_BUILD_LLVM_DIR names an LLVM prefix (the Flatpak
|
mkdir -p share/crafter-build/wasi-runtime
|
||||||
# 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/<triple> and the system dirs,
|
|
||||||
# not <prefix>/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
|
|
||||||
|
|
||||||
cp wasi-runtime/runtime.js 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/
|
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/
|
cp interfaces/Crafter.Build.cppm share/crafter-build/
|
||||||
for p in $PARTITIONS; do
|
cp interfaces/Crafter.Build-Shader.cppm share/crafter-build/
|
||||||
cp "interfaces/Crafter.Build-$p.cppm" share/crafter-build/
|
cp interfaces/Crafter.Build-Platform.cppm share/crafter-build/
|
||||||
done
|
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 https://github.com/KhronosGroup/glslang.git ./build/glslang
|
||||||
git clone --depth 1 https://github.com/KhronosGroup/glslang.git build/glslang
|
|
||||||
fi
|
|
||||||
|
|
||||||
current_dir=$(pwd)
|
current_dir=$(pwd)
|
||||||
|
|
||||||
cd build/glslang
|
cd ./build/glslang
|
||||||
cmake -B build \
|
cmake -B build \
|
||||||
-DCMAKE_C_COMPILER="$CC" \
|
-DCMAKE_C_COMPILER=clang \
|
||||||
-DCMAKE_CXX_COMPILER="$CXX" \
|
-DCMAKE_CXX_COMPILER=clang++ \
|
||||||
-DCMAKE_CXX_FLAGS="-stdlib=libc++" \
|
-DCMAKE_CXX_FLAGS="-stdlib=libc++" \
|
||||||
-DCMAKE_EXE_LINKER_FLAGS="-stdlib=libc++ $LIBCXX_LINK_FLAGS" \
|
-DCMAKE_EXE_LINKER_FLAGS="-stdlib=libc++" \
|
||||||
-DCMAKE_SHARED_LINKER_FLAGS="-stdlib=libc++ $LIBCXX_LINK_FLAGS" \
|
-DCMAKE_SHARED_LINKER_FLAGS="-stdlib=libc++" \
|
||||||
-DCMAKE_BUILD_TYPE=Release \
|
-DCMAKE_BUILD_TYPE=Release \
|
||||||
-DCMAKE_ARCHIVE_OUTPUT_DIRECTORY="$current_dir/build" \
|
-DCMAKE_ARCHIVE_OUTPUT_DIRECTORY=$current_dir/build \
|
||||||
-DCMAKE_LIBRARY_OUTPUT_DIRECTORY="$current_dir/build" \
|
-DCMAKE_LIBRARY_OUTPUT_DIRECTORY=$current_dir/build \
|
||||||
-DENABLE_OPT=OFF
|
-DENABLE_OPT=OFF
|
||||||
|
|
||||||
cmake --build build --config Release
|
cmake --build build --config Release
|
||||||
cd ../../
|
cd ../../
|
||||||
|
|
||||||
MARCH="${CRAFTER_BUILD_MARCH:-native}"
|
MARCH="${CRAFTER_BUILD_MARCH:-native}"
|
||||||
MTUNE="${CRAFTER_BUILD_MTUNE:-native}"
|
MTUNE="${CRAFTER_BUILD_MTUNE:-native}"
|
||||||
# The bootstrap targets the toolchain's own triple: x86_64-pc-linux-gnu on
|
# The bootstrap targets the host toolchain's own triple: x86_64-pc-linux-gnu
|
||||||
# glibc distros, x86_64-unknown-linux-gnu for the bundled LLVM,
|
# on glibc distros, x86_64-alpine-linux-musl on Alpine. Override with
|
||||||
# x86_64-alpine-linux-musl on Alpine. Override with CRAFTER_BUILD_TARGET.
|
# CRAFTER_BUILD_TARGET. The per-target define (triple with '-' -> '_') is the
|
||||||
TARGET="${CRAFTER_BUILD_TARGET:-$("$CXX" -print-target-triple)}"
|
# 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 - _)"
|
TARGET_DEFINE="CRAFTER_BUILD_CONFIGURATION_TARGET_$(printf '%s' "$TARGET" | tr - _)"
|
||||||
|
|
||||||
# Crafter.Build-Lint includes libclang's clang-c/Index.h. glibc distros keep
|
# 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
|
# it in /usr/include; Alpine keeps LLVM under /usr/lib/llvmNN, so ask
|
||||||
# ask llvm-config and add the dir when it is not already on the default path
|
# 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).
|
# (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
|
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
|
# Asset partition: bootstrap compiles it WITHOUT CRAFTER_BUILD_HAS_ASSET so
|
||||||
# CompressAsset takes the stub branch and the impl unit doesn't try to
|
# 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
|
# `import Crafter.Asset` (no Crafter.Asset PCM exists at this stage — the
|
||||||
# self-host pass that follows wires in the real dep).
|
# self-host pass that follows wires in the real dep).
|
||||||
OBJECTS=""
|
clang++ $common_options -fmodule-output interfaces/Crafter.Build-Asset.cppm -o ./build/Crafter.Build-Asset.o
|
||||||
for p in $PARTITIONS; do
|
clang++ $common_options -fmodule-output interfaces/Crafter.Build.cppm -o ./build/Crafter.Build.o
|
||||||
"$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"
|
|
||||||
|
|
||||||
for p in $PARTITIONS; do
|
clang++ $common_options ./implementations/Crafter.Build-Shader.cpp -o ./build/Crafter.Build-Shader_impl.o
|
||||||
"$CXX" $common_options "./implementations/Crafter.Build-$p.cpp" -o "./build/Crafter.Build-${p}_impl.o"
|
clang++ $common_options ./implementations/Crafter.Build-Platform.cpp -o ./build/Crafter.Build-Platform_impl.o
|
||||||
OBJECTS="$OBJECTS ./build/Crafter.Build-${p}_impl.o"
|
clang++ $common_options ./implementations/Crafter.Build-Interface.cpp -o ./build/Crafter.Build-Interface_impl.o
|
||||||
done
|
clang++ $common_options ./implementations/Crafter.Build-Implementation.cpp -o ./build/Crafter.Build-Implementation_impl.o
|
||||||
"$CXX" $common_options ./implementations/main.cpp -o ./build/main.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
|
ar rcs ./lib/libcrafter-build.a \
|
||||||
# previous layout left behind.
|
./build/Crafter.Build-Shader.o \
|
||||||
rm -f ./lib/libcrafter-build.a
|
./build/Crafter.Build-Platform.o \
|
||||||
"$AR" rcs ./lib/libcrafter-build.a $OBJECTS
|
./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 \
|
clang++ --target=$TARGET -std=c++26 -stdlib=libc++ -O3 -march=$MARCH -mtune=$MTUNE -fuse-ld=lld \
|
||||||
-Wl,--export-dynamic $LIBCXX_LINK_FLAGS \
|
-Wl,--export-dynamic \
|
||||||
-L./build \
|
-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 \
|
./build/main.o \
|
||||||
-lSPIRV -lGenericCodeGen -lglslang -lOSDependent -lMachineIndependent -lglslang-default-resource-limits \
|
-lSPIRV -lGenericCodeGen -lglslang -lOSDependent -lMachineIndependent -lglslang-default-resource-limits \
|
||||||
-ldl \
|
-ldl \
|
||||||
-o ./bin/crafter-build
|
-o ./bin/crafter-build
|
||||||
|
|
@ -11,6 +11,7 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view>)
|
||||||
cfg.path = "./";
|
cfg.path = "./";
|
||||||
cfg.name = "hello";
|
cfg.name = "hello";
|
||||||
cfg.outputName = "hello";
|
cfg.outputName = "hello";
|
||||||
|
cfg.target = "x86_64-pc-linux-gnu";
|
||||||
cfg.type = ConfigurationType::Executable;
|
cfg.type = ConfigurationType::Executable;
|
||||||
|
|
||||||
std::array<fs::path, 0> ifaces = {};
|
std::array<fs::path, 0> ifaces = {};
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view>)
|
||||||
lib->path = "./mylib/";
|
lib->path = "./mylib/";
|
||||||
lib->name = "MyMath";
|
lib->name = "MyMath";
|
||||||
lib->outputName = "MyMath";
|
lib->outputName = "MyMath";
|
||||||
|
lib->target = "x86_64-pc-linux-gnu";
|
||||||
lib->type = ConfigurationType::LibraryStatic;
|
lib->type = ConfigurationType::LibraryStatic;
|
||||||
{
|
{
|
||||||
std::array<fs::path, 1> ifaces = { "MyMath" };
|
std::array<fs::path, 1> ifaces = { "MyMath" };
|
||||||
|
|
@ -28,6 +29,7 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view>)
|
||||||
cfg.path = "./";
|
cfg.path = "./";
|
||||||
cfg.name = "math-app";
|
cfg.name = "math-app";
|
||||||
cfg.outputName = "math-app";
|
cfg.outputName = "math-app";
|
||||||
|
cfg.target = "x86_64-pc-linux-gnu";
|
||||||
cfg.type = ConfigurationType::Executable;
|
cfg.type = ConfigurationType::Executable;
|
||||||
cfg.dependencies = { lib.get() };
|
cfg.dependencies = { lib.get() };
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view>)
|
||||||
cfg.path = "./mylib/";
|
cfg.path = "./mylib/";
|
||||||
cfg.name = "MyMath";
|
cfg.name = "MyMath";
|
||||||
cfg.outputName = "MyMath";
|
cfg.outputName = "MyMath";
|
||||||
|
cfg.target = "x86_64-pc-linux-gnu";
|
||||||
cfg.type = ConfigurationType::LibraryStatic;
|
cfg.type = ConfigurationType::LibraryStatic;
|
||||||
|
|
||||||
std::array<fs::path, 1> ifaces = { "MyMath" };
|
std::array<fs::path, 1> ifaces = { "MyMath" };
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view>)
|
||||||
cfg.path = "./";
|
cfg.path = "./";
|
||||||
cfg.name = "greeter-app";
|
cfg.name = "greeter-app";
|
||||||
cfg.outputName = "greeter-app";
|
cfg.outputName = "greeter-app";
|
||||||
|
cfg.target = "x86_64-pc-linux-gnu";
|
||||||
cfg.type = ConfigurationType::Executable;
|
cfg.type = ConfigurationType::Executable;
|
||||||
|
|
||||||
std::array<fs::path, 1> ifaces = { "interfaces/Greeter" };
|
std::array<fs::path, 1> ifaces = { "interfaces/Greeter" };
|
||||||
|
|
|
||||||
|
|
@ -2,17 +2,23 @@
|
||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||||
|
|
||||||
module;
|
module;
|
||||||
|
#if defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc) || defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32)
|
||||||
|
#include <winsock2.h>
|
||||||
|
#include <ws2tcpip.h>
|
||||||
|
// <rpc.h> (pulled in transitively) defines `interface` as a macro for `struct`,
|
||||||
|
// which collides with local variables named `interface` in this TU.
|
||||||
|
#undef interface
|
||||||
|
#else
|
||||||
#include <sys/socket.h>
|
#include <sys/socket.h>
|
||||||
#include <netinet/in.h>
|
#include <netinet/in.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
#endif
|
||||||
export module Crafter.Build:Clang_impl;
|
export module Crafter.Build:Clang_impl;
|
||||||
import std;
|
import std;
|
||||||
import :Clang;
|
import :Clang;
|
||||||
import :Platform;
|
import :Platform;
|
||||||
import :Toolchain;
|
|
||||||
import :Test;
|
import :Test;
|
||||||
import :Lint;
|
import :Lint;
|
||||||
import :Script;
|
|
||||||
import :Progress;
|
import :Progress;
|
||||||
import :Asset;
|
import :Asset;
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
|
|
@ -247,14 +253,11 @@ void Configuration::GetInterfacesAndImplementations(std::span<fs::path> interfac
|
||||||
}
|
}
|
||||||
|
|
||||||
fs::path Crafter::StdPcmDir(const Configuration& config) {
|
fs::path Crafter::StdPcmDir(const Configuration& config) {
|
||||||
// The std PCM is cached per target+march+toolchain: a PCM is only valid
|
// The std PCM is cached per target+march, but a wasm variant pass compiles
|
||||||
// for the exact clang that wrote it, so a compiler bump starts a cold
|
// it with extra codegen flags (e.g. -mrelaxed-simd) — its BMI must not be
|
||||||
// directory instead of handing the new clang a PCM it will crash on. A
|
// shared with the baseline's, or the consuming TUs see a target-feature
|
||||||
// wasm variant pass compiles it with extra codegen flags (e.g.
|
// mismatch. Suffix the cache dir with the variant flags when present.
|
||||||
// -mrelaxed-simd) — its BMI must not be shared with the baseline's, or
|
std::string stdPcmKey = std::format("{}-{}", config.target, config.march);
|
||||||
// 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());
|
|
||||||
for (const std::string& f : config.wasmVariantFlags) {
|
for (const std::string& f : config.wasmVariantFlags) {
|
||||||
stdPcmKey += "+";
|
stdPcmKey += "+";
|
||||||
for (char c : f) stdPcmKey += (c == '/' || c == '\\') ? '_' : c;
|
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) {
|
fs::path Crafter::ExpectedOutput(const Configuration& config) {
|
||||||
const fs::path dir = config.BinDir();
|
const fs::path dir = config.BinDir();
|
||||||
const bool mingwTarget = config.target == "x86_64-w64-mingw32";
|
const bool windowsTarget = config.target == "x86_64-w64-mingw32" || config.target == "x86_64-pc-windows-msvc";
|
||||||
if (config.type == ConfigurationType::Executable) {
|
if (config.type == ConfigurationType::Executable) {
|
||||||
if (config.target.starts_with("wasm32")) return dir / std::format("{}.wasm", config.outputName);
|
if (config.target.starts_with("wasm32")) return dir / std::format("{}.wasm", config.outputName);
|
||||||
return mingwTarget ? dir / std::format("{}.exe", config.outputName) : dir / config.outputName;
|
return config.target == "x86_64-w64-mingw32" ? dir / std::format("{}.exe", config.outputName) : dir / config.outputName;
|
||||||
}
|
}
|
||||||
if (config.type == ConfigurationType::LibraryStatic) {
|
if (config.type == ConfigurationType::LibraryStatic) {
|
||||||
// The archive step writes lib<name>.a for every target, mingw included:
|
return windowsTarget ? dir / std::format("{}.lib", config.outputName) : dir / std::format("lib{}.a", config.outputName);
|
||||||
// llvm-ar does not care about the object format.
|
|
||||||
return dir / std::format("lib{}.a", config.outputName);
|
|
||||||
}
|
}
|
||||||
// LibraryDynamic — the .dll on the mingw target so an mtime check sees the
|
// LibraryDynamic — the .dll on Windows targets so an mtime check sees the
|
||||||
// freshly linked library; on Unix the .so is that file.
|
// freshly linked library; on Unix the .so is that file.
|
||||||
return mingwTarget ? dir / std::format("{}.dll", config.outputName) : dir / std::format("lib{}.so", config.outputName);
|
return windowsTarget ? dir / std::format("{}.dll", config.outputName) : dir / std::format("lib{}.so", config.outputName);
|
||||||
}
|
}
|
||||||
|
|
||||||
CompileCommand Crafter::GetCompileCommand(const Configuration& config) {
|
CompileCommand Crafter::GetCompileCommand(const Configuration& config) {
|
||||||
|
|
@ -297,21 +298,6 @@ CompileCommand Crafter::GetCompileCommand(const Configuration& config) {
|
||||||
|
|
||||||
if (!config.sysroot.empty()) {
|
if (!config.sysroot.empty()) {
|
||||||
out.command += std::format(" --sysroot={}", config.sysroot);
|
out.command += std::format(" --sysroot={}", config.sysroot);
|
||||||
// Same reason as in BuildStdPcm: the driver ranks the host
|
|
||||||
// toolchain's own libc++ headers (<install>/../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) {
|
if (isWasm) {
|
||||||
// -mllvm is consumed by codegen but not the link driver, which is the
|
// -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) {
|
if(config.type == ConfigurationType::LibraryDynamic) {
|
||||||
|
#ifdef __linux__
|
||||||
out.command += " -fPIC -D CRAFTER_BUILD_CONFIGURATION_TYPE_SHARED_LIBRARY";
|
out.command += " -fPIC -D CRAFTER_BUILD_CONFIGURATION_TYPE_SHARED_LIBRARY";
|
||||||
|
#endif
|
||||||
|
#if defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc) || defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32)
|
||||||
|
out.command += " -D CRAFTER_BUILD_CONFIGURATION_TYPE_SHARED_LIBRARY";
|
||||||
|
#endif
|
||||||
} else if(config.type == ConfigurationType::Executable) {
|
} else if(config.type == ConfigurationType::Executable) {
|
||||||
out.command += " -D CRAFTER_BUILD_CONFIGURATION_TYPE_EXECUTABLE";
|
out.command += " -D CRAFTER_BUILD_CONFIGURATION_TYPE_EXECUTABLE";
|
||||||
|
// On Windows targets the API uses __declspec(dllimport) when consuming
|
||||||
|
// a DLL. Set the macro for executables so CRAFTER_API resolves to
|
||||||
|
// dllimport in their PCM cache (separate from the lib's PCM cache,
|
||||||
|
// which gets dllexport). Harmless if the exe doesn't actually link a
|
||||||
|
// crafter DLL — CRAFTER_API only matters at API call sites.
|
||||||
|
if (config.target == "x86_64-w64-mingw32" || config.target == "x86_64-pc-windows-msvc") {
|
||||||
|
out.command += " -D CRAFTER_BUILD_DLL_IMPORT";
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
out.command += " -D CRAFTER_BUILD_CONFIGURATION_TYPE_LIBRARY";
|
out.command += " -D CRAFTER_BUILD_CONFIGURATION_TYPE_LIBRARY";
|
||||||
}
|
}
|
||||||
|
|
@ -424,11 +423,6 @@ CompileCommand Crafter::GetCompileCommand(const Configuration& config) {
|
||||||
}
|
}
|
||||||
|
|
||||||
BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, std::shared_future<BuildResult>>& depResults, std::mutex& depMutex) {
|
BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, std::shared_future<BuildResult>>& 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
|
// Reset per-build cached state on every Module/ModulePartition so that
|
||||||
// successive Build() calls on the same Configuration re-evaluate mtimes
|
// successive Build() calls on the same Configuration re-evaluate mtimes
|
||||||
// (incremental-rebuild test scenarios).
|
// (incremental-rebuild test scenarios).
|
||||||
|
|
@ -470,12 +464,15 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
||||||
config.ResolvePendingImports();
|
config.ResolvePendingImports();
|
||||||
|
|
||||||
// Auto-detect the WASI sysroot before any compile step runs so BuildStdPcm
|
// Auto-detect the WASI sysroot before any compile step runs so BuildStdPcm
|
||||||
// and the main compile command see the same value. Covers all wasm32-*
|
// and the main compile command see the same value. Linux-only — Windows
|
||||||
// triples (wasi, wasip1, wasip2, ...); the sysroot's per-triple subdirs
|
// users supply cfg.sysroot pointing at their wasi-sdk install. Covers all
|
||||||
// handle the differences.
|
// wasm32-* triples (wasi, wasip1, wasip2, ...); the sysroot's per-triple
|
||||||
|
// subdirs handle the differences.
|
||||||
|
#ifdef __linux__
|
||||||
if (config.sysroot.empty() && config.target.starts_with("wasm32")) {
|
if (config.sysroot.empty() && config.target.starts_with("wasm32")) {
|
||||||
config.sysroot = GetToolchain().wasiSysroot.string();
|
config.sysroot = "/usr/share/wasi-sysroot";
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
fs::path buildDir = config.BuildDir();
|
fs::path buildDir = config.BuildDir();
|
||||||
fs::path outputDir = config.BinDir();
|
fs::path outputDir = config.BinDir();
|
||||||
|
|
@ -518,31 +515,13 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
||||||
|
|
||||||
std::string buildError;
|
std::string buildError;
|
||||||
std::atomic<bool> buildCancelled{false};
|
std::atomic<bool> 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<std::string, const Shader*> 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) {
|
for (const Shader& shader : config.shaders) {
|
||||||
if (buildCancelled.load(std::memory_order_relaxed)) break;
|
if (shader.Check(outputDir)) continue;
|
||||||
if (shader.Check(outputDir, buildDir, shaderIncludeDirs)) continue;
|
threads.emplace_back([&shader, &outputDir, &shaderIncludeDirs, &buildError, &buildCancelled]() {
|
||||||
threads.emplace_back([&shader, &outputDir, &buildDir, &shaderIncludeDirs, &buildError, &buildCancelled]() {
|
Progress::Task task(std::format("Compiling shader {}", shader.path.filename().string()));
|
||||||
// A source registered more than once shows up once per variant;
|
|
||||||
// name the variant so the lines can be told apart.
|
|
||||||
Progress::Task task(shader.variant.empty() ? std::format("Compiling shader {}", shader.path.filename().string()) : std::format("Compiling shader {} ({})", shader.path.filename().string(), shader.variant));
|
|
||||||
if (buildCancelled.load(std::memory_order_relaxed)) return;
|
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;
|
if (result.empty()) return;
|
||||||
|
|
||||||
bool expected = false;
|
bool expected = false;
|
||||||
|
|
@ -699,13 +678,7 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
||||||
std::string stdPcmResult;
|
std::string stdPcmResult;
|
||||||
{
|
{
|
||||||
Progress::Task task(std::format("Building std PCM ({}-{})", config.target, config.march));
|
Progress::Task task(std::format("Building std PCM ({}-{})", config.target, config.march));
|
||||||
// The shader and external threads are running: an exception leaving
|
stdPcmResult = BuildStdPcm(config, stdPcmDir/"std.pcm");
|
||||||
// here would destroy them joinable, which terminates the process.
|
|
||||||
try {
|
|
||||||
stdPcmResult = BuildStdPcm(config, stdPcmDir/"std.pcm");
|
|
||||||
} catch (const std::exception& e) {
|
|
||||||
stdPcmResult = std::format("building the std module for {}: {}", config.target, e.what());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if(!stdPcmResult.empty()) {
|
if(!stdPcmResult.empty()) {
|
||||||
buildCancelled.store(true);
|
buildCancelled.store(true);
|
||||||
|
|
@ -732,10 +705,11 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
||||||
const std::string& ltoCompileFlags = compile.ltoCompileFlags;
|
const std::string& ltoCompileFlags = compile.ltoCompileFlags;
|
||||||
const std::string& ltoLinkFlags = compile.ltoLinkFlags;
|
const std::string& ltoLinkFlags = compile.ltoLinkFlags;
|
||||||
const bool isWasm = config.target.starts_with("wasm32");
|
const bool isWasm = config.target.starts_with("wasm32");
|
||||||
|
const bool useLto = compile.useLto;
|
||||||
|
|
||||||
std::string files;
|
std::string files;
|
||||||
std::set<std::string> libSet;
|
std::unordered_set<std::string> libSet;
|
||||||
std::set<std::string> publicFlagSet;
|
std::unordered_set<std::string> publicFlagSet;
|
||||||
std::mutex fileMutex;
|
std::mutex fileMutex;
|
||||||
std::vector<std::thread> depThreads;
|
std::vector<std::thread> depThreads;
|
||||||
depThreads.reserve(config.dependencies.size());
|
depThreads.reserve(config.dependencies.size());
|
||||||
|
|
@ -810,9 +784,6 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
||||||
: std::format(" -march={} -mtune={}", config.march, config.mtune);
|
: std::format(" -march={} -mtune={}", config.march, config.mtune);
|
||||||
if (!config.sysroot.empty()) {
|
if (!config.sysroot.empty()) {
|
||||||
cArchFlags += std::format(" --sysroot={}", config.sysroot);
|
cArchFlags += std::format(" --sysroot={}", config.sysroot);
|
||||||
} else if (config.target == "x86_64-w64-mingw32" && !GetToolchain().mingwSysroot.empty()) {
|
|
||||||
// The C++ side gets it through GetBaseCommand.
|
|
||||||
cArchFlags += std::format(" --sysroot={}", GetToolchain().mingwSysroot.string());
|
|
||||||
}
|
}
|
||||||
if (cIsWasm) {
|
if (cIsWasm) {
|
||||||
// Matches the C++ path's wasi flag set so any libc shim defines
|
// Matches the C++ path's wasi flag set so any libc shim defines
|
||||||
|
|
@ -832,7 +803,7 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
||||||
Progress::Task task(std::format("Compiling {}.c", cFile.filename().string()));
|
Progress::Task task(std::format("Compiling {}.c", cFile.filename().string()));
|
||||||
if (buildCancelled.load(std::memory_order_relaxed)) return;
|
if (buildCancelled.load(std::memory_order_relaxed)) return;
|
||||||
|
|
||||||
std::string result = RunBuildCommand(std::format("{8} {0}.c --target={1}{2} -O3{3} -c{4}{5}{6} -MD -MF {7}_source.o.d -o {7}_source.o", cFile.string(), config.target, cArchFlags, ltoCompileFlags, includeFlags, defineFlags, userFlags, (buildDir / cFile.filename()).string(), GetToolchain().clang.string()));
|
std::string result = RunBuildCommand(std::format("clang {0}.c --target={1}{2} -O3{3} -c{4}{5}{6} -MD -MF {7}_source.o.d -o {7}_source.o", cFile.string(), config.target, cArchFlags, ltoCompileFlags, includeFlags, defineFlags, userFlags, (buildDir / cFile.filename()).string()));
|
||||||
if (result.empty()) return;
|
if (result.empty()) return;
|
||||||
|
|
||||||
bool expected = false;
|
bool expected = false;
|
||||||
|
|
@ -921,7 +892,7 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
||||||
if (!seen.insert(dep).second) return;
|
if (!seen.insert(dep).second) return;
|
||||||
fs::path depBinDir = dep->BinDir();
|
fs::path depBinDir = dep->BinDir();
|
||||||
for (const Shader& shader : dep->shaders) {
|
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;
|
if (!fs::exists(src)) continue;
|
||||||
copyTree(src, outputDir / src.filename());
|
copyTree(src, outputDir / src.filename());
|
||||||
}
|
}
|
||||||
|
|
@ -1057,13 +1028,6 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
||||||
// link below ends with linkExtras, so this reaches them all; the static-lib
|
// link below ends with linkExtras, so this reaches them all; the static-lib
|
||||||
// archive path uses ar instead and is handled separately.
|
// archive path uses ar instead and is handled separately.
|
||||||
linkExtras += ltoLinkFlags;
|
linkExtras += ltoLinkFlags;
|
||||||
// Host links use the toolchain's own libc++. When the toolchain lives
|
|
||||||
// outside /usr (a bundled one) the binary needs an rpath to find
|
|
||||||
// libc++.so.1 at run time. mingw links libstdc++ and every sysroot build
|
|
||||||
// (wasm included) brings its own libc++.
|
|
||||||
if (config.target != "x86_64-w64-mingw32" && config.sysroot.empty()) {
|
|
||||||
linkExtras += GetToolchain().LibcxxLinkFlags();
|
|
||||||
}
|
|
||||||
// mingw uses libstdc++; C++26 std::print/format extras live in libstdc++exp.
|
// mingw uses libstdc++; C++26 std::print/format extras live in libstdc++exp.
|
||||||
// libstdc++ on mingw uses winpthreads for std::atomic_wait /
|
// libstdc++ on mingw uses winpthreads for std::atomic_wait /
|
||||||
// counting_semaphore / stop_token, so -lpthread is required as soon as
|
// counting_semaphore / stop_token, so -lpthread is required as soon as
|
||||||
|
|
@ -1158,49 +1122,10 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The exact command the link (or archive) step runs, kept beside the
|
|
||||||
// objects once it has succeeded. A different one relinks even with no
|
|
||||||
// object newer than the output: a link flag changed in project.cpp, or a
|
|
||||||
// dependency's library moved, as an ExternalDependency's does when its
|
|
||||||
// options change and it is built into a new cache directory.
|
|
||||||
std::string linkCommand;
|
|
||||||
if (config.type == ConfigurationType::Executable) {
|
|
||||||
if (config.target.starts_with("wasm32")) {
|
|
||||||
linkCommand = std::format("{}{} -o {}.wasm -fuse-ld=lld{}", command, files, (outputDir/config.outputName).string(), linkExtras);
|
|
||||||
} else {
|
|
||||||
linkCommand = std::format("{}{} -o {} -fuse-ld=lld{}", command, files, (outputDir/config.outputName).string(), linkExtras);
|
|
||||||
}
|
|
||||||
} else if (config.type == ConfigurationType::LibraryStatic) {
|
|
||||||
// ThinLTO emits LLVM bitcode objects; plain `ar` writes an archive
|
|
||||||
// index that omits their symbols, so a consumer's lld link can't
|
|
||||||
// pull the needed members. llvm-ar writes a bitcode-aware index
|
|
||||||
// and handles plain ELF and COFF objects just as well, so every
|
|
||||||
// build uses it.
|
|
||||||
linkCommand = std::format("{} rcs {}.a {}", GetToolchain().llvmAr.string(), (outputDir/fs::path(std::string("lib")+config.outputName)).string(), files);
|
|
||||||
} else if (config.target == "x86_64-w64-mingw32") {
|
|
||||||
// LibraryDynamic. Output names follow each target's convention so
|
|
||||||
// consumers can link via the standard linker search:
|
|
||||||
// mingw: <name>.dll + lib<name>.dll.a (lld --out-implib)
|
|
||||||
// unix: lib<name>.so (rpath $ORIGIN)
|
|
||||||
fs::path dll = outputDir / std::format("{}.dll", config.outputName);
|
|
||||||
fs::path implib = outputDir / std::format("lib{}.dll.a", config.outputName);
|
|
||||||
linkCommand = std::format("{}{} -shared -o {} -Wl,--out-implib,{} -fuse-ld=lld{}", command, files, dll.string(), implib.string(), linkExtras);
|
|
||||||
} else {
|
|
||||||
linkCommand = std::format("{}{} -shared -o {}.so -Wl,-rpath,'$ORIGIN' -fuse-ld=lld{}", command, files, (outputDir/(std::string("lib")+config.outputName)).string(), linkExtras);
|
|
||||||
}
|
|
||||||
const fs::path linkRecord = buildDir / std::format("{}.link", config.outputName);
|
|
||||||
if (!buildResult.repack) {
|
|
||||||
std::ifstream recorded(linkRecord, std::ios::binary);
|
|
||||||
const std::string previous((std::istreambuf_iterator<char>(recorded)), std::istreambuf_iterator<char>());
|
|
||||||
if (previous != linkCommand) buildResult.repack = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(buildResult.repack) {
|
if(buildResult.repack) {
|
||||||
Progress::Task task(std::format("Linking {}", config.outputName));
|
Progress::Task task(std::format("Linking {}", config.outputName));
|
||||||
// The record goes first, so a link that fails leaves it stale.
|
|
||||||
std::error_code removed;
|
|
||||||
fs::remove(linkRecord, removed);
|
|
||||||
if(config.type == ConfigurationType::Executable) {
|
if(config.type == ConfigurationType::Executable) {
|
||||||
|
#ifdef __linux__
|
||||||
if(config.target == "x86_64-w64-mingw32") {
|
if(config.target == "x86_64-w64-mingw32") {
|
||||||
try {
|
try {
|
||||||
// Copy any LibraryDynamic dependency DLLs alongside
|
// Copy any LibraryDynamic dependency DLLs alongside
|
||||||
|
|
@ -1211,12 +1136,15 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
||||||
std::function<void(Configuration*)> copyDepDlls = [&](Configuration* dep) {
|
std::function<void(Configuration*)> copyDepDlls = [&](Configuration* dep) {
|
||||||
if (!dllSeen.insert(dep).second) return;
|
if (!dllSeen.insert(dep).second) return;
|
||||||
if (dep->type == ConfigurationType::LibraryDynamic && dep->target == "x86_64-w64-mingw32") {
|
if (dep->type == ConfigurationType::LibraryDynamic && dep->target == "x86_64-w64-mingw32") {
|
||||||
// Only the DLL: Windows resolves it from the exe's
|
fs::path depDir = dep->BinDir();
|
||||||
// directory at load time. The import lib stays in
|
// The DLL itself (Windows resolves it from the
|
||||||
// the dependency's own bin dir, which is already
|
// exe's directory at load time) and the mingw
|
||||||
// on the link line through buildResult.libs.
|
// import lib (so a downstream `crafter-build.exe`
|
||||||
fs::path src = dep->BinDir() / std::format("{}.dll", dep->outputName);
|
// can link a fresh project.dll against it without
|
||||||
if (fs::exists(src)) {
|
// hunting through sibling output dirs).
|
||||||
|
for (auto fname : {std::format("{}.dll", dep->outputName), std::format("lib{}.dll.a", dep->outputName)}) {
|
||||||
|
fs::path src = depDir / fname;
|
||||||
|
if (!fs::exists(src)) continue;
|
||||||
fs::path dest = outputDir / src.filename();
|
fs::path dest = outputDir / src.filename();
|
||||||
if (!fs::exists(dest) || fs::last_write_time(src) > fs::last_write_time(dest)) {
|
if (!fs::exists(dest) || fs::last_write_time(src) > fs::last_write_time(dest)) {
|
||||||
fs::copy(src, dest, fs::copy_options::overwrite_existing);
|
fs::copy(src, dest, fs::copy_options::overwrite_existing);
|
||||||
|
|
@ -1230,10 +1158,73 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
||||||
return {e.what(), false, {}};
|
return {e.what(), false, {}};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
if (config.target.starts_with("wasm32")) {
|
||||||
buildResult.result = RunBuildCommand(linkCommand);
|
buildResult.result = RunBuildCommand(std::format("{}{} -o {}.wasm -fuse-ld=lld{}", command, files, (outputDir/config.outputName).string(), linkExtras));
|
||||||
if (buildResult.result.empty()) {
|
} else {
|
||||||
std::ofstream(linkRecord, std::ios::binary | std::ios::trunc) << linkCommand;
|
buildResult.result = RunBuildCommand(std::format("{}{} -o {} -fuse-ld=lld{}", command, files, (outputDir/config.outputName).string(), linkExtras));
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc) || defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32)
|
||||||
|
if (config.target == "x86_64-w64-mingw32") {
|
||||||
|
// Windows host, mingw target: same shape as the Linux→mingw
|
||||||
|
// path (no LIBCXX_DIR / -lc++ / -nostdlib++ — those are MSVC
|
||||||
|
// libc++ flags). Copy LibraryDynamic dep DLLs + import libs
|
||||||
|
// alongside the launcher exe so Windows resolves them from
|
||||||
|
// the exe's own directory at load time. Runtime DLLs (libstdc++,
|
||||||
|
// libgcc, libwinpthread) come from msys2 on PATH.
|
||||||
|
std::unordered_set<Configuration*> dllSeen;
|
||||||
|
std::function<void(Configuration*)> copyDepDlls = [&](Configuration* dep) {
|
||||||
|
if (!dllSeen.insert(dep).second) return;
|
||||||
|
if (dep->type == ConfigurationType::LibraryDynamic && dep->target == "x86_64-w64-mingw32") {
|
||||||
|
fs::path depDir = dep->BinDir();
|
||||||
|
for (auto fname : {std::format("{}.dll", dep->outputName), std::format("lib{}.dll.a", dep->outputName)}) {
|
||||||
|
fs::path src = depDir / fname;
|
||||||
|
if (!fs::exists(src)) continue;
|
||||||
|
fs::path dest = outputDir / src.filename();
|
||||||
|
if (!fs::exists(dest) || fs::last_write_time(src) > fs::last_write_time(dest)) {
|
||||||
|
fs::copy(src, dest, fs::copy_options::overwrite_existing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (Configuration* sub : dep->dependencies) copyDepDlls(sub);
|
||||||
|
};
|
||||||
|
for (Configuration* dep : config.dependencies) copyDepDlls(dep);
|
||||||
|
|
||||||
|
buildResult.result = RunBuildCommand(std::format("{}{} -o {} -fuse-ld=lld{}", command, files, (outputDir/config.outputName).string(), linkExtras));
|
||||||
|
} else {
|
||||||
|
std::system(std::format("copy \"%LIBCXX_DIR%\\lib\\c++.dll\" \"{}\\c++.dll\"", outputDir.string()).c_str());
|
||||||
|
buildResult.result = RunBuildCommand(std::format("{}{} -o {}.exe -fuse-ld=lld -L %LIBCXX_DIR%\\lib -lc++ -nostdinc++ -nostdlib++{}", command, files, (outputDir/config.outputName).string(), linkExtras));
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
} else if(config.type == ConfigurationType::LibraryStatic) {
|
||||||
|
#ifdef __linux__
|
||||||
|
// ThinLTO emits LLVM bitcode objects; plain `ar` writes an archive
|
||||||
|
// index that omits their symbols, so a consumer's lld link can't
|
||||||
|
// pull the needed members. llvm-ar writes a bitcode-aware index.
|
||||||
|
buildResult.result = RunBuildCommand(std::format("{} rcs {}.a {}", useLto ? "llvm-ar" : "ar", (outputDir/fs::path(std::string("lib")+config.outputName)).string(), files));
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc) || defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32)
|
||||||
|
buildResult.result = RunBuildCommand(std::format("llvm-lib.exe {} /OUT:{}.lib", files, (outputDir/fs::path(config.outputName)).string()));
|
||||||
|
#endif
|
||||||
|
} else {
|
||||||
|
// LibraryDynamic. Output names follow each target's convention so
|
||||||
|
// consumers can link via the standard linker search:
|
||||||
|
// mingw: <name>.dll + lib<name>.dll.a (lld --out-implib)
|
||||||
|
// msvc: <name>.dll + <name>.lib (lld /IMPLIB)
|
||||||
|
// unix: lib<name>.so (rpath $ORIGIN)
|
||||||
|
if (config.target == "x86_64-w64-mingw32") {
|
||||||
|
fs::path dll = outputDir / std::format("{}.dll", config.outputName);
|
||||||
|
fs::path implib = outputDir / std::format("lib{}.dll.a", config.outputName);
|
||||||
|
buildResult.result = RunBuildCommand(std::format("{}{} -shared -o {} -Wl,--out-implib,{} -fuse-ld=lld{}", command, files, dll.string(), implib.string(), linkExtras));
|
||||||
|
} else if (config.target == "x86_64-pc-windows-msvc") {
|
||||||
|
fs::path dll = outputDir / std::format("{}.dll", config.outputName);
|
||||||
|
fs::path implib = outputDir / std::format("{}.lib", config.outputName);
|
||||||
|
buildResult.result = RunBuildCommand(std::format("{}{} -shared -o {} -Wl,/IMPLIB:{} -fuse-ld=lld{}", command, files, dll.string(), implib.string(), linkExtras));
|
||||||
|
} else {
|
||||||
|
buildResult.result = RunBuildCommand(std::format("{}{} -shared -o {}.so -Wl,-rpath,'$ORIGIN' -fuse-ld=lld{}", command, files, (outputDir/(std::string("lib")+config.outputName)).string(), linkExtras));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1499,32 +1490,14 @@ void Crafter::EnableWasiRelaxedSimdVariant(Configuration& cfg) {
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string Crafter::HostTarget() {
|
std::string Crafter::HostTarget() {
|
||||||
return GetToolchain().hostTriple;
|
static const std::string Cached = []() -> std::string {
|
||||||
}
|
CommandResult r = RunCommandChecked("clang++ -print-target-triple");
|
||||||
|
if (r.exitCode != 0) return {};
|
||||||
std::string Crafter::NormalizeTarget(std::string_view triple) {
|
std::string out = std::move(r.output);
|
||||||
// <arch>-<vendor>-<os>-<env>: drop the vendor. A three-part triple only
|
while (!out.empty() && (out.back() == '\n' || out.back() == '\r')) out.pop_back();
|
||||||
// carries one when its middle part is a vendor name rather than an OS
|
return out;
|
||||||
// (x86_64-unknown-wasip1 yes, x86_64-linux-gnu no).
|
}();
|
||||||
std::vector<std::string_view> parts;
|
return Cached;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Crafter::ArgQuery::Has(std::string_view flag) const {
|
bool Crafter::ArgQuery::Has(std::string_view flag) const {
|
||||||
|
|
@ -1571,9 +1544,11 @@ ArgQuery Crafter::ApplyStandardArgs(Configuration& cfg, std::span<const std::str
|
||||||
// includes it. (Build() also runs this once more for callers that bypassed
|
// includes it. (Build() also runs this once more for callers that bypassed
|
||||||
// ApplyStandardArgs, but doing it here makes dep PcmDirs consistent
|
// ApplyStandardArgs, but doing it here makes dep PcmDirs consistent
|
||||||
// between the consumer's command-construction and the dep's own build.)
|
// between the consumer's command-construction and the dep's own build.)
|
||||||
|
#ifdef __linux__
|
||||||
if (cfg.sysroot.empty() && cfg.target.starts_with("wasm32")) {
|
if (cfg.sysroot.empty() && cfg.target.starts_with("wasm32")) {
|
||||||
cfg.sysroot = GetToolchain().wasiSysroot.string();
|
cfg.sysroot = "/usr/share/wasi-sysroot";
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
return ArgQuery{args};
|
return ArgQuery{args};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1592,31 +1567,6 @@ std::vector<fs::path> Crafter::CleanProject(const fs::path& projectFile) {
|
||||||
return removed;
|
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<std::string> 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) {
|
static void PrintHelp(std::string_view argv0) {
|
||||||
std::println(
|
std::println(
|
||||||
R"(Usage:
|
R"(Usage:
|
||||||
|
|
@ -1624,10 +1574,8 @@ R"(Usage:
|
||||||
{0} test [test-options] [globs...] Build and run the project's tests
|
{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} 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} 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} clean Delete the project's bin/ and build/ trees
|
||||||
{0} help | -h | --help Show this help
|
{0} help | -h | --help Show this help
|
||||||
{0} --version Print the version and the compiler in use
|
|
||||||
|
|
||||||
Loads ./project.cpp (override with --project=<path>), compiles it to a shared
|
Loads ./project.cpp (override with --project=<path>), compiles it to a shared
|
||||||
object, and invokes its CrafterBuildProject() to obtain a Configuration that
|
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
|
transform. `format` rewrites changed files in place; `lint` reports the
|
||||||
same transforms as would-reformat findings without writing.
|
same transforms as would-reformat findings without writing.
|
||||||
|
|
||||||
Run (after the `run` subcommand):
|
|
||||||
<script> [args...] Run the named script. Everything after the name is
|
|
||||||
handed to the script untouched, including what
|
|
||||||
looks like an option of crafter-build itself, so
|
|
||||||
-v, -q, --project= and project args go before it.
|
|
||||||
--list List the project's scripts. A bare `run` does too.
|
|
||||||
|
|
||||||
Scripts are defined in project.cpp, as entries of cfg.scripts:
|
|
||||||
cfg.scripts.push_back({{"docs", [](Crafter::ScriptContext& ctx) {{
|
|
||||||
return ctx.Run("python3 tools/gen-docs.py");
|
|
||||||
}}}});
|
|
||||||
The callback returns the exit status. ctx.Run runs a shell command with the
|
|
||||||
terminal attached and returns its status; ctx.args holds the arguments and
|
|
||||||
ctx.projectDir the project's directory. Commands run in the directory
|
|
||||||
crafter-build was started from. Only the scripts of the project being run
|
|
||||||
are available, never a dependency's.
|
|
||||||
|
|
||||||
Clean (after the `clean` subcommand):
|
Clean (after the `clean` subcommand):
|
||||||
Removes bin/ and build/ next to the project file. Does not load project.cpp,
|
Removes bin/ and build/ next to the project file. Does not load project.cpp,
|
||||||
so it works when the project itself no longer compiles. Every target, variant
|
so it works when the project itself no longer compiles. Every target, variant
|
||||||
|
|
@ -1712,24 +1643,13 @@ Environment:
|
||||||
CRAFTER_BUILD_RUNNER_<TARGET> Default test runner for a target triple.
|
CRAFTER_BUILD_RUNNER_<TARGET> Default test runner for a target triple.
|
||||||
Replace '-' and '.' with '_' in the
|
Replace '-' and '.' with '_' in the
|
||||||
triple. CLI --runner= overrides this.
|
triple. CLI --runner= overrides this.
|
||||||
CRAFTER_BUILD_LLVM_DIR LLVM prefix to use instead of a toolchain
|
CRAFTER_MINGW_DIR Override mingw-w64 sysroot auto-detect.
|
||||||
installed beside crafter-build or PATH.
|
LIBCXX_DIR Windows libc++ install (MSVC ABI builds).
|
||||||
CRAFTER_BUILD_LIBCXX_DIR libc++ install to compile and link against
|
|
||||||
instead of the compiler's own.
|
|
||||||
CRAFTER_BUILD_LIBCLANG Exact libclang to load for lint.
|
|
||||||
CRAFTER_BUILD_WASI_SYSROOT WASI sysroot for wasm32 targets.
|
|
||||||
CRAFTER_MINGW_DIR mingw-w64 prefix (holding x86_64-w64-mingw32/
|
|
||||||
and lib/gcc/), passed as --sysroot. Default:
|
|
||||||
the mingw gcc on PATH; in the Flatpak, the
|
|
||||||
host's /usr when it has mingw-w64.
|
|
||||||
CRAFTER_BUILD_HOME Directory holding the module sources.
|
|
||||||
|
|
||||||
Exit status:
|
Exit status:
|
||||||
0 success / all non-skipped tests passed / lint clean / format applied
|
0 success / all non-skipped tests passed / lint clean / format applied
|
||||||
1 build failure, one or more tests failed, lint findings, no rules defined,
|
1 build failure, one or more tests failed, lint findings, no rules defined,
|
||||||
format write errors, or `format --check` found files that would change
|
format write errors, or `format --check` found files that would change
|
||||||
`run` exits with the status its script returned, and with 1 when there is no
|
|
||||||
such script, when the script throws, or when it returns a value outside 0..255
|
|
||||||
)", argv0);
|
)", argv0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1745,50 +1665,25 @@ std::int32_t Crafter::Run(std::int32_t argc, char** argv) {
|
||||||
bool runLint = false;
|
bool runLint = false;
|
||||||
bool runFormat = false;
|
bool runFormat = false;
|
||||||
bool runClean = false;
|
bool runClean = false;
|
||||||
bool runScript = false;
|
|
||||||
bool runAfterBuild = false;
|
bool runAfterBuild = false;
|
||||||
RunTestsOptions testOpts;
|
RunTestsOptions testOpts;
|
||||||
RunLintOptions lintOpts;
|
RunLintOptions lintOpts;
|
||||||
RunScriptOptions scriptOpts;
|
|
||||||
Progress::Verbosity verbosity = Progress::Verbosity::Default;
|
Progress::Verbosity verbosity = Progress::Verbosity::Default;
|
||||||
|
|
||||||
for (std::int32_t i = 1; i < argc; ++i) {
|
for (std::int32_t i = 1; i < argc; ++i) {
|
||||||
std::string_view arg = argv[i];
|
std::string_view arg = argv[i];
|
||||||
// The two `run` branches come before everything else on purpose.
|
if (arg == "-h" || arg == "--help" || (!runTests && !runLint && !runFormat && arg == "help")) {
|
||||||
// What follows a script's name belongs to the script, whatever
|
|
||||||
// it looks like: `run package --help` must not print this
|
|
||||||
// tool's help, `run package clean` must not delete build/, and
|
|
||||||
// none of it may reach projectArgs, where it would change the
|
|
||||||
// variant hash and so the directory the project builds into.
|
|
||||||
if (scriptOpts.named) {
|
|
||||||
scriptOpts.args.emplace_back(arg);
|
|
||||||
} else if (runScript && !arg.starts_with("-")) {
|
|
||||||
// The first word after `run`, even one that is a verb
|
|
||||||
// elsewhere. Options before it keep their usual meaning.
|
|
||||||
scriptOpts.name = arg;
|
|
||||||
scriptOpts.named = true;
|
|
||||||
} else if (arg == "--version") {
|
|
||||||
// Answered here, before the project file is looked for, so
|
|
||||||
// it works in any directory and is never forwarded to
|
|
||||||
// project.cpp as an unknown argument.
|
|
||||||
PrintVersion();
|
|
||||||
return 0;
|
|
||||||
} else if (arg == "-h" || arg == "--help" || (!runTests && !runLint && !runFormat && arg == "help")) {
|
|
||||||
PrintHelp(argv0);
|
PrintHelp(argv0);
|
||||||
return 0;
|
return 0;
|
||||||
} else if (!runLint && !runFormat && !runClean && arg == "test") {
|
} else if (!runLint && !runFormat && !runClean && arg == "test") {
|
||||||
runTests = true;
|
runTests = true;
|
||||||
} else if (!runTests && !runFormat && !runClean && arg == "lint") {
|
} else if (!runTests && !runFormat && !runClean && arg == "lint") {
|
||||||
runLint = true;
|
runLint = true;
|
||||||
} else if (!runTests && !runLint && !runFormat && arg == "clean") {
|
} else if (!runTests && !runLint && !runClean && arg == "clean") {
|
||||||
runClean = true;
|
runClean = true;
|
||||||
} else if (!runTests && !runLint && !runClean && arg == "format") {
|
} else if (!runTests && !runLint && !runClean && arg == "format") {
|
||||||
runFormat = true;
|
runFormat = true;
|
||||||
lintOpts.mode = LintMode::Apply;
|
lintOpts.mode = LintMode::Apply;
|
||||||
} else if (!runTests && !runLint && !runFormat && !runClean && arg == "run") {
|
|
||||||
runScript = true;
|
|
||||||
} else if (runScript && arg == "--list") {
|
|
||||||
scriptOpts.listOnly = true;
|
|
||||||
} else if (runFormat && arg == "--check") {
|
} else if (runFormat && arg == "--check") {
|
||||||
lintOpts.mode = LintMode::Check;
|
lintOpts.mode = LintMode::Check;
|
||||||
} else if (arg == "-r") {
|
} else if (arg == "-r") {
|
||||||
|
|
@ -1850,11 +1745,6 @@ std::int32_t Crafter::Run(std::int32_t argc, char** argv) {
|
||||||
Configuration config = LoadProject(projectFile, projectArgs);
|
Configuration config = LoadProject(projectFile, projectArgs);
|
||||||
SetParentProject(&config);
|
SetParentProject(&config);
|
||||||
|
|
||||||
if (runScript) {
|
|
||||||
scriptOpts.projectFile = fs::absolute(projectFile).lexically_normal();
|
|
||||||
return RunScript(config, scriptOpts);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (runLint || runFormat) {
|
if (runLint || runFormat) {
|
||||||
// lexically_normal: fs::absolute keeps a "./" component, which
|
// lexically_normal: fs::absolute keeps a "./" component, which
|
||||||
// would leak into displayed paths and the project-root derivation.
|
// would leak into displayed paths and the project-root derivation.
|
||||||
|
|
@ -1899,7 +1789,11 @@ std::int32_t Crafter::Run(std::int32_t argc, char** argv) {
|
||||||
if (config.target.starts_with("wasm32")) {
|
if (config.target.starts_with("wasm32")) {
|
||||||
const bool browserBuild = fs::exists(absDir / "index.html");
|
const bool browserBuild = fs::exists(absDir / "index.html");
|
||||||
auto have = [](std::string_view exe) {
|
auto have = [](std::string_view exe) {
|
||||||
|
#ifdef _WIN32
|
||||||
|
std::string probe = std::format("where {} > NUL 2>&1", exe);
|
||||||
|
#else
|
||||||
std::string probe = std::format("command -v {} > /dev/null 2>&1", exe);
|
std::string probe = std::format("command -v {} > /dev/null 2>&1", exe);
|
||||||
|
#endif
|
||||||
return std::system(probe.c_str()) == 0;
|
return std::system(probe.c_str()) == 0;
|
||||||
};
|
};
|
||||||
if (browserBuild) {
|
if (browserBuild) {
|
||||||
|
|
@ -1907,9 +1801,17 @@ std::int32_t Crafter::Run(std::int32_t argc, char** argv) {
|
||||||
// user has another dev server running we shift up rather
|
// user has another dev server running we shift up rather
|
||||||
// than letting caddy/python exit with EADDRINUSE.
|
// than letting caddy/python exit with EADDRINUSE.
|
||||||
auto findFreePort = [](std::int32_t basePort, std::int32_t span) -> std::int32_t {
|
auto findFreePort = [](std::int32_t basePort, std::int32_t span) -> std::int32_t {
|
||||||
|
#if defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc) || defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32)
|
||||||
|
static const bool WsaInit = []{ WSADATA d; return WSAStartup(MAKEWORD(2, 2), &d) == 0; }();
|
||||||
|
if (!WsaInit) return basePort;
|
||||||
|
using Sock = SOCKET;
|
||||||
|
const Sock invalid = INVALID_SOCKET;
|
||||||
|
auto closesock = [](Sock s){ closesocket(s); };
|
||||||
|
#else
|
||||||
using Sock = std::int32_t;
|
using Sock = std::int32_t;
|
||||||
const Sock invalid = -1;
|
const Sock invalid = -1;
|
||||||
auto closesock = [](Sock s){ ::close(s); };
|
auto closesock = [](Sock s){ ::close(s); };
|
||||||
|
#endif
|
||||||
for (std::int32_t p = basePort; p < basePort + span; ++p) {
|
for (std::int32_t p = basePort; p < basePort + span; ++p) {
|
||||||
Sock s = ::socket(AF_INET, SOCK_STREAM, 0);
|
Sock s = ::socket(AF_INET, SOCK_STREAM, 0);
|
||||||
if (s == invalid) return basePort;
|
if (s == invalid) return basePort;
|
||||||
|
|
@ -2032,7 +1934,11 @@ std::int32_t Crafter::Run(std::int32_t argc, char** argv) {
|
||||||
// Silence the backend's own banner/request logs so the
|
// Silence the backend's own banner/request logs so the
|
||||||
// experience is identical regardless of which server is
|
// experience is identical regardless of which server is
|
||||||
// picked — the line above is the only output.
|
// picked — the line above is the only output.
|
||||||
|
#ifdef _WIN32
|
||||||
|
cmd += " > NUL 2>&1";
|
||||||
|
#else
|
||||||
cmd += " > /dev/null 2>&1";
|
cmd += " > /dev/null 2>&1";
|
||||||
|
#endif
|
||||||
return std::system(cmd.c_str()) == 0 ? 0 : 1;
|
return std::system(cmd.c_str()) == 0 ? 0 : 1;
|
||||||
}
|
}
|
||||||
// wasi-cli wasm — needs a standalone runtime.
|
// wasi-cli wasm — needs a standalone runtime.
|
||||||
|
|
@ -2046,6 +1952,10 @@ std::int32_t Crafter::Run(std::int32_t argc, char** argv) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resolve to absolute — cmd.exe on Windows mishandles a leading
|
||||||
|
// "./" by trying to interpret it as a command. system() invokes
|
||||||
|
// through cmd /c, so the relative-prefixed path makes cmd error
|
||||||
|
// with "'.' is not recognized as an internal or external command".
|
||||||
// Run from the artifact's own directory so relative file opens
|
// Run from the artifact's own directory so relative file opens
|
||||||
// (shaders, assets copied alongside the exe via cfg.files) resolve
|
// (shaders, assets copied alongside the exe via cfg.files) resolve
|
||||||
// against the bin dir rather than the user's cwd. We exit the
|
// against the bin dir rather than the user's cwd. We exit the
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@
|
||||||
module Crafter.Build:External_impl;
|
module Crafter.Build:External_impl;
|
||||||
import std;
|
import std;
|
||||||
import :External;
|
import :External;
|
||||||
import :Toolchain;
|
|
||||||
import :Platform;
|
import :Platform;
|
||||||
import :Progress;
|
import :Progress;
|
||||||
import :Clang;
|
import :Clang;
|
||||||
|
|
@ -21,6 +20,29 @@ std::string DeriveName(const GitSource& source) {
|
||||||
return slash == std::string::npos ? url : url.substr(slash + 1);
|
return slash == std::string::npos ? url : url.substr(slash + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string ShellQuote(std::string_view s) {
|
||||||
|
// Windows cmd treats single quotes as literal characters — only double
|
||||||
|
// quotes work. Paths don't contain " in practice, so a simple wrap
|
||||||
|
// suffices; backslashes inside the path stay backslashes.
|
||||||
|
#ifdef _WIN32
|
||||||
|
std::string out = "\"";
|
||||||
|
for (char c : s) {
|
||||||
|
if (c == '"') out += "\\\"";
|
||||||
|
else out += c;
|
||||||
|
}
|
||||||
|
out += "\"";
|
||||||
|
return out;
|
||||||
|
#else
|
||||||
|
std::string out = "'";
|
||||||
|
for (char c : s) {
|
||||||
|
if (c == '\'') out += "'\\''";
|
||||||
|
else out += c;
|
||||||
|
}
|
||||||
|
out += "'";
|
||||||
|
return out;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
std::string JoinOptions(std::span<const std::string> options) {
|
std::string JoinOptions(std::span<const std::string> options) {
|
||||||
std::string out;
|
std::string out;
|
||||||
for (const std::string& opt : options) {
|
for (const std::string& opt : options) {
|
||||||
|
|
@ -36,18 +58,6 @@ std::string FetchGit(const GitSource& source, const fs::path& cloneDir) {
|
||||||
if (r.exitCode != 0) return std::format("`{}` failed (exit {}): {}", cmd, r.exitCode, r.output);
|
if (r.exitCode != 0) return std::format("`{}` failed (exit {}): {}", cmd, r.exitCode, r.output);
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
};
|
};
|
||||||
// Submodules follow the checkout only when asked to: after every clone,
|
|
||||||
// switch, checkout or pull, so they match the revision that was fetched
|
|
||||||
// instead of whichever one was checked out before, from the URLs that
|
|
||||||
// revision's .gitmodules gives (sync). A no-op, and offline, when they
|
|
||||||
// already match, which puts back submodules moved by hand; and no git at
|
|
||||||
// all without any.
|
|
||||||
auto updateSubmodules = [&runGit](const fs::path& dir) -> std::optional<std::string> {
|
|
||||||
if (!fs::exists(dir / ".gitmodules")) return std::nullopt;
|
|
||||||
const std::string quoted = ShellQuote(dir.string());
|
|
||||||
if (auto err = runGit(std::format("git -C {} submodule sync --recursive", quoted))) return err;
|
|
||||||
return runGit(std::format("git -C {} submodule update --init --recursive", quoted));
|
|
||||||
};
|
|
||||||
|
|
||||||
if (fs::exists(cloneDir)) {
|
if (fs::exists(cloneDir)) {
|
||||||
CommandResult remote = RunCommandChecked(std::format("git -C {} remote get-url origin", ShellQuote(cloneDir.string())));
|
CommandResult remote = RunCommandChecked(std::format("git -C {} remote get-url origin", ShellQuote(cloneDir.string())));
|
||||||
|
|
@ -63,9 +73,6 @@ std::string FetchGit(const GitSource& source, const fs::path& cloneDir) {
|
||||||
if (auto err = runGit(std::format("git -C {} fetch origin", ShellQuote(cloneDir.string())))) return *err;
|
if (auto err = runGit(std::format("git -C {} fetch origin", ShellQuote(cloneDir.string())))) return *err;
|
||||||
if (auto err = runGit(std::format("git -C {} checkout {}", ShellQuote(cloneDir.string()), ShellQuote(source.commit)))) return *err;
|
if (auto err = runGit(std::format("git -C {} checkout {}", ShellQuote(cloneDir.string()), ShellQuote(source.commit)))) return *err;
|
||||||
}
|
}
|
||||||
// Also when the commit already matched: a clone made before
|
|
||||||
// submodules were updated has them at the default branch's.
|
|
||||||
if (auto err = updateSubmodules(cloneDir)) return *err;
|
|
||||||
return "";
|
return "";
|
||||||
} else {
|
} else {
|
||||||
// No commit pinned — pull latest. The user opted into "track latest"
|
// No commit pinned — pull latest. The user opted into "track latest"
|
||||||
|
|
@ -76,7 +83,6 @@ std::string FetchGit(const GitSource& source, const fs::path& cloneDir) {
|
||||||
} else {
|
} else {
|
||||||
if (auto err = runGit(std::format("git -C {} pull", ShellQuote(cloneDir.string())))) return *err;
|
if (auto err = runGit(std::format("git -C {} pull", ShellQuote(cloneDir.string())))) return *err;
|
||||||
}
|
}
|
||||||
if (auto err = updateSubmodules(cloneDir)) return *err;
|
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -89,9 +95,7 @@ std::string FetchGit(const GitSource& source, const fs::path& cloneDir) {
|
||||||
fs::path tmpDir = cloneDir;
|
fs::path tmpDir = cloneDir;
|
||||||
tmpDir += std::format(".tmp.{:08x}{:08x}", rd(), rd());
|
tmpDir += std::format(".tmp.{:08x}{:08x}", rd(), rd());
|
||||||
|
|
||||||
// Not --recursive: that checks out the default branch's submodules, and
|
if (auto err = runGit(std::format("git clone --recursive {} {}", ShellQuote(source.url), ShellQuote(tmpDir.string())))) {
|
||||||
// the requested revision's come once it is checked out.
|
|
||||||
if (auto err = runGit(std::format("git clone {} {}", ShellQuote(source.url), ShellQuote(tmpDir.string())))) {
|
|
||||||
if (fs::exists(tmpDir)) fs::remove_all(tmpDir);
|
if (fs::exists(tmpDir)) fs::remove_all(tmpDir);
|
||||||
return *err;
|
return *err;
|
||||||
}
|
}
|
||||||
|
|
@ -107,10 +111,6 @@ std::string FetchGit(const GitSource& source, const fs::path& cloneDir) {
|
||||||
return *err;
|
return *err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (auto err = updateSubmodules(tmpDir)) {
|
|
||||||
fs::remove_all(tmpDir);
|
|
||||||
return *err;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::error_code ec;
|
std::error_code ec;
|
||||||
fs::rename(tmpDir, cloneDir, ec);
|
fs::rename(tmpDir, cloneDir, ec);
|
||||||
|
|
@ -131,25 +131,23 @@ std::string BuildInjectedCMakeFlags(const fs::path& cmakeBuildDir, std::string_v
|
||||||
// suffix to library names assuming a debug build, breaking -l<name>
|
// suffix to library names assuming a debug build, breaking -l<name>
|
||||||
// linking. Pin to Release.
|
// linking. Pin to Release.
|
||||||
out += " -DCMAKE_BUILD_TYPE=Release";
|
out += " -DCMAKE_BUILD_TYPE=Release";
|
||||||
const Toolchain& tc = GetToolchain();
|
out += " -DCMAKE_C_COMPILER=clang";
|
||||||
out += std::format(" -DCMAKE_C_COMPILER={}", ShellQuote(tc.clang.string()));
|
out += " -DCMAKE_CXX_COMPILER=clang++";
|
||||||
out += std::format(" -DCMAKE_CXX_COMPILER={}", ShellQuote(tc.clangxx.string()));
|
// Stdlib choice: libc++ for the Linux host build (matches how crafter-build
|
||||||
// Stdlib choice: libc++ for Linux targets (matches how crafter-build
|
// itself is built with -stdlib=libc++); mingw cross-compile uses libstdc++
|
||||||
// itself is built with -stdlib=libc++); the mingw cross-compile uses the
|
// shipped by the mingw-w64 toolchain so we leave the default; Windows
|
||||||
// libstdc++ shipped by the mingw-w64 toolchain, so it keeps the default.
|
// native (msvc target) similarly uses libc++ via LIBCXX_DIR.
|
||||||
|
#ifdef __linux__
|
||||||
if (target.ends_with("-linux-gnu") || target.ends_with("-linux-musl")) {
|
if (target.ends_with("-linux-gnu") || target.ends_with("-linux-musl")) {
|
||||||
out += std::format(" -DCMAKE_CXX_FLAGS={}", ShellQuote(std::format("-stdlib=libc++{}", tc.LibcxxCompileFlags())));
|
out += " -DCMAKE_CXX_FLAGS=-stdlib=libc++";
|
||||||
out += std::format(" -DCMAKE_EXE_LINKER_FLAGS={}", ShellQuote(std::format("-stdlib=libc++{}", tc.LibcxxLinkFlags())));
|
out += " -DCMAKE_EXE_LINKER_FLAGS=-stdlib=libc++";
|
||||||
out += std::format(" -DCMAKE_SHARED_LINKER_FLAGS={}", ShellQuote(std::format("-stdlib=libc++{}", tc.LibcxxLinkFlags())));
|
out += " -DCMAKE_SHARED_LINKER_FLAGS=-stdlib=libc++";
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
if (target == "x86_64-w64-mingw32") {
|
if (target == "x86_64-w64-mingw32") {
|
||||||
out += " -DCMAKE_SYSTEM_NAME=Windows";
|
out += " -DCMAKE_SYSTEM_NAME=Windows";
|
||||||
out += std::format(" -DCMAKE_C_COMPILER_TARGET={}", target);
|
out += std::format(" -DCMAKE_C_COMPILER_TARGET={}", target);
|
||||||
out += std::format(" -DCMAKE_CXX_COMPILER_TARGET={}", target);
|
out += std::format(" -DCMAKE_CXX_COMPILER_TARGET={}", target);
|
||||||
// The libstdc++ and the mingw-w64 install the project's own compiles
|
|
||||||
// use (GetBaseCommand).
|
|
||||||
out += " -DCMAKE_CXX_FLAGS=-stdlib=libstdc++";
|
|
||||||
if (!tc.mingwSysroot.empty()) out += std::format(" -DCMAKE_SYSROOT={}", ShellQuote(tc.mingwSysroot.string()));
|
|
||||||
}
|
}
|
||||||
fs::path absBuildDir = fs::absolute(cmakeBuildDir);
|
fs::path absBuildDir = fs::absolute(cmakeBuildDir);
|
||||||
out += std::format(" -DCMAKE_ARCHIVE_OUTPUT_DIRECTORY={}", ShellQuote(absBuildDir.string()));
|
out += std::format(" -DCMAKE_ARCHIVE_OUTPUT_DIRECTORY={}", ShellQuote(absBuildDir.string()));
|
||||||
|
|
@ -160,10 +158,6 @@ std::string BuildInjectedCMakeFlags(const fs::path& cmakeBuildDir, std::string_v
|
||||||
std::string ConfigureCMake(const fs::path& cloneDir, const fs::path& cmakeBuildDir, std::span<const std::string> options, std::string_view target) {
|
std::string ConfigureCMake(const fs::path& cloneDir, const fs::path& cmakeBuildDir, std::span<const std::string> options, std::string_view target) {
|
||||||
fs::path optionsFile = cmakeBuildDir / ".crafter-options";
|
fs::path optionsFile = cmakeBuildDir / ".crafter-options";
|
||||||
std::string optionsKey = JoinOptions(options);
|
std::string optionsKey = JoinOptions(options);
|
||||||
// The injected flags are inputs too: a different compiler or libc++
|
|
||||||
// must reconfigure rather than reuse a CMakeCache made with the old one.
|
|
||||||
const std::string injected = BuildInjectedCMakeFlags(cmakeBuildDir, target);
|
|
||||||
const std::string configureKey = injected + '\n' + optionsKey;
|
|
||||||
|
|
||||||
bool needsConfigure = !fs::exists(cmakeBuildDir / "CMakeCache.txt");
|
bool needsConfigure = !fs::exists(cmakeBuildDir / "CMakeCache.txt");
|
||||||
if (!needsConfigure) {
|
if (!needsConfigure) {
|
||||||
|
|
@ -173,7 +167,7 @@ std::string ConfigureCMake(const fs::path& cloneDir, const fs::path& cmakeBuildD
|
||||||
} else {
|
} else {
|
||||||
std::stringstream buf;
|
std::stringstream buf;
|
||||||
buf << existing.rdbuf();
|
buf << existing.rdbuf();
|
||||||
if (buf.str() != configureKey) needsConfigure = true;
|
if (buf.str() != optionsKey) needsConfigure = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -181,7 +175,7 @@ std::string ConfigureCMake(const fs::path& cloneDir, const fs::path& cmakeBuildD
|
||||||
|
|
||||||
if (!fs::exists(cmakeBuildDir)) fs::create_directories(cmakeBuildDir);
|
if (!fs::exists(cmakeBuildDir)) fs::create_directories(cmakeBuildDir);
|
||||||
|
|
||||||
std::string cmd = std::format("cmake -S {} -B {}{}", ShellQuote(fs::absolute(cloneDir).string()), ShellQuote(fs::absolute(cmakeBuildDir).string()), injected);
|
std::string cmd = std::format("cmake -S {} -B {}{}", ShellQuote(fs::absolute(cloneDir).string()), ShellQuote(fs::absolute(cmakeBuildDir).string()), BuildInjectedCMakeFlags(cmakeBuildDir, target));
|
||||||
if (!options.empty()) {
|
if (!options.empty()) {
|
||||||
cmd += " ";
|
cmd += " ";
|
||||||
cmd += optionsKey;
|
cmd += optionsKey;
|
||||||
|
|
@ -192,7 +186,7 @@ std::string ConfigureCMake(const fs::path& cloneDir, const fs::path& cmakeBuildD
|
||||||
return std::format("cmake configure failed (exit {}): {}", r.exitCode, r.output);
|
return std::format("cmake configure failed (exit {}): {}", r.exitCode, r.output);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::ofstream(optionsFile) << configureKey;
|
std::ofstream(optionsFile) << optionsKey;
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -253,9 +247,6 @@ ExternalBuildResult Crafter::BuildExternal(const ExternalDependency& dep, std::s
|
||||||
}
|
}
|
||||||
|
|
||||||
fs::path cloneDir = ExternalCloneDir(dep, target);
|
fs::path cloneDir = ExternalCloneDir(dep, target);
|
||||||
// Builds running side by side (tests built in parallel, two
|
|
||||||
// crafter-builds) share the clone: one fetches and builds it at a time.
|
|
||||||
FileLock lock(fs::path(std::format("{}.lock", cloneDir.string())));
|
|
||||||
|
|
||||||
std::string fetchErr = FetchGit(dep.source, cloneDir);
|
std::string fetchErr = FetchGit(dep.source, cloneDir);
|
||||||
if (!fetchErr.empty()) {
|
if (!fetchErr.empty()) {
|
||||||
|
|
@ -286,9 +277,9 @@ ExternalBuildResult Crafter::BuildExternal(const ExternalDependency& dep, std::s
|
||||||
// (runtime-loader). The rpath stays embedded in the produced
|
// (runtime-loader). The rpath stays embedded in the produced
|
||||||
// binary so it picks up shared deps from the cache without any
|
// binary so it picks up shared deps from the cache without any
|
||||||
// LD_LIBRARY_PATH gymnastics. Static deps (.a) ignore the rpath
|
// LD_LIBRARY_PATH gymnastics. Static deps (.a) ignore the rpath
|
||||||
// harmlessly. The PE/COFF target (mingw) resolves DLLs via PATH or
|
// harmlessly. PE/COFF targets (mingw, msvc) resolve DLLs via PATH
|
||||||
// adjacency rather than rpath, so lld warns if we pass it.
|
// or adjacency rather than rpath, so lld warns if we pass it.
|
||||||
const bool isPe = target == "x86_64-w64-mingw32";
|
const bool isPe = target == "x86_64-w64-mingw32" || target == "x86_64-pc-windows-msvc";
|
||||||
std::string buildDirAbs = fs::absolute(cmakeBuildDir).string();
|
std::string buildDirAbs = fs::absolute(cmakeBuildDir).string();
|
||||||
result.linkFlags.push_back(std::format("-L{}", buildDirAbs));
|
result.linkFlags.push_back(std::format("-L{}", buildDirAbs));
|
||||||
if (!isPe) result.linkFlags.push_back(std::format("-Wl,-rpath,{}", buildDirAbs));
|
if (!isPe) result.linkFlags.push_back(std::format("-Wl,-rpath,{}", buildDirAbs));
|
||||||
|
|
@ -302,20 +293,13 @@ ExternalBuildResult Crafter::BuildExternal(const ExternalDependency& dep, std::s
|
||||||
result.linkFlags.push_back(std::format("-l{}", lib));
|
result.linkFlags.push_back(std::format("-l{}", lib));
|
||||||
}
|
}
|
||||||
|
|
||||||
// The newest library in every directory the link searches: the build
|
if (fs::exists(cmakeBuildDir)) {
|
||||||
// directory and the libDirs, where projects like msquic put theirs,
|
for (const auto& entry : fs::directory_iterator(cmakeBuildDir)) {
|
||||||
// versioned names (libmsquic.so.2) included.
|
if (!entry.is_regular_file()) continue;
|
||||||
auto isLibrary = [](const std::string& name) {
|
const fs::path& p = entry.path();
|
||||||
return name.ends_with(".a") || name.ends_with(".so") || name.contains(".so.") || name.ends_with(".dll");
|
if (p.extension() != ".a" && p.extension() != ".so") continue;
|
||||||
};
|
|
||||||
std::vector<fs::path> searched = { cmakeBuildDir };
|
|
||||||
for (const fs::path& libDir : dep.libDirs) searched.push_back(libDir.is_absolute() ? libDir : cmakeBuildDir / libDir);
|
|
||||||
for (const fs::path& dir : searched) {
|
|
||||||
std::error_code listEc;
|
|
||||||
for (fs::directory_iterator it(dir, listEc), end; !listEc && it != end; it.increment(listEc)) {
|
|
||||||
std::error_code ec;
|
std::error_code ec;
|
||||||
if (!it->is_regular_file(ec) || !isLibrary(it->path().filename().string())) continue;
|
fs::file_time_type t = entry.last_write_time(ec);
|
||||||
fs::file_time_type t = it->last_write_time(ec);
|
|
||||||
if (!ec && t > result.latestArtifact) result.latestArtifact = t;
|
if (!ec && t > result.latestArtifact) result.latestArtifact = t;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -391,8 +375,6 @@ Configuration* Crafter::GitProject(const GitProjectSpec& spec) {
|
||||||
fs::path cloneDir = externalRoot / std::format("{}-{}", name, srcHash);
|
fs::path cloneDir = externalRoot / std::format("{}-{}", name, srcHash);
|
||||||
|
|
||||||
{
|
{
|
||||||
// One fetch of a clone at a time, as in BuildExternal.
|
|
||||||
FileLock lock(fs::path(std::format("{}.lock", cloneDir.string())));
|
|
||||||
const bool exists = fs::exists(cloneDir);
|
const bool exists = fs::exists(cloneDir);
|
||||||
Progress::Task task(std::format("{} {}", exists ? "Updating" : "Cloning", name));
|
Progress::Task task(std::format("{} {}", exists ? "Updating" : "Cloning", name));
|
||||||
if (std::string err = FetchGit(spec.source, cloneDir); !err.empty()) {
|
if (std::string err = FetchGit(spec.source, cloneDir); !err.empty()) {
|
||||||
|
|
@ -434,8 +416,6 @@ fs::path Crafter::GitFetch(const GitSource& source) {
|
||||||
fs::path cloneDir = externalRoot / std::format("{}-{:016x}", name, key);
|
fs::path cloneDir = externalRoot / std::format("{}-{:016x}", name, key);
|
||||||
|
|
||||||
{
|
{
|
||||||
// One fetch of a clone at a time, as in BuildExternal.
|
|
||||||
FileLock lock(fs::path(std::format("{}.lock", cloneDir.string())));
|
|
||||||
const bool exists = fs::exists(cloneDir);
|
const bool exists = fs::exists(cloneDir);
|
||||||
Progress::Task task(std::format("{} {}", exists ? "Updating" : "Cloning", name));
|
Progress::Task task(std::format("{} {}", exists ? "Updating" : "Cloning", name));
|
||||||
if (std::string err = FetchGit(source, cloneDir); !err.empty()) {
|
if (std::string err = FetchGit(source, cloneDir); !err.empty()) {
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,16 @@
|
||||||
|
|
||||||
module;
|
module;
|
||||||
#include <clang-c/Index.h>
|
#include <clang-c/Index.h>
|
||||||
|
#if defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc) || defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32)
|
||||||
|
#include <windows.h>
|
||||||
|
#else
|
||||||
#include <dlfcn.h>
|
#include <dlfcn.h>
|
||||||
|
#endif
|
||||||
export module Crafter.Build:Lint_impl;
|
export module Crafter.Build:Lint_impl;
|
||||||
import std;
|
import std;
|
||||||
import :Lint;
|
import :Lint;
|
||||||
import :Clang;
|
import :Clang;
|
||||||
import :Platform;
|
import :Platform;
|
||||||
import :Toolchain;
|
|
||||||
import :Progress;
|
import :Progress;
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
using namespace Crafter;
|
using namespace Crafter;
|
||||||
|
|
@ -17,13 +20,20 @@ using namespace Crafter;
|
||||||
namespace {
|
namespace {
|
||||||
// ---------------- libclang ----------------
|
// ---------------- libclang ----------------
|
||||||
//
|
//
|
||||||
// libclang is loaded at runtime rather than linked, so a plain build never
|
// libclang is loaded at runtime rather than linked. Linking -lclang would
|
||||||
// needs it and the binary carries no libclang.so.NN loader dependency; the
|
// break the mingw and MSVC cross-builds at link time and would put a
|
||||||
// clang-c header is used for its declarations only, and every call goes
|
// libclang.so.NN runtime dependency into the otherwise self-contained
|
||||||
// through a pointer resolved here. Failure to load is
|
// release tarballs; the clang-c header is used for its declarations only,
|
||||||
|
// and every call goes through a pointer resolved here. Failure to load is
|
||||||
// a hard error surfaced once by RunLint — there is deliberately no second,
|
// a hard error surfaced once by RunLint — there is deliberately no second,
|
||||||
// weaker lexer to fall back to, because two engines disagreeing about what
|
// weaker lexer to fall back to, because two engines disagreeing about what
|
||||||
// is a comment is a worse failure than not running.
|
// is a comment is a worse failure than not running.
|
||||||
|
#if defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc) || defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32)
|
||||||
|
using LibHandle = HMODULE;
|
||||||
|
LibHandle OpenLibrary(const std::string& name) { return LoadLibraryA(name.c_str()); }
|
||||||
|
void* LibrarySymbol(LibHandle handle, const std::string& name) { return reinterpret_cast<void*>(GetProcAddress(handle, name.c_str())); }
|
||||||
|
constexpr std::string_view LibClangNames[] = {"libclang.dll", "clang.dll"};
|
||||||
|
#else
|
||||||
using LibHandle = void*;
|
using LibHandle = void*;
|
||||||
LibHandle OpenLibrary(const std::string& name) { return dlopen(name.c_str(), RTLD_NOW | RTLD_LOCAL); }
|
LibHandle OpenLibrary(const std::string& name) { return dlopen(name.c_str(), RTLD_NOW | RTLD_LOCAL); }
|
||||||
void* LibrarySymbol(LibHandle handle, const std::string& name) { return dlsym(handle, name.c_str()); }
|
void* LibrarySymbol(LibHandle handle, const std::string& name) { return dlsym(handle, name.c_str()); }
|
||||||
|
|
@ -31,6 +41,7 @@ namespace {
|
||||||
"libclang.so", "libclang.so.22.1", "libclang.so.21.1", "libclang.so.20.1",
|
"libclang.so", "libclang.so.22.1", "libclang.so.21.1", "libclang.so.20.1",
|
||||||
"libclang.so.1", "libclang.dylib",
|
"libclang.so.1", "libclang.dylib",
|
||||||
};
|
};
|
||||||
|
#endif
|
||||||
|
|
||||||
// Signatures come from decltype on the header's declarations, so they can
|
// Signatures come from decltype on the header's declarations, so they can
|
||||||
// never drift from the real API. decltype is unevaluated, so naming the
|
// never drift from the real API. decltype is unevaluated, so naming the
|
||||||
|
|
@ -93,14 +104,14 @@ namespace {
|
||||||
LibClang LoadLibClang() {
|
LibClang LoadLibClang() {
|
||||||
LibClang lib;
|
LibClang lib;
|
||||||
std::vector<std::string> tried;
|
std::vector<std::string> tried;
|
||||||
// The toolchain pins libclang when it knows where it is: the
|
// CRAFTER_BUILD_LIBCLANG pins an exact path, mirroring the LIBCXX_DIR /
|
||||||
// CRAFTER_BUILD_LIBCLANG override, or the libclang.so beside the
|
// CRAFTER_MINGW_DIR overrides used elsewhere. It is exclusive: pointing
|
||||||
// compiler. A pin is exclusive: pointing the override at a broken
|
// it at a broken path must fail loudly rather than quietly succeed with
|
||||||
// path must fail loudly rather than quietly succeed with some other
|
// some other libclang, or the override is useless for diagnosing which
|
||||||
// libclang, or it is useless for diagnosing which library is in play.
|
// library is actually in play.
|
||||||
std::vector<std::string> candidates;
|
std::vector<std::string> candidates;
|
||||||
if (const std::optional<fs::path>& pinned = GetToolchain().libclang) {
|
if (const char* pinned = std::getenv("CRAFTER_BUILD_LIBCLANG"); pinned && *pinned) {
|
||||||
candidates.emplace_back(pinned->string());
|
candidates.emplace_back(pinned);
|
||||||
} else {
|
} else {
|
||||||
for (std::string_view name : LibClangNames) candidates.emplace_back(name);
|
for (std::string_view name : LibClangNames) candidates.emplace_back(name);
|
||||||
}
|
}
|
||||||
|
|
@ -120,7 +131,7 @@ namespace {
|
||||||
tried.push_back(name);
|
tried.push_back(name);
|
||||||
}
|
}
|
||||||
if (!lib.handle) {
|
if (!lib.handle) {
|
||||||
lib.error = std::format("could not load libclang (tried {}); install clang, set CRAFTER_BUILD_LLVM_DIR, or point CRAFTER_BUILD_LIBCLANG at it", join(tried));
|
lib.error = std::format("could not load libclang (tried {}); install clang, or point CRAFTER_BUILD_LIBCLANG at it", join(tried));
|
||||||
return lib;
|
return lib;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -366,8 +377,8 @@ namespace {
|
||||||
}
|
}
|
||||||
if (!args.empty()) args.erase(args.begin()); // argv[0] is the compiler
|
if (!args.empty()) args.erase(args.begin()); // argv[0] is the compiler
|
||||||
for (std::string& arg : args) {
|
for (std::string& arg : args) {
|
||||||
// Only \" unescapes; a backslash anywhere else is part of the
|
// Only \" unescapes. Erasing every backslash would destroy the
|
||||||
// argument.
|
// Windows include paths GetBaseCommand puts on the command line.
|
||||||
std::string unescaped;
|
std::string unescaped;
|
||||||
unescaped.reserve(arg.size());
|
unescaped.reserve(arg.size());
|
||||||
for (std::size_t i = 0; i < arg.size(); ++i) {
|
for (std::size_t i = 0; i < arg.size(); ++i) {
|
||||||
|
|
@ -711,7 +722,7 @@ namespace {
|
||||||
// explicitly. Cached like HostTarget, and for the same reason.
|
// explicitly. Cached like HostTarget, and for the same reason.
|
||||||
const std::string& ClangResourceDir() {
|
const std::string& ClangResourceDir() {
|
||||||
static const std::string Cached = []() -> std::string {
|
static const std::string Cached = []() -> std::string {
|
||||||
CommandResult r = RunCommandChecked(std::format("{} -print-resource-dir", GetToolchain().clangxx.string()));
|
CommandResult r = RunCommandChecked("clang++ -print-resource-dir");
|
||||||
if (r.exitCode != 0) return {};
|
if (r.exitCode != 0) return {};
|
||||||
std::string out = std::move(r.output);
|
std::string out = std::move(r.output);
|
||||||
while (!out.empty() && (out.back() == '\n' || out.back() == '\r')) out.pop_back();
|
while (!out.empty() && (out.back() == '\n' || out.back() == '\r')) out.pop_back();
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -4,8 +4,18 @@
|
||||||
module;
|
module;
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <unistd.h>
|
#if defined(_WIN32)
|
||||||
#include <sys/ioctl.h>
|
#include <io.h>
|
||||||
|
#include <windows.h>
|
||||||
|
#ifndef STDOUT_FILENO
|
||||||
|
#define STDOUT_FILENO _fileno(stdout)
|
||||||
|
#endif
|
||||||
|
#define CRAFTER_PROGRESS_ISATTY _isatty
|
||||||
|
#else
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <sys/ioctl.h>
|
||||||
|
#define CRAFTER_PROGRESS_ISATTY isatty
|
||||||
|
#endif
|
||||||
|
|
||||||
export module Crafter.Build:Progress_impl;
|
export module Crafter.Build:Progress_impl;
|
||||||
import std;
|
import std;
|
||||||
|
|
@ -22,10 +32,19 @@ namespace {
|
||||||
std::chrono::steady_clock::time_point StartTime = std::chrono::steady_clock::now();
|
std::chrono::steady_clock::time_point StartTime = std::chrono::steady_clock::now();
|
||||||
|
|
||||||
std::int32_t TerminalWidth() {
|
std::int32_t TerminalWidth() {
|
||||||
|
#if defined(_WIN32)
|
||||||
|
HANDLE h = GetStdHandle(STD_OUTPUT_HANDLE);
|
||||||
|
CONSOLE_SCREEN_BUFFER_INFO info;
|
||||||
|
if (h != INVALID_HANDLE_VALUE && GetConsoleScreenBufferInfo(h, &info)) {
|
||||||
|
std::int32_t w = info.srWindow.Right - info.srWindow.Left + 1;
|
||||||
|
if (w > 0) return w;
|
||||||
|
}
|
||||||
|
#else
|
||||||
winsize ws{};
|
winsize ws{};
|
||||||
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col > 0) {
|
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col > 0) {
|
||||||
return ws.ws_col;
|
return ws.ws_col;
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
if (const char* col = std::getenv("COLUMNS")) {
|
if (const char* col = std::getenv("COLUMNS")) {
|
||||||
try { std::int32_t c = std::stoi(col); if (c > 0) return c; } catch (...) {}
|
try { std::int32_t c = std::stoi(col); if (c > 0) return c; } catch (...) {}
|
||||||
}
|
}
|
||||||
|
|
@ -67,7 +86,7 @@ namespace Crafter::Progress {
|
||||||
void SetVerbosity(Verbosity v) {
|
void SetVerbosity(Verbosity v) {
|
||||||
std::lock_guard lock(StateMutex);
|
std::lock_guard lock(StateMutex);
|
||||||
ActiveVerbosity = v;
|
ActiveVerbosity = v;
|
||||||
IsTty = isatty(STDOUT_FILENO) != 0;
|
IsTty = CRAFTER_PROGRESS_ISATTY(STDOUT_FILENO) != 0;
|
||||||
StartTime = std::chrono::steady_clock::now();
|
StartTime = std::chrono::steady_clock::now();
|
||||||
Total.store(0);
|
Total.store(0);
|
||||||
Done.store(0);
|
Done.store(0);
|
||||||
|
|
|
||||||
|
|
@ -1,104 +0,0 @@
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
|
||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
||||||
|
|
||||||
module Crafter.Build:Script_impl;
|
|
||||||
import std;
|
|
||||||
import :Script;
|
|
||||||
import :Clang;
|
|
||||||
import :Platform;
|
|
||||||
import :Progress;
|
|
||||||
namespace fs = std::filesystem;
|
|
||||||
using namespace Crafter;
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
std::string JoinNames(std::span<const Script> scripts) {
|
|
||||||
std::string out;
|
|
||||||
for (const Script& script : scripts) {
|
|
||||||
if (!out.empty()) out += ", ";
|
|
||||||
out += script.name;
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::int32_t ScriptContext::Run(std::string_view command) const {
|
|
||||||
// Echoed by default, unlike a compile step: a script's commands are the
|
|
||||||
// only account of what it is doing to the machine.
|
|
||||||
if (Progress::GetVerbosity() != Progress::Verbosity::Quiet) std::println("$ {}", command);
|
|
||||||
return RunCommandAttached(command);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::int32_t Crafter::RunScript(Configuration& projectCfg, const RunScriptOptions& opts) {
|
|
||||||
// Loading the project may have left an in-place status line behind.
|
|
||||||
Progress::Clear();
|
|
||||||
|
|
||||||
const std::span<const Script> scripts = projectCfg.scripts;
|
|
||||||
if (scripts.empty()) {
|
|
||||||
std::println(std::cerr,
|
|
||||||
R"msg(No scripts defined.
|
|
||||||
Add them in project.cpp before returning the Configuration:
|
|
||||||
|
|
||||||
cfg.scripts.push_back({{"docs", [](Crafter::ScriptContext& ctx) {{
|
|
||||||
return ctx.Run("python3 tools/gen-docs.py");
|
|
||||||
}}}});
|
|
||||||
|
|
||||||
`crafter-build run docs [args...]` then runs it and exits with what it
|
|
||||||
returned; ctx.args holds the arguments.)msg");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checked for every script on every run, the list included, so a bad
|
|
||||||
// declaration is reported the first time anyone uses `run` and not only
|
|
||||||
// when that one name is asked for.
|
|
||||||
std::unordered_set<std::string_view> seen;
|
|
||||||
for (const Script& script : scripts) {
|
|
||||||
if (script.name.empty() || script.name.starts_with('-')) {
|
|
||||||
std::println(std::cerr, "script name '{}' cannot be run: a name must not be empty or start with '-'", script.name);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
// Running the first or the last of two would both be a guess, and
|
|
||||||
// the usual cause is a pasted line whose name was never changed.
|
|
||||||
if (!seen.insert(script.name).second) {
|
|
||||||
std::println(std::cerr, "script '{}' is defined twice", script.name);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if (!script.run) {
|
|
||||||
std::println(std::cerr, "script '{}' has no callback", script.name);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opts.listOnly || !opts.named) {
|
|
||||||
for (const Script& script : scripts) std::println("{}", script.name);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto found = std::ranges::find(scripts, opts.name, &Script::name);
|
|
||||||
if (found == scripts.end()) {
|
|
||||||
std::println(std::cerr, "No script named '{}'. Scripts: {}", opts.name, JoinNames(scripts));
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
ScriptContext ctx{
|
|
||||||
.project = projectCfg,
|
|
||||||
// lexically_normal: fs::absolute keeps a "./" component.
|
|
||||||
.projectDir = opts.projectFile.empty() ? fs::absolute(projectCfg.path).lexically_normal() : opts.projectFile.parent_path(),
|
|
||||||
.args = opts.args,
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
const std::int32_t code = found->run(ctx);
|
|
||||||
if (code >= 0 && code <= 255) return code;
|
|
||||||
std::println(std::cerr, "script '{}' returned {}, which is not an exit status (0..255); exiting with 1", found->name, code);
|
|
||||||
return 1;
|
|
||||||
} catch (const std::exception& e) {
|
|
||||||
// Never let a script's exception unwind across the project.so
|
|
||||||
// boundary and out of Run: report it as this script's failure.
|
|
||||||
Progress::Clear();
|
|
||||||
std::println(std::cerr, "script '{}' failed: {}", found->name, e.what());
|
|
||||||
return 1;
|
|
||||||
} catch (...) {
|
|
||||||
Progress::Clear();
|
|
||||||
std::println(std::cerr, "script '{}' failed: it threw something that isn't a std::exception", found->name);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -8,7 +8,6 @@ module;
|
||||||
#include "../lib/DirStackFileIncluder.h"
|
#include "../lib/DirStackFileIncluder.h"
|
||||||
module Crafter.Build:Shader_impl;
|
module Crafter.Build:Shader_impl;
|
||||||
import :Shader;
|
import :Shader;
|
||||||
import :Platform;
|
|
||||||
import std;
|
import std;
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
|
@ -32,72 +31,17 @@ namespace {
|
||||||
}
|
}
|
||||||
return EShLangVertex;
|
return EShLangVertex;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A path as a Make prerequisite, escaped the way NewestPrerequisite reads it.
|
|
||||||
std::string MakeEscaped(const std::string& path) {
|
|
||||||
std::string escaped;
|
|
||||||
for (const char c : path) {
|
|
||||||
if (c == ' ' || c == '\t' || c == '#') escaped += '\\';
|
|
||||||
if (c == '$') escaped += '$';
|
|
||||||
escaped += c;
|
|
||||||
}
|
|
||||||
return escaped;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string ReadFile(const fs::path& file) {
|
|
||||||
std::ifstream stream(file, std::ios::binary);
|
|
||||||
return std::string(std::istreambuf_iterator<char>(stream), std::istreambuf_iterator<char>());
|
|
||||||
}
|
|
||||||
|
|
||||||
// What a compile's output depends on besides files' contents, one line
|
|
||||||
// each: two entries of one source differ here, and an #include searched
|
|
||||||
// for from another source directory or in other directories can find
|
|
||||||
// another file.
|
|
||||||
std::string Settings(const Crafter::Shader& shader, std::span<const fs::path> includeDirs) {
|
|
||||||
std::string settings = std::format("source {}\nstage {}\nentry {}\n", fs::absolute(shader.path).string(), static_cast<int>(shader.type), shader.entrypoint);
|
|
||||||
for (const std::string& define : shader.defines) settings += std::format("define {}\n", define);
|
|
||||||
for (const fs::path& dir : includeDirs) settings += std::format("include {}\n", dir.string());
|
|
||||||
return settings;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Where Compile records, for Check, what the output in the bin dir was
|
|
||||||
// built from: beside the configuration's objects, as their depfiles are.
|
|
||||||
fs::path Record(const Crafter::Shader& shader, const fs::path& buildDir, std::string_view kind) {
|
|
||||||
return buildDir / std::format("{}.{}", shader.OutputName().string(), kind);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace Crafter {
|
namespace Crafter {
|
||||||
Shader::Shader(fs::path&& path, std::string&& entrypoint, ShaderType type) : path(std::move(path)), entrypoint(std::move(entrypoint)), type(type) {
|
Shader::Shader(fs::path&& path, std::string&& entrypoint, ShaderType type) : path(std::move(path)), entrypoint(std::move(entrypoint)), type(type) {
|
||||||
|
|
||||||
}
|
}
|
||||||
fs::path Shader::OutputName() const {
|
bool Shader::Check(const fs::path& outputDir) const {
|
||||||
// replace_extension swaps only the last extension, so a stage suffix
|
fs::path spv = outputDir / path.filename().replace_extension("spv");
|
||||||
// survives: "ui.comp.glsl" becomes "ui.comp.spv", or "ui.comp.sets.spv"
|
return fs::exists(spv) && fs::last_write_time(path) < fs::last_write_time(spv);
|
||||||
// for the variant "sets".
|
|
||||||
fs::path name = path.filename();
|
|
||||||
name.replace_extension(variant.empty() ? std::string("spv") : std::format("{}.spv", variant));
|
|
||||||
return name;
|
|
||||||
}
|
}
|
||||||
bool Shader::Check(const fs::path& outputDir, const fs::path& buildDir, std::span<const fs::path> includeDirs) const {
|
std::string Shader::Compile(const fs::path& outputDir, std::span<const fs::path> includeDirs) const {
|
||||||
std::error_code ec;
|
|
||||||
const fs::file_time_type built = fs::last_write_time(outputDir / OutputName(), ec);
|
|
||||||
if (ec) return false;
|
|
||||||
// The source and every file it #included, as the last compile
|
|
||||||
// recorded them: without a record, or with an included file since
|
|
||||||
// deleted, NewestPrerequisite reads max(), which is stale.
|
|
||||||
if (std::max(fs::last_write_time(path), NewestPrerequisite(Record(*this, buildDir, "d"))) >= built) return false;
|
|
||||||
return ReadFile(Record(*this, buildDir, "settings")) == Settings(*this, includeDirs);
|
|
||||||
}
|
|
||||||
std::string Shader::Compile(const fs::path& outputDir, const fs::path& buildDir, std::span<const fs::path> includeDirs) const {
|
|
||||||
// The records go first, so a compile that fails leaves the output
|
|
||||||
// stale, whatever it wrote.
|
|
||||||
const fs::path depFile = Record(*this, buildDir, "d");
|
|
||||||
const fs::path settingsFile = Record(*this, buildDir, "settings");
|
|
||||||
std::error_code removed;
|
|
||||||
fs::remove(depFile, removed);
|
|
||||||
fs::remove(settingsFile, removed);
|
|
||||||
|
|
||||||
EShLanguage glslangType = ToEShLanguage(type);
|
EShLanguage glslangType = ToEShLanguage(type);
|
||||||
glslang::InitializeProcess();
|
glslang::InitializeProcess();
|
||||||
|
|
||||||
|
|
@ -108,9 +52,6 @@ namespace Crafter {
|
||||||
auto fail = [&](std::string_view stage, std::string log) {
|
auto fail = [&](std::string_view stage, std::string log) {
|
||||||
glslang::FinalizeProcess();
|
glslang::FinalizeProcess();
|
||||||
std::string out = path.string();
|
std::string out = path.string();
|
||||||
// Several variants of one source compile in parallel; without the
|
|
||||||
// variant the message would not say which of them failed.
|
|
||||||
if (!variant.empty()) out += std::format(" ({})", variant);
|
|
||||||
out += ": ";
|
out += ": ";
|
||||||
out += stage;
|
out += stage;
|
||||||
if (!log.empty()) {
|
if (!log.empty()) {
|
||||||
|
|
@ -134,34 +75,8 @@ namespace Crafter {
|
||||||
const char* shaderSource = src.data();
|
const char* shaderSource = src.data();
|
||||||
const std::int32_t shaderSourceLen = static_cast<std::int32_t>(src.size());
|
const std::int32_t shaderSourceLen = static_cast<std::int32_t>(src.size());
|
||||||
|
|
||||||
// A variant is one component of a file name. Caught here, because a
|
|
||||||
// separator would otherwise surface as "failed to open SPIR-V output"
|
|
||||||
// after the whole compile.
|
|
||||||
if (variant.find_first_of("/\\") != std::string::npos) {
|
|
||||||
return fail(std::format("shader variant '{}' must not contain a path separator", variant), {});
|
|
||||||
}
|
|
||||||
|
|
||||||
// One #define line per entry, split like the compiler's -D: "NAME"
|
|
||||||
// defines it to 1, "NAME=VALUE" to VALUE. glslang places the preamble
|
|
||||||
// ahead of the whole source (it finds #version by scanning first), so
|
|
||||||
// line numbers in the user's file are unaffected. It keeps the
|
|
||||||
// pointer it is handed instead of copying, so the string is declared
|
|
||||||
// here to outlive parse() below.
|
|
||||||
std::string preamble;
|
|
||||||
for (const std::string& define : defines) {
|
|
||||||
const std::size_t eq = define.find('=');
|
|
||||||
const std::string name = define.substr(0, eq);
|
|
||||||
// An error inside the preamble is reported as "-1:<line>" with no
|
|
||||||
// file and no macro name, so reject the malformed ones by name.
|
|
||||||
if (name.empty() || define.find_first_of("\r\n") != std::string::npos) {
|
|
||||||
return fail(std::format("invalid shader define '{}': expected NAME or NAME=VALUE on one line", define), {});
|
|
||||||
}
|
|
||||||
preamble += std::format("#define {} {}\n", name, eq == std::string::npos ? std::string("1") : define.substr(eq + 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
glslang::TShader shader(glslangType);
|
glslang::TShader shader(glslangType);
|
||||||
shader.setStringsWithLengthsAndNames(&shaderSource, &shaderSourceLen, fileNameList, 1);
|
shader.setStringsWithLengthsAndNames(&shaderSource, &shaderSourceLen, fileNameList, 1);
|
||||||
if (!preamble.empty()) shader.setPreamble(preamble.c_str());
|
|
||||||
shader.setEntryPoint(entrypoint.c_str());
|
shader.setEntryPoint(entrypoint.c_str());
|
||||||
shader.setSourceEntryPoint(entrypoint.c_str());
|
shader.setSourceEntryPoint(entrypoint.c_str());
|
||||||
shader.setEnvTarget(glslang::EShTargetSpv, glslang::EShTargetSpv_1_4);
|
shader.setEnvTarget(glslang::EShTargetSpv, glslang::EShTargetSpv_1_4);
|
||||||
|
|
@ -195,7 +110,7 @@ namespace Crafter {
|
||||||
glslang::GlslangToSpv(*intermediate, spirv, &logger);
|
glslang::GlslangToSpv(*intermediate, spirv, &logger);
|
||||||
std::string spvLog = logger.getAllMessages();
|
std::string spvLog = logger.getAllMessages();
|
||||||
|
|
||||||
fs::path filename = OutputName();
|
fs::path filename = path.filename().replace_extension("spv");
|
||||||
std::ofstream file(outputDir/filename, std::ios::binary);
|
std::ofstream file(outputDir/filename, std::ios::binary);
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return fail("failed to open SPIR-V output", (outputDir/filename).string());
|
return fail("failed to open SPIR-V output", (outputDir/filename).string());
|
||||||
|
|
@ -205,25 +120,6 @@ namespace Crafter {
|
||||||
if (!spvLog.empty()) {
|
if (!spvLog.empty()) {
|
||||||
return fail("SPIR-V codegen reported issues", std::move(spvLog));
|
return fail("SPIR-V codegen reported issues", std::move(spvLog));
|
||||||
}
|
}
|
||||||
|
|
||||||
// What Check compares the output against next time: the source and
|
|
||||||
// the files it included, as absolute paths so that reading them back
|
|
||||||
// doesn't depend on the directory a build runs from, and the settings.
|
|
||||||
std::ofstream dep(depFile, std::ios::binary);
|
|
||||||
dep << MakeEscaped(fs::absolute(outputDir / filename).string()) << ": " << MakeEscaped(fs::absolute(path).string());
|
|
||||||
for (const std::string& included : includeDir.getIncludedFiles()) {
|
|
||||||
dep << " \\\n " << MakeEscaped(fs::absolute(included).string());
|
|
||||||
}
|
|
||||||
dep << '\n';
|
|
||||||
dep.close();
|
|
||||||
std::ofstream settings(settingsFile, std::ios::binary);
|
|
||||||
settings << Settings(*this, includeDirs);
|
|
||||||
settings.close();
|
|
||||||
if (!dep || !settings) {
|
|
||||||
fs::remove(depFile, removed);
|
|
||||||
fs::remove(settingsFile, removed);
|
|
||||||
return fail("failed to record what the shader was built from", buildDir.string());
|
|
||||||
}
|
|
||||||
glslang::FinalizeProcess();
|
glslang::FinalizeProcess();
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,11 +23,50 @@ namespace {
|
||||||
|
|
||||||
// MatchGlob/MatchAny live in :Platform — shared with the lint verb.
|
// MatchGlob/MatchAny live in :Platform — shared with the lint verb.
|
||||||
|
|
||||||
|
std::string ShellQuoteSh(std::string_view s) {
|
||||||
|
std::string out;
|
||||||
|
out.reserve(s.size() + 2);
|
||||||
|
out.push_back('\'');
|
||||||
|
for (char c : s) {
|
||||||
|
if (c == '\'') out += "'\\''";
|
||||||
|
else out.push_back(c);
|
||||||
|
}
|
||||||
|
out.push_back('\'');
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// cmd.exe doesn't recognize '...' as quoting (it would pass the single
|
||||||
|
// quotes through to the executable). Wrap in "..." for cmd; embedded "
|
||||||
|
// is rare in paths but escape it to be safe. Backslash sequences before
|
||||||
|
// " don't need the MS-CRT doubling rules because we go through
|
||||||
|
// `cmd /C "..."`, which uses cmd's parser, not the CRT's argv splitter.
|
||||||
|
std::string ShellQuoteCmd(std::string_view s) {
|
||||||
|
std::string out;
|
||||||
|
out.reserve(s.size() + 2);
|
||||||
|
out.push_back('"');
|
||||||
|
for (char c : s) {
|
||||||
|
if (c == '"') out += "\\\"";
|
||||||
|
else out.push_back(c);
|
||||||
|
}
|
||||||
|
out.push_back('"');
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Host-shell quoting: sh on Linux, cmd on Windows. For args/paths that
|
||||||
|
// hit the local shell (Local runner exec, Cmd-prefix runner exec).
|
||||||
|
std::string ShellQuoteHost(std::string_view s) {
|
||||||
|
#ifdef _WIN32
|
||||||
|
return ShellQuoteCmd(s);
|
||||||
|
#else
|
||||||
|
return ShellQuoteSh(s);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
std::string JoinAndQuoteArgs(std::span<const std::string> args) {
|
std::string JoinAndQuoteArgs(std::span<const std::string> args) {
|
||||||
std::string out;
|
std::string out;
|
||||||
for (const auto& a : args) {
|
for (const auto& a : args) {
|
||||||
if (!out.empty()) out.push_back(' ');
|
if (!out.empty()) out.push_back(' ');
|
||||||
out += ShellQuote(a);
|
out += ShellQuoteHost(a);
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
@ -152,7 +191,11 @@ TestRunner TestRunner::Cmd(std::string command) {
|
||||||
TestRunner r;
|
TestRunner r;
|
||||||
r.name = std::format("cmd:{}", command);
|
r.name = std::format("cmd:{}", command);
|
||||||
r.exec = std::format("{} {{bin}} {{args}}", command);
|
r.exec = std::format("{} {{bin}} {{args}}", command);
|
||||||
|
#ifdef _WIN32
|
||||||
|
r.probe = std::format("where {}", command);
|
||||||
|
#else
|
||||||
r.probe = std::format("which {}", command);
|
r.probe = std::format("which {}", command);
|
||||||
|
#endif
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -160,7 +203,11 @@ TestRunner TestRunner::Wine() {
|
||||||
TestRunner r;
|
TestRunner r;
|
||||||
r.name = "wine";
|
r.name = "wine";
|
||||||
r.exec = "wine {bin} {args}";
|
r.exec = "wine {bin} {args}";
|
||||||
|
#ifdef _WIN32
|
||||||
|
r.probe = "where wine";
|
||||||
|
#else
|
||||||
r.probe = "which wine";
|
r.probe = "which wine";
|
||||||
|
#endif
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -169,10 +216,13 @@ TestRunner TestRunner::ForTarget(const Configuration& cfg) {
|
||||||
|
|
||||||
// Same triple as the host → run the binary directly. Covers the common
|
// Same triple as the host → run the binary directly. Covers the common
|
||||||
// case (cfg.target defaulted to HostTarget()) without any wrapper.
|
// case (cfg.target defaulted to HostTarget()) without any wrapper.
|
||||||
if (SameTarget(target, HostTarget())) return Local();
|
if (target == HostTarget()) return Local();
|
||||||
|
|
||||||
// Windows targets run under Wine; there is no Windows host.
|
// Windows targets: native on a Windows host, Wine on Linux. We don't
|
||||||
if (TargetIsWindows(target)) return Wine();
|
// distinguish mingw vs msvc here — the produced .exe runs the same way.
|
||||||
|
if (TargetIsWindows(target)) {
|
||||||
|
return TargetIsWindows(HostTarget()) ? Local() : Wine();
|
||||||
|
}
|
||||||
|
|
||||||
// WASI: a .wasm file isn't directly executable; wasmtime is the canonical
|
// WASI: a .wasm file isn't directly executable; wasmtime is the canonical
|
||||||
// runtime. wasi-cli also works but the upstream Bytecode Alliance name is
|
// runtime. wasi-cli also works but the upstream Bytecode Alliance name is
|
||||||
|
|
@ -261,7 +311,7 @@ TestResult Crafter::RunSingleTest(const Test& test, const fs::path& binary, std:
|
||||||
|
|
||||||
if (test.runner.exec.empty()) {
|
if (test.runner.exec.empty()) {
|
||||||
// Local runner: spawn the binary directly through the host shell.
|
// Local runner: spawn the binary directly through the host shell.
|
||||||
std::string cmd = std::format("{} {}", ShellQuote(binary.string()), ph["{args}"]);
|
std::string cmd = std::format("{} {}", ShellQuoteHost(binary.string()), ph["{args}"]);
|
||||||
r = RunCommandWithTimeout(cmd, timeout);
|
r = RunCommandWithTimeout(cmd, timeout);
|
||||||
} else {
|
} else {
|
||||||
// Prefix runner (qemu-user, wasmtime, wine, ...): templated exec
|
// Prefix runner (qemu-user, wasmtime, wine, ...): templated exec
|
||||||
|
|
@ -287,7 +337,11 @@ TestResult Crafter::RunSingleTest(const Test& test, const fs::path& binary, std:
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
bool ToolOnPath(std::string_view name) {
|
bool ToolOnPath(std::string_view name) {
|
||||||
|
#ifdef _WIN32
|
||||||
|
std::string cmd = std::format("where {} > nul 2>&1", name);
|
||||||
|
#else
|
||||||
std::string cmd = std::format("which {} > /dev/null 2>&1", name);
|
std::string cmd = std::format("which {} > /dev/null 2>&1", name);
|
||||||
|
#endif
|
||||||
return std::system(cmd.c_str()) == 0;
|
return std::system(cmd.c_str()) == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -454,16 +508,13 @@ TestSummary Crafter::RunTests(Configuration& projectCfg, const RunTestsOptions&
|
||||||
// user having to know which targets exist. An explicit --target=X
|
// user having to know which targets exist. An explicit --target=X
|
||||||
// bypasses the sweep and runs that target only.
|
// bypasses the sweep and runs that target only.
|
||||||
if (opts.targetFilter.empty()) {
|
if (opts.targetFilter.empty()) {
|
||||||
// Keyed by the normalized triple so x86_64-pc-linux-gnu and
|
std::set<std::string> sweep;
|
||||||
// x86_64-unknown-linux-gnu make one pass; the first spelling seen is
|
sweep.insert(HostTarget());
|
||||||
// the one shown and filtered on.
|
|
||||||
std::map<std::string, std::string> sweep;
|
|
||||||
sweep.emplace(NormalizeTarget(HostTarget()), HostTarget());
|
|
||||||
for (const Test& t : projectCfg.tests) {
|
for (const Test& t : projectCfg.tests) {
|
||||||
if (!t.config.target.empty()) sweep.emplace(NormalizeTarget(t.config.target), t.config.target);
|
if (!t.config.target.empty()) sweep.insert(t.config.target);
|
||||||
}
|
}
|
||||||
TestSummary aggregate;
|
TestSummary aggregate;
|
||||||
for (const auto& [normalized, target] : sweep) {
|
for (const auto& target : sweep) {
|
||||||
RunTestsOptions perTarget = opts;
|
RunTestsOptions perTarget = opts;
|
||||||
perTarget.targetFilter = target;
|
perTarget.targetFilter = target;
|
||||||
if (sweep.size() > 1) {
|
if (sweep.size() > 1) {
|
||||||
|
|
@ -490,7 +541,7 @@ TestSummary Crafter::RunTests(Configuration& projectCfg, const RunTestsOptions&
|
||||||
std::vector<Test*> filtered;
|
std::vector<Test*> filtered;
|
||||||
filtered.reserve(projectCfg.tests.size());
|
filtered.reserve(projectCfg.tests.size());
|
||||||
for (auto& test : projectCfg.tests) {
|
for (auto& test : projectCfg.tests) {
|
||||||
if (!SameTarget(test.config.target, opts.targetFilter)) continue;
|
if (test.config.target != opts.targetFilter) continue;
|
||||||
if (!MatchAny(opts.globs, test.config.name)) continue;
|
if (!MatchAny(opts.globs, test.config.name)) continue;
|
||||||
test.runner = TestRunner::FromEnv(test.config.target, TestRunner::ForTarget(test.config));
|
test.runner = TestRunner::FromEnv(test.config.target, TestRunner::ForTarget(test.config));
|
||||||
if (opts.runnerOverride) {
|
if (opts.runnerOverride) {
|
||||||
|
|
|
||||||
|
|
@ -1,241 +0,0 @@
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
|
||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
||||||
|
|
||||||
module;
|
|
||||||
// Supplied on the command line by build.sh and project.cpp (from the
|
|
||||||
// CRAFTER_BUILD_VERSION environment variable or `git describe`); a build
|
|
||||||
// that passes nothing is a development build.
|
|
||||||
#ifndef CRAFTER_BUILD_VERSION
|
|
||||||
#define CRAFTER_BUILD_VERSION "0.0.0-dev"
|
|
||||||
#endif
|
|
||||||
module Crafter.Build:Toolchain_impl;
|
|
||||||
import std;
|
|
||||||
import :Toolchain;
|
|
||||||
import :Platform;
|
|
||||||
namespace fs = std::filesystem;
|
|
||||||
using namespace Crafter;
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
constexpr std::string_view EnvVars[] = {
|
|
||||||
"CRAFTER_BUILD_LLVM_DIR",
|
|
||||||
"CRAFTER_BUILD_LIBCXX_DIR",
|
|
||||||
"CRAFTER_BUILD_LIBCLANG",
|
|
||||||
"CRAFTER_BUILD_WASI_SYSROOT",
|
|
||||||
"CRAFTER_MINGW_DIR",
|
|
||||||
};
|
|
||||||
|
|
||||||
std::string_view Env(const ToolchainInputs& in, std::string_view name) {
|
|
||||||
auto it = in.env.find(name);
|
|
||||||
return it == in.env.end() ? std::string_view{} : std::string_view(it->second);
|
|
||||||
}
|
|
||||||
|
|
||||||
// "Arch Linux clang version 22.1.8" / "clang version 22.1.8" -> "22.1.8".
|
|
||||||
std::string ShortVersion(std::string_view versionLine) {
|
|
||||||
constexpr std::string_view Marker = "version ";
|
|
||||||
std::size_t at = versionLine.find(Marker);
|
|
||||||
if (at == std::string_view::npos) return "unknown";
|
|
||||||
std::string_view rest = versionLine.substr(at + Marker.size());
|
|
||||||
std::size_t end = rest.find_first_of(" \t(");
|
|
||||||
return std::string(rest.substr(0, end));
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string Trimmed(std::string s) {
|
|
||||||
while (!s.empty() && (s.back() == '\n' || s.back() == '\r' || s.back() == ' ')) s.pop_back();
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string Toolchain::LibcxxCompileFlags() const {
|
|
||||||
if (libcxxDir.empty()) return {};
|
|
||||||
return std::format(" -nostdinc++ -isystem {}", (libcxxDir / "include" / "c++" / "v1").string());
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string Toolchain::LibcxxLinkFlags() const {
|
|
||||||
// clang searches its per-target runtime directory and the system library
|
|
||||||
// paths for -lc++, not <prefix>/lib, so a toolchain outside /usr with the
|
|
||||||
// classic layout needs the search path as well as the run-time path.
|
|
||||||
fs::path lib;
|
|
||||||
if (!libcxxDir.empty()) {
|
|
||||||
lib = libcxxDir / "lib";
|
|
||||||
} else if (!prefix.empty() && prefix != "/usr") {
|
|
||||||
lib = prefix / "lib";
|
|
||||||
}
|
|
||||||
if (lib.empty()) return {};
|
|
||||||
return std::format(" -L{} -Wl,-rpath,{}", lib.string(), lib.string());
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string Toolchain::Key() const {
|
|
||||||
std::string identity;
|
|
||||||
for (const std::string& part : {clangxx.string(), clangVersion, libcxxDir.string(), stdCppm.string()}) {
|
|
||||||
identity += part;
|
|
||||||
identity += '\0';
|
|
||||||
}
|
|
||||||
return std::format("{}-{:08x}", ShortVersion(clangVersion), static_cast<std::uint32_t>(std::hash<std::string>{}(identity)));
|
|
||||||
}
|
|
||||||
|
|
||||||
Toolchain Crafter::ResolveToolchain(const ToolchainInputs& in) {
|
|
||||||
Toolchain tc;
|
|
||||||
|
|
||||||
// 1. The compiler: an explicit prefix, else one installed beside
|
|
||||||
// crafter-build, else PATH. An explicit prefix without a clang++ is an
|
|
||||||
// error rather than a fallback, for the same reason CRAFTER_BUILD_LIBCLANG
|
|
||||||
// is exclusive: an override that silently yields to something else
|
|
||||||
// cannot be used to diagnose which compiler is in play.
|
|
||||||
if (std::string_view dir = Env(in, "CRAFTER_BUILD_LLVM_DIR"); !dir.empty()) {
|
|
||||||
tc.prefix = fs::path(dir);
|
|
||||||
if (!fs::exists(tc.prefix / "bin" / "clang++")) {
|
|
||||||
tc.error = std::format("clang++ not found at {} (CRAFTER_BUILD_LLVM_DIR)", (tc.prefix / "bin" / "clang++").string());
|
|
||||||
}
|
|
||||||
} else if (!in.crafterPrefix.empty() && fs::exists(in.crafterPrefix / "bin" / "clang++")) {
|
|
||||||
tc.prefix = in.crafterPrefix;
|
|
||||||
}
|
|
||||||
if (tc.prefix.empty()) {
|
|
||||||
tc.clangxx = "clang++";
|
|
||||||
tc.clang = "clang";
|
|
||||||
tc.llvmAr = "llvm-ar";
|
|
||||||
} else {
|
|
||||||
tc.clangxx = tc.prefix / "bin" / "clang++";
|
|
||||||
tc.clang = tc.prefix / "bin" / "clang";
|
|
||||||
tc.llvmAr = tc.prefix / "bin" / "llvm-ar";
|
|
||||||
if (tc.prefix != "/usr" && fs::exists(tc.prefix / "include" / "clang-c" / "Index.h")) {
|
|
||||||
tc.llvmIncludeDir = tc.prefix / "include";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. libclang. The override is exclusive and unchecked on purpose: a
|
|
||||||
// broken path must fail loudly when dlopened, not be papered over.
|
|
||||||
if (std::string_view pinned = Env(in, "CRAFTER_BUILD_LIBCLANG"); !pinned.empty()) {
|
|
||||||
tc.libclang = fs::path(pinned);
|
|
||||||
} else if (!tc.prefix.empty() && fs::exists(tc.prefix / "lib" / "libclang.so")) {
|
|
||||||
tc.libclang = tc.prefix / "lib" / "libclang.so";
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. libc++ and its std module source. The compiler's own copy lives at
|
|
||||||
// <prefix>/share/libc++/v1 when the prefix carries libc++; the system
|
|
||||||
// copy is the last resort either way. The headers of the same install
|
|
||||||
// say which release the source belongs to.
|
|
||||||
fs::path libcxxRoot = "/usr";
|
|
||||||
if (std::string_view dir = Env(in, "CRAFTER_BUILD_LIBCXX_DIR"); !dir.empty()) {
|
|
||||||
tc.libcxxDir = fs::path(dir);
|
|
||||||
libcxxRoot = tc.libcxxDir;
|
|
||||||
} else if (!tc.prefix.empty() && fs::exists(tc.prefix / "share" / "libc++" / "v1" / "std.cppm")) {
|
|
||||||
libcxxRoot = tc.prefix;
|
|
||||||
}
|
|
||||||
tc.stdCppm = libcxxRoot / "share" / "libc++" / "v1" / "std.cppm";
|
|
||||||
tc.libcxxVersion = LibcxxVersion(libcxxRoot / "include" / "c++" / "v1");
|
|
||||||
if (!in.crafterPrefix.empty()) {
|
|
||||||
tc.stdModulesDir = in.crafterPrefix / "share" / "crafter-build" / "libc++-modules";
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Sysroots for the cross targets that have a conventional location.
|
|
||||||
if (std::string_view dir = Env(in, "CRAFTER_BUILD_WASI_SYSROOT"); !dir.empty()) {
|
|
||||||
tc.wasiSysroot = fs::path(dir);
|
|
||||||
} else if (!tc.prefix.empty() && fs::exists(tc.prefix / "share" / "wasi-sysroot")) {
|
|
||||||
tc.wasiSysroot = tc.prefix / "share" / "wasi-sysroot";
|
|
||||||
} else {
|
|
||||||
tc.wasiSysroot = "/usr/share/wasi-sysroot";
|
|
||||||
}
|
|
||||||
if (std::string_view dir = Env(in, "CRAFTER_MINGW_DIR"); !dir.empty()) {
|
|
||||||
tc.mingwSysroot = fs::path(dir);
|
|
||||||
} else if (!in.hostRoot.empty() && fs::exists(in.hostRoot / "usr" / "x86_64-w64-mingw32")) {
|
|
||||||
// The sandbox has no mingw-w64 of its own, and no mingw gcc on PATH
|
|
||||||
// for clang to find one by.
|
|
||||||
tc.mingwSysroot = in.hostRoot / "usr";
|
|
||||||
}
|
|
||||||
return tc;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Toolchain& Crafter::GetToolchain() {
|
|
||||||
static const Toolchain Resolved = []() {
|
|
||||||
ToolchainInputs in;
|
|
||||||
for (std::string_view name : EnvVars) {
|
|
||||||
if (const char* value = std::getenv(std::string(name).c_str()); value && *value) {
|
|
||||||
in.env.emplace(std::string(name), value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// GetCrafterBuildHome throws when no share/crafter-build is found
|
|
||||||
// near the executable; a toolchain can still come from PATH then.
|
|
||||||
try {
|
|
||||||
in.crafterPrefix = GetCrafterBuildHome().parent_path().parent_path();
|
|
||||||
} catch (const std::exception&) {}
|
|
||||||
if (fs::exists("/.flatpak-info")) in.hostRoot = "/run/host";
|
|
||||||
|
|
||||||
Toolchain tc = ResolveToolchain(in);
|
|
||||||
if (!tc.error.empty()) return tc;
|
|
||||||
|
|
||||||
CommandResult triple = RunCommandChecked(std::format("{} -print-target-triple", tc.clangxx.string()));
|
|
||||||
if (triple.exitCode != 0) {
|
|
||||||
tc.error = std::format("could not run {} (exit {}): {}", tc.clangxx.string(), triple.exitCode, Trimmed(triple.output));
|
|
||||||
if (tc.prefix.empty()) tc.error += "; install clang or set CRAFTER_BUILD_LLVM_DIR";
|
|
||||||
return tc;
|
|
||||||
}
|
|
||||||
tc.hostTriple = Trimmed(std::move(triple.output));
|
|
||||||
CommandResult version = RunCommandChecked(std::format("{} --version", tc.clangxx.string()));
|
|
||||||
if (version.exitCode == 0) {
|
|
||||||
std::string first = version.output.substr(0, version.output.find('\n'));
|
|
||||||
tc.clangVersion = Trimmed(std::move(first));
|
|
||||||
}
|
|
||||||
return tc;
|
|
||||||
}();
|
|
||||||
return Resolved;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Toolchain& Crafter::RequireToolchain() {
|
|
||||||
const Toolchain& tc = GetToolchain();
|
|
||||||
if (!tc.error.empty()) throw std::runtime_error(tc.error);
|
|
||||||
return tc;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::int32_t Crafter::LibcxxVersion(const fs::path& includeDir) {
|
|
||||||
// "# define _LIBCPP_VERSION 230102", the one place libc++ states its
|
|
||||||
// release in a form the preprocessor does not have to be run for.
|
|
||||||
constexpr std::string_view Marker = "define _LIBCPP_VERSION ";
|
|
||||||
std::ifstream config(includeDir / "__config");
|
|
||||||
std::string line;
|
|
||||||
while (std::getline(config, line)) {
|
|
||||||
const std::size_t at = line.find(Marker);
|
|
||||||
if (at == std::string::npos) continue;
|
|
||||||
const std::string_view digits = std::string_view(line).substr(at + Marker.size());
|
|
||||||
std::int32_t version = 0;
|
|
||||||
if (std::from_chars(digits.data(), digits.data() + digits.size(), version).ec == std::errc{}) return version;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string Crafter::LibcxxVersionString(std::int32_t version) {
|
|
||||||
return std::format("{}.{}.{}", version / 10000, version / 100 % 100, version % 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
StdModuleSource Crafter::SysrootStdModule(const Toolchain& tc, const fs::path& sysroot) {
|
|
||||||
const fs::path own = sysroot / "usr" / "share" / "libc++" / "v1" / "std.cppm";
|
|
||||||
if (fs::exists(own)) return {own, {}};
|
|
||||||
|
|
||||||
const fs::path includeDir = sysroot / "usr" / "include" / "c++" / "v1";
|
|
||||||
if (!fs::exists(includeDir / "__config")) {
|
|
||||||
return {{}, std::format("sysroot {} has no libc++ headers at {}; install its libc++ development package", sysroot.string(), includeDir.string())};
|
|
||||||
}
|
|
||||||
const std::int32_t wanted = LibcxxVersion(includeDir);
|
|
||||||
const bool comparable = wanted != 0 && tc.libcxxVersion != 0;
|
|
||||||
// Releases are matched on the major version: a release branch only takes
|
|
||||||
// fixes, so 22.1.3 headers and the 22.1.8 module source name the same
|
|
||||||
// declarations.
|
|
||||||
if (comparable && wanted / 10000 == tc.libcxxVersion / 10000 && fs::exists(tc.stdCppm)) return {tc.stdCppm, {}};
|
|
||||||
if (wanted != 0 && !tc.stdModulesDir.empty()) {
|
|
||||||
const fs::path shipped = tc.stdModulesDir / std::to_string(wanted / 10000) / "std.cppm";
|
|
||||||
if (fs::exists(shipped)) return {shipped, {}};
|
|
||||||
}
|
|
||||||
// One of the two releases is unknown, so there is nothing to compare:
|
|
||||||
// hand over the toolchain's source and let the compile decide, which is
|
|
||||||
// what happened for every sysroot before releases were compared.
|
|
||||||
if (!comparable && fs::exists(tc.stdCppm)) return {tc.stdCppm, {}};
|
|
||||||
|
|
||||||
const std::string sysrootRelease = wanted != 0 ? LibcxxVersionString(wanted) : std::string("of an unknown release");
|
|
||||||
const std::string toolchainSource = !fs::exists(tc.stdCppm)
|
|
||||||
? std::string("the toolchain has no std module source at all")
|
|
||||||
: std::format("the toolchain's is for libc++ {}", LibcxxVersionString(tc.libcxxVersion));
|
|
||||||
return {{}, std::format("sysroot {} carries libc++ {} without its std module source, and crafter-build has none for that release ({}); install the share/libc++/v1 directory of that libc++ release (std.cppm and std/) as {}", sysroot.string(), sysrootRelease, toolchainSource, own.parent_path().string())};
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string_view Crafter::CrafterBuildVersion() {
|
|
||||||
return CRAFTER_BUILD_VERSION;
|
|
||||||
}
|
|
||||||
|
|
@ -2,7 +2,16 @@
|
||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||||
|
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <unistd.h>
|
#if defined(_WIN32)
|
||||||
|
#include <io.h>
|
||||||
|
#ifndef STDOUT_FILENO
|
||||||
|
#define STDOUT_FILENO _fileno(stdout)
|
||||||
|
#endif
|
||||||
|
#define CRAFTER_MAIN_ISATTY _isatty
|
||||||
|
#else
|
||||||
|
#include <unistd.h>
|
||||||
|
#define CRAFTER_MAIN_ISATTY isatty
|
||||||
|
#endif
|
||||||
|
|
||||||
import std;
|
import std;
|
||||||
import Crafter.Build;
|
import Crafter.Build;
|
||||||
|
|
@ -17,7 +26,7 @@ int main(int argc, char** argv) {
|
||||||
// process blocks in its serve loop and never returns to flush. A TTY is
|
// process blocks in its serve loop and never returns to flush. A TTY is
|
||||||
// already line-buffered and its in-place redraw path flushes explicitly, so
|
// already line-buffered and its in-place redraw path flushes explicitly, so
|
||||||
// only adjust when redirected.
|
// only adjust when redirected.
|
||||||
if (isatty(STDOUT_FILENO) == 0) {
|
if (CRAFTER_MAIN_ISATTY(STDOUT_FILENO) == 0) {
|
||||||
std::setvbuf(stdout, nullptr, _IOLBF, 0);
|
std::setvbuf(stdout, nullptr, _IOLBF, 0);
|
||||||
}
|
}
|
||||||
return Crafter::Run(argc, argv);
|
return Crafter::Run(argc, argv);
|
||||||
|
|
|
||||||
14
interfaces/Crafter.Build-Api.h
Normal file
14
interfaces/Crafter.Build-Api.h
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#if defined(_WIN32)
|
||||||
|
#if defined(CRAFTER_BUILD_DLL_IMPORT)
|
||||||
|
#define CRAFTER_API __declspec(dllimport)
|
||||||
|
#else
|
||||||
|
#define CRAFTER_API __declspec(dllexport)
|
||||||
|
#endif
|
||||||
|
#else
|
||||||
|
#define CRAFTER_API
|
||||||
|
#endif
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||||
|
|
||||||
|
module;
|
||||||
|
#include "Crafter.Build-Api.h"
|
||||||
export module Crafter.Build:Asset;
|
export module Crafter.Build:Asset;
|
||||||
import std;
|
import std;
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
|
|
@ -17,7 +19,7 @@ namespace Crafter {
|
||||||
// that errors out at runtime if cfg.assets is non-empty — full
|
// that errors out at runtime if cfg.assets is non-empty — full
|
||||||
// support requires a self-host rebuild via crafter-build itself,
|
// support requires a self-host rebuild via crafter-build itself,
|
||||||
// which resolves the Crafter.Asset dep through project.cpp.
|
// which resolves the Crafter.Asset dep through project.cpp.
|
||||||
export std::string CompressAsset(const fs::path& input, const fs::path& output);
|
export CRAFTER_API std::string CompressAsset(const fs::path& input, const fs::path& output);
|
||||||
|
|
||||||
// Split a single .obj + .mtl file by material group into per-material
|
// Split a single .obj + .mtl file by material group into per-material
|
||||||
// .cmesh files, per-unique-albedo .ctex files, and a scene.txt manifest
|
// .cmesh files, per-unique-albedo .ctex files, and a scene.txt manifest
|
||||||
|
|
@ -34,7 +36,7 @@ namespace Crafter {
|
||||||
// Idempotent: returns "" immediately if outDir/scene.txt already exists,
|
// Idempotent: returns "" immediately if outDir/scene.txt already exists,
|
||||||
// so repeated builds skip the expensive split+resize step.
|
// so repeated builds skip the expensive split+resize step.
|
||||||
// Returns "" on success, an error string on failure.
|
// Returns "" on success, an error string on failure.
|
||||||
export std::string BuildOBJBundle(
|
export CRAFTER_API std::string BuildOBJBundle(
|
||||||
const fs::path& objPath,
|
const fs::path& objPath,
|
||||||
const fs::path& mtlPath,
|
const fs::path& mtlPath,
|
||||||
const fs::path& outDir,
|
const fs::path& outDir,
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,26 @@
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||||
|
|
||||||
|
module;
|
||||||
|
#include "Crafter.Build-Api.h"
|
||||||
export module Crafter.Build:Clang;
|
export module Crafter.Build:Clang;
|
||||||
import std;
|
import std;
|
||||||
import :Shader;
|
import :Shader;
|
||||||
import :Interface;
|
import :Interface;
|
||||||
import :Implementation;
|
import :Implementation;
|
||||||
import :External;
|
import :External;
|
||||||
import :Toolchain;
|
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
export namespace Crafter {
|
export namespace Crafter {
|
||||||
struct BuildResult {
|
struct BuildResult {
|
||||||
std::string result;
|
std::string result;
|
||||||
bool repack;
|
bool repack;
|
||||||
// Sets in order: the commands they go into are compared with the ones
|
std::unordered_set<std::string> libs;
|
||||||
// recorded last time, so they come out the same however the
|
|
||||||
// dependency threads that fill them happen to finish.
|
|
||||||
std::set<std::string> libs;
|
|
||||||
// Compile flags (typically -I include paths) the dep wants its
|
// Compile flags (typically -I include paths) the dep wants its
|
||||||
// consumers to see — sourced from its external dependencies' include
|
// consumers to see — sourced from its external dependencies' include
|
||||||
// dirs so headers a dep exposes in its public modules are reachable
|
// dirs so headers a dep exposes in its public modules are reachable
|
||||||
// when a consumer #includes them directly. Propagates transitively.
|
// when a consumer #includes them directly. Propagates transitively.
|
||||||
std::set<std::string> publicCompileFlags;
|
std::unordered_set<std::string> publicCompileFlags;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Define {
|
struct Define {
|
||||||
|
|
@ -67,28 +65,30 @@ export namespace Crafter {
|
||||||
|
|
||||||
bool IsLocal() const { return exec.empty(); }
|
bool IsLocal() const { return exec.empty(); }
|
||||||
|
|
||||||
static TestRunner Local();
|
static CRAFTER_API TestRunner Local();
|
||||||
// Prefix runner: wraps the local binary in `<command> {bin} {args}`.
|
// Prefix runner: wraps the local binary in `<command> {bin} {args}`.
|
||||||
// Used for qemu-user, wasmtime, and similar single-binary wrappers.
|
// Used for qemu-user, wasmtime, and similar single-binary wrappers.
|
||||||
static TestRunner Cmd(std::string command);
|
static CRAFTER_API TestRunner Cmd(std::string command);
|
||||||
// Run a Windows .exe through Wine. Probes `wine` on PATH.
|
// Run a Windows .exe through Wine. Probes `wine` on PATH; on a Windows
|
||||||
static TestRunner Wine();
|
// host the wine wrapper is pointless, so callers should route to Local
|
||||||
|
// before reaching here.
|
||||||
|
static CRAFTER_API TestRunner Wine();
|
||||||
// Parse a `<kind>[:<arg>]` spec used by CRAFTER_BUILD_RUNNER_<target>
|
// Parse a `<kind>[:<arg>]` spec used by CRAFTER_BUILD_RUNNER_<target>
|
||||||
// and --runner=. Supported: "local", "cmd:<binary>". Returns nullopt
|
// and --runner=. Supported: "local", "cmd:<binary>". Returns nullopt
|
||||||
// for an empty string; throws on a non-empty unrecognized spec.
|
// for an empty string; throws on a non-empty unrecognized spec.
|
||||||
static std::optional<TestRunner> FromSpec(std::string_view spec);
|
static CRAFTER_API std::optional<TestRunner> FromSpec(std::string_view spec);
|
||||||
// Honor CRAFTER_BUILD_RUNNER_<target> (power-user override). Triple
|
// Honor CRAFTER_BUILD_RUNNER_<target> (power-user override). Triple
|
||||||
// dashes/dots become underscores so they're valid in env-var names.
|
// dashes/dots become underscores so they're valid in env-var names.
|
||||||
// Returns `fallback` when the env var is unset.
|
// Returns `fallback` when the env var is unset.
|
||||||
static TestRunner FromEnv(std::string_view target, TestRunner fallback = Local());
|
static CRAFTER_API TestRunner FromEnv(std::string_view target, TestRunner fallback = Local());
|
||||||
// Derive a runner from a Configuration's target triple + sysroot.
|
// Derive a runner from a Configuration's target triple + sysroot.
|
||||||
// Returns Local() when target equals the host, Wine() for Windows
|
// Returns Local() when target equals the host, Wine() for Windows
|
||||||
// targets, `qemu-<arch>` (with QEMU_LD_PREFIX
|
// targets on a non-Windows host, `qemu-<arch>` (with QEMU_LD_PREFIX
|
||||||
// set when cfg.sysroot is non-empty) for non-host -linux- triples,
|
// set when cfg.sysroot is non-empty) for non-host -linux- triples,
|
||||||
// `wasmtime` for wasm32-wasi/wasm64-wasi, and Local() as a last
|
// `wasmtime` for wasm32-wasi/wasm64-wasi, and Local() as a last
|
||||||
// resort. CRAFTER_BUILD_RUNNER_<target> still wins as an override
|
// resort. CRAFTER_BUILD_RUNNER_<target> still wins as an override
|
||||||
// upstream of this — see FromEnv.
|
// upstream of this — see FromEnv.
|
||||||
static TestRunner ForTarget(const struct Configuration& cfg);
|
static CRAFTER_API TestRunner ForTarget(const struct Configuration& cfg);
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class TestOutcome { Pass, Fail, Crash, Timeout, Skipped };
|
enum class TestOutcome { Pass, Fail, Crash, Timeout, Skipped };
|
||||||
|
|
@ -233,18 +233,17 @@ export namespace Crafter {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Per-file view handed to each LintRule's check callback. Every member
|
// Per-file view handed to each LintRule's check callback. Every member
|
||||||
// function is out-of-line (defined in Crafter.Build:Lint's implementation
|
// function is out-of-line and CRAFTER_API (defined in Crafter.Build:Lint's
|
||||||
// unit) because rule lambdas execute from the user's project.so and
|
// implementation unit) because rule lambdas execute from the user's
|
||||||
// resolve against the running executable: clang does not emit
|
// project DLL on Windows — clang does not emit module-attached in-class
|
||||||
// module-attached in-class inline bodies into consumers (see ArgQuery
|
// inline bodies into consumers (see ArgQuery below).
|
||||||
// below).
|
|
||||||
struct LintContext {
|
struct LintContext {
|
||||||
fs::path file; // absolute path of the file under lint
|
fs::path file; // absolute path of the file under lint
|
||||||
std::string content; // whole file as read from disk
|
std::string content; // whole file as read from disk
|
||||||
std::vector<std::string_view> lines; // views into `content`, one per line, no '\n'
|
std::vector<std::string_view> lines; // views into `content`, one per line, no '\n'
|
||||||
|
|
||||||
std::string Extension() const; // ".cppm", ".cpp", ".h", ...
|
CRAFTER_API std::string Extension() const; // ".cppm", ".cpp", ".h", ...
|
||||||
std::string_view Line(std::size_t n) const; // 1-based; empty if out of range
|
CRAFTER_API std::string_view Line(std::size_t n) const; // 1-based; empty if out of range
|
||||||
// `content` with comments and string/char literal bodies blanked to
|
// `content` with comments and string/char literal bodies blanked to
|
||||||
// spaces, newlines preserved — offsets and line numbers stay valid.
|
// spaces, newlines preserved — offsets and line numbers stay valid.
|
||||||
// Built from Tokens() on first call, cached per file, so raw strings,
|
// Built from Tokens() on first call, cached per file, so raw strings,
|
||||||
|
|
@ -255,9 +254,9 @@ export namespace Crafter {
|
||||||
// Convenient for a quick scan, but Tokens() is the better tool for
|
// Convenient for a quick scan, but Tokens() is the better tool for
|
||||||
// anything structural: this view cannot tell an identifier from a
|
// anything structural: this view cannot tell an identifier from a
|
||||||
// keyword, and it has already thrown away where the literals were.
|
// keyword, and it has already thrown away where the literals were.
|
||||||
const std::string& CommentStripped();
|
CRAFTER_API const std::string& CommentStripped();
|
||||||
// Record a finding at `line` (1-based; pass 0 for a whole-file finding).
|
// Record a finding at `line` (1-based; pass 0 for a whole-file finding).
|
||||||
void Report(std::size_t line, std::string message);
|
CRAFTER_API void Report(std::size_t line, std::string message);
|
||||||
// Replace the file's content. Makes this rule a *transform*:
|
// Replace the file's content. Makes this rule a *transform*:
|
||||||
// `crafter-build format` writes the result back to disk; `lint`
|
// `crafter-build format` writes the result back to disk; `lint`
|
||||||
// derives would-reformat findings from it (dry — never writes).
|
// derives would-reformat findings from it (dry — never writes).
|
||||||
|
|
@ -266,7 +265,7 @@ export namespace Crafter {
|
||||||
// pattern: build the new string, call SetContent once at the end. Do
|
// pattern: build the new string, call SetContent once at the end. Do
|
||||||
// not assign `content` directly — that bypasses the re-split. May be
|
// not assign `content` directly — that bypasses the re-split. May be
|
||||||
// combined with Report() in the same rule.
|
// combined with Report() in the same rule.
|
||||||
void SetContent(std::string newContent);
|
CRAFTER_API void SetContent(std::string newContent);
|
||||||
// True when `rule` is suppressed at `line` (1-based; 0 = whole-file,
|
// True when `rule` is suppressed at `line` (1-based; 0 = whole-file,
|
||||||
// which only file-level directives cover) by a suppression comment:
|
// which only file-level directives cover) by a suppression comment:
|
||||||
// // lint-disable-next-line [rules...] applies to the following line
|
// // lint-disable-next-line [rules...] applies to the following line
|
||||||
|
|
@ -278,7 +277,7 @@ export namespace Crafter {
|
||||||
// itself for every line its edit touches (the driver cannot map lines
|
// itself for every line its edit touches (the driver cannot map lines
|
||||||
// across a count-changing rewrite). Parsed lazily from the raw lines;
|
// across a count-changing rewrite). Parsed lazily from the raw lines;
|
||||||
// re-parsed after SetContent.
|
// re-parsed after SetContent.
|
||||||
bool Suppressed(std::string_view rule, std::size_t line);
|
CRAFTER_API bool Suppressed(std::string_view rule, std::size_t line);
|
||||||
|
|
||||||
// The file lexed by clang, in source order. Built on first call, cached
|
// The file lexed by clang, in source order. Built on first call, cached
|
||||||
// per file, re-lexed after SetContent. Empty for extensions that are
|
// per file, re-lexed after SetContent. Empty for extensions that are
|
||||||
|
|
@ -288,16 +287,16 @@ export namespace Crafter {
|
||||||
// raw strings, line splices, digraphs and nested quoting right, and
|
// raw strings, line splices, digraphs and nested quoting right, and
|
||||||
// offsets index straight into `content`, so a transform can find in
|
// offsets index straight into `content`, so a transform can find in
|
||||||
// the token stream and edit in place.
|
// the token stream and edit in place.
|
||||||
std::span<const LintToken> Tokens();
|
CRAFTER_API std::span<const LintToken> Tokens();
|
||||||
// The token's own bytes: content.substr(tok.offset, tok.length).
|
// The token's own bytes: content.substr(tok.offset, tok.length).
|
||||||
std::string_view TokenText(const LintToken& token) const;
|
CRAFTER_API std::string_view TokenText(const LintToken& token) const;
|
||||||
// Tokens whose FIRST byte is on `line` (1-based). A token that starts
|
// Tokens whose FIRST byte is on `line` (1-based). A token that starts
|
||||||
// earlier and spans into `line` — a raw string, a block comment — is
|
// earlier and spans into `line` — a raw string, a block comment — is
|
||||||
// not included; ask Tokens() directly when that matters.
|
// not included; ask Tokens() directly when that matters.
|
||||||
std::span<const LintToken> TokensOnLine(std::size_t line);
|
CRAFTER_API std::span<const LintToken> TokensOnLine(std::size_t line);
|
||||||
// True when a comment token starts on `line`. Replaces `Line(n).contains("//")`,
|
// True when a comment token starts on `line`. Replaces `Line(n).contains("//")`,
|
||||||
// which false-positives on a `//` inside a string literal.
|
// which false-positives on a `//` inside a string literal.
|
||||||
bool LineHasComment(std::size_t line);
|
CRAFTER_API bool LineHasComment(std::size_t line);
|
||||||
// True when `line` (1-based) is touched by a token that spans more
|
// True when `line` (1-based) is touched by a token that spans more
|
||||||
// than one line — a raw string or a block comment. A transform that
|
// than one line — a raw string or a block comment. A transform that
|
||||||
// joins, splits or rewrites such a line changes what is inside that
|
// joins, splits or rewrites such a line changes what is inside that
|
||||||
|
|
@ -305,7 +304,7 @@ export namespace Crafter {
|
||||||
// Replaces `Line(n).contains("R\"")`, which both misses raw strings
|
// Replaces `Line(n).contains("R\"")`, which both misses raw strings
|
||||||
// opened on an earlier line and fires on the characters R" appearing
|
// opened on an earlier line and fires on the characters R" appearing
|
||||||
// inside an ordinary literal.
|
// inside an ordinary literal.
|
||||||
bool LineHasMultiLineToken(std::size_t line);
|
CRAFTER_API bool LineHasMultiLineToken(std::size_t line);
|
||||||
|
|
||||||
// Declarations written in this file, flattened depth-first with
|
// Declarations written in this file, flattened depth-first with
|
||||||
// parents before children (see LintDecl). Empty when the AST is not
|
// parents before children (see LintDecl). Empty when the AST is not
|
||||||
|
|
@ -322,11 +321,11 @@ export namespace Crafter {
|
||||||
// slice. Declarations inside a preprocessor branch that is inactive
|
// slice. Declarations inside a preprocessor branch that is inactive
|
||||||
// for the host are absent here, though Tokens() still sees them. Rules
|
// for the host are absent here, though Tokens() still sees them. Rules
|
||||||
// that must cover every platform belong on tokens.
|
// that must cover every platform belong on tokens.
|
||||||
std::span<const LintDecl> Decls();
|
CRAFTER_API std::span<const LintDecl> Decls();
|
||||||
bool AstAvailable();
|
CRAFTER_API bool AstAvailable();
|
||||||
// Why AstAvailable() is false — a missing PCM, a parse error, a file
|
// Why AstAvailable() is false — a missing PCM, a parse error, a file
|
||||||
// that is not a translation unit. Empty when the AST is available.
|
// that is not a translation unit. Empty when the AST is available.
|
||||||
std::string_view AstUnavailableReason();
|
CRAFTER_API std::string_view AstUnavailableReason();
|
||||||
|
|
||||||
// Driver wiring — set by RunLint before each check call. Not for rules.
|
// Driver wiring — set by RunLint before each check call. Not for rules.
|
||||||
std::string activeRule;
|
std::string activeRule;
|
||||||
|
|
@ -359,60 +358,12 @@ export namespace Crafter {
|
||||||
bool needsAst = false;
|
bool needsAst = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Configuration;
|
// The host target triple, detected once per process by running
|
||||||
|
// `clang++ -print-target-triple` and cached. Used as the default for
|
||||||
// What a Script's callback is handed when `crafter-build run <name>`
|
|
||||||
// starts it.
|
|
||||||
struct ScriptContext {
|
|
||||||
// The Configuration project.cpp returned. Read the project through
|
|
||||||
// this rather than capturing the local in CrafterBuildProject by
|
|
||||||
// reference: that local is gone by the time a script runs.
|
|
||||||
Configuration& project;
|
|
||||||
// Absolute directory of the project file. Commands run in the
|
|
||||||
// directory crafter-build was started from, like everything else it
|
|
||||||
// does, so a script names its files through this.
|
|
||||||
fs::path projectDir;
|
|
||||||
// Everything the user typed after the script's name, untouched:
|
|
||||||
// crafter-build interprets none of it.
|
|
||||||
std::span<const std::string> args;
|
|
||||||
// Run `command` through sh with the terminal attached — output
|
|
||||||
// appears as it is produced, the command can prompt, Ctrl-C reaches
|
|
||||||
// it — after echoing it as `$ command` (not under -q). Returns its
|
|
||||||
// exit code, 128+signal when it was killed; return that from the
|
|
||||||
// script to make it the exit code of crafter-build. Quote anything
|
|
||||||
// that is not a literal with ShellQuote. Out-of-line like
|
|
||||||
// LintContext's members: the callback runs from project.so.
|
|
||||||
std::int32_t Run(std::string_view command) const;
|
|
||||||
};
|
|
||||||
|
|
||||||
// A project script: a chore that belongs to the project but is not part
|
|
||||||
// of building it (packaging, installing, publishing, serving), run by
|
|
||||||
// name with `crafter-build run <name> [args...]`. `run` returns the
|
|
||||||
// process exit code. It is only stored when project.cpp is loaded, which
|
|
||||||
// happens for every verb and for every project that depends on this one,
|
|
||||||
// so whatever a script needs to probe it probes when it runs. Only the
|
|
||||||
// scripts of the project crafter-build was pointed at can be run, never
|
|
||||||
// a dependency's.
|
|
||||||
struct Script {
|
|
||||||
std::string name;
|
|
||||||
std::function<std::int32_t(ScriptContext&)> run;
|
|
||||||
};
|
|
||||||
|
|
||||||
// The host target triple: what the resolved toolchain's clang++ prints
|
|
||||||
// for -print-target-triple (see GetToolchain). Used as the default for
|
|
||||||
// Configuration::target so projects don't have to hardcode it for the
|
// Configuration::target so projects don't have to hardcode it for the
|
||||||
// no-cross-compile case. Returns "" when no compiler could be resolved;
|
// no-cross-compile case. Returns "" if clang isn't on PATH or doesn't
|
||||||
// those projects still need an explicit cfg.target, and the first build
|
// print a triple — those projects still need an explicit cfg.target.
|
||||||
// step reports the missing compiler.
|
CRAFTER_API std::string HostTarget();
|
||||||
std::string HostTarget();
|
|
||||||
// Target equality that ignores the vendor field: x86_64-pc-linux-gnu,
|
|
||||||
// x86_64-unknown-linux-gnu and x86_64-linux-gnu are one target as far as
|
|
||||||
// "does this test run on this host" goes, and which spelling a clang
|
|
||||||
// reports depends on how it was built. The strings themselves are never
|
|
||||||
// rewritten: clang rejects a PCM whose triple string differs from the
|
|
||||||
// importing unit's, so what the user wrote is what the compiler sees.
|
|
||||||
std::string NormalizeTarget(std::string_view triple);
|
|
||||||
bool SameTarget(std::string_view a, std::string_view b);
|
|
||||||
|
|
||||||
struct Configuration {
|
struct Configuration {
|
||||||
fs::path path;
|
fs::path path;
|
||||||
|
|
@ -488,13 +439,7 @@ export namespace Crafter {
|
||||||
std::vector<Test> tests;
|
std::vector<Test> tests;
|
||||||
// Lint rules for `crafter-build lint`. Populate via AddLintRule.
|
// Lint rules for `crafter-build lint`. Populate via AddLintRule.
|
||||||
std::vector<LintRule> lintRules;
|
std::vector<LintRule> lintRules;
|
||||||
// Scripts for `crafter-build run <name>`, a plain list like defines:
|
CRAFTER_API void GetInterfacesAndImplementations(std::span<fs::path> interfaces, std::span<fs::path> implementations);
|
||||||
// cfg.scripts.push_back({"docs", [](ScriptContext& ctx) {
|
|
||||||
// return ctx.Run("python3 tools/gen-docs.py");
|
|
||||||
// }});
|
|
||||||
// Not part of VariantId: a script changes nothing that is compiled.
|
|
||||||
std::vector<Script> scripts;
|
|
||||||
void GetInterfacesAndImplementations(std::span<fs::path> interfaces, std::span<fs::path> implementations);
|
|
||||||
// Retry the `import X;` names GetInterfacesAndImplementations could not
|
// Retry the `import X;` names GetInterfacesAndImplementations could not
|
||||||
// place, against the dependency DAG as it stands now. Sources are
|
// place, against the dependency DAG as it stands now. Sources are
|
||||||
// scanned when they're declared, but `dependencies` is often assigned
|
// scanned when they're declared, but `dependencies` is often assigned
|
||||||
|
|
@ -503,30 +448,30 @@ export namespace Crafter {
|
||||||
// change and this Configuration's objects would be silently reused
|
// change and this Configuration's objects would be silently reused
|
||||||
// against the new layout. Build() calls this before checking mtimes;
|
// against the new layout. Build() calls this before checking mtimes;
|
||||||
// calling it again is harmless.
|
// calling it again is harmless.
|
||||||
void ResolvePendingImports();
|
CRAFTER_API void ResolvePendingImports();
|
||||||
// Declare a test. Sources default to `tests/<name>/main.cpp` resolved
|
// Declare a test. Sources default to `tests/<name>/main.cpp` resolved
|
||||||
// against this Configuration's path; target/march/mtune/sysroot/debug
|
// against this Configuration's path; target/march/mtune/sysroot/debug
|
||||||
// are inherited from this Configuration so cross-arch projects don't
|
// are inherited from this Configuration so cross-arch projects don't
|
||||||
// have to re-specify. Returns a builder for chaining defines, deps,
|
// have to re-specify. Returns a builder for chaining defines, deps,
|
||||||
// etc. Defined in Crafter.Build:Test.
|
// etc. Defined in Crafter.Build:Test.
|
||||||
struct TestBuilder AddTest(std::string_view name);
|
CRAFTER_API struct TestBuilder AddTest(std::string_view name);
|
||||||
// Same as AddTest, but compiles the parent's `interfaces` directly
|
// Same as AddTest, but compiles the parent's `interfaces` directly
|
||||||
// into this test's Configuration (rather than going through a dep).
|
// into this test's Configuration (rather than going through a dep).
|
||||||
// Use when the test must rebuild those interfaces with its own
|
// Use when the test must rebuild those interfaces with its own
|
||||||
// compile flags — typically per-march SIMD codegen.
|
// compile flags — typically per-march SIMD codegen.
|
||||||
struct TestBuilder AddTest(std::string_view name, std::span<fs::path> interfaces);
|
CRAFTER_API struct TestBuilder AddTest(std::string_view name, std::span<fs::path> interfaces);
|
||||||
// Math-style fan-out: one Test per MarchTier, all sharing the same
|
// Math-style fan-out: one Test per MarchTier, all sharing the same
|
||||||
// `tests/<name>/main.cpp` source and the same interface set, each
|
// `tests/<name>/main.cpp` source and the same interface set, each
|
||||||
// compiled with the tier's `-march`/`-mtune`. Test names are
|
// compiled with the tier's `-march`/`-mtune`. Test names are
|
||||||
// `<name>-<march>`.
|
// `<name>-<march>`.
|
||||||
void AddMarchVariants(std::string_view name, std::span<fs::path> interfaces, std::span<const struct MarchTier> tiers);
|
CRAFTER_API void AddMarchVariants(std::string_view name, std::span<fs::path> interfaces, std::span<const struct MarchTier> tiers);
|
||||||
// Register a lint rule for `crafter-build lint` / `format`. Rules
|
// Register a lint rule for `crafter-build lint` / `format`. Rules
|
||||||
// registered on any Configuration whose path lies inside the project
|
// registered on any Configuration whose path lies inside the project
|
||||||
// root are collected (deduplicated by name, root-first) — attach them
|
// root are collected (deduplicated by name, root-first) — attach them
|
||||||
// to the lib or the exe config, either works. Rules that call
|
// to the lib or the exe config, either works. Rules that call
|
||||||
// ctx.SetContent are transforms (see LintContext::SetContent).
|
// ctx.SetContent are transforms (see LintContext::SetContent).
|
||||||
// Defined in Crafter.Build:Lint.
|
// Defined in Crafter.Build:Lint.
|
||||||
void AddLintRule(std::string name, std::function<void(LintContext&)> check);
|
CRAFTER_API void AddLintRule(std::string name, std::function<void(LintContext&)> check);
|
||||||
// Same, for a rule that reads LintContext::Decls(). Declared
|
// Same, for a rule that reads LintContext::Decls(). Declared
|
||||||
// separately rather than as a flag on AddLintRule so existing
|
// separately rather than as a flag on AddLintRule so existing
|
||||||
// registrations keep compiling. Such a rule makes the run build the
|
// registrations keep compiling. Such a rule makes the run build the
|
||||||
|
|
@ -534,24 +479,13 @@ export namespace Crafter {
|
||||||
// produced becomes an error instead of a silent pass. Prefer
|
// produced becomes an error instead of a silent pass. Prefer
|
||||||
// report-only: a transform running after the first one forces a
|
// report-only: a transform running after the first one forces a
|
||||||
// re-parse of everything it changed.
|
// re-parse of everything it changed.
|
||||||
void AddAstLintRule(std::string name, std::function<void(LintContext&)> check);
|
CRAFTER_API void AddAstLintRule(std::string name, std::function<void(LintContext&)> check);
|
||||||
// Suffix that uniquely identifies this Configuration's compile state.
|
// Suffix that uniquely identifies this Configuration's compile state.
|
||||||
// target+march+mtune are spelled out for readability; the rest
|
// target+march+mtune are spelled out for readability; the rest
|
||||||
// (type, debug, sysroot, defines, compileFlags, the toolchain)
|
// (type, debug, sysroot, defines, compileFlags) collapse into a short
|
||||||
// collapse into a short hash so two Configurations sharing a path but
|
// hash so two Configurations sharing a path but with different
|
||||||
// with different compile state can't clobber each other's outputs.
|
// compile state can't clobber each other's outputs.
|
||||||
std::string VariantId() const { return VariantId(GetToolchain()); }
|
std::string VariantId() const {
|
||||||
// The variant under an explicit toolchain; VariantId() is this for
|
|
||||||
// the running one. The compiler is compile state like any flag: a
|
|
||||||
// PCM can only be read by the clang release that wrote it, and
|
|
||||||
// staleness is decided by mtimes, which a compiler update does not
|
|
||||||
// touch. Before the toolchain was hashed, a project built with clang
|
|
||||||
// 22 kept its PCMs when crafter-build moved to clang 23, and the
|
|
||||||
// first edit afterwards failed with "module file ... uses an older
|
|
||||||
// format that is no longer supported" until the project was cleaned.
|
|
||||||
// Keyed here, an update starts an empty directory and rebuilds; the
|
|
||||||
// old one stays until `crafter-build clean`.
|
|
||||||
std::string VariantId(const Toolchain& toolchain) const {
|
|
||||||
std::string compileKey;
|
std::string compileKey;
|
||||||
compileKey += std::to_string(static_cast<std::int32_t>(type));
|
compileKey += std::to_string(static_cast<std::int32_t>(type));
|
||||||
compileKey += '|';
|
compileKey += '|';
|
||||||
|
|
@ -581,8 +515,6 @@ export namespace Crafter {
|
||||||
compileKey += "|A:";
|
compileKey += "|A:";
|
||||||
compileKey += a;
|
compileKey += a;
|
||||||
}
|
}
|
||||||
compileKey += "|T:";
|
|
||||||
compileKey += toolchain.Key();
|
|
||||||
std::size_t configHash = std::hash<std::string>{}(compileKey);
|
std::size_t configHash = std::hash<std::string>{}(compileKey);
|
||||||
return std::format("{}-{}-{}-{}-{:08x}", name, target, march, mtune, configHash);
|
return std::format("{}-{}-{}-{}-{:08x}", name, target, march, mtune, configHash);
|
||||||
}
|
}
|
||||||
|
|
@ -612,14 +544,14 @@ export namespace Crafter {
|
||||||
// Directory holding the std module PCM for this Configuration. Keyed on
|
// Directory holding the std module PCM for this Configuration. Keyed on
|
||||||
// target+march because a BMI is only loadable by a TU with matching target
|
// target+march because a BMI is only loadable by a TU with matching target
|
||||||
// features, and suffixed with the wasm variant flags for the same reason.
|
// features, and suffixed with the wasm variant flags for the same reason.
|
||||||
fs::path StdPcmDir(const Configuration& config);
|
CRAFTER_API fs::path StdPcmDir(const Configuration& config);
|
||||||
|
|
||||||
// Where Build leaves this Configuration's linked artifact: the executable
|
// Where Build leaves this Configuration's linked artifact: the executable
|
||||||
// (with the target's suffix), `lib<name>.a` / `<name>.lib`, or the shared
|
// (with the target's suffix), `lib<name>.a` / `<name>.lib`, or the shared
|
||||||
// library / DLL. The one rule every "is the consumer older than its
|
// library / DLL. The one rule every "is the consumer older than its
|
||||||
// dependency" check reads, so a staleness test and the link step can't
|
// dependency" check reads, so a staleness test and the link step can't
|
||||||
// disagree about the file name.
|
// disagree about the file name.
|
||||||
fs::path ExpectedOutput(const Configuration& config);
|
CRAFTER_API fs::path ExpectedOutput(const Configuration& config);
|
||||||
|
|
||||||
// The clang invocation every C++ translation unit in this Configuration is
|
// The clang invocation every C++ translation unit in this Configuration is
|
||||||
// compiled with, before anything that depends on work having happened
|
// compiled with, before anything that depends on work having happened
|
||||||
|
|
@ -654,14 +586,14 @@ export namespace Crafter {
|
||||||
fs::path stdPcmDir;
|
fs::path stdPcmDir;
|
||||||
fs::path pcmDir;
|
fs::path pcmDir;
|
||||||
};
|
};
|
||||||
CompileCommand GetCompileCommand(const Configuration& config);
|
CRAFTER_API CompileCommand GetCompileCommand(const Configuration& config);
|
||||||
|
|
||||||
BuildResult Build(Configuration& config, std::unordered_map<fs::path, std::shared_future<BuildResult>>& depResults, std::mutex& depMutex);
|
CRAFTER_API BuildResult Build(Configuration& config, std::unordered_map<fs::path, std::shared_future<BuildResult>>& depResults, std::mutex& depMutex);
|
||||||
|
|
||||||
// Takes main's argv verbatim so the CLI entry point is a one-liner;
|
// Takes main's argv verbatim so the CLI entry point is a one-liner;
|
||||||
// the shape is inherited from the language, not chosen here.
|
// the shape is inherited from the language, not chosen here.
|
||||||
// lint-disable-next-line no-char-pointer
|
// lint-disable-next-line no-char-pointer
|
||||||
std::int32_t Run(std::int32_t argc, char** argv);
|
CRAFTER_API std::int32_t Run(std::int32_t argc, char** argv);
|
||||||
|
|
||||||
// Delete the bin/ and build/ trees beside `projectFile`, returning the paths
|
// Delete the bin/ and build/ trees beside `projectFile`, returning the paths
|
||||||
// that existed and were removed. Backs `crafter-build clean`.
|
// that existed and were removed. Backs `crafter-build clean`.
|
||||||
|
|
@ -671,7 +603,7 @@ export namespace Crafter {
|
||||||
// to compile (and its git dependencies to be present) is useless exactly
|
// to compile (and its git dependencies to be present) is useless exactly
|
||||||
// then. That means variant directories are not enumerated — the whole tree
|
// then. That means variant directories are not enumerated — the whole tree
|
||||||
// goes, which is what the by-hand `rm -rf bin build` did anyway.
|
// goes, which is what the by-hand `rm -rf bin build` did anyway.
|
||||||
std::vector<fs::path> CleanProject(const fs::path& projectFile);
|
CRAFTER_API std::vector<fs::path> CleanProject(const fs::path& projectFile);
|
||||||
|
|
||||||
// Add a small index.html + runtime.js pair next to the .wasm output so the
|
// Add a small index.html + runtime.js pair next to the .wasm output so the
|
||||||
// build can be loaded directly in a browser (just `serve` the bin dir and
|
// build can be loaded directly in a browser (just `serve` the bin dir and
|
||||||
|
|
@ -679,7 +611,7 @@ export namespace Crafter {
|
||||||
// this and shouldn't carry the extra files. Call from project.cpp after
|
// this and shouldn't carry the extra files. Call from project.cpp after
|
||||||
// outputName is set; index.html is generated against the current
|
// outputName is set; index.html is generated against the current
|
||||||
// outputName so renaming the binary later requires another call.
|
// outputName so renaming the binary later requires another call.
|
||||||
void EnableWasiBrowserRuntime(Configuration& cfg);
|
CRAFTER_API void EnableWasiBrowserRuntime(Configuration& cfg);
|
||||||
|
|
||||||
// Register the relaxed-SIMD codegen variant on cfg. Builds an additional
|
// Register the relaxed-SIMD codegen variant on cfg. Builds an additional
|
||||||
// outputName.relaxed-simd.wasm compiled with -mrelaxed-simd (FMA,
|
// outputName.relaxed-simd.wasm compiled with -mrelaxed-simd (FMA,
|
||||||
|
|
@ -690,17 +622,17 @@ export namespace Crafter {
|
||||||
// 2026). Idempotent. Call before EnableWasiBrowserRuntime so the emitted
|
// 2026). Idempotent. Call before EnableWasiBrowserRuntime so the emitted
|
||||||
// variants.json manifest includes it. A no-op for non-wasm targets at build
|
// variants.json manifest includes it. A no-op for non-wasm targets at build
|
||||||
// time (no wasm is produced), but harmless to declare unconditionally.
|
// time (no wasm is produced), but harmless to declare unconditionally.
|
||||||
void EnableWasiRelaxedSimdVariant(Configuration& cfg);
|
CRAFTER_API void EnableWasiRelaxedSimdVariant(Configuration& cfg);
|
||||||
|
|
||||||
// View over the project's args with simple query helpers. Has(flag) for
|
// View over the project's args with simple query helpers. Has(flag) for
|
||||||
// boolean switches; Get(prefix) for valued options (e.g. Get("--prefix=")
|
// boolean switches; Get(prefix) for valued options (e.g. Get("--prefix=")
|
||||||
// returns the substring after the equals). Definitions are out-of-line so
|
// returns the substring after the equals). Definitions are out-of-line
|
||||||
// project.so resolves them from the executable: clang does not emit
|
// and CRAFTER_API so they cross the Windows DLL boundary cleanly — clang
|
||||||
// module-attached in-class inline bodies into consumers.
|
// does not emit module-attached in-class inline bodies into consumers.
|
||||||
struct ArgQuery {
|
struct ArgQuery {
|
||||||
std::span<const std::string_view> args;
|
std::span<const std::string_view> args;
|
||||||
bool Has(std::string_view flag) const;
|
CRAFTER_API bool Has(std::string_view flag) const;
|
||||||
std::optional<std::string> Get(std::string_view prefix) const;
|
CRAFTER_API std::optional<std::string> Get(std::string_view prefix) const;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Apply the framework's standard CLI args + env vars onto cfg:
|
// Apply the framework's standard CLI args + env vars onto cfg:
|
||||||
|
|
@ -719,5 +651,5 @@ export namespace Crafter {
|
||||||
// pre-set defaults. Returns an ArgQuery over the same span so projects
|
// pre-set defaults. Returns an ArgQuery over the same span so projects
|
||||||
// can query their own flags (`--timing`, ...) without re-rolling the
|
// can query their own flags (`--timing`, ...) without re-rolling the
|
||||||
// for-arg-in-args loop.
|
// for-arg-in-args loop.
|
||||||
ArgQuery ApplyStandardArgs(Configuration& cfg, std::span<const std::string_view> args);
|
CRAFTER_API ArgQuery ApplyStandardArgs(Configuration& cfg, std::span<const std::string_view> args);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||||
|
|
||||||
|
module;
|
||||||
|
#include "Crafter.Build-Api.h"
|
||||||
export module Crafter.Build:External;
|
export module Crafter.Build:External;
|
||||||
import std;
|
import std;
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
|
|
@ -46,13 +48,13 @@ export namespace Crafter {
|
||||||
// declaration and target — BuildExternal derives its own working directory
|
// declaration and target — BuildExternal derives its own working directory
|
||||||
// through this, so a caller that only needs to know where the headers ended
|
// through this, so a caller that only needs to know where the headers ended
|
||||||
// up cannot drift from where they actually are.
|
// up cannot drift from where they actually are.
|
||||||
fs::path ExternalCloneDir(const ExternalDependency& dep, std::string_view target);
|
CRAFTER_API fs::path ExternalCloneDir(const ExternalDependency& dep, std::string_view target);
|
||||||
// The -I flags this dependency contributes, from its declared includeDirs.
|
// The -I flags this dependency contributes, from its declared includeDirs.
|
||||||
// Also pure, which is what lets the linter's AST layer parse a source that
|
// Also pure, which is what lets the linter's AST layer parse a source that
|
||||||
// includes an external's headers without running a build to find out where
|
// includes an external's headers without running a build to find out where
|
||||||
// they are. The paths only resolve once something has fetched the clone.
|
// they are. The paths only resolve once something has fetched the clone.
|
||||||
std::vector<std::string> ExternalIncludeFlags(const ExternalDependency& dep, std::string_view target);
|
CRAFTER_API std::vector<std::string> ExternalIncludeFlags(const ExternalDependency& dep, std::string_view target);
|
||||||
ExternalBuildResult BuildExternal(const ExternalDependency& dep, std::string_view target, std::atomic<bool>& cancelled);
|
CRAFTER_API ExternalBuildResult BuildExternal(const ExternalDependency& dep, std::string_view target, std::atomic<bool>& cancelled);
|
||||||
|
|
||||||
// Specification for a sibling crafter-build project to fetch and depend on.
|
// Specification for a sibling crafter-build project to fetch and depend on.
|
||||||
// GitSource picks the revision: leave branch + commit empty for the
|
// GitSource picks the revision: leave branch + commit empty for the
|
||||||
|
|
@ -71,7 +73,7 @@ export namespace Crafter {
|
||||||
// remote's project.cpp, and returns a stable Configuration* you can drop
|
// remote's project.cpp, and returns a stable Configuration* you can drop
|
||||||
// into cfg.dependencies. The Configuration is owned by an internal cache
|
// into cfg.dependencies. The Configuration is owned by an internal cache
|
||||||
// for the lifetime of the build; identical specs share one Configuration.
|
// for the lifetime of the build; identical specs share one Configuration.
|
||||||
Configuration* GitProject(const GitProjectSpec& spec);
|
CRAFTER_API Configuration* GitProject(const GitProjectSpec& spec);
|
||||||
|
|
||||||
// Specification for a local sibling crafter-build project (e.g. an
|
// Specification for a local sibling crafter-build project (e.g. an
|
||||||
// example folder depending on its parent project, or a workspace where
|
// example folder depending on its parent project, or a workspace where
|
||||||
|
|
@ -85,7 +87,7 @@ export namespace Crafter {
|
||||||
// cache copy. Resolves projectFile to its canonical absolute path and
|
// cache copy. Resolves projectFile to its canonical absolute path and
|
||||||
// recursively LoadProject's it. Returns a stable Configuration* owned
|
// recursively LoadProject's it. Returns a stable Configuration* owned
|
||||||
// by the same internal cache as GitProject.
|
// by the same internal cache as GitProject.
|
||||||
Configuration* LocalProject(const LocalProjectSpec& spec);
|
CRAFTER_API Configuration* LocalProject(const LocalProjectSpec& spec);
|
||||||
|
|
||||||
// Clone (or update) the given git source into the shared external
|
// Clone (or update) the given git source into the shared external
|
||||||
// cache and return the path to the cloned working tree. Intended for
|
// cache and return the path to the cloned working tree. Intended for
|
||||||
|
|
@ -95,5 +97,5 @@ export namespace Crafter {
|
||||||
// ExternalDependency / GitProject. Caching key = url + branch + commit;
|
// ExternalDependency / GitProject. Caching key = url + branch + commit;
|
||||||
// identical calls reuse the existing clone. Pin `source.commit` to a
|
// identical calls reuse the existing clone. Pin `source.commit` to a
|
||||||
// SHA for reproducible, offline-after-first-fetch builds.
|
// SHA for reproducible, offline-after-first-fetch builds.
|
||||||
fs::path GitFetch(const GitSource& source);
|
CRAFTER_API fs::path GitFetch(const GitSource& source);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||||
|
|
||||||
|
module;
|
||||||
|
#include "Crafter.Build-Api.h"
|
||||||
export module Crafter.Build:Implementation;
|
export module Crafter.Build:Implementation;
|
||||||
import std;
|
import std;
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
|
|
@ -17,8 +19,8 @@ namespace Crafter {
|
||||||
// no reachable Configuration provided when the source was scanned.
|
// no reachable Configuration provided when the source was scanned.
|
||||||
std::vector<std::string> pendingImports;
|
std::vector<std::string> pendingImports;
|
||||||
fs::path path;
|
fs::path path;
|
||||||
Implementation(fs::path&& path);
|
CRAFTER_API Implementation(fs::path&& path);
|
||||||
bool Check(const fs::path& buildDir, const fs::path& pcmDir, fs::file_time_type sourceFloor = fs::file_time_type::min()) const;
|
CRAFTER_API bool Check(const fs::path& buildDir, const fs::path& pcmDir, fs::file_time_type sourceFloor = fs::file_time_type::min()) const;
|
||||||
void Compile(const std::string_view clang, const fs::path& buildDir, std::atomic<bool>& buildCancelled, std::string& buildError) const;
|
CRAFTER_API void Compile(const std::string_view clang, const fs::path& buildDir, std::atomic<bool>& buildCancelled, std::string& buildError) const;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||||
|
|
||||||
|
module;
|
||||||
|
#include "Crafter.Build-Api.h"
|
||||||
export module Crafter.Build:Interface;
|
export module Crafter.Build:Interface;
|
||||||
import std;
|
import std;
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
|
|
@ -24,9 +26,9 @@ namespace Crafter {
|
||||||
bool checked = false;
|
bool checked = false;
|
||||||
std::string name;
|
std::string name;
|
||||||
fs::path path;
|
fs::path path;
|
||||||
ModulePartition(std::string&& name, fs::path&& path);
|
CRAFTER_API ModulePartition(std::string&& name, fs::path&& path);
|
||||||
bool Check(const fs::path& pcmDir, fs::file_time_type sourceFloor = fs::file_time_type::min());
|
CRAFTER_API bool Check(const fs::path& pcmDir, fs::file_time_type sourceFloor = fs::file_time_type::min());
|
||||||
void Compile(const std::string_view clang, const fs::path& pcmDir, const fs::path& buildDir, std::atomic<bool>& buildCancelled, std::string& buildError);
|
CRAFTER_API void Compile(const std::string_view clang, const fs::path& pcmDir, const fs::path& buildDir, std::atomic<bool>& buildCancelled, std::string& buildError);
|
||||||
};
|
};
|
||||||
|
|
||||||
export class Module {
|
export class Module {
|
||||||
|
|
@ -46,8 +48,8 @@ namespace Crafter {
|
||||||
std::vector<std::unique_ptr<ModulePartition>> partitions;
|
std::vector<std::unique_ptr<ModulePartition>> partitions;
|
||||||
std::string name;
|
std::string name;
|
||||||
fs::path path;
|
fs::path path;
|
||||||
Module(std::string&& name, fs::path&& path);
|
CRAFTER_API Module(std::string&& name, fs::path&& path);
|
||||||
bool Check(const fs::path& pcmDir, fs::file_time_type sourceFloor = fs::file_time_type::min());
|
CRAFTER_API bool Check(const fs::path& pcmDir, fs::file_time_type sourceFloor = fs::file_time_type::min());
|
||||||
void Compile(const std::string_view clang, const fs::path& pcmDir, const fs::path& buildDir, std::atomic<bool>& buildCancelled, std::string& buildError);
|
CRAFTER_API void Compile(const std::string_view clang, const fs::path& pcmDir, const fs::path& buildDir, std::atomic<bool>& buildCancelled, std::string& buildError);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||||
|
|
||||||
|
module;
|
||||||
|
#include "Crafter.Build-Api.h"
|
||||||
export module Crafter.Build:Lint;
|
export module Crafter.Build:Lint;
|
||||||
import std;
|
import std;
|
||||||
import :Clang;
|
import :Clang;
|
||||||
|
|
@ -61,5 +63,5 @@ export namespace Crafter {
|
||||||
// final content differs from the original. Prints per-mode output:
|
// final content differs from the original. Prints per-mode output:
|
||||||
// findings compiler-style (Report), would-change paths (Check), or
|
// findings compiler-style (Report), would-change paths (Check), or
|
||||||
// formatted paths (Apply), plus a summary line.
|
// formatted paths (Apply), plus a summary line.
|
||||||
LintSummary RunLint(Configuration& projectCfg, const RunLintOptions& opts);
|
CRAFTER_API LintSummary RunLint(Configuration& projectCfg, const RunLintOptions& opts);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||||
|
|
||||||
|
module;
|
||||||
|
#include "Crafter.Build-Api.h"
|
||||||
export module Crafter.Build:Platform;
|
export module Crafter.Build:Platform;
|
||||||
import std;
|
import std;
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
|
|
@ -14,21 +16,6 @@ namespace Crafter {
|
||||||
bool timedOut = false;
|
bool timedOut = false;
|
||||||
std::int32_t signal = 0;
|
std::int32_t signal = 0;
|
||||||
};
|
};
|
||||||
// An exclusive lock on the file `path` (created if missing), held from
|
|
||||||
// construction to destruction. An flock, so it keeps out the other
|
|
||||||
// threads of this process (each opens the file anew) and other
|
|
||||||
// crafter-build processes alike. The OS drops it when the descriptor
|
|
||||||
// closes, process death included, so an interrupted build never leaves a
|
|
||||||
// stale lock; the descriptor is close-on-exec, so the tools a holder
|
|
||||||
// spawns don't keep it past its release.
|
|
||||||
class FileLock {
|
|
||||||
std::int32_t fd = -1;
|
|
||||||
public:
|
|
||||||
explicit FileLock(const fs::path& path);
|
|
||||||
~FileLock();
|
|
||||||
FileLock(const FileLock&) = delete;
|
|
||||||
FileLock& operator=(const FileLock&) = delete;
|
|
||||||
};
|
|
||||||
std::string BuildStdPcm(const Configuration& config, fs::path stdPcm);
|
std::string BuildStdPcm(const Configuration& config, fs::path stdPcm);
|
||||||
fs::path GetCacheDir();
|
fs::path GetCacheDir();
|
||||||
CommandResult RunCommandChecked(std::string_view command);
|
CommandResult RunCommandChecked(std::string_view command);
|
||||||
|
|
@ -43,21 +30,9 @@ namespace Crafter {
|
||||||
// Progress::Diagnostic), otherwise the compiler's diagnostics. Callers
|
// Progress::Diagnostic), otherwise the compiler's diagnostics. Callers
|
||||||
// therefore keep the `if (!result.empty())` shape they already had.
|
// therefore keep the `if (!result.empty())` shape they already had.
|
||||||
std::string RunBuildCommand(std::string_view command);
|
std::string RunBuildCommand(std::string_view command);
|
||||||
export CommandResult RunCommandWithTimeout(std::string_view command, std::chrono::seconds timeout);
|
export CRAFTER_API CommandResult RunCommandWithTimeout(std::string_view command, std::chrono::seconds timeout);
|
||||||
// Runs `command` through sh with this process's own stdin, stdout and
|
|
||||||
// stderr, so its output appears as it is produced and it can prompt.
|
|
||||||
// Every helper above captures instead, which suits a compile step and
|
|
||||||
// not a command that runs for half an hour. Returns the exit code, or
|
|
||||||
// 128+signal when the command was killed; a nonzero exit is the caller's
|
|
||||||
// to judge and never throws. While it runs, Ctrl-C goes to the command
|
|
||||||
// and not to crafter-build, which then sees it as exit code 130.
|
|
||||||
// Project scripts reach this through ScriptContext::Run.
|
|
||||||
std::int32_t RunCommandAttached(std::string_view command);
|
|
||||||
// `s` as one single-quoted sh word, safe to splice into a command line
|
|
||||||
// whatever it contains: paths with spaces, arguments a user typed.
|
|
||||||
export std::string ShellQuote(std::string_view s);
|
|
||||||
std::string GetBaseCommand(const Configuration& config);
|
std::string GetBaseCommand(const Configuration& config);
|
||||||
export Configuration LoadProject(const fs::path& projectFile, std::span<const std::string_view> args);
|
export CRAFTER_API Configuration LoadProject(const fs::path& projectFile, std::span<const std::string_view> args);
|
||||||
// A dependency the project file currently running its CrafterBuildProject
|
// A dependency the project file currently running its CrafterBuildProject
|
||||||
// declared with `#pragma crafter dependency`, so it can also be wired
|
// declared with `#pragma crafter dependency`, so it can also be wired
|
||||||
// into the build graph (a test that links the same module, say) without
|
// into the build graph (a test that links the same module, say) without
|
||||||
|
|
@ -65,7 +40,7 @@ namespace Crafter {
|
||||||
// name or any module it exports — whatever the project file `import`s.
|
// name or any module it exports — whatever the project file `import`s.
|
||||||
// Throws when no such dependency was declared, or when called from
|
// Throws when no such dependency was declared, or when called from
|
||||||
// anywhere other than inside LoadProject.
|
// anywhere other than inside LoadProject.
|
||||||
export Configuration* ProjectDependency(std::string_view name);
|
export CRAFTER_API Configuration* ProjectDependency(std::string_view name);
|
||||||
// True while the project file crafter-build was pointed at runs its
|
// True while the project file crafter-build was pointed at runs its
|
||||||
// CrafterBuildProject, and when no project file is running at all; false
|
// CrafterBuildProject, and when no project file is running at all; false
|
||||||
// inside a dependency's project file (a `#pragma crafter dependency`,
|
// inside a dependency's project file (a `#pragma crafter dependency`,
|
||||||
|
|
@ -74,11 +49,11 @@ namespace Crafter {
|
||||||
// library's test links crafter-build itself, so when crafter-build loads
|
// library's test links crafter-build itself, so when crafter-build loads
|
||||||
// that library as a dependency, the test's GitProject loads crafter-build's
|
// that library as a dependency, the test's GitProject loads crafter-build's
|
||||||
// project file in turn and the two recurse until the cycle guard throws.
|
// project file in turn and the two recurse until the cycle guard throws.
|
||||||
export bool IsTopLevelProject();
|
export CRAFTER_API bool IsTopLevelProject();
|
||||||
// Resolves the directory holding distributed runtime assets (Crafter.Build
|
// Resolves the directory holding distributed runtime assets (Crafter.Build
|
||||||
// module sources, wasi-runtime/, etc). Honors CRAFTER_BUILD_HOME; otherwise
|
// module sources, wasi-runtime/, etc). Honors CRAFTER_BUILD_HOME; otherwise
|
||||||
// derives <prefix>/share/crafter-build from the running executable's path.
|
// derives <prefix>/share/crafter-build from the running executable's path.
|
||||||
export fs::path GetCrafterBuildHome();
|
export CRAFTER_API fs::path GetCrafterBuildHome();
|
||||||
// Newest mtime among the prerequisites a compiler recorded in `depFile`
|
// Newest mtime among the prerequisites a compiler recorded in `depFile`
|
||||||
// (clang's `-MD -MF <artifact>.d`). This is how a build step learns which
|
// (clang's `-MD -MF <artifact>.d`). This is how a build step learns which
|
||||||
// headers its source pulled in: the module scanner only reads `import`
|
// headers its source pulled in: the module scanner only reads `import`
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,22 @@
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||||
|
|
||||||
|
module;
|
||||||
|
#include "Crafter.Build-Api.h"
|
||||||
export module Crafter.Build:Progress;
|
export module Crafter.Build:Progress;
|
||||||
import std;
|
import std;
|
||||||
|
|
||||||
export namespace Crafter::Progress {
|
export namespace Crafter::Progress {
|
||||||
enum class Verbosity { Quiet, Default, Verbose };
|
enum class Verbosity { Quiet, Default, Verbose };
|
||||||
|
|
||||||
void SetVerbosity(Verbosity v);
|
CRAFTER_API void SetVerbosity(Verbosity v);
|
||||||
Verbosity GetVerbosity();
|
CRAFTER_API Verbosity GetVerbosity();
|
||||||
|
|
||||||
// RAII work-unit marker. Construction inflates the running total; destruction
|
// RAII work-unit marker. Construction inflates the running total; destruction
|
||||||
// increments completed and (on Default + TTY) redraws the status line. Move
|
// increments completed and (on Default + TTY) redraws the status line. Move
|
||||||
// semantics are disabled — keep these as plain stack locals inside the worker
|
// semantics are disabled — keep these as plain stack locals inside the worker
|
||||||
// lambda so the destructor fires when that thread's work ends.
|
// lambda so the destructor fires when that thread's work ends.
|
||||||
class Task {
|
class CRAFTER_API Task {
|
||||||
public:
|
public:
|
||||||
explicit Task(std::string label);
|
explicit Task(std::string label);
|
||||||
~Task();
|
~Task();
|
||||||
|
|
@ -27,18 +29,18 @@ export namespace Crafter::Progress {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Verbose-mode command echo. No-op outside Verbose.
|
// Verbose-mode command echo. No-op outside Verbose.
|
||||||
void EchoCommand(std::string_view command);
|
CRAFTER_API void EchoCommand(std::string_view command);
|
||||||
|
|
||||||
// Non-fatal output from a build step that succeeded — compiler and linker
|
// Non-fatal output from a build step that succeeded — compiler and linker
|
||||||
// warnings. Erases the status line first so the text doesn't land on top of
|
// warnings. Erases the status line first so the text doesn't land on top of
|
||||||
// it, then writes to stderr. No-op on Quiet.
|
// it, then writes to stderr. No-op on Quiet.
|
||||||
void Diagnostic(std::string_view text);
|
CRAFTER_API void Diagnostic(std::string_view text);
|
||||||
|
|
||||||
// Erase the in-place status line so subsequent stderr writes (errors,
|
// Erase the in-place status line so subsequent stderr writes (errors,
|
||||||
// banners) don't collide with it. No-op when not in TTY-redraw mode.
|
// banners) don't collide with it. No-op when not in TTY-redraw mode.
|
||||||
void Clear();
|
CRAFTER_API void Clear();
|
||||||
|
|
||||||
// Print "Built N targets in Xms" (or equivalent) and clear in-place state.
|
// Print "Built N targets in Xms" (or equivalent) and clear in-place state.
|
||||||
// Safe to call multiple times; subsequent calls are no-ops.
|
// Safe to call multiple times; subsequent calls are no-ops.
|
||||||
void Finalize();
|
CRAFTER_API void Finalize();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
|
||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
||||||
|
|
||||||
export module Crafter.Build:Script;
|
|
||||||
import std;
|
|
||||||
import :Clang;
|
|
||||||
|
|
||||||
export namespace Crafter {
|
|
||||||
struct RunScriptOptions {
|
|
||||||
// The script to run; read only when `named` is set.
|
|
||||||
std::string name;
|
|
||||||
// A name was given. Kept apart from `name` because an empty string
|
|
||||||
// is a name someone can type (`run ""`): it has to be looked up and
|
|
||||||
// refused, not read as a bare `run`, which lists.
|
|
||||||
bool named = false;
|
|
||||||
// What followed the name on the command line, handed to the script
|
|
||||||
// as ScriptContext::args.
|
|
||||||
std::vector<std::string> args;
|
|
||||||
// Print the script names instead of running one.
|
|
||||||
bool listOnly = false;
|
|
||||||
// Absolute path of the loaded project.cpp; its directory becomes
|
|
||||||
// ScriptContext::projectDir. Empty => fall back to cfg.path.
|
|
||||||
std::filesystem::path projectFile;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Backs `crafter-build run`. Runs the script `opts` names out of
|
|
||||||
// `projectCfg.scripts` and returns what it returned, which the CLI uses
|
|
||||||
// as the process exit code. Without a name, or with listOnly, prints the
|
|
||||||
// names and returns 0.
|
|
||||||
//
|
|
||||||
// Returns 1, with the reason on stderr and without running anything,
|
|
||||||
// when the project defines no scripts, when a script is declared under a
|
|
||||||
// name that could never be typed (empty, or starting with '-') or under
|
|
||||||
// one name twice, and when no script has the requested name. A script
|
|
||||||
// that throws also yields 1, as does one that returns a nonzero value
|
|
||||||
// outside 1..255: an exit status is eight bits, and a value such as 256
|
|
||||||
// (a raw wait status passed along by mistake) would otherwise wrap to
|
|
||||||
// success.
|
|
||||||
std::int32_t RunScript(Configuration& projectCfg, const RunScriptOptions& opts);
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||||
|
|
||||||
|
module;
|
||||||
|
#include "Crafter.Build-Api.h"
|
||||||
export module Crafter.Build:Shader;
|
export module Crafter.Build:Shader;
|
||||||
import std;
|
import std;
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
|
|
@ -28,33 +30,8 @@ namespace Crafter {
|
||||||
fs::path path;
|
fs::path path;
|
||||||
std::string entrypoint;
|
std::string entrypoint;
|
||||||
ShaderType type;
|
ShaderType type;
|
||||||
// Preprocessor macros for this compile, spelled like the compiler's -D:
|
CRAFTER_API Shader(fs::path&& path, std::string&& entrypoint, ShaderType type);
|
||||||
// "NAME" (defined to 1) or "NAME=VALUE". They are placed ahead of the
|
CRAFTER_API bool Check(const fs::path& outputDir) const;
|
||||||
// source, so one file can be registered more than once and take a
|
CRAFTER_API std::string Compile(const fs::path& outputDir, std::span<const fs::path> includeDirs) const;
|
||||||
// different #ifdef branch each time. Note the spelling differs from
|
|
||||||
// Configuration::defines, which is a list of {name, value} pairs.
|
|
||||||
std::vector<std::string> defines;
|
|
||||||
// Names the output when one source is compiled more than once: empty
|
|
||||||
// gives "<name>.spv", "sets" gives "<name>.sets.spv". One file-name
|
|
||||||
// component: no path separators. Every entry of a configuration must
|
|
||||||
// end up with its own OutputName(); Build() fails if two collide, so
|
|
||||||
// each extra compile of a source needs its own variant.
|
|
||||||
std::string variant;
|
|
||||||
Shader(fs::path&& path, std::string&& entrypoint, ShaderType type);
|
|
||||||
// File name of the compiled SPIR-V, relative to the output directory.
|
|
||||||
// The single source of truth for it: Check, Compile and the artifact
|
|
||||||
// forwarding in Build() all go through here.
|
|
||||||
fs::path OutputName() const;
|
|
||||||
// Whether the output in `outputDir` is up to date: newer than the
|
|
||||||
// source and than every file the source #included, and compiled with
|
|
||||||
// this entry's stage, entry point and defines, searching
|
|
||||||
// `includeDirs`. Compile records the last two in `buildDir`; without
|
|
||||||
// its record an output is stale.
|
|
||||||
bool Check(const fs::path& outputDir, const fs::path& buildDir, std::span<const fs::path> includeDirs) const;
|
|
||||||
// Compiles into `outputDir`, searching `includeDirs` for #include, and
|
|
||||||
// records what Check needs in `buildDir`: a depfile naming the source
|
|
||||||
// and every file it included, and the settings it was compiled with.
|
|
||||||
// Returns an error message, empty on success.
|
|
||||||
std::string Compile(const fs::path& outputDir, const fs::path& buildDir, std::span<const fs::path> includeDirs) const;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||||
|
|
||||||
|
module;
|
||||||
|
#include "Crafter.Build-Api.h"
|
||||||
export module Crafter.Build:Test;
|
export module Crafter.Build:Test;
|
||||||
import std;
|
import std;
|
||||||
import :Clang;
|
import :Clang;
|
||||||
|
|
@ -26,7 +28,7 @@ export namespace Crafter {
|
||||||
// avoid the double-`GetInterfacesAndImplementations` footgun (calling
|
// avoid the double-`GetInterfacesAndImplementations` footgun (calling
|
||||||
// it twice would re-parse the same .cppm files into duplicate Module
|
// it twice would re-parse the same .cppm files into duplicate Module
|
||||||
// entries).
|
// entries).
|
||||||
struct TestBuilder {
|
struct CRAFTER_API TestBuilder {
|
||||||
Configuration* parent;
|
Configuration* parent;
|
||||||
std::size_t index;
|
std::size_t index;
|
||||||
|
|
||||||
|
|
@ -83,8 +85,8 @@ export namespace Crafter {
|
||||||
bool AllPassed() const { return failed == 0 && crashed == 0 && timedOut == 0; }
|
bool AllPassed() const { return failed == 0 && crashed == 0 && timedOut == 0; }
|
||||||
};
|
};
|
||||||
|
|
||||||
TestSummary RunTests(Configuration& projectCfg, const RunTestsOptions& opts, std::span<const std::string_view> projectArgs = {});
|
CRAFTER_API TestSummary RunTests(Configuration& projectCfg, const RunTestsOptions& opts, std::span<const std::string_view> projectArgs = {});
|
||||||
TestResult RunSingleTest(const Test& test, const std::filesystem::path& binary, std::chrono::seconds timeout);
|
CRAFTER_API TestResult RunSingleTest(const Test& test, const std::filesystem::path& binary, std::chrono::seconds timeout);
|
||||||
|
|
||||||
// Parent-project access for test fixtures. Run() sets the pointer to the
|
// Parent-project access for test fixtures. Run() sets the pointer to the
|
||||||
// root project's Configuration before discovery; fixtures call ParentLib()
|
// root project's Configuration before discovery; fixtures call ParentLib()
|
||||||
|
|
@ -92,6 +94,6 @@ export namespace Crafter {
|
||||||
// parent itself) so they can declare it as a dependency without rebuilding
|
// parent itself) so they can declare it as a dependency without rebuilding
|
||||||
// the lib config from scratch. Lookup is by Configuration::name and
|
// the lib config from scratch. Lookup is by Configuration::name and
|
||||||
// recurses through the parent's dependency graph.
|
// recurses through the parent's dependency graph.
|
||||||
void SetParentProject(Configuration* parent);
|
CRAFTER_API void SetParentProject(Configuration* parent);
|
||||||
Configuration* ParentLib(std::string_view name);
|
CRAFTER_API Configuration* ParentLib(std::string_view name);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,115 +0,0 @@
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
|
||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
||||||
|
|
||||||
export module Crafter.Build:Toolchain;
|
|
||||||
import std;
|
|
||||||
namespace fs = std::filesystem;
|
|
||||||
|
|
||||||
export namespace Crafter {
|
|
||||||
// The compiler crafter-build drives and what sits next to it, resolved
|
|
||||||
// once per process by GetToolchain(). Every clang++, clang and llvm-ar
|
|
||||||
// the tool spawns, the libclang the linter dlopens, the std module
|
|
||||||
// source and the default sysroots come from here, so a toolchain
|
|
||||||
// installed anywhere other than /usr (the Flatpak carries its own under
|
|
||||||
// /app) needs no per-call-site knowledge.
|
|
||||||
struct Toolchain {
|
|
||||||
// LLVM install prefix. Empty when the tools are bare names on PATH.
|
|
||||||
fs::path prefix;
|
|
||||||
fs::path clangxx;
|
|
||||||
fs::path clang;
|
|
||||||
fs::path llvmAr;
|
|
||||||
// Where clang-c/Index.h lives when it is not on the default include
|
|
||||||
// path; empty otherwise.
|
|
||||||
fs::path llvmIncludeDir;
|
|
||||||
// The exact libclang to dlopen; nullopt means search the usual names.
|
|
||||||
std::optional<fs::path> libclang;
|
|
||||||
// A libc++ install other than the compiler's own
|
|
||||||
// (CRAFTER_BUILD_LIBCXX_DIR); empty means clang finds its own.
|
|
||||||
fs::path libcxxDir;
|
|
||||||
// libc++'s std module source for host builds.
|
|
||||||
fs::path stdCppm;
|
|
||||||
// The _LIBCPP_VERSION of the libc++ stdCppm belongs to (220108 for
|
|
||||||
// 22.1.8); 0 when its <__config> could not be read.
|
|
||||||
std::int32_t libcxxVersion = 0;
|
|
||||||
// Std module sources for libc++ releases other than the toolchain's
|
|
||||||
// own, one <major>/std.cppm each, for sysroots that carry libc++
|
|
||||||
// without them: <crafter prefix>/share/crafter-build/libc++-modules.
|
|
||||||
// Empty when the crafter prefix is unknown; need not exist.
|
|
||||||
fs::path stdModulesDir;
|
|
||||||
fs::path wasiSysroot;
|
|
||||||
// The mingw-w64 install prefix, the directory that holds
|
|
||||||
// x86_64-w64-mingw32/ and lib/gcc/x86_64-w64-mingw32/ (/usr for a
|
|
||||||
// distro package), passed to clang as --sysroot for the mingw target.
|
|
||||||
// Empty means clang's own discovery, which follows the mingw gcc on
|
|
||||||
// PATH. Set by CRAFTER_MINGW_DIR, else the host's /usr when running
|
|
||||||
// sandboxed and it has mingw-w64 (the sandbox's own /usr never does).
|
|
||||||
fs::path mingwSysroot;
|
|
||||||
// `clang++ -print-target-triple` and the first line of
|
|
||||||
// `clang++ --version`; both empty when clang++ could not be run.
|
|
||||||
std::string hostTriple;
|
|
||||||
std::string clangVersion;
|
|
||||||
// Why clang++ could not be resolved or run; empty when it could.
|
|
||||||
std::string error;
|
|
||||||
|
|
||||||
// Flags for compiling against libcxxDir; empty without one.
|
|
||||||
std::string LibcxxCompileFlags() const;
|
|
||||||
// Flags for linking against the libc++ the compiler will use: a -L
|
|
||||||
// and an rpath for <prefix>/lib when the prefix is not the system one
|
|
||||||
// (clang searches only its per-target runtime directory and the
|
|
||||||
// system paths, and a bundled toolchain's libc++.so.1 is on neither
|
|
||||||
// the linker's nor the loader's default path), or for an explicit
|
|
||||||
// libcxxDir.
|
|
||||||
std::string LibcxxLinkFlags() const;
|
|
||||||
// Names this toolchain for cache directory names: the clang version
|
|
||||||
// followed by a hash of what decides which headers and libraries a
|
|
||||||
// PCM is built against. Two toolchains with different keys never
|
|
||||||
// share a PCM.
|
|
||||||
std::string Key() const;
|
|
||||||
};
|
|
||||||
|
|
||||||
// What ResolveToolchain reads, so a test can feed it a staged layout.
|
|
||||||
struct ToolchainInputs {
|
|
||||||
// The CRAFTER_* variables, present only when set and non-empty.
|
|
||||||
std::map<std::string, std::string, std::less<>> env;
|
|
||||||
// The <prefix> of <prefix>/share/crafter-build; empty when unknown.
|
|
||||||
fs::path crafterPrefix;
|
|
||||||
// Where the host's root shows inside a sandbox (/run/host in a
|
|
||||||
// Flatpak); empty outside one.
|
|
||||||
fs::path hostRoot;
|
|
||||||
};
|
|
||||||
// Pure path resolution: an env override wins, then a toolchain installed
|
|
||||||
// beside crafter-build (<crafterPrefix>/bin/clang++), then bare names on
|
|
||||||
// PATH. Never runs a process, so hostTriple and clangVersion stay empty.
|
|
||||||
Toolchain ResolveToolchain(const ToolchainInputs& inputs);
|
|
||||||
// ResolveToolchain over the real environment plus the two clang probes,
|
|
||||||
// computed once and cached. Never throws.
|
|
||||||
const Toolchain& GetToolchain();
|
|
||||||
// GetToolchain(), or a std::runtime_error carrying its error.
|
|
||||||
const Toolchain& RequireToolchain();
|
|
||||||
// _LIBCPP_VERSION as the <__config> in `includeDir` (an include/c++/v1)
|
|
||||||
// defines it, XXYYZZ; 0 when the file is missing or has no such line.
|
|
||||||
std::int32_t LibcxxVersion(const fs::path& includeDir);
|
|
||||||
// "23.1.2" for 230102.
|
|
||||||
std::string LibcxxVersionString(std::int32_t version);
|
|
||||||
|
|
||||||
struct StdModuleSource {
|
|
||||||
fs::path stdCppm;
|
|
||||||
// One line saying why no source fits; empty when stdCppm is set.
|
|
||||||
std::string error;
|
|
||||||
};
|
|
||||||
// The std module source to precompile for a native (non-wasm) build
|
|
||||||
// against `sysroot`. The module source is a list of using-declarations
|
|
||||||
// for names the headers of the same libc++ release declare, so one from
|
|
||||||
// another release fails on every name that release added or dropped. In
|
|
||||||
// order: the sysroot's own <sysroot>/usr/share/libc++/v1/std.cppm, the
|
|
||||||
// toolchain's when both are the same major release, the copy for the
|
|
||||||
// sysroot's release under toolchain.stdModulesDir. Not every distro
|
|
||||||
// packages the module sources with its libc++ (Alpine does not), which
|
|
||||||
// is why the sysroot's own copy cannot be the only answer. When the
|
|
||||||
// sysroot's release cannot be read, the toolchain's source is returned
|
|
||||||
// and the compile is the check.
|
|
||||||
StdModuleSource SysrootStdModule(const Toolchain& toolchain, const fs::path& sysroot);
|
|
||||||
// The version baked in when crafter-build was built; "0.0.0-dev" for a
|
|
||||||
// build that was given none.
|
|
||||||
std::string_view CrafterBuildVersion();
|
|
||||||
}
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||||
|
|
||||||
export module Crafter.Build;
|
export module Crafter.Build;
|
||||||
export import :Toolchain;
|
|
||||||
export import :Clang;
|
export import :Clang;
|
||||||
export import :Platform;
|
export import :Platform;
|
||||||
export import :Implementation;
|
export import :Implementation;
|
||||||
|
|
@ -10,6 +9,5 @@ export import :Shader;
|
||||||
export import :External;
|
export import :External;
|
||||||
export import :Test;
|
export import :Test;
|
||||||
export import :Lint;
|
export import :Lint;
|
||||||
export import :Script;
|
|
||||||
export import :Progress;
|
export import :Progress;
|
||||||
export import :Asset;
|
export import :Asset;
|
||||||
|
|
|
||||||
25361
lib/json.hpp
Normal file
25361
lib/json.hpp
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,8 +0,0 @@
|
||||||
[Flatpak Repo]
|
|
||||||
Title=Catcrafts
|
|
||||||
Url=https://flatpak.catcrafts.net/repo/
|
|
||||||
Homepage=https://forgejo.catcrafts.net/Catcrafts
|
|
||||||
Comment=Catcrafts tools and applications
|
|
||||||
Description=crafter-build and other Catcrafts software, built and signed by Catcrafts CI
|
|
||||||
DefaultBranch=stable
|
|
||||||
GPGKey=GPG_KEY_BASE64_PLACEHOLDER
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
[Flatpak Ref]
|
|
||||||
Title=Crafter Build
|
|
||||||
Name=net.catcrafts.CrafterBuild
|
|
||||||
Branch=stable
|
|
||||||
Url=https://flatpak.catcrafts.net/repo/
|
|
||||||
SuggestRemoteName=catcrafts
|
|
||||||
Homepage=https://forgejo.catcrafts.net/Catcrafts/Crafter.Build
|
|
||||||
Comment=C++26 modules build system
|
|
||||||
Description=crafter-build with its own LLVM toolchain and WASI sysroot
|
|
||||||
Icon=
|
|
||||||
RuntimeRepo=https://dl.flathub.org/repo/flathub.flatpakrepo
|
|
||||||
IsRuntime=false
|
|
||||||
GPGKey=GPG_KEY_BASE64_PLACEHOLDER
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!-- SPDX-License-Identifier: CC0-1.0 -->
|
|
||||||
<component type="console-application">
|
|
||||||
<id>net.catcrafts.CrafterBuild</id>
|
|
||||||
<name>Crafter Build</name>
|
|
||||||
<summary>C++26 modules build system</summary>
|
|
||||||
<metadata_license>CC0-1.0</metadata_license>
|
|
||||||
<project_license>LGPL-3.0-only</project_license>
|
|
||||||
<developer id="net.catcrafts">
|
|
||||||
<name>Catcrafts</name>
|
|
||||||
</developer>
|
|
||||||
<description>
|
|
||||||
<p>
|
|
||||||
A build system for C++26 modules. Project descriptions are C++: a
|
|
||||||
project.cpp constructs a Configuration and returns it, and crafter-build
|
|
||||||
compiles, loads and executes it. The Flatpak carries its own LLVM, so
|
|
||||||
every machine builds with the same clang, lld, libc++ and WASI sysroot.
|
|
||||||
</p>
|
|
||||||
</description>
|
|
||||||
<url type="homepage">https://forgejo.catcrafts.net/Catcrafts/Crafter.Build</url>
|
|
||||||
<url type="bugtracker">https://forgejo.catcrafts.net/Catcrafts/Crafter.Build/issues</url>
|
|
||||||
<url type="vcs-browser">https://forgejo.catcrafts.net/Catcrafts/Crafter.Build</url>
|
|
||||||
<provides>
|
|
||||||
<binary>crafter-build</binary>
|
|
||||||
</provides>
|
|
||||||
<content_rating type="oars-1.1"/>
|
|
||||||
<releases>
|
|
||||||
<release version="2.2.0" date="2026-10-16">
|
|
||||||
<description>
|
|
||||||
<p>A Shader entry can carry defines, spelled like the compiler's -D (NAME is defined to 1, NAME=VALUE to VALUE), and a variant that names its output, so one source compiles into several outputs: ui.comp.glsl with variant sets becomes ui.comp.sets.spv. A configuration in which two entries share an output name is refused with both named, as are a malformed define and a variant with a path separator.</p>
|
|
||||||
<p>Compiles with a sysroot now use the sysroot's own libc++ headers instead of the bundled toolchain's, which fixes cross builds against a musl sysroot that include a standard header textually and stops glibc sysroot builds from silently compiling against the wrong libc++ release. The project.so stamp hashes every part of the host build, so a changed declaration recompiles it. The vendored json.hpp is gone.</p>
|
|
||||||
</description>
|
|
||||||
</release>
|
|
||||||
<release version="2.1.0" date="2026-10-03">
|
|
||||||
<description>
|
|
||||||
<p>The bundled toolchain is now LLVM 23.1.2. Build outputs are kept per compiler, so the first build after updating recompiles each project into a fresh directory and nothing has to be cleaned by hand.</p>
|
|
||||||
<p>Cross builds pick the std module source that matches the sysroot's libc++ release. The source for libc++ 22 ships beside the bundled 23 for sysroots that carry libc++ without one, such as Alpine, and a sysroot nothing matches fails with one line naming both releases. crafter-build --version prints the libc++ release in use.</p>
|
|
||||||
</description>
|
|
||||||
</release>
|
|
||||||
<release version="2.0.0" date="2026-10-03">
|
|
||||||
<description>
|
|
||||||
<p>First release distributed as a Flatpak, with a bundled LLVM 22 toolchain and WASI sysroot. Windows host support is removed; crafter-build gains --version and resolves its compiler from one place.</p>
|
|
||||||
</description>
|
|
||||||
</release>
|
|
||||||
</releases>
|
|
||||||
</component>
|
|
||||||
|
|
@ -1,250 +0,0 @@
|
||||||
# SPDX-License-Identifier: LGPL-3.0-only
|
|
||||||
# SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
||||||
#
|
|
||||||
# crafter-build as a Flatpak. The runtime is the Freedesktop *Sdk*, not the
|
|
||||||
# Platform: a build tool needs glibc's headers and crt objects, gcc's
|
|
||||||
# libgcc_s, cmake, ninja, make, git and python at run time. Everything
|
|
||||||
# compiler-shaped is ours and lives under /app: clang, lld, libclang, libc++,
|
|
||||||
# compiler-rt (native and wasm32) and the WASI sysroot, all built from one
|
|
||||||
# LLVM source tree so the modules crafter-build builds and the compiler that
|
|
||||||
# consumes them can never drift apart.
|
|
||||||
app-id: net.catcrafts.CrafterBuild
|
|
||||||
runtime: org.freedesktop.Sdk
|
|
||||||
runtime-version: '26.08'
|
|
||||||
sdk: org.freedesktop.Sdk
|
|
||||||
command: crafter-build
|
|
||||||
finish-args:
|
|
||||||
# git dependencies, and the local web server `-r` starts for browser builds
|
|
||||||
- --share=network
|
|
||||||
- --socket=ssh-auth
|
|
||||||
# projects live anywhere; user sysroots too
|
|
||||||
- --filesystem=host
|
|
||||||
# so `-r` can launch what it built, games included
|
|
||||||
- --share=ipc
|
|
||||||
- --socket=wayland
|
|
||||||
- --socket=fallback-x11
|
|
||||||
- --socket=pulseaudio
|
|
||||||
- --device=all
|
|
||||||
# gdb and friends from the Sdk
|
|
||||||
- --allow=devel
|
|
||||||
- --env=PATH=/app/bin:/usr/bin
|
|
||||||
cleanup:
|
|
||||||
- /share/man
|
|
||||||
- /share/doc
|
|
||||||
modules:
|
|
||||||
- name: llvm
|
|
||||||
buildsystem: cmake-ninja
|
|
||||||
builddir: true
|
|
||||||
subdir: llvm
|
|
||||||
build-options:
|
|
||||||
# flatpak-builder's default CFLAGS carry -g. For all of LLVM that is a
|
|
||||||
# 30 GB build tree and a multi-gigabyte .Debug extension, so build
|
|
||||||
# without debug info, strip, and skip the split. The hardening flags
|
|
||||||
# from the defaults stay.
|
|
||||||
no-debuginfo: true
|
|
||||||
strip: true
|
|
||||||
cflags: -O2 -pipe -fexceptions -fasynchronous-unwind-tables -fstack-protector-strong -fstack-clash-protection -fcf-protection
|
|
||||||
cflags-override: true
|
|
||||||
cxxflags: -O2 -pipe -fexceptions -fasynchronous-unwind-tables -fstack-protector-strong -fstack-clash-protection -fcf-protection
|
|
||||||
cxxflags-override: true
|
|
||||||
config-opts:
|
|
||||||
- -DCMAKE_BUILD_TYPE=Release
|
|
||||||
- -DLLVM_ENABLE_ASSERTIONS=OFF
|
|
||||||
- -DLLVM_ENABLE_PROJECTS=clang;lld
|
|
||||||
- -DLLVM_ENABLE_RUNTIMES=compiler-rt;libcxx;libcxxabi;libunwind
|
|
||||||
- -DLLVM_TARGETS_TO_BUILD=X86;AArch64;WebAssembly
|
|
||||||
# Classic layout: libc++ in lib/ and include/c++/v1, not under a
|
|
||||||
# per-target subdirectory, the same shape as a distro install.
|
|
||||||
# crafter-build adds -L and an rpath for <prefix>/lib itself, since
|
|
||||||
# clang searches only its per-target directory and the system paths.
|
|
||||||
- -DLLVM_ENABLE_PER_TARGET_RUNTIME_DIR=OFF
|
|
||||||
# One shared LLVM and one shared clang library instead of a statically
|
|
||||||
# linked copy per tool: a fraction of the install size.
|
|
||||||
- -DLLVM_BUILD_LLVM_DYLIB=ON
|
|
||||||
- -DLLVM_LINK_LLVM_DYLIB=ON
|
|
||||||
- -DCLANG_LINK_CLANG_DYLIB=ON
|
|
||||||
- -DCLANG_DEFAULT_CXX_STDLIB=libc++
|
|
||||||
- -DCLANG_DEFAULT_LINKER=lld
|
|
||||||
- -DLIBCXX_INSTALL_MODULES=ON
|
|
||||||
- -DLLVM_INCLUDE_TESTS=OFF
|
|
||||||
- -DLLVM_INCLUDE_EXAMPLES=OFF
|
|
||||||
- -DLLVM_INCLUDE_DOCS=OFF
|
|
||||||
- -DLLVM_INCLUDE_BENCHMARKS=OFF
|
|
||||||
- -DLLVM_INSTALL_UTILS=OFF
|
|
||||||
- -DCLANG_INCLUDE_TESTS=OFF
|
|
||||||
- -DCOMPILER_RT_INCLUDE_TESTS=OFF
|
|
||||||
# The Sdk links with binutils' ld; two libLLVM-sized links at a time
|
|
||||||
# fit the CX53's memory with room to spare.
|
|
||||||
- -DLLVM_PARALLEL_LINK_JOBS=2
|
|
||||||
cleanup:
|
|
||||||
- /include/llvm
|
|
||||||
- /include/llvm-c
|
|
||||||
- /include/clang
|
|
||||||
- /include/clang-tidy
|
|
||||||
- /include/lld
|
|
||||||
- /lib/cmake
|
|
||||||
- /lib/*.a
|
|
||||||
- /share/opt-viewer
|
|
||||||
- /share/scan-build
|
|
||||||
- /share/scan-view
|
|
||||||
- /bin/clang-check
|
|
||||||
- /bin/clang-extdef-mapping
|
|
||||||
- /bin/clang-import-test
|
|
||||||
- /bin/clang-linker-wrapper
|
|
||||||
- /bin/clang-nvlink-wrapper
|
|
||||||
- /bin/clang-offload-bundler
|
|
||||||
- /bin/clang-offload-packager
|
|
||||||
- /bin/clang-refactor
|
|
||||||
- /bin/clang-rename
|
|
||||||
- /bin/clang-repl
|
|
||||||
- /bin/clang-sycl-linker
|
|
||||||
- /bin/diagtool
|
|
||||||
- /bin/git-clang-format
|
|
||||||
- /bin/hmaptool
|
|
||||||
- /bin/llvm-exegesis
|
|
||||||
- /bin/scan-build*
|
|
||||||
- /bin/scan-view
|
|
||||||
sources:
|
|
||||||
- type: archive
|
|
||||||
url: https://github.com/llvm/llvm-project/releases/download/llvmorg-23.1.2/llvm-project-23.1.2.src.tar.xz
|
|
||||||
sha256: c98bbef08a2b4c2613cd50e9aa9ae7b69b1fe6c16b2c40373bc0ab6116fdf78a
|
|
||||||
|
|
||||||
- name: wasi-sysroot
|
|
||||||
# wasi-sdk's sysroot-only mode, driven by the clang built above:
|
|
||||||
# wasi-libc, compiler-rt builtins (installed into our clang's resource
|
|
||||||
# dir, where the wasm32 driver looks for them) and libc++ for wasip1 and
|
|
||||||
# wasip1-threads. The llvm-project tree is the same release as above so
|
|
||||||
# the wasm libc++ matches the native one.
|
|
||||||
buildsystem: cmake-ninja
|
|
||||||
builddir: true
|
|
||||||
build-options:
|
|
||||||
no-debuginfo: true
|
|
||||||
# flatpak-builder's default CFLAGS/LDFLAGS are x86-64 hardening flags
|
|
||||||
# (-fcf-protection, -z relro) that the nested wasm32 builds inherit
|
|
||||||
# through the environment and clang rejects for that target.
|
|
||||||
cflags: ""
|
|
||||||
cflags-override: true
|
|
||||||
cxxflags: ""
|
|
||||||
cxxflags-override: true
|
|
||||||
ldflags: ""
|
|
||||||
ldflags-override: true
|
|
||||||
strip: true
|
|
||||||
config-opts:
|
|
||||||
- -DCMAKE_TOOLCHAIN_FILE=/run/build/wasi-sysroot/wasi-host-toolchain.cmake
|
|
||||||
- -DWASI_SDK_BUILD_TOOLCHAIN=OFF
|
|
||||||
- -DWASI_SDK_INSTALL_TO_CLANG_RESOURCE_DIR=ON
|
|
||||||
- -DWASI_SDK_TARGETS=wasm32-wasip1;wasm32-wasip1-threads
|
|
||||||
- -DWASI_SDK_INCLUDE_TESTS=OFF
|
|
||||||
- -DCMAKE_INSTALL_PREFIX=/app
|
|
||||||
post-install:
|
|
||||||
# wasi-sdk installs into <prefix>/share/wasi-sysroot; crafter-build
|
|
||||||
# looks there (Crafter.Build:Toolchain), so nothing to move.
|
|
||||||
- test -f /app/share/wasi-sysroot/share/libc++/v1/std.cppm
|
|
||||||
sources:
|
|
||||||
- type: git
|
|
||||||
url: https://github.com/WebAssembly/wasi-sdk.git
|
|
||||||
tag: wasi-sdk-34
|
|
||||||
disable-submodules: true
|
|
||||||
# The compiler-rt sub-build gets the C target but not the ASM target, so
|
|
||||||
# its two wasm/*.S stubs come out as host objects when the driving clang
|
|
||||||
# does not default to wasm32. Reported upstream; drop once wasi-sdk has it.
|
|
||||||
- type: patch
|
|
||||||
path: wasi-sdk-asm-target.patch
|
|
||||||
- type: git
|
|
||||||
url: https://github.com/WebAssembly/wasi-libc.git
|
|
||||||
commit: 2e6fb9d8ee0cdf9e431fbcabe8af3115de000a13
|
|
||||||
dest: src/wasi-libc
|
|
||||||
# A git checkout, where the llvm module above unpacks the release
|
|
||||||
# tarball of the same tag: wasi-sdk treats src/llvm-project as the
|
|
||||||
# submodule it is upstream. It patches the tree with `git apply` before
|
|
||||||
# building libc++, and records `git rev-parse HEAD` in the sysroot's
|
|
||||||
# VERSION file. Unpacked from a tarball the directory is a plain
|
|
||||||
# subdirectory of the wasi-sdk checkout, where git treats the patched
|
|
||||||
# paths as outside the current directory, skips every one of them and
|
|
||||||
# exits 0. LLVM 22 happened to build without the patches; LLVM 23's
|
|
||||||
# libc++ does not compile for WASI without one of them.
|
|
||||||
- type: git
|
|
||||||
url: https://github.com/llvm/llvm-project.git
|
|
||||||
tag: llvmorg-23.1.2
|
|
||||||
commit: 85ac560262434c9ccfc0c183ec22d4138ed647fb
|
|
||||||
dest: src/llvm-project
|
|
||||||
- type: file
|
|
||||||
path: wasi-host-toolchain.cmake
|
|
||||||
|
|
||||||
- name: wasmtime
|
|
||||||
# The runner for wasm32-wasi tests and `-r` on a non-browser wasm build.
|
|
||||||
buildsystem: simple
|
|
||||||
build-commands:
|
|
||||||
- install -Dm755 wasmtime /app/bin/wasmtime
|
|
||||||
sources:
|
|
||||||
- type: archive
|
|
||||||
url: https://github.com/bytecodealliance/wasmtime/releases/download/v49.0.2/wasmtime-v49.0.2-x86_64-linux.tar.xz
|
|
||||||
sha256: a4d6e9e3a5a60f527cf7793d674c48930c80c2e8977995b8a275cad3254b9322
|
|
||||||
|
|
||||||
- name: libcxx-22-modules
|
|
||||||
# The std module source of the libc++ release before the bundled one, for
|
|
||||||
# cross sysroots that carry that libc++ without it: Alpine packages the
|
|
||||||
# headers and the library but not share/libc++/v1, and a module source
|
|
||||||
# only compiles against the headers of its own release. crafter-build
|
|
||||||
# picks <major>/std.cppm here by the sysroot's _LIBCPP_VERSION
|
|
||||||
# (SysrootStdModule in Crafter.Build:Toolchain). Add a module like this
|
|
||||||
# one per release worth supporting; the bundled clang has to be able to
|
|
||||||
# compile that release's headers.
|
|
||||||
buildsystem: simple
|
|
||||||
build-commands:
|
|
||||||
# Configure only. The cxx-modules component is std.cppm as configure
|
|
||||||
# generates it plus the .inc files straight from the source tree, so
|
|
||||||
# nothing is compiled, and libc++'s own install rule stays the one
|
|
||||||
# place that knows what a module source install consists of.
|
|
||||||
- cmake -S runtimes -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=/app/bin/clang -DCMAKE_CXX_COMPILER=/app/bin/clang++ "-DLLVM_ENABLE_RUNTIMES=libcxx;libcxxabi" -DLIBCXXABI_USE_LLVM_UNWINDER=OFF -DLIBCXX_INSTALL_MODULES=ON
|
|
||||||
# Staged, not installed to /app: the component also writes
|
|
||||||
# lib/libc++.modules.json, which would replace the bundled libc++'s.
|
|
||||||
- cmake --install build --component cxx-modules --prefix "$PWD/stage"
|
|
||||||
- grep -q "_LIBCPP_VERSION 22" libcxx/include/__config
|
|
||||||
- mkdir -p /app/share/crafter-build/libc++-modules
|
|
||||||
- cp -r stage/share/libc++/v1 /app/share/crafter-build/libc++-modules/22
|
|
||||||
sources:
|
|
||||||
- type: archive
|
|
||||||
url: https://github.com/llvm/llvm-project/releases/download/llvmorg-22.1.8/llvm-project-22.1.8.src.tar.xz
|
|
||||||
sha256: 922f1817a0df7b1489272d18134ee0087a8b068828f87ac63b9861b1a9965888
|
|
||||||
|
|
||||||
- name: crafter-build
|
|
||||||
buildsystem: simple
|
|
||||||
build-options:
|
|
||||||
# The self-host build fetches Crafter.Math, Crafter.Asset,
|
|
||||||
# Crafter.LintRules and glslang from git; they are unpinned on purpose
|
|
||||||
# (sibling repos tracking their default branch, see AGENTS.md).
|
|
||||||
build-args:
|
|
||||||
- --share=network
|
|
||||||
# On this module, not at the top of the manifest: top-level build
|
|
||||||
# options are part of every module's cache key, and the version changes
|
|
||||||
# with every commit, which made flatpak-builder rebuild LLVM each run.
|
|
||||||
env:
|
|
||||||
CRAFTER_BUILD_LLVM_DIR: /app
|
|
||||||
# The shipped binary runs on any x86_64 from the last decade; user
|
|
||||||
# projects still build with -march=native by default.
|
|
||||||
CRAFTER_BUILD_MARCH: x86-64-v2
|
|
||||||
CRAFTER_BUILD_MTUNE: generic
|
|
||||||
# CI replaces this with the version; a local build is a development
|
|
||||||
# build.
|
|
||||||
CRAFTER_BUILD_VERSION: 0.0.0-dev
|
|
||||||
build-commands:
|
|
||||||
# Bootstrap by hand, then let the result build itself: only the
|
|
||||||
# self-hosted binary has asset support and ThinLTO.
|
|
||||||
- ./build.sh
|
|
||||||
- ./bin/crafter-build
|
|
||||||
- CRAFTER_BUILD_HOME=$PWD/share/crafter-build ./bin/crafter-build test
|
|
||||||
- install -Dm755 bin/crafter.build-exe-*/crafter-build /app/bin/crafter-build
|
|
||||||
- install -Dm644 bin/crafter.build-lib-*/libcrafter-build.a /app/lib/libcrafter-build.a
|
|
||||||
- mkdir -p /app/share && cp -r share/crafter-build /app/share/
|
|
||||||
- install -Dm644 LICENSE LICENSE.GPL -t /app/share/licenses/net.catcrafts.CrafterBuild/
|
|
||||||
- install -Dm644 packaging/flatpak/net.catcrafts.CrafterBuild.metainfo.xml -t /app/share/metainfo/
|
|
||||||
sources:
|
|
||||||
- type: dir
|
|
||||||
path: ../..
|
|
||||||
skip:
|
|
||||||
- .git
|
|
||||||
- bin
|
|
||||||
- build
|
|
||||||
- lib/libcrafter-build.a
|
|
||||||
- share/crafter-build
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
# SPDX-License-Identifier: LGPL-3.0-only
|
|
||||||
# SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
||||||
#
|
|
||||||
# Points wasi-sdk's sysroot build at the clang the Flatpak built, instead of
|
|
||||||
# the one wasi-sdk would build itself. Mirrors the wasi-sdk.cmake toolchain
|
|
||||||
# file an installed wasi-sdk ships, with our prefix.
|
|
||||||
set(WASI_SDK_PREFIX /app)
|
|
||||||
set(CMAKE_SYSTEM_NAME WASI)
|
|
||||||
set(CMAKE_SYSTEM_VERSION 1)
|
|
||||||
set(CMAKE_SYSTEM_PROCESSOR wasm32)
|
|
||||||
set(triple wasm32-wasip1)
|
|
||||||
set(CMAKE_C_COMPILER ${WASI_SDK_PREFIX}/bin/clang)
|
|
||||||
set(CMAKE_CXX_COMPILER ${WASI_SDK_PREFIX}/bin/clang++)
|
|
||||||
set(CMAKE_ASM_COMPILER ${WASI_SDK_PREFIX}/bin/clang)
|
|
||||||
set(CMAKE_AR ${WASI_SDK_PREFIX}/bin/llvm-ar)
|
|
||||||
set(CMAKE_RANLIB ${WASI_SDK_PREFIX}/bin/llvm-ranlib)
|
|
||||||
set(CMAKE_NM ${WASI_SDK_PREFIX}/bin/llvm-nm)
|
|
||||||
set(CMAKE_C_COMPILER_TARGET ${triple})
|
|
||||||
set(CMAKE_CXX_COMPILER_TARGET ${triple})
|
|
||||||
set(CMAKE_ASM_COMPILER_TARGET ${triple})
|
|
||||||
# The sysroot does not exist yet when cmake probes the compiler.
|
|
||||||
set(CMAKE_C_COMPILER_WORKS ON)
|
|
||||||
set(CMAKE_CXX_COMPILER_WORKS ON)
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
wasi-sdk-sysroot.cmake: tell the compiler-rt sub-build the ASM target too
|
|
||||||
|
|
||||||
The sub-build gets CMAKE_C_COMPILER_TARGET but nothing for the ASM
|
|
||||||
language, so compiler-rt's wasm/*.S stubs (the setjmp/longjmp and C++
|
|
||||||
exception tags) are assembled for the host when the driving clang is not
|
|
||||||
a wasm-default build. wasm-ld then warns that the archive members are
|
|
||||||
neither Wasm objects nor bitcode, and code that needs the tags fails to
|
|
||||||
link. wasi-sdk's own toolchain hides this because its clang defaults to
|
|
||||||
wasm32-wasip1; the sysroot-only mode against a host clang does not.
|
|
||||||
|
|
||||||
--- a/cmake/wasi-sdk-sysroot.cmake
|
|
||||||
+++ b/cmake/wasi-sdk-sysroot.cmake
|
|
||||||
@@ -107,6 +107,7 @@
|
|
||||||
-DCOMPILER_RT_BUILD_ORC=OFF
|
|
||||||
-DCOMPILER_RT_BUILD_GWP_ASAN=OFF
|
|
||||||
-DCMAKE_C_COMPILER_TARGET=${target}
|
|
||||||
+ -DCMAKE_ASM_COMPILER_TARGET=${target}
|
|
||||||
-DCMAKE_C_FLAGS=${WASI_SDK_CPU_CFLAGS}
|
|
||||||
-DCMAKE_CXX_FLAGS=${WASI_SDK_CPU_CFLAGS}
|
|
||||||
-DCMAKE_ASM_FLAGS=${WASI_SDK_CPU_CFLAGS}
|
|
||||||
205
project.cpp
205
project.cpp
|
|
@ -8,134 +8,6 @@ import Crafter.LintRules;
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
using namespace Crafter;
|
using namespace Crafter;
|
||||||
|
|
||||||
// The local Flatpak: `crafter-build run flatpak` builds it from this checkout
|
|
||||||
// and `crafter-build run install` installs that build. The development box
|
|
||||||
// compiles with its own clang, the shipped artifact with the LLVM bundled in
|
|
||||||
// the Flatpak, so this is how a change is tried against the real thing.
|
|
||||||
namespace {
|
|
||||||
struct FlatpakPaths {
|
|
||||||
fs::path manifest;
|
|
||||||
// flatpak-builder's downloads, ccache and per-module cache. The
|
|
||||||
// LLVM build lives here: about 2 GB, and 25 minutes to rebuild.
|
|
||||||
fs::path state;
|
|
||||||
// The OSTree repo the build is exported to; `install` reads it.
|
|
||||||
fs::path repo;
|
|
||||||
fs::path buildDir;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Everything under <project>/build/flatpak, and a fixed name on purpose
|
|
||||||
// instead of a BuildDir(): the Flatpak is compiled by the toolchain
|
|
||||||
// inside the sandbox, so it does not depend on the host variant, and a
|
|
||||||
// variant-keyed directory would strand the LLVM cache on every host
|
|
||||||
// compiler update. The manifest's `skip: build` keeps it out of the
|
|
||||||
// sources the sandbox copies. flatpak-builder keys its cache on the
|
|
||||||
// manifest's absolute path, so the manifest is used where it is, never
|
|
||||||
// through a rewritten copy the way CI stamps the version in; a local
|
|
||||||
// build therefore reports version 0.0.0-dev.
|
|
||||||
FlatpakPaths FlatpakLayout(const fs::path& projectDir) {
|
|
||||||
const fs::path root = projectDir / "build" / "flatpak";
|
|
||||||
return {
|
|
||||||
.manifest = projectDir / "packaging" / "flatpak" / "net.catcrafts.CrafterBuild.yaml",
|
|
||||||
.state = root / "state",
|
|
||||||
.repo = root / "repo",
|
|
||||||
.buildDir = root / "build-dir",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Both scripts drive the host's flatpak, and the sandbox crafter-build
|
|
||||||
// itself runs in as a Flatpak has none.
|
|
||||||
bool RefusedInSandbox(std::string_view script) {
|
|
||||||
if (!fs::exists("/.flatpak-info")) return false;
|
|
||||||
std::println(std::cerr, "run {0}: needs the host's flatpak, which the Flatpak build of crafter-build cannot reach. Use a native build: ./build.sh, then ./bin/crafter-build run {0}", script);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// flatpak-builder as a host package, else the org.flatpak.Builder app.
|
|
||||||
// Empty when neither is installed.
|
|
||||||
std::string FlatpakBuilder() {
|
|
||||||
if (RunCommandWithTimeout("flatpak-builder --version", std::chrono::seconds(10)).exitCode == 0) return "flatpak-builder";
|
|
||||||
if (RunCommandWithTimeout("flatpak info org.flatpak.Builder", std::chrono::seconds(10)).exitCode == 0) return "flatpak run org.flatpak.Builder";
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
// A repo path as the file:// URL of a remote. Percent-encoded, because
|
|
||||||
// `remote-modify --url=` stores what it is given as it is.
|
|
||||||
std::string FileUrl(const fs::path& path) {
|
|
||||||
std::string url = "file://";
|
|
||||||
for (const char c : path.string()) {
|
|
||||||
const bool plain = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || std::string_view("/-._~").contains(c);
|
|
||||||
if (plain) url.push_back(c);
|
|
||||||
else url += std::format("%{:02X}", static_cast<std::uint8_t>(c));
|
|
||||||
}
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::int32_t BuildFlatpak(ScriptContext& ctx) {
|
|
||||||
if (RefusedInSandbox("flatpak")) return 1;
|
|
||||||
// Nothing else is passed through: a flag such as --stop-at or
|
|
||||||
// --download-only would end the run without a build and leave the
|
|
||||||
// smoke test below running against the previous one.
|
|
||||||
std::string jobs;
|
|
||||||
for (const std::string& arg : ctx.args) {
|
|
||||||
const std::string_view count = arg.starts_with("--jobs=") ? std::string_view(arg).substr(7) : std::string_view();
|
|
||||||
if (count.empty() || !std::ranges::all_of(count, [](char c) { return c >= '0' && c <= '9'; })) {
|
|
||||||
std::println(std::cerr, "run flatpak: unknown argument '{}' (only --jobs=<N> is accepted)", arg);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
jobs = std::format(" --jobs={}", count);
|
|
||||||
}
|
|
||||||
const std::string builder = FlatpakBuilder();
|
|
||||||
if (builder.empty()) {
|
|
||||||
std::println(std::cerr, "run flatpak: neither flatpak-builder nor the org.flatpak.Builder app is installed (flatpak install flathub org.flatpak.Builder)");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
const FlatpakPaths paths = FlatpakLayout(ctx.projectDir);
|
|
||||||
const std::string buildDir = ShellQuote(paths.buildDir.string());
|
|
||||||
if (const std::int32_t code = ctx.Run(std::format("{}{} --user --force-clean --ccache --state-dir={} --repo={} --default-branch=local {} {}", builder, jobs, ShellQuote(paths.state.string()), ShellQuote(paths.repo.string()), buildDir, ShellQuote(paths.manifest.string()))); code != 0) return code;
|
|
||||||
// The smoke test CI runs on its build, but through `flatpak build`
|
|
||||||
// and not `<builder> --run`: the org.flatpak.Builder app (1.4.9)
|
|
||||||
// exits 1 when the command it ran succeeded and 0 when it failed,
|
|
||||||
// and --run also wants the state dir again or it drops a
|
|
||||||
// .flatpak-builder/ into the working directory.
|
|
||||||
return ctx.Run(std::format("flatpak build {} crafter-build --version", buildDir));
|
|
||||||
}
|
|
||||||
|
|
||||||
std::int32_t InstallFlatpak(ScriptContext& ctx) {
|
|
||||||
if (RefusedInSandbox("install")) return 1;
|
|
||||||
if (!ctx.args.empty()) {
|
|
||||||
std::println(std::cerr, "run install: takes no arguments (got '{}')", ctx.args.front());
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
const FlatpakPaths paths = FlatpakLayout(ctx.projectDir);
|
|
||||||
// Deliberately not built from here: flatpak-builder never caches a
|
|
||||||
// `type: dir` module, so every build reruns build.sh and the whole
|
|
||||||
// suite inside the sandbox. Installing is the cheap half.
|
|
||||||
if (!fs::exists(paths.repo / "config")) {
|
|
||||||
std::println(std::cerr, "run install: nothing built yet, run: crafter-build run flatpak");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
const std::string url = ShellQuote(FileUrl(paths.repo));
|
|
||||||
// Its own branch from its own remote, next to whichever published
|
|
||||||
// branch is installed. The remote may already exist and point at
|
|
||||||
// another checkout's repo; --if-not-exists would keep that silently.
|
|
||||||
if (const std::int32_t code = ctx.Run(std::format("flatpak remote-add --user --if-not-exists --no-gpg-verify crafter-build-local {}", url)); code != 0) return code;
|
|
||||||
if (const std::int32_t code = ctx.Run(std::format("flatpak remote-modify --user --url={} crafter-build-local", url)); code != 0) return code;
|
|
||||||
if (const std::int32_t code = ctx.Run("flatpak install --user --noninteractive --or-update crafter-build-local net.catcrafts.CrafterBuild//local"); code != 0) return code;
|
|
||||||
// Only a first install becomes the branch `flatpak run` starts; an
|
|
||||||
// update leaves that choice where it was.
|
|
||||||
if (const std::int32_t code = ctx.Run("flatpak make-current --user net.catcrafts.CrafterBuild local"); code != 0) return code;
|
|
||||||
std::println(
|
|
||||||
R"(
|
|
||||||
`flatpak run net.catcrafts.CrafterBuild` now starts this build.
|
|
||||||
Back to a published branch, keeping this one installed:
|
|
||||||
flatpak make-current --user net.catcrafts.CrafterBuild <branch>
|
|
||||||
Remove it (do this before `crafter-build clean`, which deletes the repo the remote points at):
|
|
||||||
flatpak uninstall --user net.catcrafts.CrafterBuild//local
|
|
||||||
flatpak remote-delete --user crafter-build-local)");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> args) {
|
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> args) {
|
||||||
std::vector<std::string> depArgs(args.begin(), args.end());
|
std::vector<std::string> depArgs(args.begin(), args.end());
|
||||||
Configuration* math = GitProject({
|
Configuration* math = GitProject({
|
||||||
|
|
@ -152,36 +24,30 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
CrafterBuildLib->name = "crafter.build-lib";
|
CrafterBuildLib->name = "crafter.build-lib";
|
||||||
CrafterBuildLib->outputName = "crafter-build";
|
CrafterBuildLib->outputName = "crafter-build";
|
||||||
ApplyStandardArgs(*CrafterBuildLib, args);
|
ApplyStandardArgs(*CrafterBuildLib, args);
|
||||||
// Monolithic: project.so resolves Crafter:: symbols from the running
|
// Windows builds (native msvc via build.cmd or cross-compiled mingw from
|
||||||
// executable, so the library is only ever linked statically into it.
|
// Linux) need a DLL + import lib + launcher exe so LoadProject can
|
||||||
CrafterBuildLib->type = ConfigurationType::LibraryStatic;
|
// compile project.cpp against a stable ABI boundary. Linux is monolithic.
|
||||||
|
CrafterBuildLib->type = (CrafterBuildLib->target == "x86_64-w64-mingw32" || CrafterBuildLib->target == "x86_64-pc-windows-msvc")
|
||||||
|
? ConfigurationType::LibraryDynamic
|
||||||
|
: ConfigurationType::LibraryStatic;
|
||||||
|
|
||||||
CrafterBuildLib->dependencies = { math, asset };
|
CrafterBuildLib->dependencies = { math, asset };
|
||||||
CrafterBuildLib->defines.push_back({"CRAFTER_BUILD_HAS_ASSET", ""});
|
CrafterBuildLib->defines.push_back({"CRAFTER_BUILD_HAS_ASSET", ""});
|
||||||
// Crafter.Build-Lint includes libclang's clang-c/Index.h, which is only
|
// Crafter.Build-Lint includes libclang's clang-c/Index.h. glibc distros
|
||||||
// on the default include path for a toolchain installed in /usr.
|
// keep it in /usr/include; Alpine keeps LLVM under /usr/lib/llvmNN, so ask
|
||||||
if (const fs::path& llvmInclude = GetToolchain().llvmIncludeDir; !llvmInclude.empty()) {
|
// llvm-config and add the dir when it is not already on the default path
|
||||||
CrafterBuildLib->compileFlags.push_back(std::format("-I{}", llvmInclude.string()));
|
// (never /usr/include itself: that breaks libc++'s #include_next).
|
||||||
|
if (CrafterBuildLib->target.ends_with("-linux-gnu") || CrafterBuildLib->target.ends_with("-linux-musl")) {
|
||||||
|
auto llvmInc = RunCommandWithTimeout("llvm-config --includedir 2>/dev/null", std::chrono::seconds(10));
|
||||||
|
std::string incDir = llvmInc.output;
|
||||||
|
while (!incDir.empty() && (incDir.back() == '\n' || incDir.back() == '\r')) incDir.pop_back();
|
||||||
|
if (llvmInc.exitCode == 0 && !incDir.empty() && incDir != "/usr/include") {
|
||||||
|
CrafterBuildLib->compileFlags.push_back(std::format("-I{}", incDir));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// The version the binary reports: CI sets it from the tag, a checkout
|
|
||||||
// describes itself, anything else is a development build. A define on
|
|
||||||
// the library, so each version is its own library variant.
|
|
||||||
std::string version;
|
|
||||||
if (const char* env = std::getenv("CRAFTER_BUILD_VERSION"); env && *env) {
|
|
||||||
version = env;
|
|
||||||
} else {
|
|
||||||
auto described = RunCommandWithTimeout("git describe --tags --always --dirty --exclude=latest 2>/dev/null", std::chrono::seconds(10));
|
|
||||||
version = described.output;
|
|
||||||
while (!version.empty() && (version.back() == '\n' || version.back() == '\r')) version.pop_back();
|
|
||||||
if (described.exitCode != 0 || version.empty()) version = "0.0.0-dev";
|
|
||||||
}
|
|
||||||
// Backslash-escaped quotes: the define travels through the shell before
|
|
||||||
// it reaches clang, which must still see a string literal.
|
|
||||||
CrafterBuildLib->defines.push_back({"CRAFTER_BUILD_VERSION", std::format("\\\"{}\\\"", version)});
|
|
||||||
{
|
{
|
||||||
std::array<fs::path, 13> interfaces = {
|
std::array<fs::path, 11> interfaces = {
|
||||||
"interfaces/Crafter.Build",
|
"interfaces/Crafter.Build",
|
||||||
"interfaces/Crafter.Build-Toolchain",
|
|
||||||
"interfaces/Crafter.Build-Shader",
|
"interfaces/Crafter.Build-Shader",
|
||||||
"interfaces/Crafter.Build-Platform",
|
"interfaces/Crafter.Build-Platform",
|
||||||
"interfaces/Crafter.Build-Interface",
|
"interfaces/Crafter.Build-Interface",
|
||||||
|
|
@ -190,12 +56,10 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
"interfaces/Crafter.Build-Clang",
|
"interfaces/Crafter.Build-Clang",
|
||||||
"interfaces/Crafter.Build-Test",
|
"interfaces/Crafter.Build-Test",
|
||||||
"interfaces/Crafter.Build-Lint",
|
"interfaces/Crafter.Build-Lint",
|
||||||
"interfaces/Crafter.Build-Script",
|
|
||||||
"interfaces/Crafter.Build-Progress",
|
"interfaces/Crafter.Build-Progress",
|
||||||
"interfaces/Crafter.Build-Asset",
|
"interfaces/Crafter.Build-Asset",
|
||||||
};
|
};
|
||||||
std::array<fs::path, 12> implementations = {
|
std::array<fs::path, 10> implementations = {
|
||||||
"implementations/Crafter.Build-Toolchain",
|
|
||||||
"implementations/Crafter.Build-Shader",
|
"implementations/Crafter.Build-Shader",
|
||||||
"implementations/Crafter.Build-Platform",
|
"implementations/Crafter.Build-Platform",
|
||||||
"implementations/Crafter.Build-Interface",
|
"implementations/Crafter.Build-Interface",
|
||||||
|
|
@ -204,7 +68,6 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
"implementations/Crafter.Build-Clang",
|
"implementations/Crafter.Build-Clang",
|
||||||
"implementations/Crafter.Build-Test",
|
"implementations/Crafter.Build-Test",
|
||||||
"implementations/Crafter.Build-Lint",
|
"implementations/Crafter.Build-Lint",
|
||||||
"implementations/Crafter.Build-Script",
|
|
||||||
"implementations/Crafter.Build-Progress",
|
"implementations/Crafter.Build-Progress",
|
||||||
"implementations/Crafter.Build-Asset",
|
"implementations/Crafter.Build-Asset",
|
||||||
};
|
};
|
||||||
|
|
@ -216,6 +79,12 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
glslang.source.branch = "main";
|
glslang.source.branch = "main";
|
||||||
glslang.builder = ExternalBuilder::CMake;
|
glslang.builder = ExternalBuilder::CMake;
|
||||||
glslang.options = { "-DENABLE_OPT=OFF" };
|
glslang.options = { "-DENABLE_OPT=OFF" };
|
||||||
|
// mingw cross-build: skip the standalone executable. We only consume the
|
||||||
|
// libraries, and glslang.exe pulls in libgcc_eh which needs pthread that
|
||||||
|
// mingw-w64 doesn't link by default.
|
||||||
|
if (CrafterBuildLib->target == "x86_64-w64-mingw32") {
|
||||||
|
glslang.options.push_back("-DENABLE_GLSLANG_BINARIES=OFF");
|
||||||
|
}
|
||||||
glslang.includeDirs = { "" };
|
glslang.includeDirs = { "" };
|
||||||
glslang.libs = { "SPIRV", "GenericCodeGen", "glslang", "OSDependent", "MachineIndependent", "glslang-default-resource-limits" };
|
glslang.libs = { "SPIRV", "GenericCodeGen", "glslang", "OSDependent", "MachineIndependent", "glslang-default-resource-limits" };
|
||||||
|
|
||||||
|
|
@ -237,6 +106,10 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
cfg.linkFlags.push_back("-Wl,--export-dynamic");
|
cfg.linkFlags.push_back("-Wl,--export-dynamic");
|
||||||
cfg.linkFlags.push_back("-ldl");
|
cfg.linkFlags.push_back("-ldl");
|
||||||
}
|
}
|
||||||
|
if (cfg.target == "x86_64-w64-mingw32" || cfg.target == "x86_64-pc-windows-msvc") {
|
||||||
|
// winsock for the -r wasm port probe (bind/WSAStartup).
|
||||||
|
CrafterBuildLib->linkFlags.push_back("-lws2_32");
|
||||||
|
}
|
||||||
|
|
||||||
// Self-tests link the local crafter-build library and exercise it in
|
// Self-tests link the local crafter-build library and exercise it in
|
||||||
// process. The harness (whichever crafter-build invokes `test`) compiles
|
// process. The harness (whichever crafter-build invokes `test`) compiles
|
||||||
|
|
@ -255,23 +128,11 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
cfg.AddTest("CompilerWarning").Dependencies({ CrafterBuildLib.get() });
|
cfg.AddTest("CompilerWarning").Dependencies({ CrafterBuildLib.get() });
|
||||||
cfg.AddTest("CleanProject").Dependencies({ CrafterBuildLib.get() });
|
cfg.AddTest("CleanProject").Dependencies({ CrafterBuildLib.get() });
|
||||||
cfg.AddTest("ShaderCompile").Dependencies({ CrafterBuildLib.get() });
|
cfg.AddTest("ShaderCompile").Dependencies({ CrafterBuildLib.get() });
|
||||||
cfg.AddTest("ShaderVariantBuild").Dependencies({ CrafterBuildLib.get() });
|
|
||||||
cfg.AddTest("ShaderIncludeChange").Dependencies({ CrafterBuildLib.get() });
|
|
||||||
cfg.AddTest("MingwMissing").Dependencies({ CrafterBuildLib.get() });
|
|
||||||
cfg.AddTest("GitSubmodules").Dependencies({ CrafterBuildLib.get() });
|
|
||||||
cfg.AddTest("LinkFlagsChange").Dependencies({ CrafterBuildLib.get() });
|
|
||||||
cfg.AddTest("ExternalArtifactTime").Dependencies({ CrafterBuildLib.get() });
|
|
||||||
cfg.AddTest("ConcurrentExternalFetch").Dependencies({ CrafterBuildLib.get() });
|
|
||||||
cfg.AddTest("StandardArgs").Dependencies({ CrafterBuildLib.get() });
|
cfg.AddTest("StandardArgs").Dependencies({ CrafterBuildLib.get() });
|
||||||
cfg.AddTest("TestRunnerSpec").Dependencies({ CrafterBuildLib.get() });
|
cfg.AddTest("TestRunnerSpec").Dependencies({ CrafterBuildLib.get() });
|
||||||
cfg.AddTest("VariantId").Dependencies({ CrafterBuildLib.get() });
|
cfg.AddTest("VariantId").Dependencies({ CrafterBuildLib.get() });
|
||||||
cfg.AddTest("ToolchainResolve").Dependencies({ CrafterBuildLib.get() });
|
|
||||||
cfg.AddTest("SysrootStdModule").Dependencies({ CrafterBuildLib.get() });
|
|
||||||
cfg.AddTest("SysrootCompileFlags").Dependencies({ CrafterBuildLib.get() });
|
|
||||||
cfg.AddTest("VersionFlag").Dependencies({ CrafterBuildLib.get() });
|
|
||||||
cfg.AddTest("WasiBrowserRuntime").Dependencies({ CrafterBuildLib.get() });
|
cfg.AddTest("WasiBrowserRuntime").Dependencies({ CrafterBuildLib.get() });
|
||||||
cfg.AddTest("WasmVariants").Dependencies({ CrafterBuildLib.get() });
|
cfg.AddTest("WasmVariants").Dependencies({ CrafterBuildLib.get() });
|
||||||
cfg.AddTest("WasmHelloRuns").Dependencies({ CrafterBuildLib.get() });
|
|
||||||
cfg.AddTest("RunSingleTestExit").Dependencies({ CrafterBuildLib.get() });
|
cfg.AddTest("RunSingleTestExit").Dependencies({ CrafterBuildLib.get() });
|
||||||
// LoadProject dlopens the synthesized project.so, which references
|
// LoadProject dlopens the synthesized project.so, which references
|
||||||
// Crafter:: symbols (HostTarget, Configuration ctors) that have to be
|
// Crafter:: symbols (HostTarget, Configuration ctors) that have to be
|
||||||
|
|
@ -282,12 +143,8 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
// Same LoadProject wiring as ConcurrentCacheRace above.
|
// Same LoadProject wiring as ConcurrentCacheRace above.
|
||||||
cfg.AddTest("HostCacheSourceStamp").Dependencies({ CrafterBuildLib.get() })
|
cfg.AddTest("HostCacheSourceStamp").Dependencies({ CrafterBuildLib.get() })
|
||||||
.LinkFlag("-Wl,--export-dynamic").LinkFlag("-ldl");
|
.LinkFlag("-Wl,--export-dynamic").LinkFlag("-ldl");
|
||||||
cfg.AddTest("HostCacheToolchainKey").Dependencies({ CrafterBuildLib.get() })
|
|
||||||
.LinkFlag("-Wl,--export-dynamic").LinkFlag("-ldl");
|
|
||||||
cfg.AddTest("ProjectDependency").Dependencies({ CrafterBuildLib.get() })
|
cfg.AddTest("ProjectDependency").Dependencies({ CrafterBuildLib.get() })
|
||||||
.LinkFlag("-Wl,--export-dynamic").LinkFlag("-ldl");
|
.LinkFlag("-Wl,--export-dynamic").LinkFlag("-ldl");
|
||||||
cfg.AddTest("Script").Dependencies({ CrafterBuildLib.get() })
|
|
||||||
.LinkFlag("-Wl,--export-dynamic").LinkFlag("-ldl");
|
|
||||||
cfg.AddTest("ConcurrentDependencyReset").Dependencies({ CrafterBuildLib.get() });
|
cfg.AddTest("ConcurrentDependencyReset").Dependencies({ CrafterBuildLib.get() });
|
||||||
cfg.AddTest("Lint").Dependencies({ CrafterBuildLib.get() });
|
cfg.AddTest("Lint").Dependencies({ CrafterBuildLib.get() });
|
||||||
}
|
}
|
||||||
|
|
@ -297,9 +154,5 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
// rules also auto-fix under `crafter-build format`.
|
// rules also auto-fix under `crafter-build format`.
|
||||||
LintRules::AddRules(cfg);
|
LintRules::AddRules(cfg);
|
||||||
|
|
||||||
// See the top of this file.
|
|
||||||
cfg.scripts.push_back({"flatpak", BuildFlatpak});
|
|
||||||
cfg.scripts.push_back({"install", InstallFlatpak});
|
|
||||||
|
|
||||||
return cfg;
|
return cfg;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,12 +29,6 @@ namespace {
|
||||||
std::ofstream(root / "keep-me.txt") << "untouched";
|
std::ofstream(root / "keep-me.txt") << "untouched";
|
||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::int32_t RunWith(std::vector<std::string> args) {
|
|
||||||
std::vector<char*> argv;
|
|
||||||
for (std::string& arg : args) argv.push_back(arg.data());
|
|
||||||
return Run(static_cast<std::int32_t>(argv.size()), argv.data());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int main() {
|
int main() {
|
||||||
|
|
@ -77,22 +71,6 @@ int main() {
|
||||||
Check(fs::exists(cwdBin) == cwdBinExisted, "the cwd's bin/ is untouched");
|
Check(fs::exists(cwdBin) == cwdBinExisted, "the cwd's bin/ is untouched");
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
|
||||||
// `clean` is a verb only where a verb can stand. After `format` it
|
|
||||||
// is a rule-name glob like any other word, and a glob must never
|
|
||||||
// delete the build trees. The project file cannot compile, so the
|
|
||||||
// run fails once it gets as far as loading it — which is the proof
|
|
||||||
// that it went on to format instead of cleaning and exiting 0.
|
|
||||||
fs::path root = StageProject("format-glob");
|
|
||||||
fs::path projectFile = root / "project.cpp";
|
|
||||||
std::ofstream(projectFile) << "this is not valid C++ at all\n";
|
|
||||||
|
|
||||||
const std::int32_t exitCode = RunWith({"crafter-build", "format", "clean", std::format("--project={}", projectFile.string())});
|
|
||||||
Check(fs::exists(root / "bin" / "app-x86_64-pc-linux-gnu-native-native-0badf00d" / "app"), "`format clean` leaves bin/ alone");
|
|
||||||
Check(fs::exists(root / "build" / "app-x86_64-pc-linux-gnu-native-native-0badf00d" / "main_impl.o"), "`format clean` leaves build/ alone");
|
|
||||||
Check(exitCode != 0, "`format clean` goes on to load the project instead of cleaning");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Failures > 0) {
|
if (Failures > 0) {
|
||||||
std::println(std::cerr, "{} assertions failed", Failures);
|
std::println(std::cerr, "{} assertions failed", Failures);
|
||||||
return 1;
|
return 1;
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,11 @@ namespace {
|
||||||
app.dependencies = { lib };
|
app.dependencies = { lib };
|
||||||
// The link step reads a command's output the same way a compile does,
|
// The link step reads a command's output the same way a compile does,
|
||||||
// so cover it too: an unknown -z value makes ld.lld warn and exit 0.
|
// so cover it too: an unknown -z value makes ld.lld warn and exit 0.
|
||||||
app.linkFlags = { "-Wl,-z,crafter-build-nonexistent-z-value" };
|
// GNU-style only — lld-link spells its options differently — so the
|
||||||
|
// link case rides along on hosts that use it and is skipped elsewhere.
|
||||||
|
if (!app.target.contains("windows")) {
|
||||||
|
app.linkFlags = { "-Wl,-z,crafter-build-nonexistent-z-value" };
|
||||||
|
}
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,95 +0,0 @@
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
|
||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
||||||
|
|
||||||
import std;
|
|
||||||
import Crafter.Build;
|
|
||||||
namespace fs = std::filesystem;
|
|
||||||
using namespace Crafter;
|
|
||||||
|
|
||||||
// Builds that run at the same time share their dependencies' clones in the
|
|
||||||
// cache: the tests `crafter-build test --jobs=N` builds side by side each
|
|
||||||
// declare the same ExternalDependency, and two crafter-builds can run at
|
|
||||||
// once. Fetching a clone that already exists ran `git pull` (and the CMake
|
|
||||||
// build) in it with nothing to keep a second fetch out, and the second git
|
|
||||||
// fails on the first one's index.lock. A clone's fetch and build now hold a
|
|
||||||
// lock of their own.
|
|
||||||
//
|
|
||||||
// Eight threads fetch the same tracked branch of a local repository at
|
|
||||||
// once, a few rounds over, the branch moving between rounds so every pull
|
|
||||||
// has work to do; every fetch has to succeed. Everything is local;
|
|
||||||
// XDG_CACHE_HOME keeps the clone out of the real cache.
|
|
||||||
|
|
||||||
extern "C" int setenv(const char* name, const char* value, int overwrite);
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
std::int32_t failures = 0;
|
|
||||||
|
|
||||||
void Check(bool cond, std::string_view msg) {
|
|
||||||
if (!cond) {
|
|
||||||
std::println(std::cerr, "FAIL: {}", msg);
|
|
||||||
++failures;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Git(const fs::path& dir, std::string_view args) {
|
|
||||||
const std::string command = std::format("git -C {} {}", ShellQuote(dir.string()), args);
|
|
||||||
auto result = RunCommandWithTimeout(command, std::chrono::seconds(60));
|
|
||||||
if (result.exitCode != 0) throw std::runtime_error(std::format("{} failed: {}", command, result.output));
|
|
||||||
}
|
|
||||||
|
|
||||||
void Commit(const fs::path& repo, std::uint32_t round) {
|
|
||||||
std::ofstream(repo / "round.txt", std::ios::binary | std::ios::trunc) << round;
|
|
||||||
Git(repo, "add round.txt");
|
|
||||||
Git(repo, std::format("commit -q -m round-{}", round));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int main() {
|
|
||||||
const fs::path scratch = fs::temp_directory_path() / "crafter-build-concurrent-external-fetch";
|
|
||||||
fs::remove_all(scratch);
|
|
||||||
fs::create_directories(scratch);
|
|
||||||
setenv("XDG_CACHE_HOME", (scratch / "cache").c_str(), 1);
|
|
||||||
setenv("GIT_CONFIG_NOSYSTEM", "1", 1);
|
|
||||||
setenv("GIT_CONFIG_GLOBAL", "/dev/null", 1);
|
|
||||||
for (const char* name : { "GIT_AUTHOR_NAME", "GIT_COMMITTER_NAME" }) setenv(name, "crafter-build test", 1);
|
|
||||||
for (const char* name : { "GIT_AUTHOR_EMAIL", "GIT_COMMITTER_EMAIL" }) setenv(name, "test@example.invalid", 1);
|
|
||||||
|
|
||||||
const fs::path repo = scratch / "shared";
|
|
||||||
fs::create_directories(repo);
|
|
||||||
Git(repo, "init -q -b main");
|
|
||||||
Commit(repo, 0);
|
|
||||||
const GitSource source { .url = std::format("file://{}", repo.string()), .branch = "main" };
|
|
||||||
const fs::path clone = GitFetch(source);
|
|
||||||
|
|
||||||
constexpr std::uint32_t threads = 8;
|
|
||||||
constexpr std::uint32_t rounds = 4;
|
|
||||||
std::vector<std::string> errors;
|
|
||||||
std::mutex errorsMutex;
|
|
||||||
for (std::uint32_t round = 1; round <= rounds; ++round) {
|
|
||||||
Commit(repo, round);
|
|
||||||
std::vector<std::jthread> fetchers;
|
|
||||||
for (std::uint32_t i = 0; i < threads; ++i) {
|
|
||||||
fetchers.emplace_back([&] {
|
|
||||||
try {
|
|
||||||
(void)GitFetch(source);
|
|
||||||
} catch (const std::exception& e) {
|
|
||||||
std::lock_guard lock(errorsMutex);
|
|
||||||
errors.push_back(e.what());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Check(errors.empty(), std::format("{} fetches at once, {} rounds: every one succeeds ({} failed{}{})", threads, rounds, errors.size(), errors.empty() ? "" : ", the first: ", errors.empty() ? "" : errors.front()));
|
|
||||||
std::ifstream round(clone / "round.txt");
|
|
||||||
std::uint32_t last = 0;
|
|
||||||
round >> last;
|
|
||||||
Check(last == rounds, std::format("and the clone ends at the last round ({})", last));
|
|
||||||
|
|
||||||
std::error_code ec;
|
|
||||||
fs::remove_all(scratch, ec);
|
|
||||||
if (failures > 0) {
|
|
||||||
std::println(std::cerr, "{} assertions failed", failures);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
@ -1,89 +0,0 @@
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
|
||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
||||||
|
|
||||||
import std;
|
|
||||||
import Crafter.Build;
|
|
||||||
namespace fs = std::filesystem;
|
|
||||||
using namespace Crafter;
|
|
||||||
|
|
||||||
// A CMake dependency's newest library decides when its consumers are stale
|
|
||||||
// (BuildExternal's latestArtifact). Only files directly in the CMake build
|
|
||||||
// directory named *.a or *.so were looked at, so msquic's
|
|
||||||
// bin/Release/libmsquic.so.2, in one of the dependency's libDirs and with a
|
|
||||||
// version after the .so, never counted: a rebuilt msquic left everything
|
|
||||||
// built against the old one in place.
|
|
||||||
//
|
|
||||||
// The dependency here is a local repository whose CMake project puts a
|
|
||||||
// versioned shared library in bin/Release, declared as a libDir. Everything
|
|
||||||
// is local; XDG_CACHE_HOME keeps the clone out of the real cache.
|
|
||||||
|
|
||||||
extern "C" int setenv(const char* name, const char* value, int overwrite);
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
std::int32_t failures = 0;
|
|
||||||
|
|
||||||
void Check(bool cond, std::string_view msg) {
|
|
||||||
if (!cond) {
|
|
||||||
std::println(std::cerr, "FAIL: {}", msg);
|
|
||||||
++failures;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Git(const fs::path& dir, std::string_view args) {
|
|
||||||
const std::string command = std::format("git -C {} {}", ShellQuote(dir.string()), args);
|
|
||||||
auto result = RunCommandWithTimeout(command, std::chrono::seconds(60));
|
|
||||||
if (result.exitCode != 0) throw std::runtime_error(std::format("{} failed: {}", command, result.output));
|
|
||||||
}
|
|
||||||
|
|
||||||
void Write(const fs::path& file, std::string_view text) {
|
|
||||||
std::ofstream(file, std::ios::binary | std::ios::trunc) << text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int main() {
|
|
||||||
const fs::path scratch = fs::temp_directory_path() / "crafter-build-external-artifact-time";
|
|
||||||
fs::remove_all(scratch);
|
|
||||||
fs::create_directories(scratch);
|
|
||||||
setenv("XDG_CACHE_HOME", (scratch / "cache").c_str(), 1);
|
|
||||||
setenv("GIT_CONFIG_NOSYSTEM", "1", 1);
|
|
||||||
setenv("GIT_CONFIG_GLOBAL", "/dev/null", 1);
|
|
||||||
for (const char* name : { "GIT_AUTHOR_NAME", "GIT_COMMITTER_NAME" }) setenv(name, "crafter-build test", 1);
|
|
||||||
for (const char* name : { "GIT_AUTHOR_EMAIL", "GIT_COMMITTER_EMAIL" }) setenv(name, "test@example.invalid", 1);
|
|
||||||
|
|
||||||
const fs::path repo = scratch / "versioned";
|
|
||||||
fs::create_directories(repo);
|
|
||||||
Write(repo / "lib.c", "int Versioned(void) { return 2; }\n");
|
|
||||||
Write(repo / "CMakeLists.txt",
|
|
||||||
"cmake_minimum_required(VERSION 3.16)\n"
|
|
||||||
"project(versioned C)\n"
|
|
||||||
"add_library(versioned SHARED lib.c)\n"
|
|
||||||
"set_target_properties(versioned PROPERTIES VERSION 2.0.0 SOVERSION 2\n"
|
|
||||||
" LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin/Release)\n");
|
|
||||||
Git(repo, "init -q -b main");
|
|
||||||
Git(repo, "add .");
|
|
||||||
Git(repo, "commit -q -m versioned");
|
|
||||||
|
|
||||||
ExternalDependency dep;
|
|
||||||
dep.name = "versioned";
|
|
||||||
dep.source.url = std::format("file://{}", repo.string());
|
|
||||||
dep.builder = ExternalBuilder::CMake;
|
|
||||||
dep.libDirs = { "bin/Release" };
|
|
||||||
dep.libs = { "versioned" };
|
|
||||||
std::atomic<bool> cancelled = false;
|
|
||||||
const ExternalBuildResult result = BuildExternal(dep, HostTarget(), cancelled);
|
|
||||||
Check(result.error.empty(), std::format("the dependency builds: {}", result.error));
|
|
||||||
|
|
||||||
const fs::path library = ExternalCloneDir(dep, HostTarget()) / "build" / "bin" / "Release" / "libversioned.so.2.0.0";
|
|
||||||
Check(fs::exists(library), std::format("the library is where its CMake project puts it: {}", library.string()));
|
|
||||||
if (fs::exists(library)) {
|
|
||||||
Check(result.latestArtifact == fs::last_write_time(library), "its newest artifact is the versioned library in the libDir");
|
|
||||||
}
|
|
||||||
|
|
||||||
std::error_code ec;
|
|
||||||
fs::remove_all(scratch, ec);
|
|
||||||
if (failures > 0) {
|
|
||||||
std::println(std::cerr, "{} assertions failed", failures);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
@ -1,128 +0,0 @@
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
|
||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
||||||
|
|
||||||
import std;
|
|
||||||
import Crafter.Build;
|
|
||||||
namespace fs = std::filesystem;
|
|
||||||
using namespace Crafter;
|
|
||||||
|
|
||||||
// A fetched dependency's submodules have to match the revision it was fetched
|
|
||||||
// at. They used to come from the remote's default branch: a fresh clone was
|
|
||||||
// `git clone --recursive`, which checks out the default branch's submodules,
|
|
||||||
// and the `switch` or `checkout` to the requested branch or commit that came
|
|
||||||
// after it left them where they were. A branch that was pulled forward kept
|
|
||||||
// the submodules of its old tip as well. So pinning msquic to a release built
|
|
||||||
// that release against the main branch's quictls.
|
|
||||||
//
|
|
||||||
// The test makes a repository whose submodule moves between two of its
|
|
||||||
// commits, then fetches it four ways through GitFetch:
|
|
||||||
// - pinned to the older commit;
|
|
||||||
// - by a branch at the older commit;
|
|
||||||
// - by a branch that is then moved to the newer commit and fetched again;
|
|
||||||
// - pinned again after the cached clone's submodule was checked out
|
|
||||||
// elsewhere, as an older crafter-build left it.
|
|
||||||
// Everything is local; XDG_CACHE_HOME keeps the clones out of the real cache.
|
|
||||||
|
|
||||||
extern "C" int setenv(const char* name, const char* value, int overwrite);
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
std::int32_t failures = 0;
|
|
||||||
|
|
||||||
void Check(bool cond, std::string_view msg) {
|
|
||||||
if (!cond) {
|
|
||||||
std::println(std::cerr, "FAIL: {}", msg);
|
|
||||||
++failures;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Runs git in `dir`; the output, or an exception naming the command.
|
|
||||||
std::string Git(const fs::path& dir, std::string_view args) {
|
|
||||||
const std::string command = std::format("git -C {} {}", ShellQuote(dir.string()), args);
|
|
||||||
auto result = RunCommandWithTimeout(command, std::chrono::seconds(60));
|
|
||||||
if (result.exitCode != 0) throw std::runtime_error(std::format("{} failed: {}", command, result.output));
|
|
||||||
while (!result.output.empty() && (result.output.back() == '\n' || result.output.back() == '\r')) result.output.pop_back();
|
|
||||||
return result.output;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Write(const fs::path& file, std::string_view text) {
|
|
||||||
std::ofstream(file, std::ios::binary | std::ios::trunc) << text;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string Read(const fs::path& file) {
|
|
||||||
std::ifstream stream(file, std::ios::binary);
|
|
||||||
return std::string(std::istreambuf_iterator<char>(stream), std::istreambuf_iterator<char>());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int main() {
|
|
||||||
const fs::path scratch = fs::temp_directory_path() / "crafter-build-git-submodules";
|
|
||||||
fs::remove_all(scratch);
|
|
||||||
fs::create_directories(scratch);
|
|
||||||
setenv("XDG_CACHE_HOME", (scratch / "cache").c_str(), 1);
|
|
||||||
// A known git: no user or system configuration, an identity for the
|
|
||||||
// commits, and file:// allowed for submodules (git refuses it by default),
|
|
||||||
// also for the git that GitFetch runs.
|
|
||||||
setenv("GIT_CONFIG_NOSYSTEM", "1", 1);
|
|
||||||
setenv("GIT_CONFIG_GLOBAL", "/dev/null", 1);
|
|
||||||
setenv("GIT_CONFIG_COUNT", "1", 1);
|
|
||||||
setenv("GIT_CONFIG_KEY_0", "protocol.file.allow", 1);
|
|
||||||
setenv("GIT_CONFIG_VALUE_0", "always", 1);
|
|
||||||
for (const char* name : { "GIT_AUTHOR_NAME", "GIT_COMMITTER_NAME" }) setenv(name, "crafter-build test", 1);
|
|
||||||
for (const char* name : { "GIT_AUTHOR_EMAIL", "GIT_COMMITTER_EMAIL" }) setenv(name, "test@example.invalid", 1);
|
|
||||||
|
|
||||||
// The submodule: "one", then "two".
|
|
||||||
const fs::path sub = scratch / "sub";
|
|
||||||
fs::create_directories(sub);
|
|
||||||
Git(sub, "init -q -b main");
|
|
||||||
Write(sub / "value.txt", "one");
|
|
||||||
Git(sub, "add value.txt");
|
|
||||||
Git(sub, "commit -q -m one");
|
|
||||||
const std::string one = Git(sub, "rev-parse HEAD");
|
|
||||||
Write(sub / "value.txt", "two");
|
|
||||||
Git(sub, "commit -q -am two");
|
|
||||||
const std::string two = Git(sub, "rev-parse HEAD");
|
|
||||||
|
|
||||||
// The superproject: `older` has the submodule at "one", main at "two".
|
|
||||||
const fs::path super = scratch / "super";
|
|
||||||
fs::create_directories(super);
|
|
||||||
Git(super, "init -q -b main");
|
|
||||||
Git(super, std::format("submodule --quiet add {} lib", ShellQuote(std::format("file://{}", sub.string()))));
|
|
||||||
Git(super / "lib", std::format("checkout -q {}", one));
|
|
||||||
Git(super, "add lib");
|
|
||||||
Git(super, "commit -q -m older");
|
|
||||||
const std::string older = Git(super, "rev-parse HEAD");
|
|
||||||
Git(super, "branch older");
|
|
||||||
Git(super, "branch track");
|
|
||||||
Git(super / "lib", std::format("checkout -q {}", two));
|
|
||||||
Git(super, "add lib");
|
|
||||||
Git(super, "commit -q -m newer");
|
|
||||||
const std::string url = std::format("file://{}", super.string());
|
|
||||||
|
|
||||||
// ── Pinned to the older commit ───────────────────────────────────────────
|
|
||||||
const fs::path pinned = GitFetch({ .url = url, .commit = older });
|
|
||||||
Check(Read(pinned / "lib" / "value.txt") == "one", std::format("a commit pin gets that commit's submodule (lib says {})", Read(pinned / "lib" / "value.txt")));
|
|
||||||
|
|
||||||
// ── By a branch at the older commit ──────────────────────────────────────
|
|
||||||
const fs::path branch = GitFetch({ .url = url, .branch = "older" });
|
|
||||||
Check(Read(branch / "lib" / "value.txt") == "one", std::format("a branch gets its own tip's submodule (lib says {})", Read(branch / "lib" / "value.txt")));
|
|
||||||
|
|
||||||
// ── A tracked branch moving forward ──────────────────────────────────────
|
|
||||||
const fs::path tracked = GitFetch({ .url = url, .branch = "track" });
|
|
||||||
Check(Read(tracked / "lib" / "value.txt") == "one", "a tracked branch starts with its tip's submodule");
|
|
||||||
Git(super, "branch -f track main");
|
|
||||||
const fs::path pulled = GitFetch({ .url = url, .branch = "track" });
|
|
||||||
Check(pulled == tracked && Read(pulled / "lib" / "value.txt") == "two", std::format("pulling it forward moves the submodule along (lib says {})", Read(pulled / "lib" / "value.txt")));
|
|
||||||
|
|
||||||
// ── A cached clone with its submodule out of step ────────────────────────
|
|
||||||
Git(pinned / "lib", std::format("checkout -q {}", two));
|
|
||||||
const fs::path repaired = GitFetch({ .url = url, .commit = older });
|
|
||||||
Check(repaired == pinned && Read(repaired / "lib" / "value.txt") == "one", std::format("a cached clone at the pin gets its submodule put back (lib says {})", Read(repaired / "lib" / "value.txt")));
|
|
||||||
|
|
||||||
std::error_code ec;
|
|
||||||
fs::remove_all(scratch, ec);
|
|
||||||
if (failures > 0) {
|
|
||||||
std::println(std::cerr, "{} assertions failed", failures);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
@ -119,17 +119,6 @@ int main() {
|
||||||
Check(fs::last_write_time(probePcm) == afterCold, "unchanged sources do not rebuild the cache");
|
Check(fs::last_write_time(probePcm) == afterCold, "unchanged sources do not rebuild the cache");
|
||||||
Check(ReadStamp(moduleCacheDir) == firstStamp, "unchanged sources keep the same stamp");
|
Check(ReadStamp(moduleCacheDir) == firstStamp, "unchanged sources keep the same stamp");
|
||||||
|
|
||||||
// The compiled project file is the other half of the same hazard: it is
|
|
||||||
// loaded into this process and must have been compiled against the
|
|
||||||
// declarations this process was built from.
|
|
||||||
fs::path projectSo;
|
|
||||||
for (const fs::directory_entry& entry : fs::recursive_directory_iterator(projectDir)) {
|
|
||||||
if (entry.path().filename() == "project.so") projectSo = entry.path();
|
|
||||||
}
|
|
||||||
Check(!projectSo.empty(), "the project file was compiled to a project.so");
|
|
||||||
if (projectSo.empty()) return 1;
|
|
||||||
const fs::file_time_type soBeforeEdit = fs::last_write_time(projectSo);
|
|
||||||
|
|
||||||
// Now the case the mtime rule cannot see. The edit lands in a module the
|
// Now the case the mtime rule cannot see. The edit lands in a module the
|
||||||
// probe does not name, because the cached PCMs import each other: a change
|
// probe does not name, because the cached PCMs import each other: a change
|
||||||
// anywhere in the set invalidates all of them.
|
// anywhere in the set invalidates all of them.
|
||||||
|
|
@ -150,11 +139,6 @@ int main() {
|
||||||
|
|
||||||
Check(fs::last_write_time(probePcm) > afterCold, "a content change rebuilds the cache even when the source's mtime is older");
|
Check(fs::last_write_time(probePcm) > afterCold, "a content change rebuilds the cache even when the source's mtime is older");
|
||||||
Check(ReadStamp(moduleCacheDir) != firstStamp, "the recorded stamp follows the sources");
|
Check(ReadStamp(moduleCacheDir) != firstStamp, "the recorded stamp follows the sources");
|
||||||
// project.cpp itself is untouched, so only the stamp can force this. It
|
|
||||||
// once covered the version string alone (the rest sat behind a NUL in a
|
|
||||||
// format string): a project.so built against a class of another size was
|
|
||||||
// reused and corrupted the heap when it filled one in.
|
|
||||||
Check(fs::last_write_time(projectSo) > soBeforeEdit, "changed declarations recompile project.so even though project.cpp is unchanged");
|
|
||||||
|
|
||||||
fs::remove_all(scratch, ec);
|
fs::remove_all(scratch, ec);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,154 +0,0 @@
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
|
||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
||||||
|
|
||||||
#include <stdlib.h>
|
|
||||||
import std;
|
|
||||||
import Crafter.Build;
|
|
||||||
namespace fs = std::filesystem;
|
|
||||||
using namespace Crafter;
|
|
||||||
|
|
||||||
// Freshness of the std PCM and of project.so used to be an mtime comparison
|
|
||||||
// against files that come with the install: std.cppm and the crafter-build
|
|
||||||
// executable. A Flatpak (any OSTree deployment) gives every installed file
|
|
||||||
// mtime 0, so those inputs always looked older than the cache: a clang bump
|
|
||||||
// would never rebuild std.pcm, a crafter-build update would never recompile
|
|
||||||
// project.so, and clang crashes on the stale PCM instead of saying so.
|
|
||||||
//
|
|
||||||
// Both are now keyed on content stamps (the compile command, the std module
|
|
||||||
// source, the crafter-build version, the toolchain key and the module source
|
|
||||||
// stamp) and the cache directory carries the toolchain key in its name. This
|
|
||||||
// test drives the shapes an mtime rule gets wrong: a std.pcm dated before its
|
|
||||||
// source must still be reused, a warm load must leave project.so alone, and a
|
|
||||||
// stamp that disagrees must rebuild even when the artifact is dated after
|
|
||||||
// every input.
|
|
||||||
namespace {
|
|
||||||
std::int32_t Failures = 0;
|
|
||||||
|
|
||||||
void Check(bool cond, std::string_view msg) {
|
|
||||||
if (!cond) {
|
|
||||||
std::println(std::cerr, "FAIL: {}", msg);
|
|
||||||
++Failures;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fs::path FindModuleCacheDir(const fs::path& cacheRoot) {
|
|
||||||
for (const fs::directory_entry& entry : fs::directory_iterator(cacheRoot / "crafter.build")) {
|
|
||||||
if (entry.is_directory()) return entry.path();
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Load(const fs::path& projectFile, std::string_view what) {
|
|
||||||
std::array<std::string_view, 0> noArgs = {};
|
|
||||||
try {
|
|
||||||
(void)LoadProject(projectFile, noArgs);
|
|
||||||
return true;
|
|
||||||
} catch (const std::exception& e) {
|
|
||||||
std::println(std::cerr, "FAIL: LoadProject ({}) threw: {}", what, e.what());
|
|
||||||
++Failures;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fs::path StampOf(const fs::path& artifact) {
|
|
||||||
fs::path stamp = artifact;
|
|
||||||
stamp += ".stamp";
|
|
||||||
return stamp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int main() {
|
|
||||||
fs::path installedHome = GetCrafterBuildHome();
|
|
||||||
if (!fs::exists(installedHome / "Crafter.Build.cppm")) {
|
|
||||||
std::println(std::cerr, "SKIP: no module sources at {}", installedHome.string());
|
|
||||||
return 77;
|
|
||||||
}
|
|
||||||
|
|
||||||
fs::path scratch = fs::temp_directory_path() / "crafter-build-host-cache-toolchain-key";
|
|
||||||
std::error_code ec;
|
|
||||||
fs::remove_all(scratch, ec);
|
|
||||||
fs::create_directories(scratch);
|
|
||||||
|
|
||||||
// A private copy of the module sources and a cold private cache, as in
|
|
||||||
// HostCacheSourceStamp.
|
|
||||||
fs::path home = scratch / "share" / "crafter-build";
|
|
||||||
fs::create_directories(home.parent_path());
|
|
||||||
fs::copy(installedHome, home, fs::copy_options::recursive);
|
|
||||||
setenv("CRAFTER_BUILD_HOME", home.string().c_str(), 1);
|
|
||||||
fs::path cacheRoot = scratch / "cache";
|
|
||||||
fs::create_directories(cacheRoot);
|
|
||||||
setenv("XDG_CACHE_HOME", cacheRoot.string().c_str(), 1);
|
|
||||||
|
|
||||||
fs::path projectDir = scratch / "project";
|
|
||||||
fs::create_directories(projectDir);
|
|
||||||
fs::path projectFile = projectDir / "project.cpp";
|
|
||||||
std::ofstream(projectFile) <<
|
|
||||||
"import std;\n"
|
|
||||||
"import Crafter.Build;\n"
|
|
||||||
"using namespace Crafter;\n"
|
|
||||||
"extern \"C\" Configuration CrafterBuildProject(std::span<const std::string_view>) {\n"
|
|
||||||
" Configuration cfg;\n"
|
|
||||||
" cfg.path = \"./\";\n"
|
|
||||||
" cfg.name = \"keyed\";\n"
|
|
||||||
" cfg.outputName = \"keyed\";\n"
|
|
||||||
" cfg.type = ConfigurationType::Executable;\n"
|
|
||||||
" return cfg;\n"
|
|
||||||
"}\n";
|
|
||||||
|
|
||||||
if (!Load(projectFile, "cold")) return 1;
|
|
||||||
|
|
||||||
const fs::path cacheDir = FindModuleCacheDir(cacheRoot);
|
|
||||||
Check(!cacheDir.empty(), "a module cache directory was created");
|
|
||||||
if (cacheDir.empty()) return 1;
|
|
||||||
Check(cacheDir.filename().string().contains(GetToolchain().Key()), std::format("the cache directory is named for the toolchain (got '{}', key '{}')", cacheDir.filename().string(), GetToolchain().Key()));
|
|
||||||
|
|
||||||
const fs::path stdPcm = cacheDir / "std.pcm";
|
|
||||||
const fs::path projectSo = projectDir / "build" / "project.so";
|
|
||||||
Check(fs::exists(stdPcm) && fs::exists(StampOf(stdPcm)), "std.pcm and its stamp exist after a cold load");
|
|
||||||
Check(fs::exists(projectSo) && fs::exists(StampOf(projectSo)), "project.so and its stamp exist after a cold load");
|
|
||||||
if (!fs::exists(StampOf(stdPcm)) || !fs::exists(StampOf(projectSo))) return 1;
|
|
||||||
|
|
||||||
// The OSTree shape, inverted: std.pcm dated before its source. A stamp
|
|
||||||
// rule reuses it; an mtime rule would rebuild it on every run. project.so
|
|
||||||
// keeps two mtime inputs that live next to it (project.cpp and dependency
|
|
||||||
// artifacts) but the executable is no longer one of them, so a warm load
|
|
||||||
// with nothing changed leaves it alone.
|
|
||||||
const fs::file_time_type epoch{};
|
|
||||||
fs::last_write_time(stdPcm, epoch);
|
|
||||||
const fs::file_time_type afterCold = fs::last_write_time(projectSo);
|
|
||||||
if (!Load(projectFile, "std.pcm dated at the epoch")) return 1;
|
|
||||||
Check(fs::last_write_time(stdPcm) == epoch, "std.pcm dated before its source is still reused");
|
|
||||||
Check(fs::last_write_time(projectSo) == afterCold, "project.so is left alone when nothing changed");
|
|
||||||
|
|
||||||
// The other half: artifacts dated after every input, with a stamp that
|
|
||||||
// disagrees. An mtime rule keeps them; the stamp rule rebuilds.
|
|
||||||
const fs::file_time_type future = fs::file_time_type::clock::now() + std::chrono::hours(24);
|
|
||||||
std::ofstream(StampOf(stdPcm)) << "stale";
|
|
||||||
fs::last_write_time(stdPcm, future);
|
|
||||||
if (!Load(projectFile, "stale std.pcm stamp")) return 1;
|
|
||||||
Check(fs::last_write_time(stdPcm) != future, "a disagreeing stamp rebuilds std.pcm even when it is dated after its source");
|
|
||||||
{
|
|
||||||
std::ifstream in(StampOf(stdPcm));
|
|
||||||
std::string stamp;
|
|
||||||
std::getline(in, stamp);
|
|
||||||
Check(stamp != "stale" && !stamp.empty(), "the rebuilt std.pcm gets its stamp back");
|
|
||||||
}
|
|
||||||
|
|
||||||
std::ofstream(StampOf(projectSo)) << "stale";
|
|
||||||
fs::last_write_time(projectSo, future);
|
|
||||||
if (!Load(projectFile, "stale project.so stamp")) return 1;
|
|
||||||
Check(fs::last_write_time(projectSo) != future, "a disagreeing stamp recompiles project.so even when it is dated after its inputs");
|
|
||||||
|
|
||||||
// The one mtime input that legitimately remains: the project file itself.
|
|
||||||
const fs::file_time_type beforeEdit = fs::last_write_time(projectSo);
|
|
||||||
{
|
|
||||||
std::ofstream append(projectFile, std::ios::app);
|
|
||||||
append << "// edited\n";
|
|
||||||
}
|
|
||||||
fs::last_write_time(projectFile, fs::file_time_type::clock::now() + std::chrono::seconds(2));
|
|
||||||
if (!Load(projectFile, "edited project.cpp")) return 1;
|
|
||||||
Check(fs::last_write_time(projectSo) != beforeEdit, "an edited project.cpp recompiles project.so");
|
|
||||||
|
|
||||||
fs::remove_all(scratch, ec);
|
|
||||||
return Failures == 0 ? 0 : 1;
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
|
||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
||||||
|
|
||||||
int main() {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
@ -1,119 +0,0 @@
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
|
||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
||||||
|
|
||||||
import std;
|
|
||||||
import Crafter.Build;
|
|
||||||
namespace fs = std::filesystem;
|
|
||||||
using namespace Crafter;
|
|
||||||
|
|
||||||
// A change to what an executable is linked with has to relink it, even when
|
|
||||||
// no object changed. The relink check compared modification times only, so a
|
|
||||||
// link flag added in project.cpp, or a dependency's library moving (an
|
|
||||||
// ExternalDependency whose options changed is fetched and built into a new
|
|
||||||
// cache directory, and its -L and rpath follow), left the old binary in
|
|
||||||
// place: Crafter.Network's tests kept loading the msquic they were first
|
|
||||||
// linked against.
|
|
||||||
//
|
|
||||||
// The test adds an rpath to an executable's link flags, takes it away again,
|
|
||||||
// and then builds once more with nothing changed, which must not relink.
|
|
||||||
// The flags come out of sets the dependency threads fill as they finish, so
|
|
||||||
// the recorded command lists them sorted: in the order they happened to land
|
|
||||||
// in, an unchanged build could look like a changed one.
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
std::int32_t failures = 0;
|
|
||||||
|
|
||||||
void Check(bool cond, std::string_view msg) {
|
|
||||||
if (!cond) {
|
|
||||||
std::println(std::cerr, "FAIL: {}", msg);
|
|
||||||
++failures;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
constexpr std::string_view marker = "/crafter-build-link-flags-change-marker";
|
|
||||||
|
|
||||||
BuildResult BuildOnce(const fs::path& project, std::vector<std::string> linkFlags) {
|
|
||||||
Configuration app;
|
|
||||||
app.path = project;
|
|
||||||
app.name = "linked";
|
|
||||||
app.outputName = "linked";
|
|
||||||
app.target = HostTarget();
|
|
||||||
app.type = ConfigurationType::Executable;
|
|
||||||
app.linkFlags = std::move(linkFlags);
|
|
||||||
std::array<fs::path, 0> ifaces = {};
|
|
||||||
std::array<fs::path, 1> impls = { "main" };
|
|
||||||
app.GetInterfacesAndImplementations(ifaces, impls);
|
|
||||||
std::unordered_map<fs::path, std::shared_future<BuildResult>> depResults;
|
|
||||||
std::mutex depMutex;
|
|
||||||
BuildResult result = Build(app, depResults, depMutex);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
Configuration Plain(const fs::path& project) {
|
|
||||||
Configuration app;
|
|
||||||
app.path = project;
|
|
||||||
app.name = "linked";
|
|
||||||
app.outputName = "linked";
|
|
||||||
app.target = HostTarget();
|
|
||||||
app.type = ConfigurationType::Executable;
|
|
||||||
return app;
|
|
||||||
}
|
|
||||||
|
|
||||||
fs::path Binary(const fs::path& project) {
|
|
||||||
return Plain(project).BinDir() / "linked";
|
|
||||||
}
|
|
||||||
|
|
||||||
bool HasMarker(const fs::path& binary) {
|
|
||||||
std::ifstream stream(binary, std::ios::binary);
|
|
||||||
const std::string bytes((std::istreambuf_iterator<char>(stream)), std::istreambuf_iterator<char>());
|
|
||||||
return bytes.find(marker) != std::string::npos;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int main() {
|
|
||||||
const fs::path project = fs::temp_directory_path() / "crafter-build-link-flags-change";
|
|
||||||
fs::remove_all(project);
|
|
||||||
fs::copy(fs::current_path() / "tests" / "LinkFlagsChange" / "fixture", project, fs::copy_options::recursive);
|
|
||||||
const std::string rpath = std::format("-Wl,-rpath,{}", marker);
|
|
||||||
|
|
||||||
const BuildResult first = BuildOnce(project, {});
|
|
||||||
Check(first.result.empty(), std::format("the plain build succeeds: {}", first.result));
|
|
||||||
const fs::path binary = Binary(project);
|
|
||||||
Check(fs::exists(binary) && !HasMarker(binary), "the plain binary has no rpath to the marker");
|
|
||||||
|
|
||||||
const BuildResult added = BuildOnce(project, { rpath });
|
|
||||||
Check(added.result.empty(), std::format("the build with the rpath succeeds: {}", added.result));
|
|
||||||
Check(HasMarker(binary), "adding a link flag relinks the binary");
|
|
||||||
|
|
||||||
const BuildResult removed = BuildOnce(project, {});
|
|
||||||
Check(removed.result.empty(), std::format("the build without it succeeds: {}", removed.result));
|
|
||||||
Check(!HasMarker(binary), "taking it away relinks it again");
|
|
||||||
|
|
||||||
const fs::file_time_type linked = fs::last_write_time(binary);
|
|
||||||
const BuildResult again = BuildOnce(project, {});
|
|
||||||
Check(again.result.empty() && fs::last_write_time(binary) == linked, "an unchanged build doesn't relink");
|
|
||||||
|
|
||||||
const std::vector<std::string> flags = { "-Wl,-z,now", "-Wl,-O1", "-Wl,--as-needed", "-Wl,-z,relro", "-Wl,--build-id", "-Wl,--hash-style=gnu" };
|
|
||||||
const BuildResult many = BuildOnce(project, flags);
|
|
||||||
Check(many.result.empty(), std::format("the build with six flags succeeds: {}", many.result));
|
|
||||||
std::ifstream recorded(Plain(project).BuildDir() / "linked.link", std::ios::binary);
|
|
||||||
const std::string record((std::istreambuf_iterator<char>(recorded)), std::istreambuf_iterator<char>());
|
|
||||||
std::vector<std::size_t> at;
|
|
||||||
for (const std::string& flag : flags) at.push_back(record.find(std::format(" {}", flag)));
|
|
||||||
std::vector<std::string> sorted = flags;
|
|
||||||
std::ranges::sort(sorted);
|
|
||||||
bool inOrder = !std::ranges::contains(at, std::string::npos);
|
|
||||||
for (std::size_t k = 0; inOrder && k + 1 < sorted.size(); ++k) {
|
|
||||||
const auto index = [&](const std::string& flag) { return at[static_cast<std::size_t>(std::ranges::find(flags, flag) - flags.begin())]; };
|
|
||||||
inOrder = index(sorted[k]) < index(sorted[k + 1]);
|
|
||||||
}
|
|
||||||
Check(inOrder, std::format("the link record lists the flags sorted: {}", record));
|
|
||||||
|
|
||||||
std::error_code ec;
|
|
||||||
fs::remove_all(project, ec);
|
|
||||||
if (failures > 0) {
|
|
||||||
std::println(std::cerr, "{} assertions failed", failures);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
|
||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
||||||
|
|
||||||
import std;
|
|
||||||
|
|
||||||
int main() {
|
|
||||||
std::println("hello from crafter-build");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
@ -1,97 +0,0 @@
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
|
||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
||||||
|
|
||||||
import std;
|
|
||||||
import Crafter.Build;
|
|
||||||
namespace fs = std::filesystem;
|
|
||||||
using namespace Crafter;
|
|
||||||
|
|
||||||
// Building for x86_64-w64-mingw32 without a usable mingw-w64 libstdc++ has to
|
|
||||||
// fail the build with a message that says what is missing. It used to abort
|
|
||||||
// the whole process with a bare "libc++abi: terminating": the std module's
|
|
||||||
// source was found by listing <mingw sysroot>/include/c++ with a throwing
|
|
||||||
// directory_iterator, the exception unwound through Build() while its worker
|
|
||||||
// threads were still joinable, and ~thread called std::terminate. That is
|
|
||||||
// what every mingw build inside the Flatpak did, as the sandbox has no
|
|
||||||
// /usr/x86_64-w64-mingw32.
|
|
||||||
//
|
|
||||||
// The std module's source is now found through libstdc++.modules.json in the
|
|
||||||
// mingw toolchain's library directories. CRAFTER_MINGW_DIR points the
|
|
||||||
// toolchain at a scratch prefix, which the test takes through four states:
|
|
||||||
// - missing;
|
|
||||||
// - with only a manifest in the prefix's generic lib/, where the host's
|
|
||||||
// Linux libstdc++ keeps one: not the mingw library, so still missing;
|
|
||||||
// - with a mingw manifest that names no std module;
|
|
||||||
// - with one whose std module source isn't there.
|
|
||||||
|
|
||||||
extern "C" int setenv(const char* name, const char* value, int overwrite);
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
std::int32_t failures = 0;
|
|
||||||
|
|
||||||
void Check(bool cond, std::string_view msg) {
|
|
||||||
if (!cond) {
|
|
||||||
std::println(std::cerr, "FAIL: {}", msg);
|
|
||||||
++failures;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
BuildResult BuildOnce(const fs::path& project) {
|
|
||||||
Configuration app;
|
|
||||||
app.path = project;
|
|
||||||
app.name = "hello";
|
|
||||||
app.outputName = "hello";
|
|
||||||
app.target = "x86_64-w64-mingw32";
|
|
||||||
app.type = ConfigurationType::Executable;
|
|
||||||
std::array<fs::path, 0> ifaces = {};
|
|
||||||
std::array<fs::path, 1> impls = { "main" };
|
|
||||||
app.GetInterfacesAndImplementations(ifaces, impls);
|
|
||||||
std::unordered_map<fs::path, std::shared_future<BuildResult>> depResults;
|
|
||||||
std::mutex depMutex;
|
|
||||||
return Build(app, depResults, depMutex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int main() {
|
|
||||||
const fs::path scratch = fs::temp_directory_path() / "crafter-build-mingw-missing";
|
|
||||||
fs::remove_all(scratch);
|
|
||||||
fs::create_directories(scratch);
|
|
||||||
const fs::path project = scratch / "project";
|
|
||||||
fs::copy(fs::current_path() / "tests" / "MingwMissing" / "fixture", project, fs::copy_options::recursive);
|
|
||||||
const fs::path sysroot = scratch / "mingw";
|
|
||||||
// Before anything resolves the toolchain, which reads both once. The std
|
|
||||||
// module cache goes to the scratch directory too.
|
|
||||||
setenv("CRAFTER_MINGW_DIR", sysroot.c_str(), 1);
|
|
||||||
setenv("XDG_CACHE_HOME", (scratch / "cache").c_str(), 1);
|
|
||||||
|
|
||||||
const BuildResult missing = BuildOnce(project);
|
|
||||||
Check(!missing.result.empty(), "a missing mingw prefix fails the build");
|
|
||||||
Check(missing.result.contains("mingw") && missing.result.contains(sysroot.string()) && missing.result.contains("CRAFTER_MINGW_DIR"), std::format("and says where it looked and how to point elsewhere: {}", missing.result));
|
|
||||||
|
|
||||||
auto write = [](const fs::path& file, std::string_view text) {
|
|
||||||
fs::create_directories(file.parent_path());
|
|
||||||
std::ofstream(file, std::ios::binary | std::ios::trunc) << text;
|
|
||||||
};
|
|
||||||
write(sysroot / "lib" / "libstdc++.modules.json", R"({"version": 1, "modules": [{"logical-name": "std", "source-path": "../include/c++/16/bits/std.cc", "is-std-library": true}]})");
|
|
||||||
write(sysroot / "include" / "c++" / "16" / "bits" / "std.cc", "// the host's Linux std module\n");
|
|
||||||
const BuildResult linux = BuildOnce(project);
|
|
||||||
Check(linux.result.contains("no mingw-w64 libstdc++"), std::format("a manifest outside the mingw toolchain's own directories isn't taken for it: {}", linux.result));
|
|
||||||
|
|
||||||
const fs::path manifest = sysroot / "x86_64-w64-mingw32" / "lib" / "libstdc++.modules.json";
|
|
||||||
write(manifest, R"({"version": 1, "modules": [{"logical-name": "std.compat", "source-path": "../include/c++/15.1.0/bits/std.compat.cc"}]})");
|
|
||||||
const BuildResult noStd = BuildOnce(project);
|
|
||||||
Check(noStd.result.contains(manifest.string()) && noStd.result.contains("no std module"), std::format("a manifest without the std module fails the build, naming the manifest: {}", noStd.result));
|
|
||||||
|
|
||||||
write(manifest, R"({"version": 1, "modules": [{"source-path": "../include/c++/15.1.0/bits/std.cc", "logical-name": "std"}]})");
|
|
||||||
const BuildResult noSource = BuildOnce(project);
|
|
||||||
Check(noSource.result.contains((sysroot / "x86_64-w64-mingw32" / "include" / "c++" / "15.1.0" / "bits" / "std.cc").string()) && noSource.result.contains("missing"),
|
|
||||||
std::format("a std module source that isn't there fails the build, naming it: {}", noSource.result));
|
|
||||||
|
|
||||||
std::error_code ec;
|
|
||||||
fs::remove_all(scratch, ec);
|
|
||||||
if (failures > 0) {
|
|
||||||
std::println(std::cerr, "{} assertions failed", failures);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
|
||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
||||||
|
|
||||||
import std;
|
|
||||||
import Crafter.Build;
|
|
||||||
using namespace Crafter;
|
|
||||||
|
|
||||||
// Scripts that report what they were handed. The test that loads this file
|
|
||||||
// drives the real command line and can only observe a script through its
|
|
||||||
// exit code and through what it leaves in the project directory.
|
|
||||||
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> args) {
|
|
||||||
Configuration cfg;
|
|
||||||
cfg.path = "./";
|
|
||||||
cfg.name = "script-fixture";
|
|
||||||
cfg.outputName = "script-fixture";
|
|
||||||
// Applied so that a script argument which leaked into the project's
|
|
||||||
// args would be visible: in cfg.projectArgs, or as cfg.debug.
|
|
||||||
ApplyStandardArgs(cfg, args);
|
|
||||||
|
|
||||||
cfg.scripts.push_back({"record", [](ScriptContext& ctx) {
|
|
||||||
std::ofstream out(ctx.projectDir / "record.txt");
|
|
||||||
for (const std::string& arg : ctx.args) std::println(out, "arg:{}", arg);
|
|
||||||
for (const std::string& arg : ctx.project.projectArgs) std::println(out, "projectArg:{}", arg);
|
|
||||||
std::println(out, "debug:{}", ctx.project.debug);
|
|
||||||
std::println(out, "project:{}", ctx.project.name);
|
|
||||||
return 42;
|
|
||||||
}});
|
|
||||||
cfg.scripts.push_back({"shell", [](ScriptContext& ctx) { return ctx.Run("exit 7"); }});
|
|
||||||
return cfg;
|
|
||||||
}
|
|
||||||
|
|
@ -1,241 +0,0 @@
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
|
||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
||||||
|
|
||||||
import std;
|
|
||||||
import Crafter.Build;
|
|
||||||
namespace fs = std::filesystem;
|
|
||||||
using namespace Crafter;
|
|
||||||
|
|
||||||
// `crafter-build run <name> [args...]` runs a script project.cpp put in
|
|
||||||
// cfg.scripts and exits with what the script returned.
|
|
||||||
//
|
|
||||||
// Two things are easy to get wrong and expensive when they are. The words
|
|
||||||
// after the script's name belong to the script: read by crafter-build
|
|
||||||
// instead, `run package clean` deletes the build tree, `run package --help`
|
|
||||||
// prints the wrong help, and anything else lands in the project's args,
|
|
||||||
// where it changes the variant hash and so the directory the project builds
|
|
||||||
// into. And the exit code is the script's whole result: collapsed to 0 or 1
|
|
||||||
// it loses what a caller branches on, and passed through unchecked a value
|
|
||||||
// such as 256 wraps to success.
|
|
||||||
namespace {
|
|
||||||
std::int32_t Failures = 0;
|
|
||||||
|
|
||||||
void Check(bool cond, std::string_view msg) {
|
|
||||||
if (!cond) {
|
|
||||||
std::println(std::cerr, "FAIL: {}", msg);
|
|
||||||
++Failures;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::int32_t RunWith(std::vector<std::string> args) {
|
|
||||||
std::vector<char*> argv;
|
|
||||||
for (std::string& arg : args) argv.push_back(arg.data());
|
|
||||||
return Run(static_cast<std::int32_t>(argv.size()), argv.data());
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<std::string> ReadLines(const fs::path& file) {
|
|
||||||
std::vector<std::string> lines;
|
|
||||||
std::ifstream in(file);
|
|
||||||
for (std::string line; std::getline(in, line);) lines.push_back(line);
|
|
||||||
return lines;
|
|
||||||
}
|
|
||||||
|
|
||||||
// One script that returns `code`, run by name.
|
|
||||||
std::int32_t RunReturning(std::int32_t code) {
|
|
||||||
Configuration cfg;
|
|
||||||
cfg.scripts.push_back({"it", [code](ScriptContext&) { return code; }});
|
|
||||||
return RunScript(cfg, {.name = "it", .named = true});
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoadProject compiles a project file against the installed module
|
|
||||||
// sources; without them the command-line cases cannot run at all.
|
|
||||||
bool ModuleSourcesInstalled() {
|
|
||||||
try {
|
|
||||||
return fs::exists(GetCrafterBuildHome() / "Crafter.Build.cppm");
|
|
||||||
} catch (const std::exception&) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int main() {
|
|
||||||
const fs::path fixture = fs::current_path() / "tests" / "Script" / "fixture";
|
|
||||||
const fs::path scratch = fs::temp_directory_path() / "crafter-build-script";
|
|
||||||
std::error_code ec;
|
|
||||||
fs::remove_all(scratch, ec);
|
|
||||||
fs::create_directories(scratch);
|
|
||||||
fs::copy(fixture, scratch, fs::copy_options::recursive);
|
|
||||||
|
|
||||||
// The script gets the project, the project's directory and its
|
|
||||||
// arguments exactly as given, and its return value comes back.
|
|
||||||
{
|
|
||||||
Configuration cfg;
|
|
||||||
std::int32_t calls = 0;
|
|
||||||
std::vector<std::string> seen;
|
|
||||||
const Configuration* seenProject = nullptr;
|
|
||||||
fs::path seenDir;
|
|
||||||
cfg.scripts.push_back({"other", [&](ScriptContext&) {
|
|
||||||
++calls;
|
|
||||||
return 9;
|
|
||||||
}});
|
|
||||||
cfg.scripts.push_back({"pass", [&](ScriptContext& ctx) {
|
|
||||||
++calls;
|
|
||||||
seen.assign(ctx.args.begin(), ctx.args.end());
|
|
||||||
seenProject = &ctx.project;
|
|
||||||
seenDir = ctx.projectDir;
|
|
||||||
return 3;
|
|
||||||
}});
|
|
||||||
const std::vector<std::string> given = {"a", "--help", "", "two words", "it's"};
|
|
||||||
const std::int32_t code = RunScript(cfg, {.name = "pass", .named = true, .args = given, .projectFile = scratch / "project.cpp"});
|
|
||||||
Check(code == 3, std::format("the script's return value is the result (got {})", code));
|
|
||||||
Check(calls == 1, std::format("only the named script ran (ran {})", calls));
|
|
||||||
Check(seen == given, "the arguments arrive verbatim, empty ones and option-like ones included");
|
|
||||||
Check(seenProject == &cfg, "ctx.project is the configuration the script was declared on");
|
|
||||||
Check(seenDir == scratch, std::format("ctx.projectDir is the project file's directory (got '{}')", seenDir.string()));
|
|
||||||
|
|
||||||
// Asking for the list runs nothing, with or without a name.
|
|
||||||
calls = 0;
|
|
||||||
Check(RunScript(cfg, {}) == 0, "a run without a name lists and succeeds");
|
|
||||||
Check(RunScript(cfg, {.name = "pass", .named = true, .listOnly = true}) == 0, "--list succeeds");
|
|
||||||
Check(calls == 0, "listing runs no script");
|
|
||||||
|
|
||||||
// A name nobody declared. The empty string is one: it is a name
|
|
||||||
// that was given, not the absence of a name.
|
|
||||||
Check(RunScript(cfg, {.name = "nope", .named = true}) == 1, "an unknown name fails");
|
|
||||||
Check(RunScript(cfg, {.name = "", .named = true}) == 1, "an empty name is looked up and fails instead of listing");
|
|
||||||
Check(RunScript(cfg, {.name = "pas", .named = true}) == 1, "names match exactly, not by prefix");
|
|
||||||
Check(calls == 0, "an unknown name runs no script");
|
|
||||||
}
|
|
||||||
|
|
||||||
// The exit status passes through whole, and only an exit status does.
|
|
||||||
Check(RunReturning(0) == 0, "0 stays 0");
|
|
||||||
Check(RunReturning(1) == 1, "1 stays 1");
|
|
||||||
Check(RunReturning(77) == 77, "77 stays 77");
|
|
||||||
Check(RunReturning(255) == 255, "255 stays 255");
|
|
||||||
Check(RunReturning(256) == 1, "256 would wrap to success as an exit status and becomes 1");
|
|
||||||
Check(RunReturning(-1) == 1, "a negative value becomes 1");
|
|
||||||
|
|
||||||
// A script that throws is that script's failure, not a crash.
|
|
||||||
{
|
|
||||||
Configuration cfg;
|
|
||||||
cfg.scripts.push_back({"throws", [](ScriptContext&) -> std::int32_t { throw std::runtime_error("no such tool"); }});
|
|
||||||
Check(RunScript(cfg, {.name = "throws", .named = true}) == 1, "a throwing script exits 1");
|
|
||||||
cfg.scripts.push_back({"throwsInt", [](ScriptContext&) -> std::int32_t { throw 7; }});
|
|
||||||
Check(RunScript(cfg, {.name = "throwsInt", .named = true}) == 1, "and so does one throwing something that isn't a std::exception");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Declarations that cannot work are refused before anything runs, also
|
|
||||||
// when a different, valid script was asked for.
|
|
||||||
{
|
|
||||||
Configuration none;
|
|
||||||
Check(RunScript(none, {.name = "it", .named = true}) == 1, "a project without scripts fails");
|
|
||||||
Check(RunScript(none, {}) == 1, "listing a project without scripts fails too");
|
|
||||||
|
|
||||||
std::int32_t calls = 0;
|
|
||||||
const auto count = [&](ScriptContext&) {
|
|
||||||
++calls;
|
|
||||||
return 0;
|
|
||||||
};
|
|
||||||
Configuration twice;
|
|
||||||
twice.scripts.push_back({"a", count});
|
|
||||||
twice.scripts.push_back({"b", count});
|
|
||||||
twice.scripts.push_back({"a", count});
|
|
||||||
Check(RunScript(twice, {.name = "b", .named = true}) == 1, "a name declared twice fails the run");
|
|
||||||
Check(RunScript(twice, {}) == 1, "a name declared twice fails the list");
|
|
||||||
|
|
||||||
Configuration dashed;
|
|
||||||
dashed.scripts.push_back({"ok", count});
|
|
||||||
dashed.scripts.push_back({"-x", count});
|
|
||||||
Check(RunScript(dashed, {.name = "ok", .named = true}) == 1, "a name starting with '-' could never be typed and is refused");
|
|
||||||
|
|
||||||
Configuration unnamed;
|
|
||||||
unnamed.scripts.push_back({"", count});
|
|
||||||
Check(RunScript(unnamed, {.name = "", .named = true}) == 1, "an empty name is refused");
|
|
||||||
|
|
||||||
Configuration hollow;
|
|
||||||
hollow.scripts.push_back({"hollow", nullptr});
|
|
||||||
Check(RunScript(hollow, {.name = "hollow", .named = true}) == 1, "a script without a callback is refused");
|
|
||||||
Check(calls == 0, "none of these ran a script");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ctx.Run: the command's real exit code, a signal death the way a shell
|
|
||||||
// reports it, and ShellQuote carrying awkward text through sh intact.
|
|
||||||
{
|
|
||||||
const std::string awkward = "it's a \"test\" $HOME `id` \\ *";
|
|
||||||
const fs::path echoed = scratch / "echoed file.txt";
|
|
||||||
Configuration cfg;
|
|
||||||
cfg.scripts.push_back({"exit", [](ScriptContext& ctx) { return ctx.Run("exit 7"); }});
|
|
||||||
cfg.scripts.push_back({"signal", [](ScriptContext& ctx) { return ctx.Run("kill -TERM $$"); }});
|
|
||||||
cfg.scripts.push_back({"quote", [&](ScriptContext& ctx) { return ctx.Run(std::format("printf %s {} > {}", ShellQuote(awkward), ShellQuote(echoed.string()))); }});
|
|
||||||
// A script is usually several commands: stop at the first failure.
|
|
||||||
cfg.scripts.push_back({"steps", [&](ScriptContext& ctx) {
|
|
||||||
if (const std::int32_t code = ctx.Run("exit 5"); code != 0) return code;
|
|
||||||
return ctx.Run(std::format("touch {}", ShellQuote((scratch / "not-reached").string())));
|
|
||||||
}});
|
|
||||||
Check(RunScript(cfg, {.name = "exit", .named = true}) == 7, "ctx.Run returns the command's exit code");
|
|
||||||
Check(RunScript(cfg, {.name = "signal", .named = true}) == 143, "a command killed by SIGTERM reads as 128+15");
|
|
||||||
Check(RunScript(cfg, {.name = "quote", .named = true}) == 0, "a quoted command line runs");
|
|
||||||
const std::vector<std::string> lines = ReadLines(echoed);
|
|
||||||
Check(lines.size() == 1 && lines[0] == awkward, "ShellQuote passes quotes, spaces and expansions through sh untouched");
|
|
||||||
Check(RunScript(cfg, {.name = "steps", .named = true}) == 5, "a failed step's code can be returned as the script's");
|
|
||||||
Check(!fs::exists(scratch / "not-reached"), "and the steps after it do not run");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ModuleSourcesInstalled()) {
|
|
||||||
// A skip must never cover for a failure above.
|
|
||||||
if (Failures > 0) return 1;
|
|
||||||
std::println(std::cerr, "SKIP: no module sources to load the fixture project with");
|
|
||||||
return 77;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The same through the command line, where the parsing lives. Sentinels
|
|
||||||
// stand in for a built project: a word after `run` that is read as the
|
|
||||||
// clean verb takes them with it.
|
|
||||||
const std::string project = std::format("--project={}", (scratch / "project.cpp").string());
|
|
||||||
const fs::path binSentinel = scratch / "bin" / "sentinel";
|
|
||||||
const fs::path buildSentinel = scratch / "build" / "sentinel";
|
|
||||||
fs::create_directories(binSentinel.parent_path());
|
|
||||||
fs::create_directories(buildSentinel.parent_path());
|
|
||||||
std::ofstream(binSentinel) << "binary";
|
|
||||||
std::ofstream(buildSentinel) << "object";
|
|
||||||
|
|
||||||
// Everything after the name is the script's, including this tool's own
|
|
||||||
// options and verbs; what stands before `run` is still the project's.
|
|
||||||
{
|
|
||||||
const std::int32_t code = RunWith({"crafter-build", project, "--flavour=x", "run", "record", "a", "--help", "-v", "clean", "--debug", "--version", "run"});
|
|
||||||
Check(code == 42, std::format("the script ran and its return value is the exit code (got {})", code));
|
|
||||||
const std::vector<std::string> expected = {
|
|
||||||
"arg:a", "arg:--help", "arg:-v", "arg:clean", "arg:--debug", "arg:--version", "arg:run",
|
|
||||||
"projectArg:--flavour=x",
|
|
||||||
"debug:false",
|
|
||||||
"project:script-fixture",
|
|
||||||
};
|
|
||||||
const std::vector<std::string> record = ReadLines(scratch / "record.txt");
|
|
||||||
Check(record == expected, "the script got every word after its name and the project args got none of them");
|
|
||||||
if (record != expected) {
|
|
||||||
for (const std::string& line : record) std::println(std::cerr, " recorded: {}", line);
|
|
||||||
}
|
|
||||||
Check(fs::exists(binSentinel) && fs::exists(buildSentinel), "`clean` as a script argument deleted nothing");
|
|
||||||
}
|
|
||||||
|
|
||||||
// The first word after `run` is the name even when it is a verb.
|
|
||||||
{
|
|
||||||
fs::remove(scratch / "record.txt");
|
|
||||||
Check(RunWith({"crafter-build", project, "run", "clean"}) == 1, "`run clean` looks for a script called clean");
|
|
||||||
Check(fs::exists(binSentinel) && fs::exists(buildSentinel), "`run clean` deleted nothing");
|
|
||||||
Check(RunWith({"crafter-build", project, "run", "help"}) == 1, "`run help` looks for a script called help");
|
|
||||||
}
|
|
||||||
|
|
||||||
Check(RunWith({"crafter-build", project, "run", "shell"}) == 7, "a command's exit code reaches the process exit code");
|
|
||||||
Check(RunWith({"crafter-build", project, "run", "-q", "shell"}) == 7, "an option between `run` and the name is still this tool's");
|
|
||||||
Check(RunWith({"crafter-build", project, "run"}) == 0, "a bare `run` lists");
|
|
||||||
Check(RunWith({"crafter-build", project, "run", "--list"}) == 0, "`run --list` lists");
|
|
||||||
Check(!fs::exists(scratch / "record.txt"), "listing ran no script");
|
|
||||||
Check(RunWith({"crafter-build", project, "run", "nope"}) == 1, "an unknown script fails");
|
|
||||||
|
|
||||||
if (Failures > 0) {
|
|
||||||
std::println(std::cerr, "{} assertions failed", Failures);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
#version 450
|
|
||||||
|
|
||||||
// Compiled twice by the ShaderCompile test: once as is, once with VARIANT_RED
|
|
||||||
// and VARIANT_ALPHA=0.5 defined. The red branch does not compile unless the
|
|
||||||
// NAME=VALUE define arrives, and the two outputs only differ if the bare one
|
|
||||||
// does.
|
|
||||||
layout(location = 0) out vec4 outColor;
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
#ifdef VARIANT_RED
|
|
||||||
outColor = vec4(1.0, 0.0, 0.0, VARIANT_ALPHA);
|
|
||||||
#else
|
|
||||||
outColor = vec4(0.0, 1.0, 0.0, 1.0);
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
@ -8,10 +8,7 @@ using namespace Crafter;
|
||||||
|
|
||||||
// Compile a tiny GLSL fragment shader directly via the Shader API. Verifies
|
// Compile a tiny GLSL fragment shader directly via the Shader API. Verifies
|
||||||
// the SPIR-V output lands in the target dir and starts with the SPIR-V magic
|
// the SPIR-V output lands in the target dir and starts with the SPIR-V magic
|
||||||
// number, without dragging in the full Build() pipeline. The second half
|
// number, without dragging in the full Build() pipeline.
|
||||||
// registers one source twice, with and without defines, and checks that the
|
|
||||||
// variant gets its own output, its own staleness and the other #ifdef branch
|
|
||||||
// (tests/ShaderVariantBuild drives the same thing through Build()).
|
|
||||||
int main() {
|
int main() {
|
||||||
fs::path src = fs::current_path() / "tests" / "ShaderCompile" / "fixture" / "triangle.frag";
|
fs::path src = fs::current_path() / "tests" / "ShaderCompile" / "fixture" / "triangle.frag";
|
||||||
if (!fs::exists(src)) {
|
if (!fs::exists(src)) {
|
||||||
|
|
@ -27,7 +24,7 @@ int main() {
|
||||||
Shader shader(fs::path(src), "main", ShaderType::Fragment);
|
Shader shader(fs::path(src), "main", ShaderType::Fragment);
|
||||||
|
|
||||||
std::array<fs::path, 0> includeDirs = {};
|
std::array<fs::path, 0> includeDirs = {};
|
||||||
std::string err = shader.Compile(outDir, outDir, includeDirs);
|
std::string err = shader.Compile(outDir, includeDirs);
|
||||||
if (!err.empty()) {
|
if (!err.empty()) {
|
||||||
std::println(std::cerr, "shader compile failed: {}", err);
|
std::println(std::cerr, "shader compile failed: {}", err);
|
||||||
return 1;
|
return 1;
|
||||||
|
|
@ -51,96 +48,9 @@ int main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compiling again should be a no-op since the source isn't newer.
|
// Compiling again should be a no-op since the source isn't newer.
|
||||||
if (!shader.Check(outDir, outDir, includeDirs)) {
|
if (!shader.Check(outDir)) {
|
||||||
std::println(std::cerr, "Shader::Check returned false on an up-to-date spv");
|
std::println(std::cerr, "Shader::Check returned false on an up-to-date spv");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
// Searched elsewhere, an #include could find another file: stale.
|
|
||||||
const std::array<fs::path, 1> elsewhere = { outDir / "elsewhere" };
|
|
||||||
if (shader.Check(outDir, outDir, elsewhere)) {
|
|
||||||
std::println(std::cerr, "Shader::Check called an spv up to date for other include directories");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
// So could a source of the same name moved to another directory, which
|
|
||||||
// its #includes are searched from first.
|
|
||||||
const fs::path movedDir = outDir / "moved";
|
|
||||||
fs::create_directories(movedDir);
|
|
||||||
fs::copy_file(src, movedDir / src.filename());
|
|
||||||
fs::last_write_time(movedDir / src.filename(), fs::last_write_time(src));
|
|
||||||
Shader moved(movedDir / src.filename(), "main", ShaderType::Fragment);
|
|
||||||
if (moved.Check(outDir, outDir, includeDirs)) {
|
|
||||||
std::println(std::cerr, "Shader::Check called an spv up to date for a source of the same name elsewhere");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// One source registered twice: the second entry carries defines and a
|
|
||||||
// variant name. It must land in its own file instead of overwriting the
|
|
||||||
// first, and it must take the other #ifdef branch. Crafter.Graphics ships
|
|
||||||
// each UI shader in a descriptor-heap and a descriptor-set flavour this
|
|
||||||
// way; if the variant were dropped from the output name, whichever compile
|
|
||||||
// finished last would silently win.
|
|
||||||
fs::path variantSrc = fs::current_path() / "tests" / "ShaderCompile" / "fixture" / "variant.frag";
|
|
||||||
Shader plain(fs::path(variantSrc), "main", ShaderType::Fragment);
|
|
||||||
Shader red(fs::path(variantSrc), "main", ShaderType::Fragment);
|
|
||||||
red.defines = {"VARIANT_RED", "VARIANT_ALPHA=0.5"};
|
|
||||||
red.variant = "red";
|
|
||||||
|
|
||||||
if (plain.OutputName() != "variant.spv" || red.OutputName() != "variant.red.spv") {
|
|
||||||
std::println(std::cerr, "unexpected output names: {} and {}", plain.OutputName().string(), red.OutputName().string());
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
for (const Shader* each : {&plain, &red}) {
|
|
||||||
// Staleness is per output, not per source: while only variant.spv
|
|
||||||
// exists, the variant is still due. A Check() that ignored the
|
|
||||||
// variant would call it up to date here, and Build() would then
|
|
||||||
// never compile a variant added to an already built project.
|
|
||||||
if (each == &red && red.Check(outDir, outDir, includeDirs)) {
|
|
||||||
std::println(std::cerr, "Shader::Check called the variant up to date before it was ever compiled");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
std::string variantErr = each->Compile(outDir, outDir, includeDirs);
|
|
||||||
if (!variantErr.empty()) {
|
|
||||||
std::println(std::cerr, "variant shader compile failed: {}", variantErr);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if (!each->Check(outDir, outDir, includeDirs)) {
|
|
||||||
std::println(std::cerr, "Shader::Check returned false for {}", each->OutputName().string());
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Malformed defines and variants are refused by name. Left to glslang, an
|
|
||||||
// empty macro name surfaces as "-1:<line>" with no file, and a variant
|
|
||||||
// with a separator as a failed open after the whole compile.
|
|
||||||
for (const char* bad : {"", "=1", "A\nB"}) {
|
|
||||||
Shader malformed(fs::path(variantSrc), "main", ShaderType::Fragment);
|
|
||||||
malformed.defines = {bad};
|
|
||||||
malformed.variant = "malformed";
|
|
||||||
if (!malformed.Compile(outDir, outDir, includeDirs).contains("invalid shader define")) {
|
|
||||||
std::println(std::cerr, "a malformed define was not rejected by name");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Shader nested(fs::path(variantSrc), "main", ShaderType::Fragment);
|
|
||||||
nested.variant = "sub/red";
|
|
||||||
if (!nested.Compile(outDir, outDir, includeDirs).contains("path separator")) {
|
|
||||||
std::println(std::cerr, "a variant with a path separator was not rejected by name");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto readAll = [](const fs::path& file) {
|
|
||||||
std::ifstream stream(file, std::ios::binary);
|
|
||||||
return std::string(std::istreambuf_iterator<char>(stream), std::istreambuf_iterator<char>());
|
|
||||||
};
|
|
||||||
std::string plainSpv = readAll(outDir / "variant.spv");
|
|
||||||
std::string redSpv = readAll(outDir / "variant.red.spv");
|
|
||||||
if (plainSpv.empty() || redSpv.empty()) {
|
|
||||||
std::println(std::cerr, "a variant output is missing or empty");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if (plainSpv == redSpv) {
|
|
||||||
std::println(std::cerr, "both variants compiled to the same SPIR-V: the defines did not reach the preprocessor");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
const vec4 kTint = vec4(0.0, 1.0, 0.0, 1.0);
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
#version 450
|
|
||||||
#extension GL_GOOGLE_include_directive : require
|
|
||||||
|
|
||||||
// Built by the ShaderIncludeChange test, which edits palette.glsl and this
|
|
||||||
// entry's defines between builds.
|
|
||||||
#include "palette.glsl"
|
|
||||||
|
|
||||||
layout(location = 0) out vec4 outColor;
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
#ifdef LIT_DIM
|
|
||||||
outColor = kTint * 0.5;
|
|
||||||
#else
|
|
||||||
outColor = kTint;
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
|
||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
||||||
|
|
||||||
int main() {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
#version 450
|
|
||||||
|
|
||||||
// Includes nothing: the ShaderIncludeChange test checks it is left alone.
|
|
||||||
layout(location = 0) out vec4 outColor;
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
outColor = vec4(0.0, 0.0, 1.0, 1.0);
|
|
||||||
}
|
|
||||||
|
|
@ -1,126 +0,0 @@
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
|
||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
||||||
|
|
||||||
import std;
|
|
||||||
import Crafter.Build;
|
|
||||||
namespace fs = std::filesystem;
|
|
||||||
using namespace Crafter;
|
|
||||||
|
|
||||||
// A shader's output is stale when anything that went into it changed, not
|
|
||||||
// only its own source. Check() used to compare the .spv against the source's
|
|
||||||
// mtime alone, so:
|
|
||||||
// - editing a file the shader #includes left the .spv built against the old
|
|
||||||
// contents, the GLSL twin of IncrementalHeaderChange;
|
|
||||||
// - changing an entry's defines without touching the source did too.
|
|
||||||
// Each pass changes one thing and checks both directions: the shader it
|
|
||||||
// reaches is recompiled, to new contents, and the shader that includes
|
|
||||||
// nothing is left alone. A build with nothing changed compiles nothing.
|
|
||||||
namespace {
|
|
||||||
std::int32_t failures = 0;
|
|
||||||
|
|
||||||
void Check(bool cond, std::string_view msg) {
|
|
||||||
if (!cond) {
|
|
||||||
std::println(std::cerr, "FAIL: {}", msg);
|
|
||||||
++failures;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The fixture is edited between passes, so work on a copy outside the repo.
|
|
||||||
fs::path StageFixture() {
|
|
||||||
fs::path source = fs::current_path() / "tests" / "ShaderIncludeChange" / "fixture";
|
|
||||||
fs::path staged = fs::temp_directory_path() / "crafter-build-shader-include-change";
|
|
||||||
fs::remove_all(staged);
|
|
||||||
fs::copy(source, staged, fs::copy_options::recursive);
|
|
||||||
return staged;
|
|
||||||
}
|
|
||||||
|
|
||||||
Configuration MakeApp(const fs::path& staged) {
|
|
||||||
Configuration app;
|
|
||||||
app.path = staged;
|
|
||||||
app.name = "lit";
|
|
||||||
app.outputName = "lit";
|
|
||||||
app.target = HostTarget();
|
|
||||||
app.type = ConfigurationType::Executable;
|
|
||||||
std::array<fs::path, 0> ifaces = {};
|
|
||||||
std::array<fs::path, 1> impls = { "main" };
|
|
||||||
app.GetInterfacesAndImplementations(ifaces, impls);
|
|
||||||
app.shaders.emplace_back(staged / "lit.frag", std::string("main"), ShaderType::Fragment);
|
|
||||||
app.shaders.emplace_back(staged / "plain.frag", std::string("main"), ShaderType::Fragment);
|
|
||||||
// Puts include/ on the shaders' #include path.
|
|
||||||
app.buildFiles = { staged / "include" / "palette.glsl" };
|
|
||||||
return app;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool BuildOk(Configuration& app, std::string_view label) {
|
|
||||||
// A fresh depResults per pass: the map memoizes a Configuration's
|
|
||||||
// build for the duration of one pass.
|
|
||||||
std::unordered_map<fs::path, std::shared_future<BuildResult>> depResults;
|
|
||||||
std::mutex depMutex;
|
|
||||||
BuildResult r = Build(app, depResults, depMutex);
|
|
||||||
Check(r.result.empty(), std::format("{} builds: {}", label, r.result));
|
|
||||||
return r.result.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string ReadAll(const fs::path& file) {
|
|
||||||
std::ifstream stream(file, std::ios::binary);
|
|
||||||
return std::string(std::istreambuf_iterator<char>(stream), std::istreambuf_iterator<char>());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int main() {
|
|
||||||
const fs::path staged = StageFixture();
|
|
||||||
Configuration app = MakeApp(staged);
|
|
||||||
const fs::path lit = app.BinDir() / "lit.spv";
|
|
||||||
const fs::path plain = app.BinDir() / "plain.spv";
|
|
||||||
const fs::path palette = staged / "include" / "palette.glsl";
|
|
||||||
|
|
||||||
// Pass 1: everything from scratch.
|
|
||||||
if (!BuildOk(app, "the first pass")) return 1;
|
|
||||||
Check(fs::exists(lit) && fs::exists(plain), "both shaders are in the bin dir");
|
|
||||||
const fs::file_time_type plainBuilt = fs::last_write_time(plain);
|
|
||||||
const std::string green = ReadAll(lit);
|
|
||||||
|
|
||||||
// Pass 2: nothing changed.
|
|
||||||
fs::file_time_type litBuilt = fs::last_write_time(lit);
|
|
||||||
BuildOk(app, "an unchanged project");
|
|
||||||
Check(fs::last_write_time(lit) == litBuilt && fs::last_write_time(plain) == plainBuilt, "with nothing changed, no shader is recompiled");
|
|
||||||
|
|
||||||
// Pass 3: the included file changes. Dated just after the output it must
|
|
||||||
// outdate, so the rebuilt output is newer than it again.
|
|
||||||
{
|
|
||||||
std::ofstream out(palette, std::ios::trunc);
|
|
||||||
out << "const vec4 kTint = vec4(1.0, 0.0, 0.0, 1.0);\n";
|
|
||||||
}
|
|
||||||
fs::last_write_time(palette, litBuilt + std::chrono::milliseconds(1));
|
|
||||||
BuildOk(app, "an edited include");
|
|
||||||
const std::string red = ReadAll(lit);
|
|
||||||
Check(fs::last_write_time(lit) > litBuilt && red != green, "editing an included file recompiles the shader that includes it, to new contents");
|
|
||||||
Check(fs::last_write_time(plain) == plainBuilt, "and leaves the shader that includes nothing alone");
|
|
||||||
|
|
||||||
// Pass 4: the entry's defines change, no file does.
|
|
||||||
app.shaders[0].defines = { "LIT_DIM" };
|
|
||||||
litBuilt = fs::last_write_time(lit);
|
|
||||||
BuildOk(app, "a new define");
|
|
||||||
const std::string dim = ReadAll(lit);
|
|
||||||
Check(fs::last_write_time(lit) > litBuilt && dim != red, "a define added to the entry recompiles it, taking the #ifdef branch");
|
|
||||||
Check(fs::last_write_time(plain) == plainBuilt, "and leaves the other entry alone");
|
|
||||||
|
|
||||||
// Pass 5: and back.
|
|
||||||
app.shaders[0].defines = {};
|
|
||||||
BuildOk(app, "the define taken out");
|
|
||||||
Check(ReadAll(lit) == red, "taking the define out again recompiles it as it was");
|
|
||||||
|
|
||||||
// Pass 6: nothing changed again; what the passes above recorded holds.
|
|
||||||
litBuilt = fs::last_write_time(lit);
|
|
||||||
BuildOk(app, "an unchanged project, again");
|
|
||||||
Check(fs::last_write_time(lit) == litBuilt && fs::last_write_time(plain) == plainBuilt, "with nothing changed since, no shader is recompiled");
|
|
||||||
|
|
||||||
std::error_code ec;
|
|
||||||
fs::remove_all(staged, ec);
|
|
||||||
|
|
||||||
if (failures > 0) {
|
|
||||||
std::println(std::cerr, "{} assertions failed", failures);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
|
||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
||||||
|
|
||||||
int main() {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
#version 450
|
|
||||||
|
|
||||||
// Built twice by the ShaderVariantBuild test: as is, and with TINT_RED defined.
|
|
||||||
layout(location = 0) out vec4 outColor;
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
#if TINT_RED
|
|
||||||
outColor = vec4(1.0, 0.0, 0.0, 1.0);
|
|
||||||
#else
|
|
||||||
outColor = vec4(0.0, 1.0, 0.0, 1.0);
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
@ -1,121 +0,0 @@
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
|
||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
||||||
|
|
||||||
import std;
|
|
||||||
import Crafter.Build;
|
|
||||||
namespace fs = std::filesystem;
|
|
||||||
using namespace Crafter;
|
|
||||||
|
|
||||||
// One shader source can be registered more than once, each entry with its own
|
|
||||||
// defines and a variant that names its output ("tint.spv", "tint.red.spv").
|
|
||||||
// Build() compiles the entries in parallel into one directory, which is where
|
|
||||||
// the feature can go wrong in ways the Shader API alone does not show:
|
|
||||||
//
|
|
||||||
// - both entries must land in the bin dir, with different contents;
|
|
||||||
// - an entry added later must still be compiled when the other one's output
|
|
||||||
// is already there and up to date;
|
|
||||||
// - two entries that would write the same file (defines set, variant
|
|
||||||
// forgotten) used to race silently: the file held whichever compile
|
|
||||||
// finished last, the build reported success, and Check() called both up
|
|
||||||
// to date from then on. That has to be a build error naming the cause.
|
|
||||||
//
|
|
||||||
// The fixture's `#if TINT_RED` also pins that a bare define is defined to 1,
|
|
||||||
// like the compiler's -D: an empty definition would not compile.
|
|
||||||
namespace {
|
|
||||||
std::int32_t Failures = 0;
|
|
||||||
|
|
||||||
void Check(bool cond, std::string_view msg) {
|
|
||||||
if (!cond) {
|
|
||||||
std::println(std::cerr, "FAIL: {}", msg);
|
|
||||||
++Failures;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shader outputs are compared by mtime against the source, and the test
|
|
||||||
// adds an entry between passes, so work on a fresh copy outside the repo.
|
|
||||||
fs::path StageFixture() {
|
|
||||||
fs::path source = fs::current_path() / "tests" / "ShaderVariantBuild" / "fixture";
|
|
||||||
fs::path staged = fs::temp_directory_path() / "crafter-build-shader-variant-build";
|
|
||||||
fs::remove_all(staged);
|
|
||||||
fs::copy(source, staged, fs::copy_options::recursive);
|
|
||||||
return staged;
|
|
||||||
}
|
|
||||||
|
|
||||||
Configuration MakeApp(const fs::path& staged) {
|
|
||||||
Configuration app;
|
|
||||||
app.path = staged;
|
|
||||||
app.name = "tinted";
|
|
||||||
app.outputName = "tinted";
|
|
||||||
app.target = HostTarget();
|
|
||||||
app.type = ConfigurationType::Executable;
|
|
||||||
std::array<fs::path, 0> ifaces = {};
|
|
||||||
std::array<fs::path, 1> impls = { "main" };
|
|
||||||
app.GetInterfacesAndImplementations(ifaces, impls);
|
|
||||||
return app;
|
|
||||||
}
|
|
||||||
|
|
||||||
Shader& AddTint(Configuration& app, const fs::path& staged) {
|
|
||||||
return app.shaders.emplace_back(staged / "tint.frag", std::string("main"), ShaderType::Fragment);
|
|
||||||
}
|
|
||||||
|
|
||||||
BuildResult BuildOnce(Configuration& app) {
|
|
||||||
// A fresh depResults per pass: the map memoizes a Configuration's
|
|
||||||
// build for the duration of one pass.
|
|
||||||
std::unordered_map<fs::path, std::shared_future<BuildResult>> depResults;
|
|
||||||
std::mutex depMutex;
|
|
||||||
return Build(app, depResults, depMutex);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string ReadAll(const fs::path& file) {
|
|
||||||
std::ifstream stream(file, std::ios::binary);
|
|
||||||
return std::string(std::istreambuf_iterator<char>(stream), std::istreambuf_iterator<char>());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int main() {
|
|
||||||
const fs::path staged = StageFixture();
|
|
||||||
|
|
||||||
// Pass 1: the plain entry alone.
|
|
||||||
Configuration app = MakeApp(staged);
|
|
||||||
AddTint(app, staged);
|
|
||||||
BuildResult first = BuildOnce(app);
|
|
||||||
Check(first.result.empty(), std::format("the plain entry builds: {}", first.result));
|
|
||||||
const fs::path plainSpv = app.BinDir() / "tint.spv";
|
|
||||||
const fs::path redSpv = app.BinDir() / "tint.red.spv";
|
|
||||||
Check(fs::exists(plainSpv), "tint.spv is in the bin dir");
|
|
||||||
Check(!fs::exists(redSpv), "no variant output before the variant is registered");
|
|
||||||
|
|
||||||
// Pass 2: the variant is added while tint.spv is already up to date. It
|
|
||||||
// has its own staleness check and must be compiled, into its own file.
|
|
||||||
Shader& red = AddTint(app, staged);
|
|
||||||
red.defines = {"TINT_RED"};
|
|
||||||
red.variant = "red";
|
|
||||||
BuildResult second = BuildOnce(app);
|
|
||||||
Check(second.result.empty(), std::format("plain + variant builds: {}", second.result));
|
|
||||||
Check(fs::exists(redSpv), "tint.red.spv is in the bin dir");
|
|
||||||
const std::string plainBytes = ReadAll(plainSpv);
|
|
||||||
const std::string redBytes = ReadAll(redSpv);
|
|
||||||
Check(!plainBytes.empty() && !redBytes.empty(), "both outputs have content");
|
|
||||||
Check(plainBytes != redBytes, "the variant took the other #if branch");
|
|
||||||
|
|
||||||
// Pass 3: the same define without a variant. Both entries would write
|
|
||||||
// tint.spv, so the build has to stop and say so, and must leave the file
|
|
||||||
// from pass 2 alone.
|
|
||||||
Configuration clash = MakeApp(staged);
|
|
||||||
AddTint(clash, staged);
|
|
||||||
Shader& unnamed = AddTint(clash, staged);
|
|
||||||
unnamed.defines = {"TINT_RED"};
|
|
||||||
BuildResult third = BuildOnce(clash);
|
|
||||||
Check(!third.result.empty(), "two entries with one output name fail the build");
|
|
||||||
Check(third.result.contains("tint.spv") && third.result.contains("variant"), std::format("the error names the file and the fix: {}", third.result));
|
|
||||||
Check(ReadAll(plainSpv) == plainBytes, "the existing output is not overwritten by the failed build");
|
|
||||||
|
|
||||||
std::error_code ec;
|
|
||||||
fs::remove_all(staged, ec);
|
|
||||||
|
|
||||||
if (Failures > 0) {
|
|
||||||
std::println(std::cerr, "{} assertions failed", Failures);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
@ -1,65 +0,0 @@
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
|
||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
||||||
|
|
||||||
import std;
|
|
||||||
import Crafter.Build;
|
|
||||||
namespace fs = std::filesystem;
|
|
||||||
using namespace Crafter;
|
|
||||||
|
|
||||||
// clang's driver ranks the host toolchain's own libc++ headers
|
|
||||||
// (<install>/../include/c++/v1) above --sysroot. A unit that only does
|
|
||||||
// `import std;` never notices, because the std module is precompiled with the
|
|
||||||
// sysroot's headers forced. A unit that includes a standard header textually
|
|
||||||
// (vendored C++ in a dependency, say) got the host's libc++ configured for the
|
|
||||||
// host's libc: against a musl sysroot every such unit died with "unknown rune
|
|
||||||
// table for this platform", and against a glibc sysroot it compiled against
|
|
||||||
// the wrong release without a word.
|
|
||||||
//
|
|
||||||
// GetCompileCommand therefore has to point every sysroot compile at the
|
|
||||||
// sysroot's copy. It must not do so where there is no sysroot, for wasm (which
|
|
||||||
// has its own sysroot handling) or for mingw (libstdc++, no libc++ at all).
|
|
||||||
// The command is checked as text: no compiler runs.
|
|
||||||
namespace {
|
|
||||||
std::int32_t Failures = 0;
|
|
||||||
|
|
||||||
void Check(bool cond, std::string_view msg) {
|
|
||||||
if (!cond) {
|
|
||||||
std::println(std::cerr, "FAIL: {}", msg);
|
|
||||||
++Failures;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string CommandFor(std::string_view target, std::string_view sysroot) {
|
|
||||||
Configuration cfg;
|
|
||||||
cfg.path = "/tmp/crafter-build-sysroot-compile-flags";
|
|
||||||
cfg.name = "flags";
|
|
||||||
cfg.outputName = "flags";
|
|
||||||
cfg.target = std::string(target);
|
|
||||||
cfg.march = "generic";
|
|
||||||
cfg.mtune = "generic";
|
|
||||||
cfg.type = ConfigurationType::Executable;
|
|
||||||
cfg.sysroot = std::string(sysroot);
|
|
||||||
return GetCompileCommand(cfg).command;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int main() {
|
|
||||||
const std::string sysroot = "/opt/crafter-build-test-sysroot";
|
|
||||||
const std::string forced = std::format("-nostdinc++ -isystem {}/usr/include/c++/v1", sysroot);
|
|
||||||
|
|
||||||
for (std::string_view target : {"aarch64-alpine-linux-musl", "aarch64-unknown-linux-gnu", "x86_64-pc-linux-gnu"}) {
|
|
||||||
const std::string command = CommandFor(target, sysroot);
|
|
||||||
Check(command.contains(std::format("--sysroot={}", sysroot)), std::format("{}: the sysroot is passed", target));
|
|
||||||
Check(command.contains(forced), std::format("{}: the sysroot's libc++ headers are forced", target));
|
|
||||||
}
|
|
||||||
|
|
||||||
Check(!CommandFor("x86_64-pc-linux-gnu", "").contains("/usr/include/c++/v1 -Wno-unused-command-line-argument"), "no sysroot: nothing is forced");
|
|
||||||
Check(!CommandFor("wasm32-wasip1", sysroot).contains(forced), "wasm: left to its own sysroot handling");
|
|
||||||
Check(!CommandFor("x86_64-w64-mingw32", sysroot).contains(forced), "mingw: libstdc++ target, libc++ headers are not forced");
|
|
||||||
|
|
||||||
if (Failures > 0) {
|
|
||||||
std::println(std::cerr, "{} assertions failed", Failures);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
@ -1,156 +0,0 @@
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
|
||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
||||||
|
|
||||||
import std;
|
|
||||||
import Crafter.Build;
|
|
||||||
namespace fs = std::filesystem;
|
|
||||||
using namespace Crafter;
|
|
||||||
|
|
||||||
// A sysroot build precompiles the std module from a module source that has to
|
|
||||||
// belong to the libc++ release whose headers the sysroot carries. Not every
|
|
||||||
// distro packages that source with its libc++ (Alpine does not), and the
|
|
||||||
// fallback used to be the toolchain's own copy whatever its release: with a
|
|
||||||
// libc++ 22 toolchain and a libc++ 23 sysroot the build died on a page of "no
|
|
||||||
// member named 'runtime_format' in namespace 'std'" that named neither
|
|
||||||
// release nor what to do about it.
|
|
||||||
//
|
|
||||||
// Staged, compiler-free layouts cover the order SysrootStdModule picks in: the
|
|
||||||
// sysroot's own source, the toolchain's for the same major release, the copy
|
|
||||||
// crafter-build ships for another release, and otherwise one line naming both
|
|
||||||
// releases and where the matching source belongs. The last block drives
|
|
||||||
// Build() against a sysroot nothing matches and expects that line back in
|
|
||||||
// place of compiler output.
|
|
||||||
namespace {
|
|
||||||
std::int32_t Failures = 0;
|
|
||||||
|
|
||||||
void Check(bool cond, std::string_view msg) {
|
|
||||||
if (!cond) {
|
|
||||||
std::println(std::cerr, "FAIL: {}", msg);
|
|
||||||
++Failures;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Write(const fs::path& file, std::string_view content) {
|
|
||||||
fs::create_directories(file.parent_path());
|
|
||||||
std::ofstream(file) << content;
|
|
||||||
}
|
|
||||||
|
|
||||||
// What libc++ installs as include/c++/v1/__config, reduced to the line
|
|
||||||
// that states the release.
|
|
||||||
void StageHeaders(const fs::path& includeDir, std::int32_t version) {
|
|
||||||
Write(includeDir / "__config", std::format("#ifndef _LIBCPP___CONFIG\n# define _LIBCPP___CONFIG\n# define _LIBCPP_VERSION {}\n#endif\n", version));
|
|
||||||
}
|
|
||||||
|
|
||||||
fs::path StageSysroot(const fs::path& sysroot, std::int32_t version) {
|
|
||||||
StageHeaders(sysroot / "usr" / "include" / "c++" / "v1", version);
|
|
||||||
return sysroot;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int main() {
|
|
||||||
const fs::path scratch = fs::temp_directory_path() / "crafter-build-sysroot-std-module";
|
|
||||||
std::error_code ec;
|
|
||||||
fs::remove_all(scratch, ec);
|
|
||||||
fs::create_directories(scratch);
|
|
||||||
|
|
||||||
{
|
|
||||||
StageHeaders(scratch / "headers", 230102);
|
|
||||||
Check(LibcxxVersion(scratch / "headers") == 230102, "the release is read from __config");
|
|
||||||
Check(LibcxxVersion(scratch / "no-such-dir") == 0, "missing headers read as an unknown release");
|
|
||||||
Write(scratch / "other" / "__config", "#define _LIBCPP_ABI_VERSION 1\n");
|
|
||||||
Check(LibcxxVersion(scratch / "other") == 0, "a __config without the version line reads as an unknown release");
|
|
||||||
Check(LibcxxVersionString(230102) == "23.1.2" && LibcxxVersionString(220108) == "22.1.8", "XXYYZZ is spelled as a dotted release");
|
|
||||||
}
|
|
||||||
|
|
||||||
// A toolchain on libc++ 22.1.8 that also ships the sources for 23.
|
|
||||||
Toolchain tc;
|
|
||||||
tc.stdCppm = scratch / "toolchain" / "share" / "libc++" / "v1" / "std.cppm";
|
|
||||||
tc.libcxxVersion = 220108;
|
|
||||||
tc.stdModulesDir = scratch / "shipped";
|
|
||||||
Write(tc.stdCppm, "");
|
|
||||||
Write(tc.stdModulesDir / "23" / "std.cppm", "");
|
|
||||||
|
|
||||||
{
|
|
||||||
// The sysroot's own source matches its headers by construction, so
|
|
||||||
// it wins whatever the releases are.
|
|
||||||
const fs::path sysroot = StageSysroot(scratch / "sysroot-own", 240100);
|
|
||||||
const fs::path own = sysroot / "usr" / "share" / "libc++" / "v1" / "std.cppm";
|
|
||||||
Write(own, "");
|
|
||||||
StdModuleSource source = SysrootStdModule(tc, sysroot);
|
|
||||||
Check(source.error.empty() && source.stdCppm == own, "a sysroot that ships the module source uses its own");
|
|
||||||
}
|
|
||||||
{
|
|
||||||
const fs::path sysroot = StageSysroot(scratch / "sysroot-same-major", 220103);
|
|
||||||
StdModuleSource source = SysrootStdModule(tc, sysroot);
|
|
||||||
Check(source.error.empty() && source.stdCppm == tc.stdCppm, "a point release of the toolchain's libc++ uses the toolchain's source");
|
|
||||||
}
|
|
||||||
{
|
|
||||||
// The reported case: libc++ 23 headers, a libc++ 22 toolchain.
|
|
||||||
const fs::path sysroot = StageSysroot(scratch / "sysroot-next-major", 230102);
|
|
||||||
StdModuleSource source = SysrootStdModule(tc, sysroot);
|
|
||||||
Check(source.error.empty() && source.stdCppm == tc.stdModulesDir / "23" / "std.cppm", "another release uses the source shipped for it, not the toolchain's");
|
|
||||||
}
|
|
||||||
{
|
|
||||||
const fs::path sysroot = StageSysroot(scratch / "sysroot-unshipped", 210108);
|
|
||||||
StdModuleSource source = SysrootStdModule(tc, sysroot);
|
|
||||||
Check(source.stdCppm.empty() && !source.error.empty(), "a release nothing was shipped for is an error, not a compile against the wrong source");
|
|
||||||
Check(source.error.contains("21.1.8") && source.error.contains("22.1.8"), std::format("the error names both releases, got '{}'", source.error));
|
|
||||||
Check(source.error.contains((sysroot / "usr" / "share" / "libc++" / "v1").string()), std::format("the error says where the matching source belongs, got '{}'", source.error));
|
|
||||||
Check(!source.error.contains('\n'), "the error is one line");
|
|
||||||
}
|
|
||||||
{
|
|
||||||
const fs::path sysroot = scratch / "sysroot-no-libcxx";
|
|
||||||
fs::create_directories(sysroot / "usr" / "include");
|
|
||||||
StdModuleSource source = SysrootStdModule(tc, sysroot);
|
|
||||||
Check(source.stdCppm.empty() && source.error.contains("no libc++ headers"), std::format("a sysroot without libc++ says so, got '{}'", source.error));
|
|
||||||
}
|
|
||||||
{
|
|
||||||
// Nothing to compare when either release is unknown: the toolchain's
|
|
||||||
// source goes to the compiler, as it did before releases were read.
|
|
||||||
const fs::path sysroot = scratch / "sysroot-unreadable";
|
|
||||||
Write(sysroot / "usr" / "include" / "c++" / "v1" / "__config", "#define _LIBCPP_ABI_VERSION 1\n");
|
|
||||||
Check(SysrootStdModule(tc, sysroot).stdCppm == tc.stdCppm, "an unreadable sysroot release falls back to the toolchain's source");
|
|
||||||
Toolchain unknown = tc;
|
|
||||||
unknown.libcxxVersion = 0;
|
|
||||||
unknown.stdModulesDir.clear();
|
|
||||||
Check(SysrootStdModule(unknown, StageSysroot(scratch / "sysroot-vs-unknown", 230102)).stdCppm == tc.stdCppm, "an unreadable toolchain release falls back to the toolchain's source");
|
|
||||||
}
|
|
||||||
{
|
|
||||||
Toolchain bare;
|
|
||||||
bare.stdCppm = scratch / "nowhere" / "std.cppm";
|
|
||||||
bare.libcxxVersion = 220108;
|
|
||||||
StdModuleSource source = SysrootStdModule(bare, StageSysroot(scratch / "sysroot-vs-bare", 220108));
|
|
||||||
Check(source.stdCppm.empty() && source.error.contains("no std module source"), std::format("a toolchain without a module source is an error too, got '{}'", source.error));
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
// End to end with the real toolchain: a sysroot on a libc++ release
|
|
||||||
// that does not exist fails the build with the one-line explanation
|
|
||||||
// before clang is run. The target is the host's so the std PCM cache
|
|
||||||
// directory is the one every other test already uses.
|
|
||||||
const Toolchain& real = GetToolchain();
|
|
||||||
Check(real.libcxxVersion != 0, "the running toolchain's libc++ release is read from its headers");
|
|
||||||
|
|
||||||
const fs::path project = scratch / "project";
|
|
||||||
Write(project / "main.cpp", "int main() { return 0; }\n");
|
|
||||||
Configuration app;
|
|
||||||
app.path = project;
|
|
||||||
app.name = "sysroot-std-module";
|
|
||||||
app.outputName = "sysroot-std-module";
|
|
||||||
app.target = HostTarget();
|
|
||||||
app.type = ConfigurationType::Executable;
|
|
||||||
app.sysroot = StageSysroot(scratch / "sysroot-future", 990000).string();
|
|
||||||
std::array<fs::path, 0> ifaces = {};
|
|
||||||
std::array<fs::path, 1> impls = { "main" };
|
|
||||||
app.GetInterfacesAndImplementations(ifaces, impls);
|
|
||||||
|
|
||||||
std::unordered_map<fs::path, std::shared_future<BuildResult>> depResults;
|
|
||||||
std::mutex depMutex;
|
|
||||||
BuildResult r = Build(app, depResults, depMutex);
|
|
||||||
Check(r.result.contains("libc++ 99.0.0"), std::format("the build fails naming the sysroot's release, got '{}'", r.result));
|
|
||||||
Check(r.result.contains(LibcxxVersionString(real.libcxxVersion)), std::format("the build fails naming the toolchain's release, got '{}'", r.result));
|
|
||||||
Check(!r.result.contains('\n') && !r.result.contains("error:"), std::format("the failure is one line, not compiler output, got '{}'", r.result));
|
|
||||||
}
|
|
||||||
|
|
||||||
return Failures == 0 ? 0 : 1;
|
|
||||||
}
|
|
||||||
|
|
@ -6,7 +6,9 @@ import Crafter.Build;
|
||||||
using namespace Crafter;
|
using namespace Crafter;
|
||||||
|
|
||||||
// POSIX env helpers — libc++ exports only std::getenv via `import std`, so
|
// POSIX env helpers — libc++ exports only std::getenv via `import std`, so
|
||||||
// forward-declare the setters we need against the C library directly.
|
// forward-declare the setters we need against the C library directly. The
|
||||||
|
// test only runs on POSIX hosts (the project.cpp gates new tests by
|
||||||
|
// x86_64-pc-linux-gnu) so we don't need a Windows alternative here.
|
||||||
extern "C" int setenv(const char* name, const char* value, int overwrite);
|
extern "C" int setenv(const char* name, const char* value, int overwrite);
|
||||||
extern "C" int unsetenv(const char* name);
|
extern "C" int unsetenv(const char* name);
|
||||||
|
|
||||||
|
|
@ -85,36 +87,12 @@ int main() {
|
||||||
Check(r.exec.find("QEMU_LD_PREFIX=/opt/alarm-sysroot") != std::string::npos, "ForTarget propagates sysroot to QEMU_LD_PREFIX");
|
Check(r.exec.find("QEMU_LD_PREFIX=/opt/alarm-sysroot") != std::string::npos, "ForTarget propagates sysroot to QEMU_LD_PREFIX");
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
// The only way to run x86_64-w64-mingw32 output is wine.
|
// From a Linux host the only sensible way to run x86_64-w64-mingw32 is wine.
|
||||||
Configuration cfg;
|
Configuration cfg;
|
||||||
cfg.target = "x86_64-w64-mingw32";
|
cfg.target = "x86_64-w64-mingw32";
|
||||||
TestRunner r = TestRunner::ForTarget(cfg);
|
TestRunner r = TestRunner::ForTarget(cfg);
|
||||||
Check(r.name == "wine", "ForTarget(mingw) uses wine");
|
if (HostTarget().find("linux") != std::string::npos) {
|
||||||
}
|
Check(r.name == "wine", "ForTarget(mingw) on Linux host uses wine");
|
||||||
|
|
||||||
// SameTarget: the vendor field is spelling, not identity. A clang built
|
|
||||||
// without a vendor reports x86_64-unknown-linux-gnu where Arch's reports
|
|
||||||
// x86_64-pc-linux-gnu; both are the host, and a project that wrote the
|
|
||||||
// other spelling must still run its tests locally.
|
|
||||||
{
|
|
||||||
Check(SameTarget("x86_64-pc-linux-gnu", "x86_64-unknown-linux-gnu"), "pc and unknown vendors compare equal");
|
|
||||||
Check(SameTarget("x86_64-pc-linux-gnu", "x86_64-linux-gnu"), "a vendorless triple compares equal");
|
|
||||||
Check(SameTarget("x86_64-alpine-linux-musl", "x86_64-linux-musl"), "a distro vendor compares equal");
|
|
||||||
Check(SameTarget("aarch64-unknown-linux-gnu", "aarch64-linux-gnu"), "the vendor is dropped on other arches too");
|
|
||||||
Check(SameTarget("wasm32-unknown-wasip1", "wasm32-wasip1"), "a three-part triple with a vendor compares equal");
|
|
||||||
Check(!SameTarget("x86_64-pc-linux-gnu", "x86_64-linux-musl"), "a different environment stays different");
|
|
||||||
Check(!SameTarget("x86_64-pc-linux-gnu", "aarch64-unknown-linux-gnu"), "a different arch stays different");
|
|
||||||
Check(!SameTarget("x86_64-w64-mingw32", "x86_64-linux-gnu"), "mingw's w64 is not a vendor to drop");
|
|
||||||
Check(NormalizeTarget("x86_64-w64-mingw32") == "x86_64-w64-mingw32", "a mingw triple is left alone");
|
|
||||||
Check(NormalizeTarget("wasm32-wasip1") == "wasm32-wasip1", "a two-part triple is left alone");
|
|
||||||
|
|
||||||
Configuration pc;
|
|
||||||
pc.target = "x86_64-pc-linux-gnu";
|
|
||||||
Configuration unknown;
|
|
||||||
unknown.target = "x86_64-unknown-linux-gnu";
|
|
||||||
if (SameTarget(HostTarget(), "x86_64-linux-gnu")) {
|
|
||||||
Check(TestRunner::ForTarget(pc).IsLocal(), "ForTarget runs the pc spelling locally on an x86_64 glibc host");
|
|
||||||
Check(TestRunner::ForTarget(unknown).IsLocal(), "ForTarget runs the unknown spelling locally on an x86_64 glibc host");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,192 +0,0 @@
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
|
||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
||||||
|
|
||||||
import std;
|
|
||||||
import Crafter.Build;
|
|
||||||
namespace fs = std::filesystem;
|
|
||||||
using namespace Crafter;
|
|
||||||
|
|
||||||
// ResolveToolchain is pure path resolution, so this test needs no compiler:
|
|
||||||
// it stages fake prefixes under the temp dir and checks which one wins,
|
|
||||||
// what is picked up beside it, and what the defaults are when nothing is.
|
|
||||||
// The last block runs the real GetToolchain() once, which is the only part
|
|
||||||
// that spawns clang.
|
|
||||||
namespace {
|
|
||||||
std::int32_t Failures = 0;
|
|
||||||
|
|
||||||
void Check(bool cond, std::string_view msg) {
|
|
||||||
if (!cond) {
|
|
||||||
std::println(std::cerr, "FAIL: {}", msg);
|
|
||||||
++Failures;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Touch(const fs::path& file) {
|
|
||||||
fs::create_directories(file.parent_path());
|
|
||||||
std::ofstream(file) << "";
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Contains(std::string_view haystack, std::string_view needle) {
|
|
||||||
return haystack.find(needle) != std::string_view::npos;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int main() {
|
|
||||||
const fs::path scratch = fs::temp_directory_path() / "crafter-build-toolchain-resolve";
|
|
||||||
std::error_code ec;
|
|
||||||
fs::remove_all(scratch, ec);
|
|
||||||
fs::create_directories(scratch);
|
|
||||||
|
|
||||||
// A prefix with everything crafter-build looks for beside the compiler,
|
|
||||||
// and one with the compiler alone.
|
|
||||||
const fs::path full = scratch / "full";
|
|
||||||
for (const char* rel : {"bin/clang++", "bin/clang", "bin/llvm-ar", "lib/libclang.so", "include/clang-c/Index.h", "include/c++/v1/__config", "share/libc++/v1/std.cppm", "share/wasi-sysroot/include/.keep"}) {
|
|
||||||
Touch(full / rel);
|
|
||||||
}
|
|
||||||
// The prefix's libc++ headers state its release.
|
|
||||||
std::ofstream(full / "include/c++/v1/__config") << "# define _LIBCPP_VERSION 230102\n";
|
|
||||||
const fs::path bare = scratch / "bare";
|
|
||||||
Touch(bare / "bin/clang++");
|
|
||||||
|
|
||||||
{
|
|
||||||
// Nothing set and nothing beside crafter-build: bare names on PATH
|
|
||||||
// and the system locations.
|
|
||||||
Toolchain tc = ResolveToolchain({});
|
|
||||||
Check(tc.error.empty(), "the PATH fallback is not an error");
|
|
||||||
Check(tc.prefix.empty(), "the PATH fallback has no prefix");
|
|
||||||
Check(tc.clangxx == "clang++" && tc.clang == "clang" && tc.llvmAr == "llvm-ar", "the PATH fallback uses bare tool names");
|
|
||||||
Check(!tc.libclang.has_value(), "the PATH fallback searches libclang by name");
|
|
||||||
Check(tc.llvmIncludeDir.empty(), "the PATH fallback adds no include dir");
|
|
||||||
Check(tc.stdCppm == "/usr/share/libc++/v1/std.cppm", "the system std module source is the default");
|
|
||||||
Check(tc.libcxxVersion == LibcxxVersion("/usr/include/c++/v1"), "the system libc++ headers state the release of the system module source");
|
|
||||||
Check(tc.stdModulesDir.empty(), "no crafter-build prefix, no shipped module sources");
|
|
||||||
Check(tc.wasiSysroot == "/usr/share/wasi-sysroot", "the system wasi sysroot is the default");
|
|
||||||
Check(tc.mingwSysroot.empty(), "outside a sandbox, clang finds mingw-w64 itself: no --sysroot");
|
|
||||||
Check(tc.LibcxxCompileFlags().empty() && tc.LibcxxLinkFlags().empty(), "the PATH fallback adds no libc++ flags");
|
|
||||||
}
|
|
||||||
{
|
|
||||||
// A toolchain installed beside crafter-build wins over PATH, and
|
|
||||||
// everything found beside it is used.
|
|
||||||
ToolchainInputs in;
|
|
||||||
in.crafterPrefix = full;
|
|
||||||
Toolchain tc = ResolveToolchain(in);
|
|
||||||
Check(tc.prefix == full, "the prefix beside crafter-build is picked");
|
|
||||||
Check(tc.clangxx == full / "bin/clang++" && tc.clang == full / "bin/clang" && tc.llvmAr == full / "bin/llvm-ar", "the tools come from that prefix");
|
|
||||||
Check(tc.libclang == full / "lib/libclang.so", "libclang beside the compiler is pinned");
|
|
||||||
Check(tc.llvmIncludeDir == full / "include", "clang-c headers beside the compiler are added to the include path");
|
|
||||||
Check(tc.stdCppm == full / "share/libc++/v1/std.cppm", "the prefix's std module source is used");
|
|
||||||
Check(tc.libcxxVersion == 230102, "the prefix's libc++ headers state its release");
|
|
||||||
Check(tc.stdModulesDir == full / "share/crafter-build/libc++-modules", "module sources for other releases are looked up in crafter-build's own share directory");
|
|
||||||
Check(tc.wasiSysroot == full / "share/wasi-sysroot", "the prefix's wasi sysroot is used");
|
|
||||||
Check(tc.LibcxxCompileFlags().empty(), "the compiler finds its own libc++ headers without flags");
|
|
||||||
Check(Contains(tc.LibcxxLinkFlags(), std::format("-L{}", (full / "lib").string())) && Contains(tc.LibcxxLinkFlags(), std::format("-Wl,-rpath,{}", (full / "lib").string())), "binaries get -L and an rpath for the prefix's lib dir");
|
|
||||||
}
|
|
||||||
{
|
|
||||||
ToolchainInputs in;
|
|
||||||
in.crafterPrefix = scratch / "nothing-here";
|
|
||||||
Toolchain tc = ResolveToolchain(in);
|
|
||||||
Check(tc.prefix.empty() && tc.clangxx == "clang++", "a crafter-build prefix without clang++ falls back to PATH");
|
|
||||||
}
|
|
||||||
{
|
|
||||||
// The environment wins over the prefix beside crafter-build. A
|
|
||||||
// prefix with nothing but the compiler keeps the system defaults for
|
|
||||||
// everything else.
|
|
||||||
ToolchainInputs in;
|
|
||||||
in.crafterPrefix = full;
|
|
||||||
in.env["CRAFTER_BUILD_LLVM_DIR"] = bare.string();
|
|
||||||
Toolchain tc = ResolveToolchain(in);
|
|
||||||
Check(tc.error.empty(), "an explicit prefix with a clang++ is accepted");
|
|
||||||
Check(tc.prefix == bare, "CRAFTER_BUILD_LLVM_DIR wins over the prefix beside crafter-build");
|
|
||||||
Check(!tc.libclang.has_value(), "no libclang.so in the prefix: search by name");
|
|
||||||
Check(tc.llvmIncludeDir.empty(), "no clang-c headers in the prefix: nothing added");
|
|
||||||
Check(tc.stdCppm == "/usr/share/libc++/v1/std.cppm", "no std module in the prefix: the system copy");
|
|
||||||
Check(tc.wasiSysroot == "/usr/share/wasi-sysroot", "no wasi sysroot in the prefix: the system one");
|
|
||||||
Check(Contains(tc.LibcxxLinkFlags(), std::format("-L{}", (bare / "lib").string())) && Contains(tc.LibcxxLinkFlags(), std::format("-Wl,-rpath,{}", (bare / "lib").string())), "the link flags follow the explicit prefix");
|
|
||||||
}
|
|
||||||
{
|
|
||||||
ToolchainInputs in;
|
|
||||||
in.env["CRAFTER_BUILD_LLVM_DIR"] = (scratch / "missing").string();
|
|
||||||
Toolchain tc = ResolveToolchain(in);
|
|
||||||
Check(!tc.error.empty() && Contains(tc.error, "CRAFTER_BUILD_LLVM_DIR"), "an explicit prefix without clang++ is an error, not a fallback");
|
|
||||||
}
|
|
||||||
{
|
|
||||||
ToolchainInputs in;
|
|
||||||
in.env["CRAFTER_BUILD_LLVM_DIR"] = "/usr";
|
|
||||||
Toolchain tc = ResolveToolchain(in);
|
|
||||||
Check(tc.LibcxxLinkFlags().empty(), "the system prefix needs no rpath");
|
|
||||||
Check(tc.llvmIncludeDir.empty(), "the system prefix needs no include dir");
|
|
||||||
}
|
|
||||||
{
|
|
||||||
ToolchainInputs in;
|
|
||||||
in.crafterPrefix = full;
|
|
||||||
in.env["CRAFTER_BUILD_LIBCLANG"] = "/nonexistent/libclang.so";
|
|
||||||
Check(ResolveToolchain(in).libclang == fs::path("/nonexistent/libclang.so"), "CRAFTER_BUILD_LIBCLANG is taken verbatim, even when it does not exist");
|
|
||||||
}
|
|
||||||
{
|
|
||||||
// An explicit libc++ install: headers, library and std module all
|
|
||||||
// come from it, whatever the compiler would have used.
|
|
||||||
const fs::path libcxx = scratch / "libcxx";
|
|
||||||
fs::create_directories(libcxx / "include/c++/v1");
|
|
||||||
std::ofstream(libcxx / "include/c++/v1/__config") << "# define _LIBCPP_VERSION 210108\n";
|
|
||||||
ToolchainInputs in;
|
|
||||||
in.crafterPrefix = full;
|
|
||||||
in.env["CRAFTER_BUILD_LIBCXX_DIR"] = libcxx.string();
|
|
||||||
Toolchain tc = ResolveToolchain(in);
|
|
||||||
Check(tc.libcxxDir == libcxx, "CRAFTER_BUILD_LIBCXX_DIR is recorded");
|
|
||||||
Check(tc.stdCppm == libcxx / "share/libc++/v1/std.cppm", "the std module source follows the explicit libc++");
|
|
||||||
Check(tc.libcxxVersion == 210108, "the release is read from the explicit libc++, not the compiler's");
|
|
||||||
Check(Contains(tc.LibcxxCompileFlags(), std::format("-nostdinc++ -isystem {}", (libcxx / "include/c++/v1").string())), "compiles use the explicit libc++ headers");
|
|
||||||
Check(Contains(tc.LibcxxLinkFlags(), std::format("-L{}", (libcxx / "lib").string())) && Contains(tc.LibcxxLinkFlags(), std::format("-Wl,-rpath,{}", (libcxx / "lib").string())), "links use the explicit libc++ library dir, with an rpath");
|
|
||||||
}
|
|
||||||
{
|
|
||||||
ToolchainInputs in;
|
|
||||||
in.env["CRAFTER_BUILD_WASI_SYSROOT"] = "/opt/wasi";
|
|
||||||
in.env["CRAFTER_MINGW_DIR"] = "/opt/mingw";
|
|
||||||
Toolchain tc = ResolveToolchain(in);
|
|
||||||
Check(tc.wasiSysroot == "/opt/wasi", "CRAFTER_BUILD_WASI_SYSROOT overrides the wasi sysroot");
|
|
||||||
Check(tc.mingwSysroot == "/opt/mingw", "CRAFTER_MINGW_DIR names the mingw prefix passed as --sysroot");
|
|
||||||
}
|
|
||||||
{
|
|
||||||
// Sandboxed, the host's /usr stands in for the missing mingw-w64,
|
|
||||||
// unless CRAFTER_MINGW_DIR says otherwise.
|
|
||||||
const fs::path host = fs::temp_directory_path() / "crafter-build-toolchain-host";
|
|
||||||
fs::remove_all(host);
|
|
||||||
ToolchainInputs in;
|
|
||||||
in.hostRoot = host;
|
|
||||||
Check(ResolveToolchain(in).mingwSysroot.empty(), "a host without mingw-w64 gives no --sysroot");
|
|
||||||
fs::create_directories(host / "usr" / "x86_64-w64-mingw32");
|
|
||||||
Check(ResolveToolchain(in).mingwSysroot == host / "usr", "a host with mingw-w64 gives its /usr as the mingw prefix");
|
|
||||||
in.env["CRAFTER_MINGW_DIR"] = "/opt/mingw";
|
|
||||||
Check(ResolveToolchain(in).mingwSysroot == "/opt/mingw", "CRAFTER_MINGW_DIR wins over the host's");
|
|
||||||
fs::remove_all(host);
|
|
||||||
}
|
|
||||||
{
|
|
||||||
// The cache key separates toolchains that would build different
|
|
||||||
// PCMs and nothing else.
|
|
||||||
Toolchain a = ResolveToolchain({});
|
|
||||||
a.clangVersion = "clang version 22.1.8";
|
|
||||||
Toolchain b = a;
|
|
||||||
b.clangVersion = "Arch Linux clang version 22.1.9";
|
|
||||||
Toolchain c = a;
|
|
||||||
c.libcxxDir = "/opt/libcxx";
|
|
||||||
Toolchain d = ResolveToolchain({});
|
|
||||||
d.clangVersion = a.clangVersion;
|
|
||||||
Check(a.Key().starts_with("22.1.8-"), std::format("the key starts with the short clang version (got '{}')", a.Key()));
|
|
||||||
Check(b.Key().starts_with("22.1.9-"), "the short version is parsed past a distro prefix");
|
|
||||||
Check(a.Key() != b.Key(), "a different clang version changes the key");
|
|
||||||
Check(a.Key() != c.Key(), "a different libc++ location changes the key");
|
|
||||||
Check(a.Key() == d.Key(), "the key is deterministic");
|
|
||||||
}
|
|
||||||
{
|
|
||||||
// The real toolchain on this machine resolves and the probes work;
|
|
||||||
// HostTarget is the probe's answer.
|
|
||||||
const Toolchain& real = GetToolchain();
|
|
||||||
Check(real.error.empty(), std::format("the real toolchain resolves (error: '{}')", real.error));
|
|
||||||
Check(!real.hostTriple.empty() && real.hostTriple == HostTarget(), "HostTarget comes from the toolchain probe");
|
|
||||||
Check(!real.clangVersion.empty(), "the clang version was probed");
|
|
||||||
Check(!real.Key().starts_with("unknown-"), std::format("the real clang version parses (got '{}')", real.Key()));
|
|
||||||
}
|
|
||||||
|
|
||||||
fs::remove_all(scratch, ec);
|
|
||||||
return Failures == 0 ? 0 : 1;
|
|
||||||
}
|
|
||||||
|
|
@ -133,33 +133,6 @@ int main() {
|
||||||
Check(a.BuildDir() != b.BuildDir(), "different VariantId yields different BuildDir");
|
Check(a.BuildDir() != b.BuildDir(), "different VariantId yields different BuildDir");
|
||||||
Check(a.BinDir() != b.BinDir(), "different VariantId yields different BinDir");
|
Check(a.BinDir() != b.BinDir(), "different VariantId yields different BinDir");
|
||||||
}
|
}
|
||||||
{
|
|
||||||
// The compiler is compile state as much as any flag: a PCM can only
|
|
||||||
// be read by the clang release that wrote it, and nothing in a
|
|
||||||
// project's sources changes when the compiler does. With the
|
|
||||||
// toolchain left out of the id, a project built with clang 22 kept
|
|
||||||
// its PCMs after crafter-build moved to clang 23; the build after
|
|
||||||
// the update found nothing stale, and the first edit failed with
|
|
||||||
// "module file ... uses an older format that is no longer supported"
|
|
||||||
// until the project was cleaned by hand.
|
|
||||||
Configuration cfg = MakeBase();
|
|
||||||
Toolchain older;
|
|
||||||
older.clangxx = "/app/bin/clang++";
|
|
||||||
older.clangVersion = "clang version 22.1.8";
|
|
||||||
Toolchain newer = older;
|
|
||||||
newer.clangVersion = "clang version 23.1.2";
|
|
||||||
Check(cfg.VariantId(older) != cfg.VariantId(newer), "a compiler update at the same path perturbs VariantId");
|
|
||||||
Check(cfg.VariantId(older) == cfg.VariantId(older), "VariantId under one toolchain is deterministic");
|
|
||||||
|
|
||||||
// Two installs of one release are still two toolchains: their
|
|
||||||
// libc++ need not be the same build.
|
|
||||||
Toolchain elsewhere = older;
|
|
||||||
elsewhere.clangxx = "/usr/bin/clang++";
|
|
||||||
Check(cfg.VariantId(older) != cfg.VariantId(elsewhere), "the same release installed elsewhere perturbs VariantId");
|
|
||||||
|
|
||||||
Check(cfg.VariantId() == cfg.VariantId(GetToolchain()), "VariantId() is the variant under the running toolchain");
|
|
||||||
Check(cfg.BuildDir().filename() == cfg.VariantId(GetToolchain()), "BuildDir is named for the running toolchain's variant");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Failures > 0) {
|
if (Failures > 0) {
|
||||||
std::println(std::cerr, "{} assertions failed", Failures);
|
std::println(std::cerr, "{} assertions failed", Failures);
|
||||||
|
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
|
||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
||||||
|
|
||||||
import std;
|
|
||||||
import Crafter.Build;
|
|
||||||
namespace fs = std::filesystem;
|
|
||||||
using namespace Crafter;
|
|
||||||
|
|
||||||
// `--version` has to work from any directory, before a project file is
|
|
||||||
// looked for, and must be answered rather than forwarded to project.cpp as
|
|
||||||
// one more unknown argument.
|
|
||||||
namespace {
|
|
||||||
std::int32_t Failures = 0;
|
|
||||||
|
|
||||||
void Check(bool cond, std::string_view msg) {
|
|
||||||
if (!cond) {
|
|
||||||
std::println(std::cerr, "FAIL: {}", msg);
|
|
||||||
++Failures;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::int32_t RunWith(std::vector<std::string> args) {
|
|
||||||
std::vector<char*> argv;
|
|
||||||
for (std::string& arg : args) argv.push_back(arg.data());
|
|
||||||
return Run(static_cast<std::int32_t>(argv.size()), argv.data());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int main() {
|
|
||||||
Check(!CrafterBuildVersion().empty(), "a version string is always baked in");
|
|
||||||
|
|
||||||
// An empty directory: no project.cpp to fall back to.
|
|
||||||
const fs::path scratch = fs::temp_directory_path() / "crafter-build-version-flag";
|
|
||||||
std::error_code ec;
|
|
||||||
fs::remove_all(scratch, ec);
|
|
||||||
fs::create_directories(scratch);
|
|
||||||
fs::current_path(scratch);
|
|
||||||
|
|
||||||
Check(RunWith({"crafter-build", "--version"}) == 0, "--version exits 0 without a project file");
|
|
||||||
Check(RunWith({"crafter-build", "--version", "--some-project-flag"}) == 0, "--version is answered before unknown arguments are forwarded");
|
|
||||||
Check(RunWith({"crafter-build", "--some-project-flag", "--version"}) == 0, "--version is answered wherever it appears");
|
|
||||||
|
|
||||||
fs::current_path(fs::temp_directory_path());
|
|
||||||
fs::remove_all(scratch, ec);
|
|
||||||
return Failures == 0 ? 0 : 1;
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
|
||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
||||||
|
|
||||||
import std;
|
|
||||||
|
|
||||||
int main() {
|
|
||||||
std::println("hello from wasm");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
|
||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
||||||
|
|
||||||
import std;
|
|
||||||
import Crafter.Build;
|
|
||||||
namespace fs = std::filesystem;
|
|
||||||
using namespace Crafter;
|
|
||||||
|
|
||||||
// A wasm build end to end: the fixture is compiled for wasm32-wasip1 against
|
|
||||||
// the resolved toolchain's WASI sysroot and the result is run under
|
|
||||||
// wasmtime. The other wasm tests only inspect generated files; this is the
|
|
||||||
// one that proves the sysroot, the std module for wasm and wasm-ld all work
|
|
||||||
// together. It skips when the sysroot or wasmtime is missing, so a
|
|
||||||
// development box without them is not a failure. Inside the Flatpak both are
|
|
||||||
// bundled, and there the skip would be the failure to look for.
|
|
||||||
int main() {
|
|
||||||
const Toolchain& tc = GetToolchain();
|
|
||||||
if (!fs::exists(tc.wasiSysroot / "include")) {
|
|
||||||
std::println(std::cerr, "SKIP: no WASI sysroot at {}", tc.wasiSysroot.string());
|
|
||||||
return 77;
|
|
||||||
}
|
|
||||||
if (std::system("which wasmtime > /dev/null 2>&1") != 0) {
|
|
||||||
std::println(std::cerr, "SKIP: wasmtime not on PATH");
|
|
||||||
return 77;
|
|
||||||
}
|
|
||||||
|
|
||||||
Configuration cfg;
|
|
||||||
cfg.path = fs::current_path() / "tests" / "WasmHelloRuns" / "fixture";
|
|
||||||
cfg.name = "wasm-hello";
|
|
||||||
cfg.outputName = "wasm-hello";
|
|
||||||
cfg.target = "wasm32-wasip1";
|
|
||||||
cfg.type = ConfigurationType::Executable;
|
|
||||||
std::array<fs::path, 0> ifaces = {};
|
|
||||||
std::array<fs::path, 1> impls = { "main" };
|
|
||||||
cfg.GetInterfacesAndImplementations(ifaces, impls);
|
|
||||||
|
|
||||||
std::unordered_map<fs::path, std::shared_future<BuildResult>> depResults;
|
|
||||||
std::mutex depMutex;
|
|
||||||
BuildResult r = Build(cfg, depResults, depMutex);
|
|
||||||
if (!r.result.empty()) {
|
|
||||||
std::println(std::cerr, "build failed: {}", r.result);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fs::path wasm = ExpectedOutput(cfg);
|
|
||||||
if (!fs::exists(wasm)) {
|
|
||||||
std::println(std::cerr, "no .wasm produced at {}", wasm.string());
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto run = RunCommandWithTimeout(std::format("wasmtime {}", wasm.string()), std::chrono::seconds(60));
|
|
||||||
if (run.exitCode != 0 || !run.output.contains("hello from wasm")) {
|
|
||||||
std::println(std::cerr, "wasmtime run failed (exit {}): {}", run.exitCode, run.output);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue