Cockatrice/.ci/docker.sh
Lukas Brübach 6f3297a4a0 [CI] Size compiler caches for one configuration and split the Fedora leg
The bounds in #7667 were set below what one build configuration writes, so
ccache filled to its limit and evicted the entries the next build was about to
ask for. The probe measured what that costs: a 6G cache on one leg served 1059
of 1059 calls and the 700M bound on the same leg served 14%. The quota also
went to legs that finish under the Windows leg, which has no compiler cache.

Debug information is most of an object's size. A measured Arch Debug
configuration is 1.2 GB with the built-in flags and 660 MB at -g1; Debian 13
Release is 1.1 GB and 614 MB. CI therefore builds every non-tag leg at level 1
through a new COCKATRICE_DEBUG_INFO_LEVEL option, which stays empty by default
so local, distro and release builds are unchanged. Level 1 keeps file and line,
which is what a failing test or a sanitizer backtrace reports. Tags and the
nightly legs keep full DWARF for the Sentry symbols.

With one configuration at half the size, the quota goes to fewer legs at a size
that holds one whole configuration, and the legs that duplicate another leg's
coverage give their cache up: Servatrice, Ubuntu 26.04 behind the sanitize leg
of the same distro, macOS 15 and macOS 26 Debug behind macOS 14. Their keys
leave LIVE_CCACHE_KEYS, so prune-caches reclaims the existing entries.

Fedora 44 built Debug for the suite and Release for the package in one job, so
the package compile queued behind the debug one and the leg took 30.8 min - the
only leg above Windows - while one 1.6 GB cache held two configurations. It is
split into two legs with one cache each, running in parallel.

The option reaches compile.sh inside the container through docker.sh, and
compile.sh turns it into the CMake flag.

Bounds: Arch 850M, Debian 13 800M, Fedora 44 debug 850M and release 800M,
Ubuntu 26.04 sanitize 1200M, macOS 14 800M. That is 5.3 GB of compiler cache
on top of roughly 4.5 GB of buildkit, vcpkg, Qt and CodeQL entries.

