#!/usr/bin/env bash # # Run a CI job locally, in the container image the workflow uses. # # The GitHub jobs are the only place some build configurations get exercised, # and a failure there costs a push and a wait. This runs the same steps on the # same base image so a configuration can be checked before it is submitted. # # ci/run-local.sh # list the jobs # ci/run-local.sh linux-vulkan # one job # ci/run-local.sh linux # every job whose name starts with "linux" # ci/run-local.sh all # # Requires docker. Build trees live in build/ci-local- and are reused # between runs; pass --clean to start from nothing. set -euo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" IMAGE_UBUNTU="ubuntu:24.04" IMAGE_NIX="nixos/nix:latest" CLEAN=0 JOBS=() for arg in "$@"; do case "$arg" in --clean) CLEAN=1 ;; -*) echo "unknown option: $arg" >&2; exit 2 ;; *) JOBS+=("$arg") ;; esac done ALL_JOBS=(linux-cpu linux-vulkan nix-cpu nix-vulkan) # Docker state shared by every job. DEPS_IMAGE="audiocpp-ci-local:ubuntu-24.04" NIX_STORE_VOLUME="audiocpp-ci-local-nix-store" usage() { cat <... jobs: linux-cpu linux-build.yml, ENGINE_ENABLE_VULKAN=OFF linux-vulkan linux-build.yml, ENGINE_ENABLE_VULKAN=ON (compiles the Vulkan shaders) nix-cpu nix-build.yml, nix build .#cpu nix-vulkan nix-build.yml, nix build .#vulkan linux both linux jobs nix both nix jobs all everything EOF } if [ ${#JOBS[@]} -eq 0 ]; then usage exit 0 fi # Expand the group names. EXPANDED=() for job in "${JOBS[@]}"; do case "$job" in all) EXPANDED+=("${ALL_JOBS[@]}") ;; linux) EXPANDED+=(linux-cpu linux-vulkan) ;; nix) EXPANDED+=(nix-cpu nix-vulkan) ;; *) found=0 for known in "${ALL_JOBS[@]}"; do [ "$job" = "$known" ] && found=1 done if [ $found -eq 0 ]; then echo "unknown job: $job" >&2 usage >&2 exit 2 fi EXPANDED+=("$job") ;; esac done if ! command -v docker >/dev/null 2>&1; then echo "docker is required" >&2 exit 1 fi # Discard the shared docker state once, before any job runs: the dependency # image is cached by tag and would otherwise survive a change to the package # list, and the nix store must not be wiped between the two nix jobs. if [ $CLEAN -eq 1 ]; then docker image rm "$DEPS_IMAGE" >/dev/null 2>&1 || true docker volume rm "$NIX_STORE_VOLUME" >/dev/null 2>&1 || true fi # Run as the invoking user so the build tree is not left root-owned. docker_run() { local image="$1"; shift # linux-build.yml sets CC/CXX as job-level env, so they reach every step. # The Vulkan shader generator is an ExternalProject that configures itself # during the build step, and it needs them there too. docker run --rm \ --user "$(id -u):$(id -g)" \ --env HOME=/tmp \ --env CC=gcc-13 \ --env CXX=g++-13 \ --volume "$REPO_ROOT:/src" \ --workdir /src \ "$image" bash -euo pipefail -c "$*" } # The apt install needs root, so the dependencies are baked into a thin image # once rather than installed on every run. ubuntu_image_with_deps() { local tag="$DEPS_IMAGE" if ! docker image inspect "$tag" >/dev/null 2>&1; then echo "--- building $tag (one time)" >&2 # Context is stdin-only: the Dockerfile copies nothing, and the repo # root would ship the build trees to the daemon for no reason. docker build -q -t "$tag" - >/dev/null </dev/null || return 1 echo "--- nix build .#$package" # The container runs as root against a checkout owned by someone else, so # libgit2 refuses the flake input until the path is marked safe. docker run --rm \ --volume "$REPO_ROOT:/src" \ --volume "$store_volume:/nix" \ --workdir /src \ "$IMAGE_NIX" \ bash -euo pipefail -c " git config --global --add safe.directory /src nix --extra-experimental-features 'nix-command flakes' \ build '.#$package' --no-link --print-build-logs " } failed=() for job in "${EXPANDED[@]}"; do echo echo "================ $job" start=$SECONDS ok=1 case "$job" in linux-cpu) run_linux_job cpu OFF || ok=0 ;; linux-vulkan) run_linux_job vulkan ON || ok=0 ;; nix-cpu) run_nix_job cpu || ok=0 ;; nix-vulkan) run_nix_job vulkan || ok=0 ;; esac elapsed=$(( SECONDS - start )) if [ $ok -eq 1 ]; then echo "================ $job PASSED (${elapsed}s)" else echo "================ $job FAILED (${elapsed}s)" failed+=("$job") fi done echo if [ ${#failed[@]} -eq 0 ]; then echo "all jobs passed" else echo "failed: ${failed[*]}" exit 1 fi