sets the name of the build dir, default is "build"
# --cmake-generator sets CMAKE_GENERATOR as used by cmake
# --target-macos-version sets the min os version - only used for macOS builds
-# --profile writes an MSBuild performance summary and binary log, Windows only
-# uses env: BUILDTYPE MAKE_INSTALL MAKE_PACKAGE PACKAGE_TYPE PACKAGE_SUFFIX MAKE_SERVER MAKE_NO_CLIENT MAKE_TEST USE_CCACHE CCACHE_SIZE CCACHE_EVICTION_AGE BUILD_DIR CMAKE_GENERATOR TARGET_MACOS_VERSION MAKE_SANITIZE MAKE_COVERAGE VCPKG_TARGET_MACOS_VERSION BUILD_PARALLEL_LEVEL BUILD_PROFILE
-# (correspond to args: --debug/--release --install --package --suffix --server --test --sanitize --coverage --ccache --dir --profile)
-# boolean env flags are enabled with the value 1 (e.g. MAKE_SERVER=1)
+# uses env: BUILDTYPE MAKE_INSTALL MAKE_PACKAGE PACKAGE_TYPE PACKAGE_SUFFIX MAKE_SERVER MAKE_NO_CLIENT MAKE_TEST USE_CCACHE CCACHE_SIZE CCACHE_EVICTION_AGE BUILD_DIR CMAKE_GENERATOR TARGET_MACOS_VERSION MAKE_SANITIZE MAKE_COVERAGE
+# (correspond to args: --debug/--release --install --package --suffix --server --test --sanitize --coverage --ccache --dir )
# exitcode: 1 for failure, 3 for invalid arguments
# Read arguments
@@ -117,10 +115,6 @@ while [[ $# != 0 ]]; do
export CMAKE_GENERATOR=$1
shift
;;
- '--profile')
- BUILD_PROFILE=1
- shift
- ;;
'--target-macos-version')
shift
if [[ $# == 0 ]]; then
@@ -139,21 +133,17 @@ done
set -e
-# Schema version consistency (only relevant to the server)
-if [[ ${MAKE_SERVER:-} == "1" ]]; then
- ./servatrice/check_schema_version.sh
-fi
-
-# Use default values if unset
-if [[ -z $BUILDTYPE ]]; then
+# Setup
+./servatrice/check_schema_version.sh
+if [[ ! $BUILDTYPE ]]; then
BUILDTYPE=Release
fi
-if [[ -z $BUILD_DIR ]]; then
+if [[ ! $BUILD_DIR ]]; then
BUILD_DIR="build"
fi
mkdir -p "$BUILD_DIR"
-if [[ ${MAKE_COVERAGE:-} == "1" ]]; then
+if [[ $MAKE_COVERAGE ]]; then
# Resolved while the working directory is still the source root. $0 is relative to the
# directory the script was invoked from, so after the cd below it no longer resolves, and
# under `set -e` the failed subshell would abort the build rather than just yielding an empty
@@ -168,66 +158,49 @@ export CMAKE_POLICY_VERSION_MINIMUM=3.10
# Add cmake flags
flags=("-DCMAKE_BUILD_TYPE=$BUILDTYPE")
-if [[ ${MAKE_SERVER:-} == "1" ]]; then
+if [[ $MAKE_SERVER ]]; then
flags+=("-DWITH_SERVER=1")
fi
-if [[ ${MAKE_NO_CLIENT:-} == "1" ]]; then
+if [[ $MAKE_NO_CLIENT ]]; then
flags+=("-DWITH_CLIENT=0" "-DWITH_ORACLE=0")
fi
-if [[ ${MAKE_TEST:-} == "1" ]]; then
+if [[ $MAKE_TEST ]]; then
flags+=("-DTEST=1")
fi
-if [[ ${MAKE_SANITIZE:-} == "1" ]]; then
+if [[ $MAKE_SANITIZE ]]; then
flags+=("-DENABLE_SANITIZERS=ON")
else
- # Every leg states the flag explicitly rather than relying on the CMake
- # default, so a build directory reused from a sanitized configuration can
- # never leak libasan/libubsan into a shipped binary.
+ # The release-package step re-runs cmake in the same build dir, so an
+ # ENABLE_SANITIZERS=ON left over from the debug step would otherwise leak
+ # into the shipped binaries (libasan/libubsan Depends in the DEB).
flags+=("-DENABLE_SANITIZERS=OFF")
fi
-if [[ ${MAKE_COVERAGE:-} == "1" ]]; then
+if [[ $MAKE_COVERAGE ]]; then
flags+=("-DENABLE_COVERAGE=ON")
fi
-if [[ ${USE_CCACHE:-} == "1" ]]; then
+if [[ $USE_CCACHE ]]; then
flags+=("-DUSE_CCACHE=1")
- if [[ -n $CCACHE_SIZE ]]; then
- # This setting persists after running the script
+ # PCH-aware caching is required or ccache refuses to cache any TU that
+ # consumes a precompiled header, silently recompiling everything on every run.
+ ccache --set-config sloppiness=pch_defines,time_macros
+ if [[ $CCACHE_SIZE ]]; then
+ # note, this setting persists after running the script
ccache --max-size "$CCACHE_SIZE"
fi
fi
-if [[ -n $PACKAGE_TYPE ]]; then
+if [[ $PACKAGE_TYPE ]]; then
flags+=("-DCPACK_GENERATOR=$PACKAGE_TYPE")
fi
-if [[ ${USE_VCPKG:-} == "1" ]]; then
+if [[ $USE_VCPKG ]]; then
flags+=("-DUSE_VCPKG=1")
flags+=("-DVCPKG_INSTALL_OPTIONS=--x-abi-tools-use-exact-versions")
fi
-if [[ ${WITH_CRASH_REPORTING:-} == "ON" ]]; then
- # Release CI opts into the sentry-native SDK (default is OFF so from-source, distro and
- # offline package builds never fetch it). Debug builds stay SDK-free so crashes from
- # developer builds never reach Sentry.
- flags+=("-DWITH_CRASH_REPORTING=ON")
-fi
-if [[ ${COCKATRICE_DEBUG_INFO_LEVEL:-} ]]; then
- # CI lowers the DWARF level so one build configuration fits its compiler-cache bound;
- # see the option in CMakeLists.txt. Empty keeps the built-in flags.
- flags+=("-DCOCKATRICE_DEBUG_INFO_LEVEL=$COCKATRICE_DEBUG_INFO_LEVEL")
-fi
-if [[ ${BUILDTYPE:-Release} == "Release" && -n ${COCKATRICE_SENTRY_DSN:-} ]]; then
- # Release-only: Debug builds stay DSN-free so crashes from developer builds
- # never reach Sentry. Empty (local builds, forks without the secret, and non-tag
- # CI builds, which never receive the secret) keeps today's mock fallback via the
- # compiled-in default. CI passes the value from the SENTRY_DSN secret on tag
- # Release builds only, so pre-release test artifacts never report to Sentry.
- flags+=("-DCOCKATRICE_SENTRY_DSN=$COCKATRICE_SENTRY_DSN")
-fi
# Add cmake --build flags
buildflags=(--config "$BUILDTYPE")
function ccachestatsverbose() {
- ccache --version
- # Verbose only works on newer ccache, discard the error
+ # note, verbose only works on newer ccache, discard the error
local got
if got="$(ccache --show-stats --verbose 2>/dev/null)"; then
echo "$got"
@@ -236,22 +209,6 @@ function ccachestatsverbose() {
fi
}
-function cpuinfo() {
- # The number of cores decides how much build parallelism is useful; the hosted
- # runners are not all sized alike, so read it off the machine instead of assuming.
- case "$RUNNER_OS" in
- Windows)
- echo "cores: ${NUMBER_OF_PROCESSORS:-unknown}"
- ;;
- macOS)
- echo "cores: $(sysctl -n hw.ncpu)"
- ;;
- *)
- echo "cores: $(nproc 2>/dev/null || echo unknown)"
- ;;
- esac
-}
-
# Compile
if [[ $RUNNER_OS == macOS ]]; then
# QTDIR is needed for macOS since we actually only use the cached thin Qt binaries instead of the install-qt-action,
@@ -267,44 +224,36 @@ if [[ $RUNNER_OS == macOS ]]; then
# this works independent of the qt version as there should be only one version installed on the runner at a time
export QTDIR
- if [[ -n $TARGET_MACOS_VERSION ]]; then
- # CMAKE_OSX_DEPLOYMENT_TARGET is a vanilla CMake flag needed to compile to the target macOS version
+ if [[ $TARGET_MACOS_VERSION ]]; then
+ # CMAKE_OSX_DEPLOYMENT_TARGET is a vanilla cmake flag needed to compile to target macOS version
flags+=("-DCMAKE_OSX_DEPLOYMENT_TARGET=$TARGET_MACOS_VERSION")
- if [[ ${USE_VCPKG:-} == "1" ]]; then
- # vcpkg dependencies need a vcpkg triplet file to compile to the target macOS version
- # An easy way is to copy the x64-osx.cmake file and modify it
- triplets_dir="/tmp/cmake/triplets"
- triplet_version="custom-triplet"
- triplet_file="$triplets_dir/$triplet_version.cmake"
- arch=$(uname -m)
- if [[ $arch == x86_64 ]]; then
- arch="x64"
- fi
- mkdir -p "$triplets_dir"
- triplet_source="../vcpkg/triplets/$arch-osx.cmake"
- if [[ ! -f "$triplet_source" ]]; then
- triplet_source="../vcpkg/triplets/community/$arch-osx.cmake"
- fi
- cp "$triplet_source" "$triplet_file"
- # The triplet is what vcpkg hashes into the ABI hash of every port, so two legs that
- # agree here reuse each other's binary cache. Targets 14, 15 and 26 on the same Xcode
- # produce identical binaries when they share the lowest deployment target, so they all
- # build against 14 to keep one vcpkg cache scope instead of three.
- # Keep in sync with "vcpkg_scope" and "vcpkg_deployment_target" in desktop-build.yml.
- vcpkg_target="${VCPKG_TARGET_MACOS_VERSION:-$TARGET_MACOS_VERSION}"
- echo "set(VCPKG_CMAKE_SYSTEM_VERSION $vcpkg_target)" >>"$triplet_file"
- echo "set(VCPKG_OSX_DEPLOYMENT_TARGET $vcpkg_target)" >>"$triplet_file"
- flags+=("-DVCPKG_OVERLAY_TRIPLETS=$triplets_dir")
- flags+=("-DVCPKG_HOST_TRIPLET=$triplet_version")
- flags+=("-DVCPKG_TARGET_TRIPLET=$triplet_version")
- echo "::group::Generated triplet $triplet_file"
- cat "$triplet_file"
- echo "::endgroup::"
+ # vcpkg dependencies need a vcpkg triplet file to compile to the target macOS version
+ # an easy way is to copy the x64-osx.cmake file and modify it
+ triplets_dir="/tmp/cmake/triplets"
+ triplet_version="custom-triplet"
+ triplet_file="$triplets_dir/$triplet_version.cmake"
+ arch=$(uname -m)
+ if [[ $arch == x86_64 ]]; then
+ arch="x64"
fi
+ mkdir -p "$triplets_dir"
+ triplet_source="../vcpkg/triplets/$arch-osx.cmake"
+ if [[ ! -f "$triplet_source" ]]; then
+ triplet_source="../vcpkg/triplets/community/$arch-osx.cmake"
+ fi
+ cp "$triplet_source" "$triplet_file"
+ echo "set(VCPKG_CMAKE_SYSTEM_VERSION $TARGET_MACOS_VERSION)" >>"$triplet_file"
+ echo "set(VCPKG_OSX_DEPLOYMENT_TARGET $TARGET_MACOS_VERSION)" >>"$triplet_file"
+ flags+=("-DVCPKG_OVERLAY_TRIPLETS=$triplets_dir")
+ flags+=("-DVCPKG_HOST_TRIPLET=$triplet_version")
+ flags+=("-DVCPKG_TARGET_TRIPLET=$triplet_version")
+ echo "::group::Generated triplet $triplet_file"
+ cat "$triplet_file"
+ echo "::endgroup::"
fi
- echo "::group::Setup signing certificate"
+ echo "::group::Signing Certificate"
if [[ -n "$MACOS_CERTIFICATE_NAME" ]]; then
echo "$MACOS_CERTIFICATE" | base64 --decode >"certificate.p12"
security create-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain
@@ -319,177 +268,57 @@ if [[ $RUNNER_OS == macOS ]]; then
fi
echo "::endgroup::"
- if [[ ${MAKE_PACKAGE:-} == "1" ]]; then
- # Workaround https://github.com/actions/runner-images/issues/7522: XProtect holds a
- # freshly written DMG for a moment ("Resource busy"), and a mount that outlived a
- # failed detach stays busy for the rest of the job. Retry every hdiutil call through
- # a wrapper that widens the delay so the scan can finish, retries a detach with
- # -force, and kills a wedged DiskImages helper instead of letting it hold the job
- # until the 100 minute timeout.
- # The wrapper has to exit non-zero once it gives up: CPack reads that status to
- # decide whether the disk image was created, so a zero exit turns a failed hdiutil
- # into a much later, misleading "Problem copying the package" error that hides
- # hdiutil's own message.
+ if [[ $MAKE_PACKAGE ]]; then
+ # Workaround https://github.com/actions/runner-images/issues/7522
+ # have hdiutil repeat the command 10 times in hope of success
hdiutil_script="/tmp/hdiutil.sh"
# shellcheck disable=SC2016
echo '#!/bin/bash
- attempts=10
- timeout_seconds=${HDIUTIL_TIMEOUT:-300}
- subcommand=$1
- # CPACK_DMG_VOLUME_NAME in CMakeLists.txt; a -volname argument wins when there is one.
- volume_name=Cockatrice
- arg_count=$#
- for (( i = 1; i < arg_count; i++ )); do
- if [[ ${!i} == -volname ]] && (( i + 1 <= arg_count )); then
- (( i++ ))
- volume_name=${!i}
+ i=0
+ while ! hdiutil "$@"; do
+ if (( ++i >= 10 )); then
+ echo "Error: hdiutil failed $i times!" >&2
+ break
fi
- done
-
- # macOS ships no timeout(1), so a watchdog kills the supervised call. It writes to
- # /dev/null because a process holding the output pipe open would make CPack wait for
- # the watchdog instead of for hdiutil; it is killed once the call returns.
- run_supervised() {
- "$@" &
- pid=$!
- ( sleep "$timeout_seconds" && kill -9 "$pid" ) >/dev/null 2>&1 &
- watchdog=$!
- wait "$pid"
- rc=$?
- kill "$watchdog" 2>/dev/null
- wait "$watchdog" 2>/dev/null
- return "$rc"
- }
-
- force_detach_stale_mount() {
- [[ -d /Volumes/$volume_name ]] || return 0
- echo "hdiutil $subcommand: force-detaching stale /Volumes/$volume_name" >&2
- run_supervised hdiutil detach -force "/Volumes/$volume_name" >/dev/null 2>&1
- return 0
- }
-
- timeouts=0
- attempt=0
- delay=1
- while (( attempt < attempts )); do
- (( attempt++ ))
- cmd=(hdiutil "$@")
- # A busy volume is what a detach fails on, and -force is what clears it.
- if (( attempt > 1 )) && [[ $subcommand == detach ]]; then
- cmd=(hdiutil detach -force "${@:2}")
- fi
- run_supervised "${cmd[@]}"
- rc=$?
- (( rc == 0 )) && exit 0
- if (( rc == 137 )); then
- (( timeouts++ ))
- echo "hdiutil $subcommand attempt $attempt/$attempts timed out after ${timeout_seconds}s" >&2
- # A wedged DiskImages helper is not the busy-file flake: two kills are all the
- # evidence the loop needs before failing is a better use of the job budget.
- if (( timeouts >= 2 )); then
- echo "::error::hdiutil $subcommand timed out $timeouts times, giving up" >&2
- exit "$rc"
- fi
- else
- echo "hdiutil $subcommand attempt $attempt/$attempts failed (status $rc), retrying" >&2
- fi
- (( attempt < attempts )) || break
- if [[ $subcommand == create || $subcommand == attach ]]; then
- force_detach_stale_mount
- fi
- sleep "$delay"
- (( delay = delay * 2 > 30 ? 30 : delay * 2 ))
- done
- echo "::error::hdiutil $subcommand failed $attempt times (last status $rc), giving up" >&2
- exit "$rc"' >"$hdiutil_script"
+ sleep 1
+ done' >"$hdiutil_script"
chmod +x "$hdiutil_script"
flags+=(-DCPACK_COMMAND_HDIUTIL="$hdiutil_script")
fi
elif [[ $RUNNER_OS == Windows ]]; then
- if [[ $CMAKE_GENERATOR == "Visual Studio"* ]]; then
- # cmake only hands MSBuild a job count when one is asked for, and without it
- # MSBuild builds a single node: one project at a time, whatever MTT does inside
- # it. Ask for a node per core.
- if [[ -z $BUILD_PARALLEL_LEVEL ]]; then
- BUILD_PARALLEL_LEVEL="${NUMBER_OF_PROCESSORS:-1}"
- fi
- buildflags+=(--parallel "$BUILD_PARALLEL_LEVEL")
-
- # Enable MTT, see https://devblogs.microsoft.com/cppblog/improved-parallelism-in-msbuild/
- # and https://devblogs.microsoft.com/cppblog/cpp-build-throughput-investigation-and-tune-up/#multitooltask-mtt
- # EnforceProcessCountAcrossBuilds makes the CL_MPCount ceiling apply across all
- # the projects the nodes are building, instead of each node running its own
- # cl.exe processes and /m and MTT multiplying into an oversubscribed machine.
- buildflags+=(-- -p:UseMultiToolTask=true -p:EnableClServerMode=true -p:EnforceProcessCountAcrossBuilds=true -p:CL_MPCount="$BUILD_PARALLEL_LEVEL")
- if [[ $BUILD_PROFILE ]]; then
- # A performance summary says which projects and tasks cost what, the binary log
- # keeps the full timeline; both are far too noisy to always produce. The files
- # land next to the solution, i.e. in the build dir the workflow uploads. These
- # join the MTT options above, which already carried the "--" separator.
- buildflags+=("-flp:PerformanceSummary;v=q;LogFile=msbuild-perf.log" -bl:msbuild.binlog)
- fi
- else
- # Ninja runs the compiler itself, so it neither takes the MSBuild switches
- # (it rejects -p: options outright) nor a job count: it sizes its own pool
- # for the cores it finds.
- echo "ninja $(ninja --version)"
- fi
+ # Enable MTT, see https://devblogs.microsoft.com/cppblog/improved-parallelism-in-msbuild/
+ # and https://devblogs.microsoft.com/cppblog/cpp-build-throughput-investigation-and-tune-up/#multitooltask-mtt
+ buildflags+=(-- -p:UseMultiToolTask=true -p:EnableClServerMode=true)
fi
-if [[ ${USE_CCACHE:-} == "1" ]]; then
+if [[ $USE_CCACHE ]]; then
echo "::group::Show ccache stats"
ccachestatsverbose
echo "::endgroup::"
fi
-# Configure CMake
-echo "::group::Generate build system"
+echo "::group::Configure cmake"
cmake --version
-echo "Running CMake configuration with the following flags: ${flags[*]}"
+echo "Running cmake with flags: ${flags[*]}"
cmake .. "${flags[@]}"
echo "::endgroup::"
-if [[ ${USE_CCACHE:-} == "1" ]]; then
- # PCH-aware caching is required or ccache refuses to cache any TU that consumes a
- # precompiled header, silently recompiling everything on every run. The mtime and ctime
- # entries cover files created during the build itself: the moc/uic/rcc output and the
- # protobuf headers are generated after ccache starts, so without them every TU including
- # one is read as a race and never cached. Checked-out sources are older than the compile
- # that follows them and do not trip the check.
- #
- # This has to run after configure: CMakeLists.txt sets sloppiness=pch_defines,time_macros
- # at configure time, and --set-config replaces the whole list rather than extending it, so
- # setting it earlier would leave the build below with two entries. Re-asserting all four
- # here is what actually reaches the compiler; the order is the point.
- ccache --set-config sloppiness=pch_defines,time_macros,include_file_mtime,include_file_ctime
-fi
-
-# Build
echo "::group::Build project"
-cpuinfo
-echo "Running CMake with the following build flags: ${buildflags[*]}"
+echo "Running cmake --build with flags: ${buildflags[*]}"
cmake --build . "${buildflags[@]}"
echo "::endgroup::"
-if [[ $BUILD_PROFILE && $RUNNER_OS == Windows && -f msbuild-perf.log ]]; then
- # Repeat the summary in the job log so the numbers are readable without
- # downloading the binary log, which is hundreds of megabytes.
- echo "::group::MSBuild performance summary"
- cat msbuild-perf.log
- echo "::endgroup::"
-fi
-
-if [[ ${USE_CCACHE:-} == "1" ]]; then
- if [[ -n $CCACHE_EVICTION_AGE ]]; then
- echo "::group::Evict ccache files older than $CCACHE_EVICTION_AGE"
+if [[ $USE_CCACHE ]]; then
+ if [[ $CCACHE_EVICTION_AGE ]]; then
+ echo "::group::evict ccache files older than $CCACHE_EVICTION_AGE"
ccache --evict-older-than "$CCACHE_EVICTION_AGE"
echo "::endgroup::"
fi
echo "::group::Show ccache stats again"
ccachestatsverbose
echo "::endgroup::"
-elif [[ -n $CCACHE_EVICTION_AGE ]]; then
+elif [[ $CCACHE_EVICTION_AGE ]]; then
echo "::error file=$0::ccache eviction is enabled while ccache is disabled!"
fi
@@ -506,9 +335,9 @@ if [[ $RUNNER_OS == macOS ]]; then
echo "::endgroup::"
fi
-if [[ ${MAKE_TEST:-} == "1" ]]; then
+if [[ $MAKE_TEST ]]; then
echo "::group::Run tests"
- if [[ ${MAKE_SANITIZE:-} == "1" ]]; then
+ if [[ $MAKE_SANITIZE ]]; then
# detect_leaks=0: Qt and the sanitizers' own runtimes both leak at exit by design, and
# LeakSanitizer's default is aggressive enough to bury our real findings under hundreds
# of process-teardown reports. Malloc/free misuse and use-after-free are what ASan is for
@@ -526,12 +355,11 @@ if [[ ${MAKE_TEST:-} == "1" ]]; then
# bound and -j recovers nearly all of the per-process overhead. The test binaries are safe to
# run concurrently: the only one that claims a fixed resource is single_instance_manager_test,
# which already scopes its local socket name to its own pid so parallel copies cannot collide.
- ctest --version
ctest -C "$BUILDTYPE" --output-on-failure -j "${CTEST_JOBS:-4}"
echo "::endgroup::"
fi
-if [[ ${MAKE_COVERAGE:-} == "1" ]]; then
+if [[ $MAKE_COVERAGE ]]; then
# gcovr is not a build dependency and is not available everywhere, so a missing one is reported
# and skipped rather than failing an otherwise green build.
echo "::group::Coverage report"
@@ -556,221 +384,18 @@ if [[ ${MAKE_COVERAGE:-} == "1" ]]; then
echo "::endgroup::"
fi
-if [[ $RUNNER_OS == Windows && ${MAKE_NO_CLIENT:-} != "1" ]]; then
- echo "::group::Probe the installer's holder lookup"
- # The working directory is still the build dir here: the package steps below are what
- # leave and re-enter it, and that is what made a $BUILD_DIR-prefixed path name
- # build/build. Both paths are relative to the dir the probe was built into.
- probe="./installer-holder-probe.exe"
- if [[ ! -f $probe ]]; then
- echo "::error file=$0::installer-holder-probe.exe was not built"
- exit 1
- fi
- # The probe exists to name a holder that a process-and-module scan cannot see: one
- # that has a file open without having loaded it. Holding a temporary DLL with
- # FileShare.None is exactly that case, so this asserts the reason the probe was
- # added instead of only that the Restart Manager answers at all. It needs no NSIS
- # and no packaging, so it runs on every Windows build that gets this far.
- # The holder is a process of its own: the probe never names the process that started it,
- # which is what keeps an in-place uninstaller from being named as a holder of the
- # directory it is emptying. The same DLL held by the script that starts the probe
- # therefore has to come back as nothing, and that is the second case below.
- probe_check="./holder-probe-check.ps1"
- cat >"$probe_check" <<'POWERSHELL'
-$ErrorActionPreference = "Stop"
-$dir = Join-Path ([IO.Path]::GetTempPath()) "cockatrice-holder-probe-$PID"
-Remove-Item -Recurse -Force $dir -ErrorAction SilentlyContinue
-New-Item -ItemType Directory -Force $dir | Out-Null
-$probe = (Resolve-Path -LiteralPath $env:PROBE_EXE).Path
-
-function Invoke-Probe([string] $guardDir) {
- $start = New-Object System.Diagnostics.ProcessStartInfo
- $start.FileName = $probe
- $start.UseShellExecute = $false
- $start.RedirectStandardOutput = $true
- $start.EnvironmentVariables["COCKATRICE_GUARD_DIR"] = $guardDir
- $process = [System.Diagnostics.Process]::Start($start)
- $read = $process.StandardOutput.ReadToEndAsync()
- if (-not $process.WaitForExit(20000)) {
- try { $process.Kill() } catch { }
- throw "the holder probe did not finish within 20 s for $guardDir"
- }
- $output = $read.Result
- $code = $process.ExitCode
- $process.Dispose()
- return @{ Code = $code; Output = $output }
-}
-
-function Wait-UntilLocked([string] $path) {
- $deadline = (Get-Date).AddSeconds(15)
- while ((Get-Date) -lt $deadline) {
- try {
- $open = [IO.File]::Open($path, "Open", "ReadWrite", "None")
- $open.Dispose()
- } catch {
- return
- }
- Start-Sleep -Milliseconds 100
- }
- throw "the holder never locked $path"
-}
-
-try {
- $file = Join-Path $dir "locked.dll"
- [IO.File]::WriteAllText($file, "x")
-
- # The holder is a process of its own, not this script: the probe never names the process
- # that started it (the case below asserts that), so a file held here would be left out on
- # purpose and the check would pass for the wrong reason.
- $holderScript = Join-Path $dir "hold.ps1"
- Set-Content -LiteralPath $holderScript -Value @'
-param([string] $Path)
-$handle = [IO.File]::Open($Path, "Open", "ReadWrite", "None")
-Start-Sleep -Seconds 120
-'@
- $powershell = Join-Path $env:SystemRoot "System32\WindowsPowerShell\v1.0\powershell.exe"
- $holder = Start-Process -FilePath $powershell -PassThru -ArgumentList @(
- "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "`"$holderScript`"", "-Path", "`"$file`"")
- try {
- Wait-UntilLocked $file
- $locked = Invoke-Probe $dir
- if ($locked.Code -ne 1) { throw "expected exit 1 for a locked file, got $($locked.Code) : $($locked.Output)" }
- if ($locked.Output -notmatch "pid\s+$($holder.Id)\b") {
- throw "the lookup did not name the process holding the file: $($locked.Output)"
- }
- } finally {
- try { $holder.Kill() } catch { }
- try { $holder.WaitForExit(5000) | Out-Null } catch { }
- }
-
- # The probe is started by the installer, or by an uninstaller that runs in place from the
- # directory it is emptying, so the asking process holds files there itself. Naming it would
- # ask the user to close the program that is asking - a question neither the close nor the
- # end offer can answer, so the guard repeated it until the update was cancelled. A holder
- # that is only the asking process has to come back as nothing.
- $handle = [IO.File]::Open($file, "Open", "ReadWrite", "None")
- try {
- $self = Invoke-Probe $dir
- } finally {
- $handle.Dispose()
- }
- if ($self.Code -ne 0) { throw "expected exit 0 when the asking process is the only holder, got $($self.Code) : $($self.Output)" }
- if ($self.Output -match "pid\s+$PID\b") { throw "the lookup named the process that started it: $($self.Output)" }
-
- # A payload executable that refuses to be replaced is running, whether or not this probe
- # may name it: the Restart Manager does not report every mapping, and the image path of a
- # process under another account cannot be read from here, so both process lookups can
- # come back empty while the program is still there. Held here by the asking process, so
- # that it cannot be named - the answer has to be "could not name it", never "nothing is
- # holding it", which is what sent an update into an uninstall that could only fail
- # halfway. Exit 3 is the code the guard answers by asking the user to close it and retry.
- $image = Join-Path $dir "oracle.exe"
- [IO.File]::WriteAllText($image, "x")
- $handle = [IO.File]::Open($image, "Open", "ReadWrite", "None")
- try {
- $unnameable = Invoke-Probe $dir
- } finally {
- $handle.Dispose()
- }
- if ($unnameable.Code -ne 3) {
- throw "expected exit 3 for a payload executable that is in use but cannot be named, got $($unnameable.Code) : $($unnameable.Output)"
- }
- if ($unnameable.Output -notmatch "oracle\.exe is in use") {
- throw "the answer did not name the executable it could not replace: $($unnameable.Output)"
- }
-
- $empty = Join-Path $dir "empty"
- New-Item -ItemType Directory -Force $empty | Out-Null
- $nothing = Invoke-Probe $empty
- if ($nothing.Code -ne 0) { throw "expected exit 0 for a directory with nothing open, got $($nothing.Code) : $($nothing.Output)" }
- # A process running from the directory is a holder even when it has no file open: an
- # instance holds its payload as mapped images, not as handles, and that is the state
- # the guard used to miss - it passed, the purge failed, and the install ended in a
- # warning with no offer to close and try again. The image is a copy of a native
- # Windows program, so it starts without its usual directory next to it, and it carries
- # one of the payload's own names because that is what the lookup names.
- $copy = Join-Path $dir "cockatrice.exe"
- Copy-Item -LiteralPath (Join-Path $env:SystemRoot "System32\cmd.exe") -Destination $copy
- $running = Start-Process -FilePath $copy -ArgumentList "/c", "ping -n 20 127.0.0.1 >nul" -PassThru
- try {
- Start-Sleep -Milliseconds 1500
- $byImage = Invoke-Probe $dir
- } finally {
- try { $running.Kill() } catch { }
- # Wait for the image to be released, or the cleanup below leaves the copy behind.
- try { $running.WaitForExit(5000) | Out-Null } catch { }
- }
- if ($byImage.Code -ne 1) { throw "expected exit 1 for a process running from the directory, got $($byImage.Code) : $($byImage.Output)" }
- if ($byImage.Output -notmatch "cockatrice\.exe \(pid\s+$($running.Id)") {
- throw "the lookup did not name the process running from the directory: $($byImage.Output)"
- }
-
- # A program in a folder below the directory is not an instance of the installation, even
- # when it carries a payload program's name: the install directory can be a folder the
- # user keeps other applications in, each in its own subfolder, and naming one of those
- # asked for a close the probe never sends and an end it refuses to perform, so the guard
- # could only abort the update. A program that really does hold a payload file is still
- # named, by the Restart Manager lookup exercised above.
- $nested = Join-Path $dir "other"
- New-Item -ItemType Directory -Force $nested | Out-Null
- $nestedImage = Join-Path $nested "cockatrice.exe"
- Copy-Item -LiteralPath (Join-Path $env:SystemRoot "System32\cmd.exe") -Destination $nestedImage
- $nestedRunning = Start-Process -FilePath $nestedImage -ArgumentList "/c", "ping -n 20 127.0.0.1 >nul" -PassThru
- try {
- Start-Sleep -Milliseconds 1500
- $inSubfolder = Invoke-Probe $dir
- } finally {
- try { $nestedRunning.Kill() } catch { }
- try { $nestedRunning.WaitForExit(5000) | Out-Null } catch { }
- }
- if ($inSubfolder.Code -ne 0) {
- throw "expected exit 0 for a program in a folder below the directory, got $($inSubfolder.Code) : $($inSubfolder.Output)"
- }
-
- # A program the user keeps in the directory itself is not a payload program either: the
- # guard ends only what the payload installs, and naming anything else asks for an answer
- # that cannot free a single file.
- $foreign = Join-Path $dir "tool.exe"
- Copy-Item -LiteralPath (Join-Path $env:SystemRoot "System32\cmd.exe") -Destination $foreign
- $foreignRunning = Start-Process -FilePath $foreign -ArgumentList "/c", "ping -n 20 127.0.0.1 >nul" -PassThru
- try {
- Start-Sleep -Milliseconds 1500
- $ignored = Invoke-Probe $dir
- } finally {
- try { $foreignRunning.Kill() } catch { }
- try { $foreignRunning.WaitForExit(5000) | Out-Null } catch { }
- }
- if ($ignored.Code -ne 0) {
- throw "expected exit 0 for a program the payload does not install, got $($ignored.Code) : $($ignored.Output)"
- }
- Write-Host "holder lookup named pid $($holder.Id) for a file another process holds, named nothing for a file only the asking process holds, reported a payload executable it could not name as in use instead of free, named nothing for an empty directory, named pid $($running.Id) for a payload program running from the directory, and named nothing for a payload program in a folder below it or for a program the payload does not install"
-} finally {
- Remove-Item -Recurse -Force $dir -ErrorAction SilentlyContinue
-}
-POWERSHELL
- if ! PROBE_EXE="$probe" powershell -NoProfile -File "$probe_check"; then
- rm -f "$probe_check"
- exit 1
- fi
- rm -f "$probe_check"
- echo "::endgroup::"
-fi
-
-# Install
-if [[ ${MAKE_INSTALL:-} == "1" ]]; then
+if [[ $MAKE_INSTALL ]]; then
echo "::group::Install"
cmake --build . --target install --config "$BUILDTYPE"
echo "::endgroup::"
fi
-# Package
-if [[ ${MAKE_PACKAGE:-} == "1" ]]; then
+if [[ $MAKE_PACKAGE ]]; then
echo "::group::Create package"
- cpack --version
cmake --build . --target package --config "$BUILDTYPE"
echo "::endgroup::"
- if [[ -n $PACKAGE_SUFFIX ]]; then
+ if [[ $PACKAGE_SUFFIX ]]; then
echo "::group::Update package name"
cd ..
BUILD_DIR="$BUILD_DIR" .ci/name_build.sh "$PACKAGE_SUFFIX"
diff --git a/.ci/docker.sh b/.ci/docker.sh
index c78ba3c31..412ad6a1d 100644
--- a/.ci/docker.sh
+++ b/.ci/docker.sh
@@ -175,17 +175,6 @@ function RUN ()
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 $?
diff --git a/.ci/release_template.md b/.ci/release_template.md
index 8eb4c7ebd..23b475150 100644
--- a/.ci/release_template.md
+++ b/.ci/release_template.md
@@ -21,8 +21,8 @@ Available pre-compiled binaries for installation:
• Ubuntu 24.04 LTSNoble Numbat
• Debian 13Trixie
• Debian 12Bookworm
- • Fedora 45
• Fedora 44
+ • Fedora 43General Linux support is available via a flatpak package hosted at Flathub (x86 & ARM)!Thanks to courtesy of @FFY00, the app is also available in Arch Linux's official extra repository.
diff --git a/.dockerignore b/.dockerignore
index fc07b2b9d..8e0b69332 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -2,6 +2,6 @@ build/
.github/
.tx/
cockatrice/
-docs/
+doc/
oracle/
Dockerfile
diff --git a/docs/CONTRIBUTING.md b/.github/CONTRIBUTING.md
similarity index 100%
rename from docs/CONTRIBUTING.md
rename to .github/CONTRIBUTING.md
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index 75494dc3e..75fbc59f1 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -60,10 +60,8 @@ jobs:
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
- ca-certificates \
cmake \
g++ \
- libcurl4-openssl-dev \
libgl-dev \
liblzma-dev \
libmariadb-dev-compat \
diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml
index b93d76b3d..098aabc4f 100644
--- a/.github/workflows/desktop-build.yml
+++ b/.github/workflows/desktop-build.yml
@@ -15,7 +15,7 @@ on:
- '!**.md'
- '!.github/**'
- '!.tx/**'
- - '!docs/**'
+ - '!doc/**'
- '.github/workflows/desktop-build.yml'
- 'CMakeLists.txt'
- 'vcpkg.json'
@@ -28,39 +28,17 @@ on:
- '!**.md'
- '!.github/**'
- '!.tx/**'
- - '!docs/**'
+ - '!doc/**'
- '.github/workflows/desktop-build.yml'
- 'CMakeLists.txt'
- 'vcpkg.json'
- 'vcpkg' # needed to match submodule bumps (gitlink)
- # The legs the pull-request matrix leaves out run here instead, so the legacy distros and
- # the slow macOS runner stay covered without gating a pull request on them. Tags still
- # build every leg, because a tag is what ships.
- schedule:
- - cron: '23 3 * * *'
# Cancel earlier, unfinished runs of this workflow on the same branch (unless on tag --> release)
concurrency:
- # The event name is part of the group because a nightly run and a push run on master share
- # refs/heads/master but must not cancel each other: the nightly would drop the push's vcpkg
- # import, and a merge landing during the nightly would drop that night's coverage.
- group: "${{ github.workflow }} @ ${{ github.ref_name }} (${{ github.event_name }})"
+ group: "${{ github.workflow }} @ ${{ github.ref_name }}"
cancel-in-progress: ${{ github.ref_type != 'tag' }}
-# Qt's online installer ships its base package as separate archives. Installing only the
-# ones Cockatrice loads drops the full 25+ archive download to this handful (Windows Qt
-# cache: 440 MB → 180 MB, macOS thin Qt: ~200 MB → 60 MB). The addons Cockatrice also needs
-# (Multimedia, WebSockets, ShaderTools, ImageFormats) are not base archives: they stay in
-# each leg's qt_modules.
-#
-# The list mirrors what cmake/FindQtRuntime.cmake requires, so a component that is missing
-# here fails configure with "No suitable version of Qt was found" instead of at runtime:
-# qtbase supplies Core, Gui, Widgets, Network, Sql, Xml, Concurrent and PrintSupport;
-# qtdeclarative supplies Qml, Quick and QuickWidgets; qtsvg supplies Svg; qttools supplies
-# Linguist (lrelease/lupdate); qttranslations ships Qt's own catalogs.
-env:
- QT_ARCHIVES: qtbase qtdeclarative qtsvg qttools qttranslations
-
jobs:
configure:
name: Configure
@@ -68,11 +46,6 @@ jobs:
outputs:
tag: ${{ steps.configure.outputs.tag }}
sha: ${{ steps.configure.outputs.sha }}
- # MAJ.MIN.PATCH[-beta[.N]] of a tag build; empty otherwise. Mirrors what
- # cmake/getversion.cmake derives as PROJECT_VERSION, which the client bakes as its
- # Sentry release (cmake/CrashReporting.cmake) — symbol uploads must use the same
- # string so stack traces de-symbolicate against this build's binaries.
- sentry_release: ${{ steps.configure.outputs.sentry_release }}
steps:
- name: "Configure"
@@ -83,9 +56,6 @@ jobs:
run: |
if [[ "$GITHUB_REF_TYPE" == 'tag' ]]; then # release
echo "tag=$GITHUB_REF_NAME" >> "$GITHUB_OUTPUT"
- # Tag format is YYYY-MM-DD-{Release|Development}-MAJ.MIN.PATCH[-beta[.N]];
- # PROJECT_VERSION is the tail after the type (see cmake/getversion.cmake).
- echo "sentry_release=$(sed -E 's/^[0-9]{4}-[0-9]{2}-[0-9]{2}-(Release|Development)-//' <<< "$GITHUB_REF_NAME")" >> "$GITHUB_OUTPUT"
fi
echo "sha=$RESOLVED_SHA" >> "$GITHUB_OUTPUT"
@@ -123,36 +93,6 @@ jobs:
--title "$release_name" \
--notes-file "$body_path"
- vcpkg-baseline:
- name: vcpkg baseline
- runs-on: ubuntu-slim
- steps:
- - name: "Checkout"
- uses: actions/checkout@v7
-
- # vcpkg-tool reads versions/baseline.json out of the commit named by
- # `builtin-baseline`, through the `vcpkg` submodule's git history. A submodule
- # pointer that lags the manifest - e.g. a stale `vcpkg/` worktree captured by
- # `git add` in a PR - makes every vcpkg leg fail in configure with "failed to
- # `git show` versions/baseline.json". That reads like a baseline or network
- # problem, when the pointer is what has to move. Check the two agree here in
- # seconds instead of on five build legs.
- - name: "Check the submodule pointer against the manifest baseline"
- shell: bash
- run: |
- pointer="$(git rev-parse HEAD:vcpkg 2>/dev/null || true)"
- baseline="$(sed -nE 's/.*"builtin-baseline"[[:space:]]*:[[:space:]]*"([0-9a-f]{40})".*/\1/p' vcpkg.json)"
- if [[ -z "$pointer" || -z "$baseline" ]]; then
- echo "::error file=vcpkg.json::could not read the vcpkg submodule pointer or the builtin-baseline"
- exit 1
- fi
- if [[ "$pointer" != "$baseline" ]]; then
- echo "::error file=vcpkg.json::vcpkg submodule is at $pointer but builtin-baseline is $baseline"
- echo "Move them together: git -C vcpkg checkout $baseline && git add vcpkg"
- exit 1
- fi
- echo "vcpkg submodule and builtin-baseline agree on $baseline"
-
build-linux:
strategy:
fail-fast: false
@@ -163,8 +103,6 @@ jobs:
allow-failure: yes
package: skip # We are packaged in Arch already
- debug_test: yes # see the note on Fedora 45 below
- ccache_size: 850M # Debug plus the suite: 680 MB working set at -g1
- distro: Servatrice_Debian
version: 12
@@ -172,168 +110,88 @@ jobs:
package: DEB
server_only: yes
test: skip
- # 4.7 min and the server alone: nothing here for a compiler cache to save.
- persist_ccache: 'false'
+
+ - distro: Debian
+ version: 12
+
+ package: DEB
+ test: skip # Running tests on all distros is superfluous
- distro: Debian
version: 13
package: DEB
- ccache_size: 800M # Release plus the suite: 625 MB working set at -g1
-
- # The single Fedora leg used to build Debug for the suite and Release for the
- # package in one job (measured on Fedora 44: 30.8 min, the only leg slower than
- # Windows), so the package compile waited behind the Debug one. Two legs run in
- # parallel and each keeps a cache that holds exactly one configuration.
- #
- # Arch and Fedora 45 are the two newest toolchains, and the -Wno-error= exceptions
- # in CMakeLists.txt exist for exactly their warnings. This leg compiles Debug
- # because only Debug passes -Werror, so it is where a new warning still fails.
- - distro: Fedora
- version: 45
- variant: debug # separate image and ccache from the release leg below
-
- package: skip
- debug_test: yes
- ccache_size: 850M # Debug plus the suite: 660 MB of objects at -g1
- distro: Fedora
- version: 45
- variant: release # separate image and ccache from the debug leg above
+ version: 43
package: RPM
- test: skip # The debug leg above runs this distro's suite
- ccache_size: 800M # Release plus the package: 614 MB of objects at -g1
+ test: skip # Running tests on all distros is superfluous
+
+ - distro: Fedora
+ version: 44
+
+ package: RPM
+
+ - distro: Ubuntu
+ version: 24.04
+
+ package: DEB
+ test: skip # Running tests on all distros is superfluous
- distro: Ubuntu
version: 26.04
package: DEB
- test: skip # Debian 13 covers the uninstrumented suite on pull requests
- # 18.1 min, shorter than the Windows leg. The sanitize leg in the nightly
- # matrix runs this distro's instrumented suite.
- persist_ccache: 'false'
+
+ # The one leg that runs the suite under AddressSanitizer and
+ # UndefinedBehaviorSanitizer. Debian 13 and Fedora 44 keep covering the
+ # uninstrumented suite, so nothing is lost by instrumenting this one.
+ sanitize: yes
+
+ - distro: Ubuntu
+ version: 26.04
+ variant: cover # separate image and ccache, same distro
+
+ # The one leg that runs instrumented for line coverage and publishes the report as
+ # a workflow artifact. It is a separate leg rather than a second flag on the
+ # sanitizer leg because the two instruments answer different questions: the
+ # sanitizer leg reports what the run did wrong, the coverage leg reports what the
+ # run never touched at all. Combining them would also slow both down. Packaging is
+ # skipped because the leg above already publishes the DEB for this distro.
+ coverage: yes
+ package: skip
name: ${{ matrix.distro }} ${{ matrix.version }}${{ matrix.variant && format(' {0}', matrix.variant) || '' }}
needs: configure
- # The nightly legs live in build-linux-nightly below. A leg is in exactly one of the two
- # matrices, so nothing that reviews a change waits for a distro nobody is changing for.
- if: github.event_name != 'schedule'
runs-on: ubuntu-latest # https://github.com/actions/runner-images
continue-on-error: ${{ matrix.allow-failure == 'yes' }}
timeout-minutes: 70
env:
CACHE: ${{ github.workspace }}/.cache/${{ matrix.distro }}${{ matrix.version }}${{ matrix.variant }} # directory for caching docker image and ccache
CCACHE_EVICTION_AGE: 7d
- # The bound is sized against the repository's 10 GB cache quota
- # (https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy),
- # not against what one leg would like. Measured 2026-11-07, the quota holds 2.97 GB
- # of these four ccache entries, 0.92 GB of Qt, 1.24 GB of vcpkg, 0.91 GB of buildkit
- # layers (mode=min in docker-release.yml) and 4.26 GB of CodeQL trap caches: one entry
- # per analyzed commit, about 1.07 GB each, of which every one but the newest is dead.
- # That is 10.30 GB against the 10 GB quota, so every save past this point costs some
- # other entry, and least-recently-used order reaches the buildkit layers before the
- # trap caches. prune-caches below keeps only the newest trap cache; the live total is
- # then 7.1 GB, which is what leaves room for the Windows and macOS compiler caches.
- #
- # Measured 2026-11-04: one configuration costs 1.2 GB with full DWARF and 660 MB with
- # -g1 (COCKATRICE_DEBUG_INFO_LEVEL below), and the size of the cache decides the hit
- # rate far more sharply than its fill suggests. A 6G cache served 1059 of 1059 calls;
- # the 700M bound on the same leg served 14%, because ccache fills to the limit and then
- # evicts the entries the next build is about to ask for. A cache that cannot hold a
- # whole configuration therefore buys almost nothing, and the quota goes to fewer legs
- # at a size that holds one:
- #
- # Arch Debug + suite 680 MB working set 850M
- # Debian 13 Release + suite 625 MB 800M
- # Fedora 45 debug Debug + suite 660 MB 850M
- # Fedora 45 release Release + package 614 MB 800M
- # Windows 10 Release + package 890 MB working set 950M
- # macOS 14/15 Release + package 0.5 GB ccache at -g1 700M
- # macOS 27 Debug Debug + suite 0.5 GB ccache at -g1 700M
- #
- # That is 6.35 GB of bounds; what lands in the quota is 5.15 GB of compiler cache,
- # 2.97 GB stored by the Linux legs, 0.95 GB by Windows and 1.23 GB by the three macOS
- # legs, for 9.3 GB of 10 in total.
- # So only legs worth their share get one. A leg keeps no cache when it duplicates
- # another leg's coverage (Ubuntu 26.04 behind Debian 13's Release + suite) or finishes
- # well inside the Windows leg (Servatrice covers the server alone in 4.7 min). The
- # macOS legs compile the client with a different toolchain (Apple clang), and one
- # configuration writes only ~0.5 GB, so all three fit beside the Windows entry even
- # though a macOS cache served 42% and saved about 32s on a 20 min leg. The four Linux
- # legs that stay compile the client on the toolchains a change can break and are the
- # ones a cache shortens by the most.
- # Measured job totals on master 2026-11-04 also show the ceiling this leaves:
- # Windows 10 is 29.5 min and compiles for 23.7 of it, so Windows sets the
- # pull-request wall clock no matter how warm these legs are. That is why the Windows
- # leg keeps the largest bound (see build-vcpkg): MSVC has no -g level to shrink, and
- # with /Z7 the whole configuration is cacheable, PCH included.
- #
- # Steady-state master data corrected this on 2026-11-07: Arch 90.1% and Debian 13
- # 96.75% per-run hits at the 850M/800M bounds, Fedora 45 debug 99.18% and release
- # 99.2%. The 75%/82% seen the day before were warming runs - the -g1 switch had
- # invalidated the cache one commit earlier - not thrashing at the bound. A cold 3G
- # run on the same day measured one configuration at 680 MB (Arch) and 625 MB
- # (Debian 13), so those bounds already hold the working set with ~170 MB of slack for
- # commit-to-commit churn, and raising them buys hit rate the numbers do not show a
- # need for. The three macOS entries are 700M each because one configuration wrote a
- # 0.5 GB ccache directory at -g1 and level 0 shrinks it further, so the bound holds the
- # whole configuration with room to spare.
- #
- # Re-add a leg only with a bound that holds its whole configuration: a smaller one is
- # worse than none.
- #
- # Every key listed in LIVE_CCACHE_KEYS in prune-caches below must match this matrix.
- CCACHE_SIZE: ${{ matrix.ccache_size || '700M' }}
- # Debug information dominates a cached object: with the built-in flags one
- # configuration is 1.2 GB and no per-leg bound can hold it, so CI lowers the level to
- # 1 (file and line, which is what a failing test or a sanitizer backtrace reports) and
- # one configuration halves. Empty on tags and on the nightly legs, which ship binaries
- # and keep full DWARF for the Sentry symbols; a tag restores a cache it cannot use and
- # is slower, which is accepted because a tag is rare and only pull requests gate.
- COCKATRICE_DEBUG_INFO_LEVEL: ${{ github.ref_type != 'tag' && '1' || '' }}
+ CCACHE_SIZE: 600M # space of all repo is 10Gi: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy
CMAKE_GENERATOR: 'Ninja'
NAME: ${{ matrix.distro }}${{ matrix.version }}
IMAGE_SUFFIX: ${{ matrix.variant }}
- # Which build the suite runs in, decided once per leg here so the steps' if: conditions
- # and their shells cannot drift apart. A step's own env is not visible to its if:, which
- # is why these live at job level rather than on the release step.
- # DEBUG_TEST the instrumented legs (symbols and instrumentation matter) and the two
- # newest distro toolchains, which only fail on a warning in Debug.
- # RUN_TESTS every other leg that tests runs the suite against the release build it may
- # also package, so testing and packaging cost one compile instead of two.
- DEBUG_TEST: ${{ matrix.sanitize == 'yes' || matrix.coverage == 'yes' || matrix.debug_test == 'yes' }}
- RUN_TESTS: ${{ matrix.test != 'skip' && !(matrix.sanitize == 'yes' || matrix.coverage == 'yes' || matrix.debug_test == 'yes') }}
- # Whether this leg restores and saves its compiler cache. Not every pull-request leg
- # keeps one: a cache only pays for itself when its bound holds a whole configuration
- # (see the sizing comment above), and a leg that duplicates another leg's coverage
- # spends quota without adding any. The nightly-only legs keep none either. Fedora 45
- # states it per leg: its two configurations have their own job and key, so neither
- # bound is shared with a build whose objects it would evict.
- PERSIST_CCACHE: ${{ matrix.persist_ccache || 'true' }}
- # Shared with build-linux-nightly so the two leg sets run the same steps.
- steps: &linux-steps
+ steps:
- name: "Checkout"
uses: actions/checkout@v7
- name: "Restore compiler cache (ccache)"
id: restore_ccache
- if: env.PERSIST_CCACHE == 'true'
uses: actions/cache/restore@v6
with:
key: ccache-${{ matrix.distro }}${{ matrix.version }}${{ matrix.variant }}
- path: ${{ env.CACHE }}/.ccache
+ path: ${{ env.CACHE }}
- name: "Build ${{ matrix.distro }} ${{ matrix.version }} Docker image"
shell: bash
run: source .ci/docker.sh --build
- # The instrumented legs and the two newest distro toolchains build and test Debug; every
- # other leg runs its suite against the release build below, so testing and packaging cost
- # one compile instead of two.
- name: "Build debug and test"
- if: env.DEBUG_TEST == 'true'
+ if: matrix.test != 'skip'
shell: bash
env:
sanitize: '${{ matrix.sanitize }}'
@@ -367,113 +225,34 @@ jobs:
- name: "Upload coverage artifact"
if: matrix.coverage == 'yes'
- uses: actions/upload-artifact@v7
+ uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage/
if-no-files-found: warn
- # One build serves both purposes: the suite runs against the same configuration that
- # gets packaged, so a packaging leg no longer pays for a full Debug compile it then
- # throws away. macOS and Windows have always tested their release build.
- #
- # Instrumented and debug-test legs reach this step only when they also package; their
- # suite already ran in the debug step above, so the tests are not repeated here.
- - name: "Build release, test and package"
+ - name: "Build release package"
id: build
- if: matrix.package != 'skip' || env.RUN_TESTS == 'true'
+ if: matrix.package != 'skip'
shell: bash
env:
SUFFIX: '-${{ matrix.distro }}${{ matrix.version }}'
package: '${{ matrix.package }}'
server_only: '${{ matrix.server_only }}'
- # Only a leg that packages builds the shipping configuration; a release build that
- # only runs tests stays free of the crash reporting SDK.
- WITH_CRASH_REPORTING: ${{ matrix.package != 'skip' && 'ON' || '' }}
- # Tag builds only: artifacts from master pushes and PR branches must not report to
- # Sentry. Forks get no secrets on pull_request, so they keep the mock fallback either way.
- # configure.outputs.tag is set for ANY tag, so beta (Development) tags get the DSN
- # and the symbol upload below just like full releases.
- COCKATRICE_SENTRY_DSN: ${{ needs.configure.outputs.tag != null && secrets.SENTRY_DSN || '' }}
run: |
source .ci/docker.sh
args=()
[[ $server_only == yes ]] && args+=(--no-client)
- [[ $RUN_TESTS == true ]] && args+=(--test)
[[ $GITHUB_REF == "refs/heads/master" ]] && args+=(--evict-ccache "$CCACHE_EVICTION_AGE")
args+=(--ccache "$CCACHE_SIZE")
args+=(--cmake-generator "$CMAKE_GENERATOR")
args+=(--suffix "$SUFFIX")
- # The container inherits no environment, so the DSN rides in explicitly. -e without
- # =value passes it through (possibly empty); compile.sh only consumes it for Release
- # configures, and empty keeps the mock fallback.
- export RUN_ARGS="${RUN_ARGS:-} -e COCKATRICE_SENTRY_DSN"
- # --package takes an optional type, so the flag and its argument have to travel
- # together or the type would become the next flag.
- package_args=()
- [[ -n $package && $package != skip ]] && package_args+=(--package "$package")
+ RUN --server --release --package "$package" "${args[@]}"
- # compile.sh runs the tests before the package target, so a failing suite stops the
- # leg before anything is built into an installer.
- RUN --server --release "${package_args[@]}" "${args[@]}"
-
- - name: "Install sentry-cli"
- if: matrix.package != 'skip' && needs.configure.outputs.tag != null
- # Downloads from Sentry's release registry: a Sentry outage or a broken install under
- # Git Bash must not take down the release assets below.
- continue-on-error: true
- shell: bash
- env:
- SENTRY_CLI_VERSION: '3.8.0'
- run: |
- export INSTALL_DIR="$GITHUB_WORKSPACE/.sentry-cli"
- curl -sL https://sentry.io/get-cli/ | bash
- echo "$GITHUB_WORKSPACE/.sentry-cli" >> "$GITHUB_PATH"
-
- - name: "Upload debug symbols to Sentry"
- # Like the DSN above, this runs for every tag — beta (Development) tags included.
- if: matrix.package != 'skip' && needs.configure.outputs.tag != null
- # A Sentry outage or rotated token must not take down the release assets below.
- continue-on-error: true
- shell: bash
- env:
- SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
- run: |
- paths=()
- # The binaries only, not the directories: a directory makes sentry-cli walk the
- # whole build tree, where it finds (and uploads) object files and other fragments
- # that can never match a stack frame. The packaged copy is split and stripped by
- # cpack, but keeps this binary's GNU build-id, which is how Sentry matches it.
- for app in cockatrice oracle servatrice; do
- binary="build/$app/$app"
- [[ -f $binary ]] && paths+=("$binary")
- done
- if [[ ${#paths[@]} == 0 ]]; then
- # Fail loudly instead of skipping: see the Windows/macOS leg below — this step
- # is the only way symbols reach Sentry and `continue-on-error` keeps the job
- # green either way, so a silent skip looks exactly like a successful upload.
- echo "::error::No debug symbol files found; nothing would be uploaded"
- exit 1
- fi
- # Project slug on Sentry; must match the project the baked DSN points at.
- # No --release here: `debug-files upload` has no such option (sentry-cli errors on
- # the unknown flag and uploads nothing), and debug information files are matched to
- # events by debug ID via the event's debug_meta, not by release. The release grouping
- # comes from the client event itself (COCKATRICE_SENTRY_RELEASE).
- sentry-cli debug-files upload --org cockatrice --project cockatrice-desktop "${paths[@]}"
-
- # Pull requests and tags only restore; only a push to refs/heads/master writes. A PR save
- # would replace the objects the next master build restores, and a PR that compiles a
- # different configuration would evict the warm set for everyone. Tags build with full
- # DWARF (COCKATRICE_DEBUG_INFO_LEVEL is empty), so a tag's cache would poison the -g1
- # set if it shared the master scope; it does not, because the save is master-only. The
- # nightly legs persist nothing at all (PERSIST_CCACHE is false) for the same reason:
- # a schedule shares refs/heads/master with the push.
- #
# Delete used cache to emulate a ccache update. See https://github.com/actions/cache/issues/342
- name: "Delete remote compiler cache (ccache)"
- if: github.ref == 'refs/heads/master' && env.PERSIST_CCACHE == 'true' && steps.restore_ccache.outputs.cache-hit
+ if: github.ref == 'refs/heads/master' && steps.restore_ccache.outputs.cache-hit
continue-on-error: true
env:
CACHE_PRIMARY_KEY: ${{ steps.restore_ccache.outputs.cache-primary-key }}
@@ -484,11 +263,11 @@ jobs:
fi
- name: "Save updated compiler cache (ccache)"
- if: github.ref == 'refs/heads/master' && env.PERSIST_CCACHE == 'true'
+ if: github.ref == 'refs/heads/master'
uses: actions/cache/save@v6
with:
key: ${{ steps.restore_ccache.outputs.cache-primary-key }}
- path: ${{ env.CACHE }}/.ccache
+ path: ${{ env.CACHE }}
- name: "Upload artifact"
id: upload_artifact
@@ -526,111 +305,33 @@ jobs:
GH_TOKEN: ${{ github.token }}
run: gh attestation verify "$BUILD_PATH" --repo Cockatrice/Cockatrice
- build-linux-nightly:
- strategy:
- fail-fast: false
- matrix:
- include:
- - distro: Debian
- version: 12 # previous stable, still covered by the nightly
-
- package: DEB
- test: skip # Running tests on all distros is superfluous
-
- - distro: Fedora
- version: 44 # previous release, still covered by the nightly
-
- package: RPM
- test: skip # Running tests on all distros is superfluous
-
- - distro: Ubuntu
- version: 24.04 # previous LTS, still covered by the nightly
-
- package: DEB
- test: skip # Running tests on all distros is superfluous
-
- # The one leg that runs the suite under AddressSanitizer and
- # UndefinedBehaviorSanitizer. It runs nightly rather than on pull requests: it is
- # not on the pull-request critical path (Windows sets that), and its 1.2G cache
- # was the largest single entry in the 10 GB quota. Debian 13 and Fedora 45 keep
- # covering the uninstrumented suite on pull requests, so nothing is lost there.
- #
- # It packages nothing. A sanitized Debug build is not something to ship; the
- # pull-request Ubuntu 26.04 leg publishes this distro's DEB.
- - distro: Ubuntu
- version: 26.04
- variant: sanitize # separate image, same distro as the coverage leg below
-
- package: skip
- sanitize: yes
- # A sanitizer finding must not redden the nightly run: vcpkg-cache-import only
- # imports the archives of a run whose conclusion is success. The job itself
- # still reports failure.
- allow-failure: yes
- # Scratch only (nightly legs persist no cache); sized so the instrumented
- # objects, which are larger than the 614 MB measured, do not thrash mid-build.
- ccache_size: 1200M
-
- # The one leg that runs instrumented for line coverage and publishes the report as
- # a workflow artifact. It stays a separate leg rather than a second flag on the
- # sanitizer leg because the two instruments answer different questions: the
- # sanitizer leg reports what the run did wrong, the coverage leg reports what the
- # run never touched at all. Combining them would also slow both down.
- #
- # It moved here from build-linux: gcovr.cfg sets no fail_under, so the leg cannot
- # fail a build, and nothing consumes the report per change, which made it an
- # artifact nobody read on every pull request. On the default branch it becomes a
- # trend instead. Image and ccache keys come from distro, version and variant, so
- # they are unchanged and the warm cache that leg built still matches.
- - distro: Ubuntu
- version: 26.04
- variant: cover # separate image and ccache, same distro
-
- coverage: yes
- package: skip
- # A report must not redden the nightly run: vcpkg-cache-import only imports the
- # archives of a run whose conclusion is success.
- allow-failure: yes
-
- name: ${{ matrix.distro }} ${{ matrix.version }}${{ matrix.variant && format(' {0}', matrix.variant) || '' }}
- needs: configure
- # A nightly and a tag build cover the legs a pull request or a merge does not wait for:
- # the previous release of each distro, and the instrumented legs.
- if: github.event_name == 'schedule' || github.ref_type == 'tag'
- runs-on: ubuntu-latest # https://github.com/actions/runner-images
- continue-on-error: ${{ matrix.allow-failure == 'yes' }}
- timeout-minutes: 70
- env:
- CACHE: ${{ github.workspace }}/.cache/${{ matrix.distro }}${{ matrix.version }}${{ matrix.variant }} # directory for caching docker image and ccache
- CCACHE_EVICTION_AGE: 7d
- # These legs do not keep a compiler cache: they run once a day (and on tags) and gate
- # nothing, so their share of the 10 GB quota goes to the pull-request legs instead.
- # A cold nightly build costs minutes on a runner nobody waits for; a cold pull request
- # build costs a review. CCACHE_SIZE is still passed so ccache trims its scratch dir.
- PERSIST_CCACHE: 'false'
- CCACHE_SIZE: ${{ matrix.ccache_size || '600M' }}
- CMAKE_GENERATOR: 'Ninja'
- NAME: ${{ matrix.distro }}${{ matrix.version }}
- IMAGE_SUFFIX: ${{ matrix.variant }}
- # The shared steps below gate on these, so they have to be derived here too. Left
- # undefined they silently skipped the build the leg exists for: the coverage leg
- # found build/ empty and its report collection failed on every nightly and tag run.
- # Same derivation as the pull request job above.
- DEBUG_TEST: ${{ matrix.sanitize == 'yes' || matrix.coverage == 'yes' || matrix.debug_test == 'yes' }}
- RUN_TESTS: ${{ matrix.test != 'skip' && !(matrix.sanitize == 'yes' || matrix.coverage == 'yes' || matrix.debug_test == 'yes') }}
- steps: *linux-steps
-
build-vcpkg:
strategy:
fail-fast: false
matrix:
include:
- os: macOS
- target: 14 # EOL 2026-09-14
- runner: xcode-27 # https://github.com/actions/runner-images/blob/main/images/macos/xcode-27-arm64-Readme.md
+ target: 13 # EOL 2025-09-15
+ runner: macos-15-intel # https://github.com/actions/runner-images/blob/main/images/macos/macos-15-Readme.md
+ # macos-26-intel is very slow and fails in CPack during DMG config if not increasing Finder timeout
+
+ ccache_eviction_age: 7d
+ cmake_generator: Ninja
+ make_package: 1
+ override_target: 13
+ package_suffix: "-macOS13_Intel"
+ qt_version: 6.11.*
+ qt_modules: qtimageformats qtmultimedia qtshadertools qtwebsockets
+ soc: Intel
+ type: Release
+ use_ccache: 1
+ xcode: "26.3"
+
+ - os: macOS
+ target: 14 # EOL 2026-??
+ runner: macos-26 # https://github.com/actions/runner-images/blob/main/images/macos/macos-26-arm64-Readme.md
ccache_eviction_age: 7d
- ccache_size: 700M # Release + package: ~0.4 GB at level 0
cmake_generator: Ninja
make_package: 1
override_target: 14
@@ -640,16 +341,13 @@ jobs:
soc: Apple
type: Release
use_ccache: 1
- vcpkg_deployment_target: 14
- vcpkg_scope: 'vcpkg/macos14-arm64-xcode26.6/'
- vcpkg_upload: 1 # only one leg per vcpkg_scope hands its archives over
+ xcode: "26.6"
- os: macOS
- target: 15 # EOL 2027-??
- runner: xcode-27 # https://github.com/actions/runner-images/blob/main/images/macos/xcode-27-arm64-Readme.md
+ target: 15
+ runner: macos-26 # https://github.com/actions/runner-images/blob/main/images/macos/macos-26-arm64-Readme.md
ccache_eviction_age: 7d
- ccache_size: 700M # Release + package: ~0.4 GB at level 0
cmake_generator: Ninja
make_package: 1
override_target: 15
@@ -659,73 +357,42 @@ jobs:
soc: Apple
type: Release
use_ccache: 1
- vcpkg_deployment_target: 14
- vcpkg_scope: 'vcpkg/macos14-arm64-xcode26.6/'
+ xcode: "26.6"
- os: macOS
- target: 27
- runner: xcode-27 # https://github.com/actions/runner-images/blob/main/images/macos/xcode-27-arm64-Readme.md
+ target: 26
+ runner: macos-26 # https://github.com/actions/runner-images/blob/main/images/macos/macos-26-arm64-Readme.md
ccache_eviction_age: 7d
- ccache_size: 700M # Debug + suite: ~0.5 GB at level 0
cmake_generator: Ninja
qt_version: 6.11.*
qt_modules: qtimageformats qtmultimedia qtshadertools qtwebsockets
soc: Apple
type: Debug
use_ccache: 1
- vcpkg_deployment_target: 14
- vcpkg_scope: 'vcpkg/macos14-arm64-xcode26.6/'
+ xcode: "26.6"
- os: Windows
target: 10
runner: windows-2025 # https://github.com/actions/runner-images/blob/main/images/windows/Windows2025-VS2026-Readme.md
- # Ninja builds the whole dependency graph at once instead of one
- # MSBuild node at a time, runs the compiler itself, and matches the
- # Linux and macOS legs. Its median build phase measured ~17 min
- # against ~21 for the Visual Studio generator (shared 4-core hosts
- # swing either by minutes, so read medians, not single runs).
- cmake_generator: Ninja
+ cmake_generator: "Visual Studio 18 2026"
+ cmake_generator_platform: x64
make_package: 1
package_suffix: "-Win10"
qt_version: 6.11.*
qt_modules: qtimageformats qtmultimedia qtshadertools qtwebsockets
type: Release
- ccache_eviction_age: 7d
- ccache_size: 950M # Release plus the package: 890 MB working set without /Z7
- use_ccache: 1
- # Scope is named after the vs2026 toolchain, not the generator: the
- # ABI hash covers cl.exe, which is identical, so this leg keeps
- # restoring the warm archives. Renaming would orphan them.
- vcpkg_scope: 'vcpkg/windows10-x64-vs2026/'
- vcpkg_upload: 1 # only one leg per vcpkg_scope hands its archives over
name: ${{ matrix.os }} ${{ matrix.target }}${{ matrix.soc == 'Intel' && ' Intel' || '' }}${{ matrix.type == 'Debug' && ' Debug' || '' }}
- needs: [configure, vcpkg-baseline]
- # The nightly leg lives in build-vcpkg-nightly below, next to the Linux nightly legs.
- if: github.event_name != 'schedule'
+ needs: configure
runs-on: ${{ matrix.runner }}
timeout-minutes: 100
env:
- # The Windows leg and all three pull-request macOS legs keep a compiler cache. ccache
- # drives MSVC as long as the compiler emits /Z7, which CMakeLists.txt sets for exactly
- # this reason, and it drives clang on macOS the same way; both cache the PCH-backed
- # targets with the sloppiness that .ci/compile.sh re-asserts. Every leg builds without
- # debug information on non-tag refs, so one macOS configuration writes ~0.4-0.5 GB and
- # the 700M bound holds it with room for commit-to-commit churn (see build-linux for the
- # quota arithmetic).
CCACHE_DIR: ${{ github.workspace }}/.cache/
- CCACHE_SIZE: ${{ matrix.ccache_size || '700M' }}
- PERSIST_CCACHE: ${{ matrix.persist_ccache || 'true' }}
- # MSVC has no -g level, so there is nothing to ask for on Windows: level 0 is the one
- # value with a meaning there, and it drops /Z7 rather than lowering a level. Pull-request
- # builds carry no debug information, which is most of what a compiler cache stores; a
- # tag is the build whose binary is shipped and symbolicated, so it keeps the full set.
- COCKATRICE_DEBUG_INFO_LEVEL: ${{ github.ref_type != 'tag' && '0' || '' }}
+ CCACHE_SIZE: 600M # space of all repo is 10Gi: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy
- # Shared with build-vcpkg-nightly so the two leg sets run the same steps.
- steps: &vcpkg-steps
+ steps:
- name: "Checkout"
uses: actions/checkout@v7
with:
@@ -737,31 +404,18 @@ jobs:
with:
msbuild-architecture: x64
- - name: "[Windows] Setup ccache"
- if: matrix.os == 'Windows' && matrix.use_ccache == 1
- shell: bash
- run: choco install ccache --no-progress -y
-
- name: "[macOS] Setup ccache"
if: matrix.os == 'macOS' && matrix.use_ccache == 1
run: brew install ccache
- - name: "Restore compiler cache (ccache)"
- if: matrix.use_ccache == 1 && env.PERSIST_CCACHE == 'true'
+ - name: "[macOS] Restore compiler cache (ccache)"
+ if: matrix.os == 'macOS' && matrix.use_ccache == 1
id: restore_ccache
uses: actions/cache/restore@v6
with:
- key: ccache-${{ matrix.os }}${{ matrix.target }}
+ key: ccache-${{ matrix.runner }}_${{ matrix.override_target }}-Xcode${{ matrix.xcode }}
path: ${{ env.CCACHE_DIR }}
- # Every macOS leg that keeps a cache restores the master entry for its own target.
- # Measured on master 2026-11-07 (before the debug-information change), one macOS
- # configuration wrote a 0.5 GB ccache directory at -g1 against the 700M bound, and the
- # restored cache served 42% of calls. Level 0 shrinks that directory to ~0.4 GB, so the
- # bound holds the whole configuration and the three entries fit the quota (see the
- # sizing note in build-linux). macOS 14 and 15 build the same Release + package
- # configuration for two deployment targets, but each is cheap enough to keep its own.
-
- name: "[macOS] Install aqtinstall"
if: matrix.os == 'macOS'
run: pipx install aqtinstall
@@ -780,7 +434,7 @@ jobs:
id: restore_qt
uses: actions/cache/restore@v6
with:
- key: Qt-${{ steps.resolve_qt_version.outputs.version }}-macOS-${{ matrix.soc }}-${{ matrix.qt_modules }}-${{ env.QT_ARCHIVES }}-thin
+ key: Qt-${{ steps.resolve_qt_version.outputs.version }}-macOS-${{ matrix.soc }}-${{ matrix.qt_modules }}-thin
path: ${{ github.workspace }}/Qt
# Using jurplel/install-qt-action to install Qt without using brew
@@ -789,7 +443,6 @@ jobs:
if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true'
uses: jurplel/install-qt-action@v4
with:
- archives: ${{ env.QT_ARCHIVES }}
cache: false
# cache-key-prefix: Qt
dir: ${{ github.workspace }} # thinning script depends on this location
@@ -813,7 +466,6 @@ jobs:
with:
# Qt 6.11.0 only works with aqtinstall directly from git until aqtinstall 3.4 is released
aqtsource: git+https://github.com/miurahr/aqtinstall.git
- archives: ${{ env.QT_ARCHIVES }}
cache: ${{ github.ref == 'refs/heads/master' }}
cache-key-prefix: Qt
modules: ${{ matrix.qt_modules }}
@@ -822,47 +474,14 @@ jobs:
- name: "[Windows] Install NSIS"
if: matrix.os == 'Windows'
shell: bash
- run: choco install nsis --no-progress
-
- # Ninja invokes cl.exe directly and needs the MSVC environment on PATH.
- - name: "[Windows] Set up MSVC environment"
- if: matrix.os == 'Windows'
- uses: TheMrMilchmann/setup-msvc-dev@368ef7d1ee4d1171b31d4a7f67f4d954f903f5a9 # v4.1.0
- with:
- arch: x64
+ run: choco install nsis
- name: "Setup vcpkg cache"
id: vcpkg-cache
uses: TAServers/vcpkg-cache@v3
with:
- # Every entry is keyed by vcpkg ABI hash, and one hash set exists per platform,
- # target and toolchain. The action restores every entry it finds under the
- # prefix, not just the hashes vcpkg asks for, so with one shared vcpkg/
- # namespace every leg downloads all 66 archives of all 5 legs on every run.
- # Each of those restores also refreshes the entry's last access date, which
- # is why a dead ABI generation is never evicted by age. Naming the scope is
- # what makes those generations identifiable, and prune-caches below able to
- # delete them.
- prefix: ${{ matrix.vcpkg_scope }}
token: ${{ secrets.GITHUB_TOKEN }}
- # The action only creates its directory when the prefix matched at least one
- # entry, and it reads that directory unguarded when it saves at the end of the
- # job. A scope nobody has written to yet therefore leaves the directory missing
- # and fails the job, which is exactly the state every scope starts in. Make the
- # directory up front so an empty scope is a cache miss instead of an error.
- # always() covers the cache step itself failing: the action's post save still runs
- # then, and it should report the real error instead of ENOENT. It cannot rescue a
- # failure before the cache step, since a post step only runs for an action whose
- # main step did. The fallback keeps this guard from failing when the cache step was
- # skipped and left no path output.
- - name: "Create vcpkg cache directory"
- if: always()
- shell: bash
- env:
- CACHE_DIR: '${{ steps.vcpkg-cache.outputs.path }}'
- run: mkdir -p "${CACHE_DIR:-$GITHUB_WORKSPACE/.vcpkg-cache}"
-
# Uses environment variables, see compile.sh for more details
- name: "Build Cockatrice"
id: build
@@ -871,11 +490,8 @@ jobs:
BUILDTYPE: '${{ matrix.type }}'
CCACHE_EVICTION_AGE: ${{ matrix.ccache_eviction_age }}
CMAKE_GENERATOR: ${{ matrix.cmake_generator }}
- # Tag builds only: artifacts from master pushes and PR branches must not report to
- # Sentry. Forks get no secrets on pull_request, so they keep the mock fallback either way.
- # configure.outputs.tag is set for ANY tag, so beta (Development) tags get the DSN
- # and the symbol upload below just like full releases.
- COCKATRICE_SENTRY_DSN: ${{ needs.configure.outputs.tag != null && secrets.SENTRY_DSN || '' }}
+ CMAKE_GENERATOR_PLATFORM: ${{ matrix.cmake_generator_platform }}
+ DEVELOPER_DIR: '/Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer'
GITHUB_TOKEN: ${{ github.token }} # needed for vcpkg dependency graph updates, see VCPKG_FEATURE_FLAGS
MACOS_CERTIFICATE: ${{ secrets.PROD_MACOS_CERTIFICATE }}
MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }}
@@ -885,153 +501,11 @@ jobs:
PACKAGE_SUFFIX: '${{ matrix.package_suffix }}'
TARGET_MACOS_VERSION: ${{ matrix.override_target }}
USE_CCACHE: ${{ matrix.use_ccache }}
- # Only pushes to the default branch may write the shared binary cache: the
- # archives a run compiles are only as trustworthy as the code they were
- # built from, and only merged code may populate the scope that master and
- # release builds restore and link. Pull requests (same-repository ones
- # included) restore the shared cache but never write it; with the source
- # left at read the collect step below finds nothing on PRs, so no
- # pull-request-scoped entry is ever saved that no later run could restore.
- # Forks pushing to their own default branch resolve github.ref against
- # their own repository, so they keep a working cache of their own there.
VCPKG_BINARY_SOURCES: "clear;files,${{ steps.vcpkg-cache.outputs.path }},${{ case(github.ref == 'refs/heads/master', 'readwrite', 'read') }}"
VCPKG_DISABLE_METRICS: 1
VCPKG_FEATURE_FLAGS: dependencygraph
- VCPKG_TARGET_MACOS_VERSION: ${{ matrix.vcpkg_deployment_target }}
- # Release legs compile the sentry-native SDK; Debug legs stay SDK-free.
- WITH_CRASH_REPORTING: ${{ matrix.type == 'Release' && 'ON' || '' }}
run: .ci/compile.sh --server --test --vcpkg
- # What the compiler cache has to hold for this leg. ccache stores the objects it
- # compiles, so their total is the working set a bound has to fit, and the cache
- # directory shows what this build actually wrote. Debug information dominates both,
- # which is why the level above is reported next to them.
- - name: "Report compiler cache footprint"
- if: always()
- shell: bash
- run: |
- echo "=== ccache directory"
- du -sh "$CCACHE_DIR" 2>/dev/null || echo "(none)"
- echo "=== objects and archives"
- find build -type f \( -name '*.o' -o -name '*.obj' -o -name '*.a' -o -name '*.lib' \) -exec du -k {} + 2>/dev/null \
- | awk '{n++; s+=$1} END {printf "%d files, %.2f GB\n", n, s/1048576}'
- echo "=== debug info level in use: '${COCKATRICE_DEBUG_INFO_LEVEL}'"
-
- - name: "Install sentry-cli"
- if: needs.configure.outputs.tag != null && matrix.make_package == '1'
- # Same as the Linux leg above: the install downloads from Sentry's registry, so a
- # failure here must not block the release upload below.
- continue-on-error: true
- shell: bash
- env:
- SENTRY_CLI_VERSION: '3.8.0'
- run: |
- export INSTALL_DIR="$GITHUB_WORKSPACE/.sentry-cli"
- curl -sL https://sentry.io/get-cli/ | bash
- echo "$GITHUB_WORKSPACE/.sentry-cli" >> "$GITHUB_PATH"
-
- - name: "Upload debug symbols to Sentry"
- # Like the DSN above, this runs for every tag — beta (Development) tags included.
- if: needs.configure.outputs.tag != null && matrix.make_package == '1'
- # A Sentry outage or rotated token must not take down the release assets below.
- continue-on-error: true
- shell: bash
- env:
- SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
- run: |
- paths=()
- if [[ '${{ matrix.os }}' == 'Windows' ]]; then
- # Top-level CMakeLists.txt links /DEBUG even in release, so the linker PDBs
- # land next to the binaries. Ninja is single-config, so that is
- # build// and NOT build//Release/ — the same globs the symbol
- # artifact step below uses.
- #
- # The executables are uploaded as well, in the same invocation. On 64-bit
- # Windows the Call Frame Information (.pdata/.xdata) lives in the PE and is
- # not copied into the PDB, so a PDB on its own leaves Sentry reporting
- # "Stack Unwinding: Missing" for the image. sentry-cli also needs to see the
- # PE and its PDB together: only then does it associate the PE's code
- # identifier with the PDB's debug identifier.
- shopt -s nullglob
- for target in cockatrice oracle servatrice; do
- paths+=("build/$target"/*.exe "build/$target"/*.pdb)
- done
- shopt -u nullglob
-
- # Qt's own runtime DLLs, taken from required-runtime.txt — the manifest
- # .ci/compile.sh asserts the installer against — so the set uploaded is
- # the set an install actually carries. The PE is the only carrier of
- # unwind information for a 64-bit image, and sending it in the same
- # invocation as a PDB is what lets sentry-cli associate the PE's code
- # identifier with the PDB's debug identifier. Qt ships no PDBs with the
- # SDK, so Qt frames resolve through the export table: names, no lines.
- qt_dir=
- qt_core_dir=
- # Existence guards matter: the step runs under `bash -eo pipefail`, so a
- # redirection-less sed over a missing file would abort the step outright
- # and continue-on-error would hide it.
- if [[ -f build/CMakeCache.txt ]]; then
- qt_core_dir=$(sed -n 's/^Qt6Core_DIR:PATH=//p' build/CMakeCache.txt | head -1)
- fi
- if [[ -n $qt_core_dir ]]; then
- qt_dir=$(dirname "$(dirname "$(dirname "$qt_core_dir")")")
- fi
- if [[ -z $qt_dir || ! -d $qt_dir ]]; then
- # Warnings, not failures: the upload of our own binaries is what this
- # step is for, and continue-on-error already means an upload failure
- # cannot be told apart from a successful one.
- echo "::warning::Qt runtime directory not resolved from build/CMakeCache.txt; Qt images get no unwind information"
- elif [[ ! -f build/required-runtime.txt ]]; then
- echo "::warning::build/required-runtime.txt is missing; cannot tell which Qt DLLs ship"
- else
- while IFS= read -r runtime; do
- name=${runtime##*/}
- if [[ $runtime == */* ]]; then
- # A Qt plugin: the install root keeps it as plugins//.
- # `|| true`: an unmatched glob makes ls exit non-zero, and pipefail
- # turns that into a step abort.
- runtime_path=$(ls "$qt_dir/plugins"/*/"$name" 2>/dev/null | head -1) || true
- else
- runtime_path="$qt_dir/bin/$name"
- fi
- if [[ -f ${runtime_path:-} ]]; then
- paths+=("$runtime_path")
- else
- echo "::warning::shipped Qt runtime $runtime not found under $qt_dir"
- fi
- done < <(grep -E '^(Qt6[^/]*\.dll|Plugins/)' build/required-runtime.txt)
- fi
- else
- # Release builds carry DWARF (-g, kept by CrashReporting.cmake); macOS keeps its
- # debug info in dSYM bundles, so materialize them before uploading.
- for app in cockatrice oracle servatrice; do
- binary="build/$app/$app.app/Contents/MacOS/$app"
- if [[ -f $binary ]]; then
- if [[ '${{ matrix.os }}' == 'macOS' ]]; then
- dsymutil "$binary" -o "$binary.dSYM"
- paths+=("$binary.dSYM")
- # The dSYM holds the debug information, but the unwind information
- # Sentry needs to walk a minidump stack lives in the Mach-O itself.
- paths+=("$binary")
- else
- paths+=("$binary")
- fi
- fi
- done
- fi
- if [[ ${#paths[@]} == 0 ]]; then
- # Fail loudly instead of skipping: this step is the only way symbols reach
- # Sentry, and `continue-on-error` keeps the job green either way, so a
- # silent skip is indistinguishable from a successful upload. It also hid a
- # wrong Windows path (build//Release, which Ninja never creates).
- echo "::error::No debug symbol files found; nothing would be uploaded"
- exit 1
- fi
- # Project slug on Sentry; must match the project the baked DSN points at.
- # No --release: see the Linux leg above — the flag does not exist on
- # `debug-files upload` and DIFs are matched by debug ID, not release.
- sentry-cli debug-files upload --org cockatrice --project cockatrice-desktop "${paths[@]}"
-
# CPack reports a failed makensis run as "Problem running NSIS command"
# plus a path to NSISOutput.log, and that log lives only on the runner, so
# the actual compile error is otherwise nowhere in the workflow output.
@@ -1043,69 +517,10 @@ jobs:
shell: bash
run: find . -name NSISOutput.log -exec cat {} +
- # A master run hands over the archives it had to build itself so that
- # vcpkg-cache-import.yml, which runs in the default branch scope, can save them
- # where later runs can restore them. Pull requests hand over nothing: their cache
- # entries would be scoped to refs/pull//merge and stay unreadable for every
- # other run, so writing them only burns the repository's quota.
- # vcpkg_upload keeps this to one leg per scope: macOS 14, 15 and 26 share a scope
- # by design, and all three rebuild the same archives, so uploading from each of them
- # would publish three identical 269 MB artifacts under one artifact name and make
- # the importer spawn a save job per copy.
- - name: "Collect new vcpkg entries"
- id: vcpkg_new
- if: github.ref == 'refs/heads/master' && matrix.vcpkg_upload == 1
- shell: bash
- env:
- CACHE_DIR: '${{ steps.vcpkg-cache.outputs.path }}'
- GH_TOKEN: ${{ github.token }}
- VCPKG_SCOPE: '${{ matrix.vcpkg_scope }}'
- run: |
- # The archive name is the ABI hash, so a missing key is exactly what vcpkg had
- # to compile from source in this job.
- gh api --paginate \
- "repos/$GITHUB_REPOSITORY/actions/caches?ref=refs/heads/master&key=$VCPKG_SCOPE&per_page=100" \
- --jq '.actions_caches[].key' | sort -u >"$RUNNER_TEMP/master-keys.txt"
- find "$CACHE_DIR" -type f -name '*.zip' -exec basename {} .zip \; |
- sed "s|^|$VCPKG_SCOPE|" | sort -u >"$RUNNER_TEMP/local-keys.txt"
- comm -23 "$RUNNER_TEMP/local-keys.txt" "$RUNNER_TEMP/master-keys.txt" >"$RUNNER_TEMP/new-keys.txt"
-
- mkdir -p "$RUNNER_TEMP/vcpkg-new"
- : >"$RUNNER_TEMP/vcpkg-new/keys.txt"
- while read -r key; do
- hash="${key##*/}"
- archive="$(find "$CACHE_DIR" -type f -name "$hash.zip" -print -quit)"
- dir="$RUNNER_TEMP/vcpkg-new/${hash:0:2}" # vcpkg's own two character fanout
- mkdir -p "$dir"
- cp "$archive" "$dir/"
- echo "$key ${hash:0:2}/$hash.zip" >>"$RUNNER_TEMP/vcpkg-new/keys.txt"
- done <"$RUNNER_TEMP/new-keys.txt"
-
- count="$(wc -l <"$RUNNER_TEMP/new-keys.txt")"
- echo "vcpkg scope $VCPKG_SCOPE: $count entries to import"
- echo "count=$count" >>"$GITHUB_OUTPUT"
- echo "slug=$(sed -e 's|^vcpkg/||' -e 's|/$||' -e 's|/|-|g' <<<"$VCPKG_SCOPE")" >>"$GITHUB_OUTPUT"
-
- # archive: false accepts a single file and nothing else, and a scope hands over one
- # archive per ABI hash vcpkg had to rebuild, which is 23 files on a cold runner.
- # Zipping the directory keeps the fanout at the artifact root, which is where
- # vcpkg-cache-import.yml looks for keys.txt once download-artifact unpacks it.
- - name: "Upload new vcpkg entries"
- # An empty count means the step above did not run at all, on master and on forks
- if: steps.vcpkg_new.outputs.count != '' && steps.vcpkg_new.outputs.count != '0'
- uses: actions/upload-artifact@v7
- with:
- if-no-files-found: error # a count above zero with nothing staged is a bug
- name: vcpkg-new-${{ steps.vcpkg_new.outputs.slug }}
- path: ${{ runner.temp }}/vcpkg-new
- retention-days: 1
-
# Delete used cache to emulate a ccache update. See https://github.com/actions/cache/issues/342
- - name: "Delete remote compiler cache (ccache)"
- if: matrix.use_ccache == 1 && env.PERSIST_CCACHE == 'true' && github.ref == 'refs/heads/master' && steps.restore_ccache.outputs.cache-hit
+ - name: "[macOS] Delete remote compiler cache (ccache)"
+ if: matrix.os == 'macOS' && matrix.use_ccache == 1 && github.ref == 'refs/heads/master' && steps.restore_ccache.outputs.cache-hit
continue-on-error: true
- # Bash, not the Windows runner's default pwsh, which cannot parse this `if`.
- shell: bash
env:
CACHE_PRIMARY_KEY: ${{ steps.restore_ccache.outputs.cache-primary-key }}
GH_TOKEN: ${{ github.token }}
@@ -1114,8 +529,8 @@ jobs:
echo "Cache deleted successfully"
fi
- - name: "Cache updated compiler cache (ccache)"
- if: matrix.use_ccache == 1 && env.PERSIST_CCACHE == 'true' && github.ref == 'refs/heads/master'
+ - name: "[macOS] Cache updated compiler cache (ccache)"
+ if: matrix.os == 'macOS' && matrix.use_ccache == 1 && github.ref == 'refs/heads/master'
uses: actions/cache/save@v6
with:
key: ${{ steps.restore_ccache.outputs.cache-primary-key }}
@@ -1177,6 +592,17 @@ jobs:
if-no-files-found: error
path: ${{ steps.build.outputs.path }}
+ - name: "[Windows] Upload PDBs (Program Databases)"
+ if: matrix.os == 'Windows' && github.ref_type != 'tag'
+ uses: actions/upload-artifact@v7
+ with:
+ if-no-files-found: error
+ name: ${{ steps.build.outputs.name }}-PDBs
+ path: |
+ build/cockatrice/Release/*.pdb
+ build/oracle/Release/*.pdb
+ build/servatrice/Release/*.pdb
+
- name: "Upload to release"
if: needs.configure.outputs.tag != null && matrix.make_package == '1'
id: upload_release
@@ -1188,33 +614,6 @@ jobs:
tag_name: ${{ needs.configure.outputs.tag }}
run: gh release upload "$tag_name" "$asset_path#$asset_name"
- # Ninja is single-config: binaries and their PDBs land directly in the
- # target directories instead of a Release subdirectory.
- #
- # Archived for tag builds only, and ordered after the release upload so
- # a missing PDB can never block a release from being published. On a tag
- # build this artifact is the only durable copy of the symbols: the Sentry
- # upload is continue-on-error, so without it a failed upload would lose
- # the ability to symbolicate that release permanently. Other builds upload
- # no symbols at all, so archiving them there would only cost storage.
- #
- # The executables travel with their PDBs: re-uploading the pair together is
- # what lets sentry-cli associate the PE's code identifier with the PDB's
- # debug identifier, and the PE is what carries the unwind information.
- - name: "[Windows] Upload debug symbols (exe + pdb)"
- if: matrix.os == 'Windows' && needs.configure.outputs.tag != null
- uses: actions/upload-artifact@v7
- with:
- if-no-files-found: error
- name: ${{ steps.build.outputs.name }}-Symbols
- path: |
- build/cockatrice/*.exe
- build/cockatrice/*.pdb
- build/oracle/*.exe
- build/oracle/*.pdb
- build/servatrice/*.exe
- build/servatrice/*.pdb
-
- name: "Attest binary provenance"
if: steps.upload_release.outcome == 'success'
id: attestation
@@ -1230,143 +629,3 @@ jobs:
BUILD_PATH: ${{ steps.build.outputs.path }}
GH_TOKEN: ${{ github.token }}
run: gh attestation verify "$BUILD_PATH" --repo Cockatrice/Cockatrice
-
- build-vcpkg-nightly:
- strategy:
- fail-fast: false
- matrix:
- include:
- - os: macOS
- target: 13 # EOL 2025-09-15
- runner: macos-15-intel # https://github.com/actions/runner-images/blob/main/images/macos/macos-15-Readme.md
- # macos-26-intel is very slow and fails in CPack during DMG config if not increasing Finder timeout
-
- cmake_generator: Ninja
- make_package: 1
- override_target: 13
- package_suffix: "-macOS13_Intel"
- qt_version: 6.11.*
- qt_modules: qtimageformats qtmultimedia qtshadertools qtwebsockets
- soc: Intel
- type: Release
- vcpkg_scope: 'vcpkg/macos13-x64-xcode26.3/'
- vcpkg_upload: 1 # only one leg per vcpkg_scope hands its archives over
-
- name: ${{ matrix.os }} ${{ matrix.target }}${{ matrix.soc == 'Intel' && ' Intel' || '' }}${{ matrix.type == 'Debug' && ' Debug' || '' }}
- needs: configure
- # The EOL target on the slowest runner is too slow to gate a change, so it runs in the
- # nightly and on tags only, where the vcpkg cache import also has to accept schedule runs.
- if: github.event_name == 'schedule' || github.ref_type == 'tag'
- runs-on: ${{ matrix.runner }}
- timeout-minutes: 100
- steps: *vcpkg-steps
-
- prune-caches:
- name: Prune caches
- # Only master writes caches at all, so only master may clean them up too
- if: github.ref == 'refs/heads/master'
- runs-on: ubuntu-slim # https://github.com/actions/runner-images/blob/main/images/ubuntu-slim/ubuntu-slim-Readme.md
- continue-on-error: true # freeing space must never fail a build
-
- steps:
- # The repository holds 10 GB of cache against entries that want more, and GitHub evicts
- # the least recently used entry whenever that is exceeded. The vcpkg entries are the
- # reason that never happens: the restore above touches every entry under the prefix on
- # every run, so a dead ABI generation keeps its access date fresh forever and age based
- # eviction can never reach it. Deleting by age alone cannot fix that, only knowing which
- # scope an entry belongs to can. The same is true of a compiler cache of a leg that no
- # longer keeps one: its restore would keep touching it, so the live key list below is
- # what retires it.
- #
- # docker-release's image builds keep their layer cache in the same quota: one blob per
- # layer, keyed buildkit-blob-1-, plus one index--1- entry per image
- # scope that lists them. They are written with mode=min, so only the final image stage
- # is kept and the set stays small. A layer a build actually needs is downloaded, which
- # refreshes its access date, so an old date means the builds have stopped using it and
- # age is the right test. Dropping a blob an index still lists costs the next image build
- # one rebuilt layer; docker-release gates nothing, so that is cheaper than keeping the
- # entries forever.
- #
- # codeql.yml's analyze job keeps its extracted database as one entry per analyzed commit,
- # keyed codeql-trap----, about 1.07 GB each. Each run
- # restores the newest entry by prefix and saves its own, so every run that analyzes a new
- # commit makes the previous entry dead, and the action never deletes them: four had
- # accumulated by 2026-11-07 (4.26 GB, 40% of the quota). Retiring them cannot wait for
- # the age rule, because they are not the oldest entries and so not what least-recently-
- # used eviction reaches: the buildkit layers are. Only the newest can be restored again,
- # and a miss costs one re-extraction in a job that reports rather than gates, so keeping
- # exactly that one is safe.
- - name: "Delete unusable cache entries"
- shell: bash
- env:
- GH_TOKEN: ${{ github.token }}
- # Must list every vcpkg_scope of the build-vcpkg and build-vcpkg-nightly
- # matrices above, or the entries of a scope that is still in use get deleted
- # on the next run.
- LIVE_VCPKG_SCOPES: 'vcpkg/macos13-x64-xcode26.3/ vcpkg/macos14-arm64-xcode26.6/ vcpkg/windows10-x64-vs2026/'
- # Must list the ccache key of every leg that keeps a compiler cache
- # (PERSIST_CCACHE in build-linux and build-vcpkg), or a live entry is deleted.
- # The nightly legs and the pull-request legs that finish under the Windows leg
- # keep none, so their leftover entries are reclaimed here instead of waiting out
- # the age rule.
- LIVE_CCACHE_KEYS: 'ccache-Arch ccache-Debian13 ccache-Fedora45debug ccache-Fedora45release ccache-Windows10 ccache-macOS14 ccache-macOS15 ccache-macOS27'
- run: |
- gh api --paginate "repos/$GITHUB_REPOSITORY/actions/caches?per_page=100" \
- --jq '.actions_caches[] | [.id, .ref, .key, .last_accessed_at] | @tsv' >"$RUNNER_TEMP/caches.tsv"
-
- cutoff="$(( $(date -u +%s) - 7 * 24 * 3600 ))"
- live=" ${LIVE_VCPKG_SCOPES} "
- live_ccache=" ${LIVE_CCACHE_KEYS} "
- # The one trap cache a later run can still restore, by prefix. ISO-8601 sorts
- # lexicographically, so this is a plain string compare. Entries scoped to another
- # ref are deleted by the ref rule below whatever this says.
- newest_trap="$(awk -F'\t' '$2 == "refs/heads/master" && $3 ~ /^codeql-trap-/ && $4 > m { m = $4; k = $3 } END { print k }' "$RUNNER_TEMP/caches.tsv")"
-
- deleted=0
- while IFS=$'\t' read -r id ref key accessed; do
- [[ -n $id ]] || continue
- # Only ever touch the key shapes our workflows create (buildkit-* and index-*
- # come from docker-release.yml, codeql-trap-* from codeql-action in codeql.yml),
- # so that an entry of some other workflow can never be deleted by accident
- case "$key" in
- vcpkg/* | ccache-* | Qt* | buildkit-* | index-* | codeql-trap-*) ;;
- *) continue ;;
- esac
-
- reason=""
- if [[ $ref != refs/heads/master ]]; then
- reason="scoped to $ref"
- elif [[ $key == ccache-* ]]; then
- # An exact token compare, because one key can be a prefix of another:
- # ccache-Fedora45 is a prefix of ccache-Fedora45debug and
- # ccache-Fedora45release, and only the latter two are live.
- if [[ $live_ccache != *" $key "* ]]; then
- reason="no pull-request leg keeps it"
- fi
- elif [[ $key == vcpkg/* ]]; then
- # "/", cut the hash off the end to get the scope
- scope="vcpkg/${key#vcpkg/}"
- scope="${scope%/*}/"
- if [[ $live != *" $scope "* ]]; then
- reason="no matrix leg uses $scope"
- fi
- elif [[ $key == codeql-trap-* ]]; then
- if [[ $key != "$newest_trap" ]]; then
- reason="superseded by the newest trap cache"
- fi
- fi
- if [[ -z $reason ]]; then
- stamp="$(date -u -d "$accessed" +%s 2>/dev/null || echo 0)"
- if [[ $stamp -ne 0 && $stamp -lt $cutoff ]]; then
- reason="unused since $accessed"
- fi
- fi
-
- if [[ -n $reason ]]; then
- echo "::notice::deleting $key, $reason"
- gh api -X DELETE "repos/$GITHUB_REPOSITORY/actions/caches/$id"
- deleted=$((deleted + 1))
- fi
- done <"$RUNNER_TEMP/caches.tsv"
-
- echo "::notice::deleted $deleted of $(wc -l <"$RUNNER_TEMP/caches.tsv") cache entries"
diff --git a/.github/workflows/desktop-lint.yml b/.github/workflows/desktop-lint.yml
index f42f26e6a..93ba79464 100644
--- a/.github/workflows/desktop-lint.yml
+++ b/.github/workflows/desktop-lint.yml
@@ -12,7 +12,7 @@ on:
- '!.ci/**'
- '!.github/**'
- '!.tx/**'
- - '!docs/**'
+ - '!doc/**'
- '.ci/lint_cpp.sh'
- '.github/workflows/desktop-lint.yml'
- '.clang-format'
diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml
index 1537d6d4d..937386940 100644
--- a/.github/workflows/docker-release.yml
+++ b/.github/workflows/docker-release.yml
@@ -84,18 +84,12 @@ jobs:
password: ${{ github.token }}
# Don't push for non-publishing triggers
- #
- # The layer cache shares the repository's 10 GB Actions cache quota with the compiler
- # caches (see CCACHE_SIZE in desktop-build.yml). mode=min keeps only the final image
- # stage: the build stage sits behind `COPY . .`, so its compile layer is invalidated by
- # every source change and was never read back, yet it was the bulk of the ~2 GB this
- # cache held. The final runtime stage is the part that still pays off.
- name: "Build image"
if: steps.login.outcome != 'success'
uses: docker/build-push-action@v7
with:
cache-from: type=gha,scope=${{ env.CACHE_SCOPE }}
- cache-to: ${{ case(github.ref == 'refs/heads/master', format('type=gha,mode=min,scope={0}', env.CACHE_SCOPE), '') }}
+ cache-to: ${{ case(github.ref == 'refs/heads/master', format('type=gha,mode=max,scope={0}', env.CACHE_SCOPE), '') }}
context: .
platforms: ${{ matrix.platform }}
push: false
@@ -107,7 +101,7 @@ jobs:
uses: docker/build-push-action@v7
with:
cache-from: type=gha,scope=${{ env.CACHE_SCOPE }}
- cache-to: type=gha,mode=min,scope=${{ env.CACHE_SCOPE }}
+ cache-to: type=gha,mode=max,scope=${{ env.CACHE_SCOPE }}
context: .
labels: |
org.opencontainers.image.description=${{ env.OCI_DESCRIPTION }}
diff --git a/.github/workflows/documentation-build.yml b/.github/workflows/documentation-build.yml
index b2f85e267..91ee14e3e 100644
--- a/.github/workflows/documentation-build.yml
+++ b/.github/workflows/documentation-build.yml
@@ -6,7 +6,7 @@ permissions:
on:
pull_request:
paths:
- - 'docs/**'
+ - 'doc/doxygen/**'
- '.github/workflows/documentation-build.yml'
- 'Doxyfile'
release:
@@ -71,7 +71,7 @@ jobs:
destination_dir: docs # docs will be available at https://cockatrice.github.io/docs/
external_repository: Cockatrice/cockatrice.github.io
publish_branch: master
- publish_dir: ./docs-output/html
+ publish_dir: ./docs/html
- name: Link to Documentation Page
if: github.event_name == 'release' || github.event_name == 'workflow_dispatch'
diff --git a/.github/workflows/vcpkg-cache-import.yml b/.github/workflows/vcpkg-cache-import.yml
deleted file mode 100644
index 0a377a8fd..000000000
--- a/.github/workflows/vcpkg-cache-import.yml
+++ /dev/null
@@ -1,123 +0,0 @@
-name: Import vcpkg cache
-
-# After a runner image update changes every port's ABI hash, the first master run
-# rebuilds protobuf, openssl and abseil from source and saves them into the shared
-# cache, where every later run can restore them.
-#
-# Only master pushes and the nightly schedule run on master are imported here: the
-# collect step, keys.txt and the zip contents all come from the workflow run being
-# imported, so importing from an unmerged pull request would let anyone who can push
-# a branch — or a bot actor such as Dependabot, whose PRs are same-repo — promote
-# arbitrary binaries into the scope that master and tag builds restore and link into
-# signed releases. Forks build untrusted code and must never reach that scope either.
-# A scheduled run is the default branch at the commit a master push already built, so
-# it carries the same trust as a push.
-#
-# repository_dispatch is not used on purpose: it would also resolve to the default branch,
-# but it counts as a low-trust trigger and may only write there when it declares a
-# write-capable cache-mode, which puts it in the same position as workflow_run.
-
-permissions:
- actions: write # needed to save entries into the default branch's cache scope
- contents: read
-
-on:
- workflow_run:
- workflows: ["Build Desktop"]
- types: [completed]
-
-# Every import writes the same scopes, so let them queue instead of racing each other
-concurrency:
- group: vcpkg-cache-import
- cancel-in-progress: false
-
-jobs:
- list:
- name: List new vcpkg entries
- # Only a successful push to the default branch has archives worth importing.
- # Only code that landed on master may populate the scope master runs restore.
- # Bot actors are excluded on top: their runs must never promote binaries into
- # the shared scope even if they somehow target the default branch.
- if: >-
- github.event.workflow_run.conclusion == 'success' &&
- (github.event.workflow_run.event == 'push' || github.event.workflow_run.event == 'schedule') &&
- github.event.workflow_run.head_branch == 'master' &&
- github.event.workflow_run.head_repository.full_name == github.repository &&
- !endsWith(github.event.workflow_run.actor.login, '[bot]') &&
- !endsWith(github.event.workflow_run.triggering_actor.login, '[bot]')
- runs-on: ubuntu-slim # https://github.com/actions/runner-images/blob/main/images/ubuntu-slim/ubuntu-slim-Readme.md
- # workflow_run is a low-trust trigger, so cache access is read-only by default even
- # though this run resolves to the default branch. Assert below that this took effect,
- # because a save under read-only access only warns and would import nothing silently.
- cache-mode: write
- outputs:
- entries: ${{ steps.scan.outputs.entries }}
-
- steps:
- - name: "Download new vcpkg entries"
- uses: actions/download-artifact@v8
- with:
- pattern: vcpkg-new-*
- path: incoming
- run-id: ${{ github.event.workflow_run.id }}
- github-token: ${{ secrets.GITHUB_TOKEN }}
-
- - name: "Assert write access"
- shell: bash
- run: |
- if [[ $ACTIONS_CACHE_MODE != *write* ]]; then
- echo "::error::expected a write-capable cache, got '$ACTIONS_CACHE_MODE'"
- exit 1
- fi
-
- - name: "List cache entries"
- id: scan
- shell: bash
- working-directory: incoming
- run: |
- entries="$(find . -name keys.txt -type f -exec sh -c '
- for manifest; do
- artifact="$(basename "$(dirname "$manifest")")"
- while read -r key path; do
- printf "{\"artifact\": \"%s\", \"key\": \"%s\", \"path\": \"%s\"}\n" \
- "$artifact" "$key" "$path"
- done <"$manifest"
- done
- ' _ {} +)"
- [[ -n $entries ]] || exit 0
- # Compact, because a multi line value cannot be written to GITHUB_OUTPUT
- entries="$(jq -cs '.' <<<"$entries")"
- echo "entries=$entries" >>"$GITHUB_OUTPUT"
- echo "::notice::importing $(jq length <<<"$entries") vcpkg entries"
-
- save:
- name: "Save ${{ matrix.entry.key }}"
- needs: list
- if: needs.list.outputs.entries
- runs-on: ubuntu-slim
- cache-mode: write
- strategy:
- # One job per archive, they run in parallel and each only uploads a single file
- fail-fast: false
- max-parallel: 20
- matrix:
- entry: ${{ fromJSON(needs.list.outputs.entries) }}
-
- steps:
- - name: "Download new vcpkg entries"
- uses: actions/download-artifact@v8
- with:
- pattern: ${{ matrix.entry.artifact }}
- path: incoming
- run-id: ${{ github.event.workflow_run.id }}
- github-token: ${{ secrets.GITHUB_TOKEN }}
-
- - name: "Save vcpkg entry"
- uses: actions/cache/save@v6
- with:
- # The payload is a Windows or macOS archive while the upload runs on Linux, and
- # without this the entry would be stamped as a Linux cache that neither of them
- # is allowed to restore.
- enableCrossOsArchive: true
- key: ${{ matrix.entry.key }}
- path: incoming/${{ matrix.entry.path }}
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index ef88b1f58..54939bdcf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,4 +15,4 @@ compile_commands.json
.cache
.gdb_history
cockatrice/resources/config/qtlogging.ini
-docs-output/
+docs/
diff --git a/.gitmodules b/.gitmodules
index e6f115eb5..732fec7ca 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -2,8 +2,5 @@
path = vcpkg
url = https://github.com/microsoft/vcpkg.git
[submodule "doxygen-awesome-css"]
- path = docs/doxygen/theme
+ path = doc/doxygen/theme
url = https://github.com/jothepro/doxygen-awesome-css.git
-[submodule "external/qttesting"]
- path = external/qttesting
- url = https://github.com/Kitware/QtTesting.git
diff --git a/.tx/github_integration_config.yml b/.tx/github_integration_config.yml
deleted file mode 100644
index 2586b4800..000000000
--- a/.tx/github_integration_config.yml
+++ /dev/null
@@ -1,16 +0,0 @@
-# Integration Docs: https://help.transifex.com/en/articles/6265125-github-installation-and-configuration#h_de29ef5cec
-# Config Parameter: https://help.transifex.com/en/articles/6265125-github-installation-and-configuration#h_a734c3e219
-# Supported Files: https://help.transifex.com/en/collections/3519116-file-formats
-
-filters:
- - filter_type: file
- file_format: QT
- source_language: en
- source_file: cockatrice/cockatrice_en@source.ts
- translation_files_expression: cockatrice/translations/cockatrice_.ts
-
- - filter_type: file
- file_format: QT
- source_language: en
- source_file: oracle/oracle_en@source.ts
- translation_files_expression: oracle/translations/oracle_.ts
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 71f195385..bee47972c 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,11 +1,11 @@
# Cockatrice's main CMakeLists.txt
#
-# This is basically a wrapper to enable/disable the compilation of the different projects:
-# Cockatrice, Oracle, Servatrice, Test
+# This is basically a wrapper to enable/disable the compilation
+# of the different projects: servatrice, cockatrice, test
# This file sets all the variables shared between the projects
# like the installation path, compilation flags etc..
-# CMake 3.16 is required for Qt6 and target_precompile_headers()
+# 3.16 required for Qt6 and target_precompile_headers()
cmake_minimum_required(VERSION 3.16)
# Use compiler cache (ccache)
@@ -20,8 +20,6 @@ option(WITH_CLIENT "Build Cockatrice client" ON)
option(WITH_ORACLE "Build Cockatrice card database tool (Oracle)" ON)
# Compile Servatrice
option(WITH_SERVER "Build Cockatrice server (Servatrice)" OFF)
-# Compile GUI tests (QtTesting widget-level playback)
-option(WITH_GUI_TEST "Build the QtTesting-based GUI test harness" OFF)
# Compile tests
option(TEST "Build tests" OFF)
# Build with AddressSanitizer and UndefinedBehaviorSanitizer
@@ -31,29 +29,25 @@ option(ENABLE_COVERAGE "Build instrumented for GCC line coverage" OFF)
# Use vcpkg regardless of OS
option(USE_VCPKG "Use vcpkg regardless of OS" OFF)
-# The GUI test harness lives in a submodule; a checkout without it must still configure.
-if(WITH_GUI_TEST AND NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/external/qttesting/CMakeLists.txt")
- message(WARNING "WITH_GUI_TEST is ON but external/qttesting is missing, so GUI tests are disabled.\n"
- "Run 'git submodule update --init external/qttesting' to enable them."
- )
- set(WITH_GUI_TEST OFF)
-endif()
-
# Default to "Release" build type
-# A user-provided CMAKE_BUILD_TYPE must be checked before the PROJECT() call
-if(NOT CMAKE_BUILD_TYPE)
+# User-provided value for CMAKE_BUILD_TYPE must be checked before the PROJECT() call
+if(DEFINED CMAKE_BUILD_TYPE)
+ set(CMAKE_BUILD_TYPE
+ ${CMAKE_BUILD_TYPE}
+ CACHE STRING "Type of build"
+ )
+else()
set(CMAKE_BUILD_TYPE
Release
- CACHE STRING "Build type"
+ CACHE STRING "Type of build"
)
endif()
-# ccache drives MSVC too (since 4.7), but only when the compiler writes debug
-# information into the object file: /Zi and /ZI keep it in a shared PDB, and ccache
-# reports them as unsupported rather than risk a stale one. The MSVC flags below
-# therefore use /Z7. MSVC precompiled headers are cached as well, with the same
-# sloppiness the other toolchains need.
-if(USE_CCACHE)
+# ccache does not support MSVC and must not auto-engage on Windows
+# (it is installed unintentionally on the Windows CI runner).
+# NOTE: this keys off the target OS, so a mingw/Ninja configuration on Windows
+# also opts out of ccache even though the GNUCXX branch below supports it.
+if(USE_CCACHE AND NOT WIN32)
find_program(CCACHE_PROGRAM ccache)
if(CCACHE_PROGRAM)
# Support Unix Makefiles and Ninja
@@ -61,16 +55,12 @@ if(USE_CCACHE)
# PCH-aware caching, matching .ci/compile.sh: without this ccache refuses
# to cache any TU that consumes a precompiled header, so every PCH-backed
# target recompiles from scratch on each build.
- # The include_file_mtime/include_file_ctime entries that CI also sets are
- # deliberately not set here: --set-config writes the user's global
- # ccache.conf, and those two would turn off the modified-during-compile
- # check for every project on the machine, not just this one.
execute_process(COMMAND ${CCACHE_PROGRAM} --set-config sloppiness=pch_defines,time_macros)
message(STATUS "Found CCache ${CCACHE_PROGRAM}")
- else()
- # An explicit opt-in must not disappear silently.
- message(STATUS "ccache requested but not found; building without a compiler cache")
endif()
+elseif(USE_CCACHE AND WIN32)
+ # An explicit opt-in must not disappear silently on Windows.
+ message(STATUS "ccache disabled: not supported for the MSVC toolchain on Windows")
endif()
if(WIN32 OR USE_VCPKG)
@@ -81,8 +71,6 @@ if(WIN32 OR USE_VCPKG)
)
# Qt path set by user or env var
if(QTDIR
- OR QTDIR32
- OR QTDIR64
OR DEFINED ENV{QTDIR}
OR DEFINED ENV{QTDIR32}
OR DEFINED ENV{QTDIR64}
@@ -102,23 +90,22 @@ endif()
# A project name is needed for CPack
# Version can be overriden by git tags, see cmake/getversion.cmake
-project("Cockatrice" VERSION 3.2.0)
+project("Cockatrice" VERSION 3.1.2)
-# Set release name if not provided via ENV/CMake var
+# Set release name if not provided via env/cmake var
if(NOT DEFINED GIT_TAG_RELEASENAME)
set(GIT_TAG_RELEASENAME "Reality Check")
endif()
-# Use C++20 for all targets
+# Use c++20 for all targets
set(CMAKE_CXX_STANDARD
20
CACHE STRING "C++ ISO Standard"
)
-set(CMAKE_CXX_STANDARD_REQUIRED ON)
-set(CMAKE_CXX_EXTENSIONS OFF)
+set(CMAKE_CXX_STANDARD_REQUIRED True)
# Set conventional loops
-set(CMAKE_ALLOW_LOOSE_LOOP_CONSTRUCTS ON)
+set(CMAKE_ALLOW_LOOSE_LOOP_CONSTRUCTS true)
# Search path for cmake modules
set(COCKATRICE_CMAKE_PATH "${PROJECT_SOURCE_DIR}/cmake")
@@ -131,31 +118,31 @@ include(createversionfile)
# Define a proper install path
if(UNIX)
- if(APPLE) # macOS
- # Due to the special bundle structure ignore the prefix eventually set by the user
+ if(APPLE)
+ # macOS
+ # Due to the special bundle structure ignore
+ # the prefix eventually set by the user.
set(CMAKE_INSTALL_PREFIX ${CMAKE_BINARY_DIR}/release)
- # Force ccache usage if requested and available
- if(USE_CCACHE)
- get_property(RULE_LAUNCH_COMPILE GLOBAL PROPERTY RULE_LAUNCH_COMPILE)
- if(RULE_LAUNCH_COMPILE)
- message(STATUS "Force enabling ccache usage under macOS")
+ # Force ccache usage if available
+ get_property(RULE_LAUNCH_COMPILE GLOBAL PROPERTY RULE_LAUNCH_COMPILE)
+ if(RULE_LAUNCH_COMPILE)
+ message(STATUS "Force enabling CCache usage under macOS")
+ # Set up wrapper scripts
+ configure_file("${COCKATRICE_CMAKE_PATH}/launch-c.in" launch-c)
+ configure_file("${COCKATRICE_CMAKE_PATH}/launch-cxx.in" launch-cxx)
+ execute_process(COMMAND chmod a+rx "${CMAKE_BINARY_DIR}/launch-c" "${CMAKE_BINARY_DIR}/launch-cxx")
- # Set up wrapper scripts
- configure_file("${COCKATRICE_CMAKE_PATH}/launch-c.in" launch-c)
- configure_file("${COCKATRICE_CMAKE_PATH}/launch-cxx.in" launch-cxx)
- execute_process(COMMAND chmod a+rx "${CMAKE_BINARY_DIR}/launch-c" "${CMAKE_BINARY_DIR}/launch-cxx")
-
- # Set Xcode project attributes to route compilation through our scripts
- set(CMAKE_XCODE_ATTRIBUTE_CC "${CMAKE_BINARY_DIR}/launch-c")
- set(CMAKE_XCODE_ATTRIBUTE_CXX "${CMAKE_BINARY_DIR}/launch-cxx")
- set(CMAKE_XCODE_ATTRIBUTE_LD "${CMAKE_BINARY_DIR}/launch-c")
- set(CMAKE_XCODE_ATTRIBUTE_LDPLUSPLUS "${CMAKE_BINARY_DIR}/launch-cxx")
- endif()
+ # Set Xcode project attributes to route compilation through our scripts
+ set(CMAKE_XCODE_ATTRIBUTE_CC "${CMAKE_BINARY_DIR}/launch-c")
+ set(CMAKE_XCODE_ATTRIBUTE_CXX "${CMAKE_BINARY_DIR}/launch-cxx")
+ set(CMAKE_XCODE_ATTRIBUTE_LD "${CMAKE_BINARY_DIR}/launch-c")
+ set(CMAKE_XCODE_ATTRIBUTE_LDPLUSPLUS "${CMAKE_BINARY_DIR}/launch-cxx")
endif()
- else() # Linux / BSD
+ else()
+ # Linux / BSD
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
- # Fix package build
+ #fix package build
if(PREFIX)
set(CMAKE_INSTALL_PREFIX ${PREFIX})
else()
@@ -163,68 +150,47 @@ if(UNIX)
endif()
endif()
endif()
-elseif(WIN32) # Windows (including 64-bit)
+elseif(WIN32)
set(CMAKE_INSTALL_PREFIX ${CMAKE_BINARY_DIR}/rundir/${CMAKE_BUILD_TYPE})
endif()
-# Define compiler flags
-if(MSVC) # Microsoft Visual C++ compiler
- # /EHsc Enable standard C++ exception handling
- # /MP Enable parallel compilation
- # /permissive- Enable more standards-conforming behavior
- # /utf-8 Set source file encoding and execution character set to UTF-8
- # /W4 Enable warning level 4
- # /Zc:__cplusplus Enable correct __cplusplus reporting
- # /Z7 Generate debugging information in each object file (ccache-cacheable)
- # instead of a compiler-shared PDB, which ccache rejects
- set(CMAKE_CXX_FLAGS "/EHsc /MP /permissive- /utf-8 /W4 /Zc:__cplusplus /Z7")
- # /O2 Balanced optimization
- # /MD Link against the multi-threaded DLL runtime library (Release CRT)
- set(CMAKE_CXX_FLAGS_RELEASE "/O2 /MD")
- # /Od Disable optimization
- # /MDd Link against the multi-threaded Debug DLL runtime library (Debug CRT)
+# Define proper compilation flags
+if(MSVC)
+ # Disable Warning C4251, C++20 compatibility, Multi-threaded Builds, Warn Detection, Unwind Semantics, Debug Symbols
+ set(CMAKE_CXX_FLAGS "/wd4251 /Zc:__cplusplus /std:c++20 /permissive- /W4 /MP /EHsc /Zi")
+ # Visual Studio: Maximum Optimization, Multi-threaded DLL
+ set(CMAKE_CXX_FLAGS_RELEASE "/Ox /MD")
+ # Visual Studio: No Optimization, Multi-threaded Debug DLL
set(CMAKE_CXX_FLAGS_DEBUG "/Od /MDd")
- # Generate PDBs, even in release builds, so developers can better analyze crash logs
- # /DEBUG Enable debug symbols and PDB generation
- # /OPT:REF Remove unreferenced functions/data
- # /OPT:ICF Fold identical COMDAT functions/data
+ # Generate PDB, even when in release (So developers can better analyze crash logs)
set(CMAKE_EXE_LINKER_FLAGS_RELEASE "${CMAKE_EXE_LINKER_FLAGS_RELEASE} /DEBUG /OPT:REF /OPT:ICF")
add_compile_definitions(_SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING)
-elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") # GCC compiler
+elseif(CMAKE_COMPILER_IS_GNUCXX)
+ # linux/gcc, bsd/gcc, windows/mingw
include(CheckCXXCompilerFlag)
- # -O2 Balanced optimization
- # -s Remove symbols from the executable
- set(CMAKE_CXX_FLAGS_RELEASE "-O2 -s")
-
- # -ggdb Produce GDB debugging symbols
- # -O0 Disable optimization
- # -Wall Enable a broad set of useful warnings
- # -Wextra Enable a set of extra warnings
- # -Werror Treat warnings as compilation errors
+ set(CMAKE_CXX_FLAGS_RELEASE "-s -O2")
if(WARNING_AS_ERROR)
set(CMAKE_CXX_FLAGS_DEBUG "-ggdb -O0 -Wall -Wextra -Werror")
else()
set(CMAKE_CXX_FLAGS_DEBUG "-ggdb -O0 -Wall -Wextra")
endif()
- # -Wcast-align Catch unsafe pointer casts
- # -Wmissing-declarations Catch defined functions without a prior declaration
- # -Wno-error=extra Downgrade some -Wextra warnings from errors to warnings
- # -Wno-error=delete-non-virtual-dtor Downgrade delete-non-virtual-dtor from error to warning
- # -Wno-error=sign-compare Downgrade signed/unsigned comparison from error to warning
- # -Wno-error=missing-declarations Downgrade -Wmissing-declarations from error to warning
- # -Wno-error=sfinae-incomplete GCC 16+: Qt MOC + protobuf forward decls trigger this
+ if(APPLE)
+ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++20")
+ endif()
+
set(ADDITIONAL_DEBUG_FLAGS
-Wcast-align
-Wmissing-declarations
+ -Wno-long-long
-Wno-error=extra
-Wno-error=delete-non-virtual-dtor
-Wno-error=sign-compare
-Wno-error=missing-declarations
- -Wno-error=sfinae-incomplete
+ -Wno-error=sfinae-incomplete # GCC 16+: Qt MOC + protobuf forward decls trigger this
)
foreach(FLAG ${ADDITIONAL_DEBUG_FLAGS})
@@ -236,16 +202,9 @@ elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") # GCC compiler
# Reduce compiler I/O by using pipes between stages instead of temp files
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pipe")
-elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang") # Clang and AppleClang compilers
- # -O2 Balanced optimization
+else()
+ # other: osx/llvm, bsd/llvm
set(CMAKE_CXX_FLAGS_RELEASE "-O2")
-
- # -g Include debug information
- # -O0 Disable optimization
- # -Wall Enable a broad set of useful warnings
- # -Wextra Enable a set of extra warnings
- # -Werror Treat warnings as compilation errors
- # -Wno-unused-parameter Suppress warnings about unused function parameters (common in Qt callbacks)
if(WARNING_AS_ERROR)
set(CMAKE_CXX_FLAGS_DEBUG "-g -O0 -Wall -Wextra -Werror -Wno-unused-parameter")
else()
@@ -254,8 +213,6 @@ elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang") # Clang and AppleClang compilers
# Reduce compiler I/O by using pipes between stages instead of temp files
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pipe")
-else() # Unknown compiler
- message(WARNING "Unknown C++ compiler: ${CMAKE_CXX_COMPILER_ID}")
endif()
# Always included, because they define cockatrice_disable_sanitizers_for() and
@@ -263,39 +220,10 @@ endif()
# when ENABLE_SANITIZERS or ENABLE_COVERAGE is on.
include(Sanitizers)
include(Coverage)
-include(CrashReporting)
-# Debug information is what makes a cached object large, and a build configuration needs more
-# room than a per-leg cache bound can spare when it carries full DWARF: ccache then fills to
-# its limit and evicts the entries the next build is about to ask for, so it serves a few
-# percent of calls while holding most of a build. CI sets a lower level so one whole
-# configuration fits; level 1 keeps file and line, which is what a failing test or a sanitizer
-# backtrace reports. Empty (the default) leaves local, distro and release builds exactly as
-# they were. This comes after CrashReporting, so the -g it adds for Sentry symbols cannot
-# raise the level again.
-set(COCKATRICE_DEBUG_INFO_LEVEL
- ""
- CACHE STRING "DWARF level for Debug and Release objects (empty keeps the built-in flags)"
-)
-if(NOT COCKATRICE_DEBUG_INFO_LEVEL STREQUAL "")
- if(MSVC)
- # MSVC has no -g level, so /Z7 is either there or it is not. Level 0 is the only value
- # that means something here: without /Z7 the objects carry no debug information, which is
- # what a cache that has to fit a bound holds. Anything else is the full debug info.
- if(COCKATRICE_DEBUG_INFO_LEVEL STREQUAL "0")
- string(REPLACE " /Z7" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
- else()
- message(WARNING "COCKATRICE_DEBUG_INFO_LEVEL is ignored on MSVC except 0")
- endif()
- else()
- string(APPEND CMAKE_CXX_FLAGS_DEBUG " -g${COCKATRICE_DEBUG_INFO_LEVEL}")
- string(APPEND CMAKE_CXX_FLAGS_RELEASE " -g${COCKATRICE_DEBUG_INFO_LEVEL}")
- endif()
-endif()
-
-# GNU systems need to define the Mersenne exponent for SFMT, so the RNG compiles without warnings
+# GNU systems need to define the Mersenne exponent for the RNG to compile w/o warning
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
- add_compile_definitions(SFMT_MEXP=19937)
+ add_definitions("-DSFMT_MEXP=19937")
endif()
find_package(Threads REQUIRED)
@@ -307,13 +235,13 @@ else()
set(_lib_suffix 32)
endif()
-if(NOT "${QTDIR${_lib_suffix}}" STREQUAL "")
+if(DEFINED QTDIR${_lib_suffix})
list(APPEND CMAKE_PREFIX_PATH "${QTDIR${_lib_suffix}}")
-elseif(NOT "${QTDIR}" STREQUAL "")
+elseif(DEFINED QTDIR)
list(APPEND CMAKE_PREFIX_PATH "${QTDIR}")
-elseif(NOT "$ENV{QTDIR${_lib_suffix}}" STREQUAL "")
+elseif(DEFINED ENV{QTDIR${_lib_suffix}})
list(APPEND CMAKE_PREFIX_PATH "$ENV{QTDIR${_lib_suffix}}")
-elseif(NOT "$ENV{QTDIR}" STREQUAL "")
+elseif(DEFINED ENV{QTDIR})
list(APPEND CMAKE_PREFIX_PATH "$ENV{QTDIR}")
endif()
@@ -321,22 +249,7 @@ message(STATUS "Update Translations: ${UPDATE_TRANSLATIONS}")
include(FindQtRuntime)
-# Reject APIs that were deprecated before the oldest Qt release we support
-# (6.4). Qt 6.5 renamed the switch, so use whichever spelling the found
-# version understands while still compiling everywhere.
-if(Qt6_VERSION VERSION_GREATER_EQUAL 6.5)
- add_compile_definitions(QT_DISABLE_DEPRECATED_UP_TO=0x060400)
-else()
- add_compile_definitions(QT_DISABLE_DEPRECATED_BEFORE=0x060400)
-endif()
-
-# Set once here for every target: these variables are inherited by the
-# subdirectories, so the per-directory copies were redundant. Only the
-# variables are set; qt_standard_project_setup would also change the runtime
-# output directories and the install RPATH.
set(CMAKE_AUTOMOC TRUE)
-set(CMAKE_AUTOUIC TRUE)
-set(CMAKE_AUTORCC TRUE)
# Find other needed libraries
find_package(Protobuf CONFIG)
@@ -348,15 +261,15 @@ if(${Protobuf_VERSION} VERSION_LESS "3.21.0.0" AND NOT EXISTS "${Protobuf_PROTOC
message(FATAL_ERROR "No protoc command found!")
endif()
-# Find OpenSSL
-if(WIN32) # Windows (including 64-bit)
+#Find OpenSSL
+if(WIN32)
find_package(OpenSSL REQUIRED)
if(OPENSSL_FOUND)
include_directories(${OPENSSL_INCLUDE_DIRS})
endif()
endif()
-# Find Visual C++ Redistributable
+#Find VCredist
if(MSVC)
find_package(VCredistRuntime)
endif()
@@ -414,7 +327,7 @@ if(UNIX)
endif()
elseif(WIN32)
set(CPACK_GENERATOR NSIS ${CPACK_GENERATOR})
- if(CMAKE_SIZEOF_VOID_P EQUAL 8)
+ if("${CMAKE_GENERATOR_PLATFORM}" MATCHES "(x64)")
set(TRICE_IS_64_BIT 1)
else()
set(TRICE_IS_64_BIT 0)
@@ -423,15 +336,6 @@ elseif(WIN32)
# Configure file with custom definitions for NSIS.
configure_file("${COCKATRICE_CMAKE_PATH}/NSIS.definitions.nsh.in" "${PROJECT_BINARY_DIR}/NSIS.definitions.nsh")
- # Installer-log sender for the AbortInstall Sentry hook (values parsed out of COCKATRICE_SENTRY_DSN in
- # cmake/CrashReporting.cmake). Generated only when a DSN was parsed: cmake/NSIS.template.in embeds it under
- # the same CPACK_SENTRY_ENABLED check, so a build without reporting never references or carries the helper.
- if(CPACK_SENTRY_ENABLED)
- configure_file(
- "${COCKATRICE_CMAKE_PATH}/SendInstallerLog.ps1.in" "${PROJECT_BINARY_DIR}/SendInstallerLog.ps1" @ONLY
- )
- endif()
-
# Every runtime DLL a Windows install has to contain, as paths relative to the
# install root. .ci/compile.sh reads this and fails the build when the packaged
# installer is missing one of them, or carries a runtime it does not know about.
@@ -506,9 +410,6 @@ if(WITH_ORACLE OR WITH_CLIENT)
add_subdirectory(${CMAKE_SOURCE_DIR}/libcockatrice_models ${CMAKE_BINARY_DIR}/libcockatrice_models)
add_subdirectory(${CMAKE_SOURCE_DIR}/libcockatrice_filters ${CMAKE_BINARY_DIR}/libcockatrice_filters)
endif()
-if(WITH_CLIENT)
- add_subdirectory(${CMAKE_SOURCE_DIR}/libcockatrice_game ${CMAKE_BINARY_DIR}/libcockatrice_game)
-endif()
if(WITH_SERVER)
add_subdirectory(servatrice)
@@ -520,46 +421,11 @@ if(WITH_CLIENT)
set(CPACK_INSTALL_CMAKE_PROJECTS "Cockatrice;Cockatrice;ALL;/" ${CPACK_INSTALL_CMAKE_PROJECTS})
endif()
-if(WIN32 AND WITH_CLIENT)
- # Runs from the installer's $PLUGINSDIR when a purge fails, and is never installed
- # (see LogProcessesUsingInstallDir in cmake/NSIS.template.in), so it has no install
- # rules and is not part of the client's staged tree.
- add_subdirectory(${CMAKE_SOURCE_DIR}/tools/installer-holder-probe ${CMAKE_BINARY_DIR}/tools/installer-holder-probe)
-endif()
-
if(WITH_ORACLE)
add_subdirectory(oracle)
set(CPACK_INSTALL_CMAKE_PROJECTS "Oracle;Oracle;ALL;/" ${CPACK_INSTALL_CMAKE_PROJECTS})
endif()
-if(WITH_GUI_TEST)
- set(QtTesting_QT_VERSION "6")
- # QtTesting registers its own unit tests when BUILD_TESTING is on. They target the
- # Qt version the harness was written for and fail on Qt 6.11 (toolbutton
- # long-press recording), and they are not Cockatrice's tests, so suppress them
- # for the duration of the subdirectory instead of painting ctest red.
- if(DEFINED BUILD_TESTING)
- set(_cockatrice_saved_build_testing ${BUILD_TESTING})
- endif()
- set(BUILD_TESTING
- OFF
- CACHE BOOL "Build the testing tree." FORCE
- )
- add_subdirectory(external/qttesting)
- if(DEFINED _cockatrice_saved_build_testing)
- set(BUILD_TESTING
- ${_cockatrice_saved_build_testing}
- CACHE BOOL "Build the testing tree." FORCE
- )
- else()
- unset(BUILD_TESTING CACHE)
- endif()
- # cl.exe has no warnings-as-errors here, and it rejects -Wno-error with D8021.
- if(NOT MSVC)
- target_compile_options(qttesting PRIVATE -Wno-error)
- endif()
-endif()
-
if(TEST)
include(CTest)
add_subdirectory(tests)
@@ -567,12 +433,5 @@ endif()
if(Qt6_FOUND AND Qt6_VERSION_MINOR GREATER_EQUAL 3)
# Qt6.3+ requires project finalization to support translations
- qt_finalize_project()
+ qt6_finalize_project()
endif()
-
-# Print compiler identification at configuration time
-message(STATUS "C++ compiler: ${CMAKE_CXX_COMPILER_ID}")
-message(STATUS "C++ compiler version: ${CMAKE_CXX_COMPILER_VERSION}")
-message(STATUS "C++ compiler path: ${CMAKE_CXX_COMPILER}")
-message(STATUS "C++ standard: ${CMAKE_CXX_STANDARD}")
-message(STATUS "C++ extensions: ${CMAKE_CXX_EXTENSIONS}")
diff --git a/Doxyfile b/Doxyfile
index 0d508ca38..c12ff4a8a 100644
--- a/Doxyfile
+++ b/Doxyfile
@@ -74,7 +74,7 @@ PROJECT_ICON = cockatrice/resources/cockatrice.png
# entered, it will be relative to the location where Doxygen was started. If
# left blank the current directory will be used.
-OUTPUT_DIRECTORY = docs-output
+OUTPUT_DIRECTORY = docs
# If the CREATE_SUBDIRS tag is set to YES then Doxygen will create up to 4096
# sub-directories (in 2 levels) under the output directory of each output format
@@ -874,7 +874,7 @@ FILE_VERSION_FILTER =
# DoxygenLayout.xml, Doxygen will parse it automatically even if the LAYOUT_FILE
# tag is left empty.
-LAYOUT_FILE = docs/doxygen/DoxygenLayout.xml
+LAYOUT_FILE = doc/doxygen/DoxygenLayout.xml
# The CITE_BIB_FILES tag can be used to specify one or more bib files containing
# the reference definitions. This must be a list of .bib files. The .bib
@@ -1091,8 +1091,8 @@ RECURSIVE = YES
EXCLUDE = build/ \
cmake/ \
cmake-build-debug/ \
- docs/doxygen/theme/docs/ \
- docs/doxygen/theme/include/ \
+ doc/doxygen/theme/docs/ \
+ doc/doxygen/theme/include/ \
vcpkg/
# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or
@@ -1144,7 +1144,7 @@ EXAMPLE_RECURSIVE = NO
# that contain images that are to be included in the documentation (see the
# \image command).
-IMAGE_PATH = docs/doxygen/images
+IMAGE_PATH = doc/doxygen/images
# The INPUT_FILTER tag can be used to specify a program that Doxygen should
# invoke to filter for each input file. Doxygen will invoke the filter program
@@ -1183,7 +1183,7 @@ INPUT_FILTER =
# need to set EXTENSION_MAPPING for the extension otherwise the files are not
# properly processed by Doxygen.
-FILTER_PATTERNS = "*.proto=python docs/doxygen/filters/proto2cpp.py"
+FILTER_PATTERNS = "*.proto=python doc/doxygen/filters/proto2cpp.py"
# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using
# INPUT_FILTER) will also be used to filter the input files that are used for
@@ -1414,7 +1414,7 @@ HTML_FILE_EXTENSION = .html
# of the possible markers and block names see the documentation.
# This tag requires that the tag GENERATE_HTML is set to YES.
-HTML_HEADER = docs/doxygen/html/header.html
+HTML_HEADER = doc/doxygen/html/header.html
# The HTML_FOOTER tag can be used to specify a user-defined HTML footer for each
# generated HTML page. If the tag is left blank Doxygen will generate a standard
@@ -1424,7 +1424,7 @@ HTML_HEADER = docs/doxygen/html/header.html
# that Doxygen normally uses.
# This tag requires that the tag GENERATE_HTML is set to YES.
-HTML_FOOTER = docs/doxygen/html/footer.html
+HTML_FOOTER = doc/doxygen/html/footer.html
# The HTML_STYLESHEET tag can be used to specify a user-defined cascading style
# sheet that is used by each HTML page. It can be used to fine-tune the look of
@@ -1454,9 +1454,9 @@ HTML_STYLESHEET =
# documentation.
# This tag requires that the tag GENERATE_HTML is set to YES.
-HTML_EXTRA_STYLESHEET = docs/doxygen/theme/doxygen-awesome.css \
- docs/doxygen/css/hide_nav_sync.css \
- docs/doxygen/css/cockatrice_docs_style.css
+HTML_EXTRA_STYLESHEET = doc/doxygen/theme/doxygen-awesome.css \
+ doc/doxygen/css/hide_nav_sync.css \
+ doc/doxygen/css/cockatrice_docs_style.css
# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or
# other source files which should be copied to the HTML output directory. Note
@@ -1466,7 +1466,7 @@ HTML_EXTRA_STYLESHEET = docs/doxygen/theme/doxygen-awesome.css \
# files will be copied as-is; there are no commands or markers available.
# This tag requires that the tag GENERATE_HTML is set to YES.
-HTML_EXTRA_FILES = docs/doxygen/js/graph_toggle.js
+HTML_EXTRA_FILES = doc/doxygen/js/graph_toggle.js
# The HTML_COLORSTYLE tag can be used to specify if the generated HTML output
# should be rendered with a dark or light theme.
diff --git a/README.md b/README.md
index 03f02e540..f2a170868 100644
--- a/README.md
+++ b/README.md
@@ -15,10 +15,9 @@
---
-To get started with Cockatrice ⇢ [view our webpage](https://cockatrice.github.io/)
-To get support, or suggest changes to the app ⇢ [file an issue](https://github.com/Cockatrice/Cockatrice/issues) ([How?](https://github.com/Cockatrice/Cockatrice/wiki/How-to-Create-a-GitHub-Ticket-Regarding-Cockatrice))
-To learn about the app and its code ⇢ [read our code documentation](https://cockatrice.github.io/docs/)
-To help with development ⇢ learn [how to contribute](#contribute)
+To get started with Cockatrice ⇢ [view our webpage](https://cockatrice.github.io/)
+To get support, or suggest changes to the app ⇢ [file an issue](https://github.com/Cockatrice/Cockatrice/issues) ([How?](https://github.com/Cockatrice/Cockatrice/wiki/How-to-Create-a-GitHub-Ticket-Regarding-Cockatrice))
+To help with development ⇢ learn [how to contribute](#contribute-)
@@ -84,7 +83,7 @@ Join our [Discord community](https://discord.gg/3Z9yzmA) to connect with other p
### Code
-To commit code to the project, please review our [Contribution Guidelines](https://github.com/Cockatrice/Cockatrice/blob/master/docs/CONTRIBUTING.md) first.
+To contribute code to the project, please review our [guidelines](https://github.com/Cockatrice/Cockatrice/blob/master/.github/CONTRIBUTING.md) first.
We maintain two tags for contributors to easier find issues to potentially work on:
- [](https://github.com/Cockatrice/Cockatrice/issues?utf8=%E2%9C%93&q=is%3Aopen%20is%3Aissue%20label%3A%22Good%20first%20issue%22%20)
Issues tagged in this way provide a simple way to get started. They don't require much experience to be worked on.
@@ -96,17 +95,16 @@ We'll happily advice on how best to implement a feature, or we can show you wher
You can also have a look at our `Todo List` in our [Code Documentation](https://cockatrice.github.io/docs) or search the repo for [`\todo` comments](https://github.com/search?q=repo%3ACockatrice%2FCockatrice%20%5Ctodo&type=code).
-### Documentation [](https://github.com/Cockatrice/Cockatrice/actions/workflows/documentation-build.yml?query=event%3Arelease+event%3Aworkflow_dispatch)
+### Documentation [](https://github.com/Cockatrice/Cockatrice/actions/workflows/documentation-build.yml?query=event%3Apush)
There are various places where useful information for different needs are maintained:
- [Official Code Documentation](https://cockatrice.github.io/docs/)
- [Official Wiki](https://github.com/Cockatrice/Cockatrice/wiki) `Community supported`
- [Official Webpage](https://cockatrice.github.io/)
- [Official README](https://github.com/Cockatrice/Cockatrice/blob/master/README.md) `This file`
-- [Official CONTRIBUTING](https://github.com/Cockatrice/Cockatrice/blob/master/docs/CONTRIBUTING.md)
+- [Official CONTRIBUTING](https://github.com/Cockatrice/Cockatrice/blob/master/.github/CONTRIBUTING.md)
-Cockatrice tries to use the [Google Developer Documentation Style Guide](https://developers.google.com/style/) to ensure consistent documentation.
-We encourage everybody to improve the documentation by suggesting edits based on this guide.
+Cockatrice tries to use the [Google Developer Documentation Style Guide](https://developers.google.com/style/) to ensure consistent documentation. We encourage you to improve the documentation by suggesting edits based on this guide.
### Translation [](https://explore.transifex.com/cockatrice/cockatrice/)
@@ -163,7 +161,7 @@ The following flags (with their non-default values) can be passed to `cmake`:
| `-DTEST=1` | Enable regression tests **Note:** `make test` to run tests, *googletest* will be downloaded if not available |
> [!TIP]
-> Build code documentation ([Doxygen](https://www.doxygen.nl)) according to our [configuration](https://github.com/Cockatrice/Cockatrice/blob/master/Doxyfile) into `/docs-output`:
+> Build code documentation ([Doxygen](https://www.doxygen.nl)) according to our [configuration](https://github.com/Cockatrice/Cockatrice/blob/master/Doxyfile) into `/docs`:
> ```bash
> doxygen Doxyfile
> ```
diff --git a/Testing/README.md b/Testing/README.md
deleted file mode 100644
index cb990ec29..000000000
--- a/Testing/README.md
+++ /dev/null
@@ -1,66 +0,0 @@
-# GUI tests
-
-Widget-level playback tests for the Cockatrice client, driven by
-[QtTesting](https://github.com/Kitware/QtTesting) (`external/qttesting`, a submodule).
-Each `*_test.xml` file is a list of events that QtTesting replays against the running
-application; a test passes when every event is handled without error.
-
-## Running
-
-```bash
-git submodule update --init external/qttesting # once
-cmake -S . -B build/debug -DCMAKE_BUILD_TYPE=Debug -DWITH_SERVER=0 -DWITH_GUI_TEST=ON
-cmake --build build/debug -j 24
-
-./Testing/run_test.sh --all # headless, offscreen
-./Testing/run_test.sh Testing/smoke_test.xml # one file
-./Testing/run_test.sh --visual Testing/smoke_test.xml # shows the window
-```
-
-`WITH_GUI_TEST` is on by default; a checkout without the submodule configures with a
-warning and no harness. `COCKATRICE_BIN` points the runner at another build, and
-`TEST_TIMEOUT` (seconds, default 180) bounds a single test — a failing event inside a
-modal dialog can otherwise leave the process alive. The client also hard-exits a few
-seconds after playback returns, whatever the result.
-
-The tests are **not** part of `ctest`: they need a card database and a developer's
-settings, so they stay a local loop for now.
-
-## Caveats
-
-- **Not hermetic.** The client reads the local settings and card database, so a test can
- pass on one machine and fail on another. Avoid asserting things that restored
- workspace state or user preferences decide (for example the number of open tabs).
-- **English labels only.** Menu entries are addressed by their (translated) text, so the
- tests expect the source language. Widgets are addressed by objectName paths, which are
- stable across translations.
-- Actions that open a dialog are followed by a `pause` event: playback continues in the
- dialog's own event loop, and the pause gives the UI time to build itself.
-- Widget paths include Qt-internal names where a QTabWidget is involved
- (`qt_tabwidget_stackedwidget`), and the deck views in a game are indexed
- (`tabbedDeckView_0`, `tabbedDeckView_1`).
-
-## Writing a test
-
-A test is a `` list. Widgets are named by their path from the main window, e.g.
-`MainWindow/tabSupervisor/stackedWidget/TabGame/phasesMenu`. Useful commands:
-
-| Command | Arguments | Effect |
-|---------|-----------|--------|
-| `activate` | action or menu name | triggers a QAction (menus are opened first) |
-| `key` | Qt key code | presses that key on the focused widget |
-| `pause` | milliseconds | waits, letting the UI settle |
-| `clickCard` | `card,zone` | double click the card found in that zone (plays it from hand) |
-| `flipCard` | `card,zone` | right-click → *Turn Over* |
-| `cardAction` | `card,zone,label` | right-click → first menu entry whose label contains `label` (submenus included) |
-| `moveCard` | `card,srcZone,dstZone` | drags the card onto the destination zone |
-| `drawArrow` | `srcCard,srcZone,dstCard,dstZone` | right-drag an arrow between two cards |
-| `createToken` | `name,color,pt` | creates a token as the active local player |
-| `selectItem` | `item label` | selects the tree/list entry with that label |
-| `typeText` | `text` | types text one key event at a time |
-| `expectProperty` | `property,value` | fails playback unless the widget property matches |
-
-The card, item and property commands are `pqCockatriceEventPlayer`
-(`cockatrice/src/game_graphics/pq_cockatrice_event_player.h`). They resolve cards by name
-and zone through the `QGraphicsScene` and send real mouse events at the card's current
-viewport position, so a test does not depend on the window size or screen resolution.
diff --git a/Testing/card_actions_test.xml b/Testing/card_actions_test.xml
deleted file mode 100644
index 8d6de4c2b..000000000
--- a/Testing/card_actions_test.xml
+++ /dev/null
@@ -1,69 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Testing/deck_editor_open_test.xml b/Testing/deck_editor_open_test.xml
deleted file mode 100644
index ab74b1c63..000000000
--- a/Testing/deck_editor_open_test.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
diff --git a/Testing/deck_editor_search_test.xml b/Testing/deck_editor_search_test.xml
deleted file mode 100644
index 6641ac3b2..000000000
--- a/Testing/deck_editor_search_test.xml
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Testing/dialog_lifecycle_test.xml b/Testing/dialog_lifecycle_test.xml
deleted file mode 100644
index 00510b802..000000000
--- a/Testing/dialog_lifecycle_test.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Testing/game_mechanics_test.xml b/Testing/game_mechanics_test.xml
deleted file mode 100644
index efef78707..000000000
--- a/Testing/game_mechanics_test.xml
+++ /dev/null
@@ -1,101 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Testing/integration_test.xml b/Testing/integration_test.xml
deleted file mode 100644
index 33191d1ac..000000000
--- a/Testing/integration_test.xml
+++ /dev/null
@@ -1,45 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Testing/local_game_lifecycle_test.xml b/Testing/local_game_lifecycle_test.xml
deleted file mode 100644
index bdc964921..000000000
--- a/Testing/local_game_lifecycle_test.xml
+++ /dev/null
@@ -1,178 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Testing/menu_open_close_test.xml b/Testing/menu_open_close_test.xml
deleted file mode 100644
index d894b47b5..000000000
--- a/Testing/menu_open_close_test.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Testing/nonmodal_manage_sets_test.xml b/Testing/nonmodal_manage_sets_test.xml
deleted file mode 100644
index 5fe2bfc0c..000000000
--- a/Testing/nonmodal_manage_sets_test.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
diff --git a/Testing/nonmodal_tip_test.xml b/Testing/nonmodal_tip_test.xml
deleted file mode 100644
index 0f4ffc825..000000000
--- a/Testing/nonmodal_tip_test.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
diff --git a/Testing/nonmodal_view_log_test.xml b/Testing/nonmodal_view_log_test.xml
deleted file mode 100644
index 3369f1fd5..000000000
--- a/Testing/nonmodal_view_log_test.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
diff --git a/Testing/run_test.sh b/Testing/run_test.sh
deleted file mode 100755
index 8ff3fe028..000000000
--- a/Testing/run_test.sh
+++ /dev/null
@@ -1,118 +0,0 @@
-#!/bin/bash
-# Run QtTesting XML test files against the Cockatrice build.
-#
-# Usage:
-# ./Testing/run_test.sh [--visual] [--all | ...]
-#
-# Options:
-# --visual Run with native display (shows the GUI window).
-# Default is headless (QT_QPA_PLATFORM=offscreen).
-# --all Run every *.xml file in Testing/.
-#
-# Environment:
-# COCKATRICE_BIN Path to the client binary (default: build/debug/cockatrice/cockatrice,
-# or the .app bundle binary on macOS).
-# TEST_TIMEOUT Seconds a single test may run before it is killed (default: 180).
-# Uses timeout, or gtimeout from coreutils on macOS.
-#
-# Requires the debug build to exist at build/debug/cockatrice/cockatrice.
-
-set -euo pipefail
-shopt -s nullglob
-
-REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
-if [ -n "${COCKATRICE_BIN:-}" ]; then
- COCKATRICE="$COCKATRICE_BIN"
-elif [ "$(uname -s)" = "Darwin" ]; then
- # A macOS build produces an app bundle, not a bare executable.
- COCKATRICE="$REPO_ROOT/build/debug/cockatrice/cockatrice.app/Contents/MacOS/cockatrice"
-else
- COCKATRICE="$REPO_ROOT/build/debug/cockatrice/cockatrice"
-fi
-TEST_DIR="$REPO_ROOT/Testing"
-TEST_TIMEOUT="${TEST_TIMEOUT:-180}"
-
-# Stock macOS has no `timeout`; coreutils provides `gtimeout`. Without either the
-# runner still works, but a hung test is not killed.
-TIMEOUT=()
-if command -v timeout >/dev/null 2>&1; then
- TIMEOUT=(timeout --foreground "$TEST_TIMEOUT")
-elif command -v gtimeout >/dev/null 2>&1; then
- TIMEOUT=(gtimeout --foreground "$TEST_TIMEOUT")
-else
- echo "Warning: neither timeout nor gtimeout found; tests are not time-limited." >&2
-fi
-
-if [ ! -f "$COCKATRICE" ]; then
- echo "Error: $COCKATRICE not found. Build first:" >&2
- echo " cmake --build build/debug" >&2
- exit 1
-fi
-
-VISUAL=0
-if [ "${1:-}" = "--visual" ]; then
- VISUAL=1
- shift
-fi
-
-if [ "${1:-}" = "--all" ]; then
- shift
- TEST_FILES=("$TEST_DIR"/*.xml)
- if [ ${#TEST_FILES[@]} -eq 0 ]; then
- echo "No *.xml files found in $TEST_DIR" >&2
- exit 1
- fi
-elif [ $# -ge 1 ]; then
- TEST_FILES=("$@")
-else
- echo "Usage: $0 [--visual] [--all | ...]" >&2
- echo " Run a single test: $0 Testing/smoke_test.xml" >&2
- echo " Run all tests: $0 --all" >&2
- echo " Run visually: $0 --visual Testing/smoke_test.xml" >&2
- echo " Run all visually: $0 --visual --all" >&2
- exit 1
-fi
-
-PASS=0
-FAIL=0
-FAILED_TESTS=()
-
-run_one() {
- local file="$1"
-
- local -a cmd=()
- if [ "${#TIMEOUT[@]}" -gt 0 ]; then
- cmd=("${TIMEOUT[@]}")
- fi
- cmd+=("$COCKATRICE" --no-connect --test "$file")
-
- if [ "$VISUAL" -eq 1 ]; then
- "${cmd[@]}"
- else
- env QT_QPA_PLATFORM=offscreen "${cmd[@]}"
- fi
-}
-
-for f in "${TEST_FILES[@]}"; do
- name="$(basename "$f")"
- printf "%-40s " "$name"
- if run_one "$f"; then
- echo "PASS"
- PASS=$((PASS + 1))
- else
- echo "FAIL"
- FAIL=$((FAIL + 1))
- FAILED_TESTS+=("$name")
- fi
-done
-
-TOTAL=$((PASS + FAIL))
-echo ""
-echo "Results: $PASS/$TOTAL passed, $FAIL failed"
-if [ "$FAIL" -gt 0 ]; then
- echo "Failed:"
- for t in "${FAILED_TESTS[@]}"; do
- echo " $t"
- done
- exit 1
-fi
diff --git a/Testing/settings_dialog_test.xml b/Testing/settings_dialog_test.xml
deleted file mode 100644
index 2dd9dda0a..000000000
--- a/Testing/settings_dialog_test.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
diff --git a/Testing/settings_navigation_test.xml b/Testing/settings_navigation_test.xml
deleted file mode 100644
index c112966e0..000000000
--- a/Testing/settings_navigation_test.xml
+++ /dev/null
@@ -1,33 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Testing/settings_search_test.xml b/Testing/settings_search_test.xml
deleted file mode 100644
index ba60251f0..000000000
--- a/Testing/settings_search_test.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Testing/smoke_test.xml b/Testing/smoke_test.xml
deleted file mode 100644
index a31edbdd4..000000000
--- a/Testing/smoke_test.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/cmake/CrashReporting.cmake b/cmake/CrashReporting.cmake
deleted file mode 100644
index 6f9cf4849..000000000
--- a/cmake/CrashReporting.cmake
+++ /dev/null
@@ -1,212 +0,0 @@
-# Crash reporting via sentry-native (Sentry protocol, MIT).
-# Included once from the root CMakeLists.txt, right after the sanitizer/coverage helpers.
-#
-# Inputs:
-# WITH_CRASH_REPORTING -- build the client with the sentry-native SDK (default OFF, our release
-# CI passes ON; packagers and from-source builds stay SDK-free until this
-# ships something real, so offline package builds never fetch the SDK)
-# COCKATRICE_SENTRY_DSN -- Sentry DSN baked into the build (default empty = SDK init skipped at runtime)
-# COCKATRICE_SENTRY_TAG -- sentry-native release tag to vendor via FetchContent
-# ENABLE_SANITIZERS -- forces the sentry backend to "none" (crashpad vs ASan conflict)
-# Defines: cockatrice_target_enable_crash_reporting()
-
-option(WITH_CRASH_REPORTING "Build with crash reporting via sentry-native" OFF)
-set(COCKATRICE_SENTRY_DSN
- ""
- CACHE STRING "Sentry DSN baked into the client; empty disables crash-report upload at runtime"
-)
-# Pinned stable release; tag verified at https://github.com/getsentry/sentry-native/tags.
-set(COCKATRICE_SENTRY_TAG
- "0.17.3"
- CACHE STRING "sentry-native release tag to vendor via FetchContent"
-)
-
-# Wire the SDK (or the no-op fallback) into a client target. Always callable: with the
-# option OFF the compile definitions below make crash_reporter.cpp compile to no-ops.
-function(cockatrice_target_enable_crash_reporting _target)
- if(WITH_CRASH_REPORTING AND TARGET sentry::sentry)
- target_link_libraries(${_target} PRIVATE sentry::sentry)
- target_compile_definitions(${_target} PRIVATE COCKATRICE_WITH_CRASH_REPORTING=1)
- else()
- target_compile_definitions(${_target} PRIVATE COCKATRICE_WITH_CRASH_REPORTING=0)
- endif()
- # Always baked in (possibly empty): main() prefers the COCKATRICE_SENTRY_DSN env var at runtime.
- target_compile_definitions(${_target} PRIVATE "COCKATRICE_SENTRY_DSN=\"${COCKATRICE_SENTRY_DSN}\"")
- # Sentry release for events from this target: PROJECT_VERSION (getversion.cmake ran before this
- # file was included). Deliberately not PROJECT_VERSION_FRIENDLY / VERSION_STRING — that carries
- # a build date, so every build of the same version would land in a different Sentry release.
- # PROJECT_VERSION matches what the installer reports in its own log and what CPack derives the
- # package version from, so client and installer events group under one release.
- target_compile_definitions(${_target} PRIVATE "COCKATRICE_SENTRY_RELEASE=\"${PROJECT_VERSION}\"")
-endfunction()
-
-if(WITH_CRASH_REPORTING AND WITH_CLIENT)
- include(FetchContent)
-
- if(ENABLE_SANITIZERS)
- # The crash handler cannot run under ASan/UBSan, so there is no backend to capture with.
- set(_sentry_backend "none")
- else()
- # Breakpad on every OS (increment 3 probe, 2026-10-14): the crashpad sources
- # do vendor friction-free (FetchContent inits all submodules recursively by
- # default, verified populated in a local build tree), but the crashpad backend
- # would need its out-of-process crashpad_handler binary installed next to the
- # exe, into the macOS bundle, and through NSIS, plus handler-path wiring and
- # notarization care. Breakpad is in-process, needs no extra binary and no
- # packaging on any OS sentry-native builds, so simplicity wins.
- set(_sentry_backend "breakpad")
- endif()
- # FORCE so toggling ENABLE_SANITIZERS on a reused build dir cannot leave a stale backend behind.
- set(SENTRY_BACKEND
- "${_sentry_backend}"
- CACHE STRING "Crash handler backend for sentry-native" FORCE
- )
- unset(_sentry_backend)
-
- if(NOT MSVC)
- # Symbol upload needs debug info in Release binaries: the top-level flags build GCC
- # Release as -s -O2 (stripped, no DWARF) and Clang/Apple Release as plain -O2 (no DWARF),
- # both of which upload nothing useful. Keep -g and drop the strip flag while the SDK is
- # on; the OFF default leaves local, distro and Debug builds untouched. MSVC already
- # links /DEBUG with PDBs in release (see top-level CMakeLists.txt), so it needs nothing.
- string(REGEX REPLACE "(^| )-s( |$)" "\\1\\2" CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE}")
- if(NOT CMAKE_CXX_FLAGS_RELEASE MATCHES "(^| )-g( |$)")
- set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -g")
- endif()
- endif()
-
- # No Qt integration: it runs find_package(Qt6) inside the vendored build, which
- # cannot see our thin/aqt Qt layouts (macOS CI) under foreign toolchains, and its
- # breadcrumbs could carry unredacted log lines into native crash events that bypass
- # before_send. Our attached redacted log preview already carries the log context.
- set(SENTRY_INTEGRATION_QT
- OFF
- CACHE BOOL "Qt log-message breadcrumbs for sentry-native (off: discovery + secret surface)" FORCE
- )
- # The transport stays at the SDK default (curl on Apple/Linux, winhttp on Windows).
- set(SENTRY_BUILD_TESTS
- OFF
- CACHE BOOL "Build sentry-native tests" FORCE
- )
- set(SENTRY_BUILD_EXAMPLES
- OFF
- CACHE BOOL "Build sentry-native examples" FORCE
- )
-
- fetchcontent_declare(
- sentry
- GIT_REPOSITORY https://github.com/getsentry/sentry-native.git
- GIT_TAG ${COCKATRICE_SENTRY_TAG}
- GIT_SHALLOW TRUE
- )
- fetchcontent_makeavailable(sentry)
-
- # Vendored sentry-native (and its breakpad/crashpad trees) warn under our -Wall -Wextra
- # (e.g. breakpad's -Wmissing-field-initializers), and WARNING_AS_ERROR would fail the build
- # on code we cannot fix. Those findings are not actionable for us, so opt the whole vendored
- # tree out of warnings-as-errors while keeping the warnings themselves visible; our own
- # targets keep -Werror. Interface libraries compile nothing, so they are skipped outright:
- # an INTERFACE -Wno-error would propagate to our consumers and weaken their build.
- function(_cockatrice_sentry_opt_out_of_werror _dir)
- get_property(
- _sentry_targets
- DIRECTORY "${_dir}"
- PROPERTY BUILDSYSTEM_TARGETS
- )
- foreach(_sentry_target IN LISTS _sentry_targets)
- get_target_property(_sentry_type ${_sentry_target} TYPE)
- if(_sentry_type STREQUAL "INTERFACE_LIBRARY")
- continue()
- elseif(_sentry_type MATCHES "LIBRARY|EXECUTABLE")
- get_target_property(_sentry_imported ${_sentry_target} IMPORTED)
- if(NOT _sentry_imported)
- target_compile_options(${_sentry_target} PRIVATE -Wno-error)
- endif()
- endif()
- endforeach()
- get_property(
- _sentry_subdirs
- DIRECTORY "${_dir}"
- PROPERTY SUBDIRECTORIES
- )
- foreach(_sentry_subdir IN LISTS _sentry_subdirs)
- _cockatrice_sentry_opt_out_of_werror("${_sentry_subdir}")
- endforeach()
- endfunction()
- if(NOT MSVC)
- _cockatrice_sentry_opt_out_of_werror("${sentry_SOURCE_DIR}")
- endif()
-
- message(STATUS "Crash reporting: sentry-native ${COCKATRICE_SENTRY_TAG} (backend: ${SENTRY_BACKEND})")
-else()
- message(STATUS "Crash reporting: SDK not vendored "
- "(WITH_CRASH_REPORTING=${WITH_CRASH_REPORTING}, WITH_CLIENT=${WITH_CLIENT})"
- )
-endif()
-
-# Packaged release binaries must not ship the DWARF that the symbol upload needs: it is
-# 80-90% of an unstripped binary and nobody downloads a 500 MB installer to get it. The
-# build tree keeps -g (CI uploads symbols from there); the *packaged copy* is split at
-# install time by a CPack install script, which extracts the debug info next to the staging
-# dir and strips the binary. Only ELF (Linux/BSD) needs this: Windows links /DEBUG with
-# separate PDBs, and macOS keeps its dSYM extracted from the bundle.
-option(COCKATRICE_SPLIT_DEBUG_SYMBOLS "Strip DWARF out of packaged release binaries" ON)
-set(COCKATRICE_DEBUG_SYMBOL_DIR
- "${CMAKE_BINARY_DIR}/debug-symbols"
- CACHE PATH "Where the packaged binaries' split debug information is written (kept out of the package)"
-)
-if(WITH_CRASH_REPORTING
- AND WITH_CLIENT
- AND COCKATRICE_SPLIT_DEBUG_SYMBOLS
- AND CMAKE_BUILD_TYPE STREQUAL "Release"
- AND UNIX
- AND NOT APPLE
-)
- find_program(COCKATRICE_OBJCOPY NAMES objcopy)
- find_program(COCKATRICE_STRIP_TOOL NAMES strip)
- if(COCKATRICE_OBJCOPY AND COCKATRICE_STRIP_TOOL)
- configure_file(
- "${CMAKE_CURRENT_LIST_DIR}/SplitDebugSymbols.cmake.in" "${CMAKE_BINARY_DIR}/cmake/SplitDebugSymbols.cmake" @ONLY
- )
- # PRE_BUILD runs after CPack stages the install tree and before the package is produced.
- # CPACK_INSTALL_SCRIPTS would run too early (staging is still empty); the script walks
- # CPACK_TEMPORARY_DIRECTORY, never CMAKE_INSTALL_PREFIX, and refuses to run otherwise.
- list(APPEND CPACK_PRE_BUILD_SCRIPTS "${CMAKE_BINARY_DIR}/cmake/SplitDebugSymbols.cmake")
- message(STATUS "Crash reporting: packaged binaries stripped, debug info split to ${COCKATRICE_DEBUG_SYMBOL_DIR}")
- else()
- message(WARNING "COCKATRICE_SPLIT_DEBUG_SYMBOLS is ON but objcopy/strip were not found; "
- "packaged release binaries will keep their debug information"
- )
- endif()
-endif()
-
-# Installer failure reporting (increment 4): parse COCKATRICE_SENTRY_DSN into the parts the Windows installer
-# needs. These are CPACK_* so CPack snapshots them at include(CPack) time and substitutes them into
-# cmake/NSIS.template.in at package time; on non-Windows they are inert (only the NSIS template consumes them).
-# Set from COCKATRICE_SENTRY_DSN on every configure, so a changed or removed DSN can never leave a stale value
-# behind. Plain variables, not CACHE: they only need to be in scope when include(CPack) later snapshots every
-# CPACK_* variable into CPackConfig.cmake.
-set(CPACK_SENTRY_ENABLED "0")
-set(CPACK_SENTRY_KEY "")
-set(CPACK_SENTRY_ENVELOPE_URL "")
-set(CPACK_SENTRY_DSN_FULL "")
-# Deliberately PROJECT_VERSION: that is what the client bakes as its Sentry release and what CI passes to
-# `sentry-cli debug-files upload`, so installer and client events group under one release. It is not the
-# CPACK_PACKAGE_VERSION the installer logs in its own log line — on Development tags PROJECT_VERSION carries a
-# "-