Windows still sets the pull-request wall clock: 29.5 min, 23.7 of it compiling
with no compiler cache, because ccache and sccache cannot store the 711 of 1054
translation units that use PCH under MSVC (#7381). That is a separate trial.
2026-11-04 13:52:05 +01:00

205 lines
6.7 KiB
Bash

#!/bin/bash
# This script is to be used by the ci environment from the project root directory, do not use it from somewhere else.
# Creates or loads docker images to use in compilation, creates RUN function to start compilation on the docker image.
#
# usage: source <script> <name> [--get] [--build] [--save] [--interactive] [--set-cache <location>]
# <name> sets the name of the docker image, these correspond to directories in .ci
# --get loads the image from a previously saved image cache, will build if no image is found
# --build builds the image from the Dockerfile in .ci/$NAME
# --save stores the image, if an image was loaded it will not be stored
# --interactive immediately starts the image interactively for debugging
# --set-cache <location> sets the location to cache the image or for ccache
#
# requires: docker
# uses env: NAME IMAGE_SUFFIX CACHE BUILD GET SAVE INTERACTIVE
# (correspond to args: <name> --set-cache <cache> --build --get --save --interactive)
# sets env: RUN CCACHE_DIR IMAGE_NAME RUN_ARGS RUN_OPTS BUILD_SCRIPT
# exitcode: 1 for failure, 2 for missing dockerfile, 3 for invalid arguments
#
# IMAGE_SUFFIX is optional and only appended to the image name, never to NAME, so that a matrix
# leg can have its own image and image cache without needing its own directory under .ci. Two legs
# building and tagging the same image concurrently would race on `docker build --tag`.
# It is deliberately not called SUFFIX: compile.sh already uses SUFFIX for output file names, and
# a job-level SUFFIX would be silently overridden by those per-step env blocks.
#
# exported RUN function will run the BUILD_SCRIPT inside of the docker container.
# note that the docker container will not inherit any environment variables!
#
# usage: RUN [arguments for build script]
# roughly equivalent to `docker run $IMAGE_NAME bash $BUILD_SCRIPT $@`
# uses env: CCACHE_DIR IMAGE_NAME RUN_ARGS RUN_OPTS BUILD_SCRIPT
# exitcode: 3 for invalid arguments, returns the returncode of docker run
export BUILD_SCRIPT=".ci/compile.sh"
project_name="cockatrice"
save_extension=".tar.gz"
image_cache="image"
ccache_cache=".ccache"
# Read arguments
while [[ $# != 0 ]]; do
case "$1" in
'--build')
BUILD=1
shift
;;
'--get')
GET=1
shift
;;
'--interactive')
INTERACTIVE=1
shift
;;
'--save')
SAVE=1
shift
;;
'--set-cache')
shift
if [[ $# == 0 ]]; then
echo "--set-cache expects an argument" >&2
exit 3
fi
CACHE=$1
shift
if ! [[ -d $CACHE ]]; then
echo "could not find cache path: $CACHE" >&2
return 3
fi
;;
*)
if [[ ${1:0:1} == - ]]; then
echo "unrecognized option: $1"
return 3
fi
NAME="$1"
shift
;;
esac
done
# Setup
if ! [[ $NAME ]]; then
echo "no build name given" >&2
return 3
fi
export IMAGE_NAME="${project_name,,}_${NAME,,}${IMAGE_SUFFIX:+_${IMAGE_SUFFIX,,}}" # lower case
docker_dir=".ci/$NAME"
if ! [[ -r $docker_dir/Dockerfile ]]; then
echo "could not find Dockerfile in $docker_dir" >&2
return 2 # even if the image is cached, we do not want to run if there is no way to build this image
fi
if ! [[ $CACHE ]]; then
echo "cache dir is not set!" >&2
CACHE="$(mktemp -d)"
echo "set cache dir to $CACHE" >&2
fi
if ! [[ -d $CACHE ]]; then
echo "could not find cache dir: $CACHE" >&2
mkdir -p "$CACHE"
unset GET # the dir is empty
fi
if [[ $GET || $SAVE ]]; then
img_dir="$CACHE/$image_cache"
img_save="$img_dir/$IMAGE_NAME$save_extension"
if ! [[ -d $img_dir ]]; then
echo "could not find image dir: $img_dir" >&2
mkdir -p "$img_dir"
fi
fi
export CCACHE_DIR="$CACHE/$ccache_cache"
if ! [[ -d $CCACHE_DIR ]]; then
echo "could not find ccache dir: $CCACHE_DIR" >&2
mkdir -p "$CCACHE_DIR"
fi
# Get the docker image from previously stored save
if [[ $GET ]]; then
if [[ $img_save ]] && docker load --input "$img_save"; then
echo "loaded image"
docker images
unset BUILD # do not overwrite the loaded image with build
unset SAVE # do not overwrite the stored image with the same image
if [[ $(find "$CCACHE_DIR" -type f -print -quit) ]]; then # check contents of ccache
echo "setting ccache to readonly"
export RUN_ARGS="$RUN_ARGS -e CCACHE_READONLY=1 -e CCACHE_NOSTATS=1" # do not overwrite ccache
else
echo "ccache is empty: $(find "$CCACHE_DIR")" >&2
fi
else
echo "could not load cached image, building instead" >&2
BUILD=1
fi
fi
# Build the docker image from dockerfile
if [[ $BUILD ]]; then
if docker build --tag "$IMAGE_NAME" "$docker_dir"; then
echo "built image"
docker images
else
echo "could not build image $IMAGE_NAME" >&2
return 1
fi
fi
# Save docker image to cache (compressed)
if [[ $SAVE ]]; then
if [[ $img_save ]] && docker save --output "$img_save" "$IMAGE_NAME"; then
echo "saved image to: $img_save"
else
echo "could not save image $IMAGE_NAME" >&2
fi
fi
# Set compile function, runs the compile script on the image, passes arguments to the script
# shellcheck disable=2120
function RUN ()
{
echo "running image:"
if [[ $(docker images) =~ $IMAGE_NAME ]]; then
local args=(--mount "type=bind,source=$PWD,target=/src")
args+=(--workdir "/src")
args+=(--user "$(id -u):$(id -g)")
if [[ $CCACHE_DIR ]]; then
args+=(--mount "type=bind,source=$CCACHE_DIR,target=/.ccache")
args+=(--env "CCACHE_DIR=/.ccache")
fi
if [[ $GITHUB_OUTPUT ]]; then
args+=(--mount "type=bind,source=$GITHUB_OUTPUT,target=/gh_output")
args+=(--env "GITHUB_OUTPUT=/gh_output")
fi
# The container does not inherit host env vars: without this, WITH_CRASH_REPORTING=ON set in
# the workflow never reaches compile.sh inside the container, so the Linux packages silently
# build with crash reporting OFF while the libcurl packages in the Dockerfiles go unused.
if [[ ${WITH_CRASH_REPORTING:-} ]]; then
args+=(--env "WITH_CRASH_REPORTING=${WITH_CRASH_REPORTING}")
fi
# Same reason: the DWARF level reaches compile.sh only if it is handed to the container,
# and without it CI would cache objects twice the size its bounds were measured for.
if [[ ${COCKATRICE_DEBUG_INFO_LEVEL:-} ]]; then
args+=(--env "COCKATRICE_DEBUG_INFO_LEVEL=${COCKATRICE_DEBUG_INFO_LEVEL}")
fi
# shellcheck disable=2086
docker run "${args[@]}" $RUN_ARGS "$IMAGE_NAME" bash "$BUILD_SCRIPT" $RUN_OPTS "$@"
return $?
else
echo "could not find docker image: $IMAGE_NAME" >&2
return 3
fi
}
# for debugging, start the docker image interactively instead of building
# starts immediately, does not require sourcing or RUN
if [[ $INTERACTIVE ]]; then
export BUILD_SCRIPT="-i"
export RUN_ARGS="$RUN_ARGS -it"
# shellcheck disable=2119
RUN
fi