diff --git a/.ci/Arch/Dockerfile b/.ci/Arch/Dockerfile index 36cf5c4ae..b08e568f3 100644 --- a/.ci/Arch/Dockerfile +++ b/.ci/Arch/Dockerfile @@ -8,10 +8,13 @@ RUN pacman --sync --refresh --sysupgrade --needed --noconfirm \ gtest \ mariadb-libs \ ninja \ + openssl \ protobuf \ qt6-base \ + qt6-declarative \ qt6-imageformats \ qt6-multimedia \ + qt6-shadertools \ qt6-svg \ qt6-tools \ qt6-translations \ diff --git a/.ci/Debian11/Dockerfile b/.ci/Debian11/Dockerfile deleted file mode 100644 index b994863bf..000000000 --- a/.ci/Debian11/Dockerfile +++ /dev/null @@ -1,26 +0,0 @@ -FROM debian:11 - -RUN apt-get update && \ - DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ - build-essential \ - ccache \ - clang-format \ - cmake \ - file \ - g++ \ - git \ - liblzma-dev \ - libmariadb-dev-compat \ - libprotobuf-dev \ - libqt5multimedia5-plugins \ - libqt5sql5-mysql \ - libqt5svg5-dev \ - libqt5websockets5-dev \ - ninja-build \ - protobuf-compiler \ - qt5-image-formats-plugins \ - qtmultimedia5-dev \ - qttools5-dev \ - qttools5-dev-tools \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* diff --git a/.ci/Debian12/Dockerfile b/.ci/Debian12/Dockerfile index 202405b84..fc756aac2 100644 --- a/.ci/Debian12/Dockerfile +++ b/.ci/Debian12/Dockerfile @@ -15,11 +15,14 @@ RUN apt-get update && \ libprotobuf-dev \ libqt6multimedia6 \ libqt6sql6-mysql \ + libssl-dev \ ninja-build \ protobuf-compiler \ + qt6-declarative-dev \ qt6-image-formats-plugins \ qt6-l10n-tools \ qt6-multimedia-dev \ + qt6-shadertools-dev \ qt6-svg-dev \ qt6-tools-dev \ qt6-tools-dev-tools \ diff --git a/.ci/Debian13/Dockerfile b/.ci/Debian13/Dockerfile index d7ab6ac86..bdecb56df 100644 --- a/.ci/Debian13/Dockerfile +++ b/.ci/Debian13/Dockerfile @@ -16,11 +16,14 @@ RUN apt-get update && \ libprotobuf-dev \ libqt6multimedia6 \ libqt6sql6-mysql \ + libssl-dev \ ninja-build \ protobuf-compiler \ + qt6-declarative-dev \ qt6-image-formats-plugins \ qt6-l10n-tools \ qt6-multimedia-dev \ + qt6-shadertools-dev \ qt6-svg-dev \ qt6-tools-dev \ qt6-tools-dev-tools \ diff --git a/.ci/Fedora43/Dockerfile b/.ci/Fedora43/Dockerfile index 27570cf99..463da5a51 100644 --- a/.ci/Fedora43/Dockerfile +++ b/.ci/Fedora43/Dockerfile @@ -7,8 +7,9 @@ RUN dnf install -y \ git \ mariadb-devel \ ninja-build \ + openssl-devel \ protobuf-devel \ - qt6-{qttools,qtsvg,qtmultimedia,qtwebsockets}-devel \ + qt6-{qtdeclarative,qtmultimedia,qtshadertools,qtsvg,qttools,qtwebsockets}-devel \ qt6-qtimageformats \ rpm-build \ xz-devel \ diff --git a/.ci/Fedora42/Dockerfile b/.ci/Fedora44/Dockerfile similarity index 67% rename from .ci/Fedora42/Dockerfile rename to .ci/Fedora44/Dockerfile index ee4a856f2..62238e760 100644 --- a/.ci/Fedora42/Dockerfile +++ b/.ci/Fedora44/Dockerfile @@ -1,4 +1,4 @@ -FROM fedora:42 +FROM fedora:44 RUN dnf install -y \ ccache \ @@ -7,8 +7,9 @@ RUN dnf install -y \ git \ mariadb-devel \ ninja-build \ + openssl-devel \ protobuf-devel \ - qt6-{qttools,qtsvg,qtmultimedia,qtwebsockets}-devel \ + qt6-{qtdeclarative,qtmultimedia,qtshadertools,qtsvg,qttools,qtwebsockets}-devel \ qt6-qtimageformats \ rpm-build \ xz-devel \ diff --git a/.ci/Servatrice_Debian11/Dockerfile b/.ci/Servatrice_Debian12/Dockerfile similarity index 73% rename from .ci/Servatrice_Debian11/Dockerfile rename to .ci/Servatrice_Debian12/Dockerfile index fadc9e0e7..321aa7c0f 100644 --- a/.ci/Servatrice_Debian11/Dockerfile +++ b/.ci/Servatrice_Debian12/Dockerfile @@ -1,4 +1,4 @@ -FROM debian:11 +FROM debian:12 RUN apt-get update && \ DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ @@ -11,11 +11,12 @@ RUN apt-get update && \ git \ libmariadb-dev-compat \ libprotobuf-dev \ - libqt5sql5-mysql \ - libqt5websockets5-dev \ + libqt6sql6-mysql \ + libssl-dev \ ninja-build \ protobuf-compiler \ - qttools5-dev \ - qttools5-dev-tools \ + qt6-tools-dev \ + qt6-tools-dev-tools \ + qt6-websockets-dev \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* diff --git a/.ci/Ubuntu24.04/Dockerfile b/.ci/Ubuntu24.04/Dockerfile index 809b2e43a..715997474 100644 --- a/.ci/Ubuntu24.04/Dockerfile +++ b/.ci/Ubuntu24.04/Dockerfile @@ -15,11 +15,14 @@ RUN apt-get update && \ libprotobuf-dev \ libqt6multimedia6 \ libqt6sql6-mysql \ + libssl-dev \ ninja-build \ protobuf-compiler \ + qt6-declarative-dev \ qt6-image-formats-plugins \ qt6-l10n-tools \ qt6-multimedia-dev \ + qt6-shadertools-dev \ qt6-svg-dev \ qt6-tools-dev \ qt6-tools-dev-tools \ diff --git a/.ci/Ubuntu22.04/Dockerfile b/.ci/Ubuntu26.04/Dockerfile similarity index 76% rename from .ci/Ubuntu22.04/Dockerfile rename to .ci/Ubuntu26.04/Dockerfile index ff2e6e43b..3166b7eb4 100644 --- a/.ci/Ubuntu22.04/Dockerfile +++ b/.ci/Ubuntu26.04/Dockerfile @@ -1,13 +1,15 @@ -FROM ubuntu:22.04 +FROM ubuntu:26.04 RUN apt-get update && \ DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ build-essential \ + ca-certificates \ ccache \ clang-format \ cmake \ file \ g++ \ + gcovr \ git \ libgl-dev \ liblzma-dev \ @@ -15,14 +17,17 @@ RUN apt-get update && \ libprotobuf-dev \ libqt6multimedia6 \ libqt6sql6-mysql \ - libqt6svg6-dev \ - libqt6websockets6-dev \ + libssl-dev \ ninja-build \ protobuf-compiler \ + qt6-declarative-dev \ qt6-image-formats-plugins \ qt6-l10n-tools \ qt6-multimedia-dev \ + qt6-shadertools-dev \ + qt6-svg-dev \ qt6-tools-dev \ qt6-tools-dev-tools \ + qt6-websockets-dev \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* diff --git a/.ci/compile.sh b/.ci/compile.sh index a7c349fb0..606b9d201 100755 --- a/.ci/compile.sh +++ b/.ci/compile.sh @@ -8,12 +8,17 @@ # --suffix renames package with this suffix, requires arg # --server compiles servatrice # --test runs tests +# --sanitize builds with AddressSanitizer and UndefinedBehaviorSanitizer, gcc/clang only +# --coverage instruments the test run for line coverage and prints a gcovr summary, gcc only + # --debug or --release sets the build type ie CMAKE_BUILD_TYPE # --ccache [] uses ccache and shows stats, optionally provide size +# --evict-ccache runs ccache eviction based on given age after build # --dir 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 -# uses env: BUILDTYPE MAKE_INSTALL MAKE_PACKAGE PACKAGE_TYPE PACKAGE_SUFFIX MAKE_SERVER MAKE_NO_CLIENT MAKE_TEST USE_CCACHE CCACHE_SIZE BUILD_DIR CMAKE_GENERATOR TARGET_MACOS_VERSION -# (correspond to args: --debug/--release --install --package --suffix --server --test --ccache --dir ) +# 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 @@ -55,6 +60,14 @@ while [[ $# != 0 ]]; do MAKE_TEST=1 shift ;; + '--sanitize') + MAKE_SANITIZE=1 + shift + ;; + '--coverage') + MAKE_COVERAGE=1 + shift + ;; '--debug') BUILDTYPE="Debug" shift @@ -71,6 +84,15 @@ while [[ $# != 0 ]]; do shift fi ;; + '--evict-ccache') + shift + if [[ $# == 0 ]]; then + echo "::error file=$0::--evict-ccache expects an argument" + exit 3 + fi + CCACHE_EVICTION_AGE=$1 + shift + ;; '--vcpkg') USE_VCPKG=1 shift @@ -84,6 +106,15 @@ while [[ $# != 0 ]]; do BUILD_DIR="$1" shift ;; + '--cmake-generator') + shift + if [[ $# == 0 ]]; then + echo "::error file=$0::--cmake-generator expects an argument" + exit 3 + fi + export CMAKE_GENERATOR=$1 + shift + ;; '--target-macos-version') shift if [[ $# == 0 ]]; then @@ -111,6 +142,15 @@ if [[ ! $BUILD_DIR ]]; then BUILD_DIR="build" fi mkdir -p "$BUILD_DIR" + +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 + # variable. Only the coverage report needs this, so only coverage pays for it. + SRC_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +fi + cd "$BUILD_DIR" # Set minimum CMake Version @@ -122,13 +162,27 @@ if [[ $MAKE_SERVER ]]; then flags+=("-DWITH_SERVER=1") fi if [[ $MAKE_NO_CLIENT ]]; then - flags+=("-DWITH_CLIENT=0" "-DWITH_ORACLE=0" "-DWITH_DBCONVERTER=0") + flags+=("-DWITH_CLIENT=0" "-DWITH_ORACLE=0") fi if [[ $MAKE_TEST ]]; then flags+=("-DTEST=1") fi +if [[ $MAKE_SANITIZE ]]; then + flags+=("-DENABLE_SANITIZERS=ON") +else + # 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 ]]; then + flags+=("-DENABLE_COVERAGE=ON") +fi if [[ $USE_CCACHE ]]; then flags+=("-DUSE_CCACHE=1") + # 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" @@ -139,6 +193,7 @@ if [[ $PACKAGE_TYPE ]]; then fi if [[ $USE_VCPKG ]]; then flags+=("-DUSE_VCPKG=1") + flags+=("-DVCPKG_INSTALL_OPTIONS=--x-abi-tools-use-exact-versions") fi # Add cmake --build flags @@ -156,6 +211,18 @@ function ccachestatsverbose() { # 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, + # which sets a few environment variables + if QTDIR=$(find "$GITHUB_WORKSPACE/Qt" -depth -maxdepth 2 -name macos -type d -print -quit); then + echo "found QTDIR at $QTDIR" + else + echo "could not find QTDIR!" + exit 2 + fi + # the qtdir is located at Qt/[qtversion]/macos + # we use find to get the first subfolder with the name "macos" + # this works independent of the qt version as there should be only one version installed on the runner at a time + export QTDIR if [[ $TARGET_MACOS_VERSION ]]; then # CMAKE_OSX_DEPLOYMENT_TARGET is a vanilla cmake flag needed to compile to target macOS version @@ -171,7 +238,11 @@ if [[ $RUNNER_OS == macOS ]]; then arch="x64" fi mkdir -p "$triplets_dir" - cp "../vcpkg/triplets/$arch-osx.cmake" "$triplet_file" + 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") @@ -239,14 +310,21 @@ cmake --build . "${buildflags[@]}" echo "::endgroup::" 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 [[ $CCACHE_EVICTION_AGE ]]; then + echo "::error file=$0::ccache eviction is enabled while ccache is disabled!" fi if [[ $RUNNER_OS == macOS ]]; then echo "::group::Inspect Mach-O binaries" - for app in cockatrice oracle servatrice dbconverter; do + for app in cockatrice oracle servatrice; do binary="$GITHUB_WORKSPACE/build/$app/$app.app/Contents/MacOS/$app" echo "Inspecting $app..." vtool -show-build "$binary" @@ -259,7 +337,50 @@ fi if [[ $MAKE_TEST ]]; then echo "::group::Run tests" - ctest -C "$BUILDTYPE" --output-on-failure + 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 + # and they stay fully on. + # abort_on_error=1: a bad access leaves the process in an unusable state, so continuing + # only produces a second, more confusing report from the wreckage. + # UBSAN halt_on_error=1: without it UBSan prints "runtime error: ..." but the + # process exits 0 (recover mode), so gtest/ctest report a pass and the job + # stays green. Combined with -fno-sanitize-recover=undefined at compile time, + # the first finding aborts the test process with a non-zero exit. + export ASAN_OPTIONS="detect_leaks=0:abort_on_error=1:print_stacktrace=1:symbolize=1" + export UBSAN_OPTIONS="print_stacktrace=1:halt_on_error=1:symbolize=1" + fi + # Every TEST() is its own CTest entry, so the suite is process-spawn bound rather than CPU + # 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 -C "$BUILDTYPE" --output-on-failure -j "${CTEST_JOBS:-4}" + echo "::endgroup::" +fi + +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" + GCOVR="$(command -v gcovr || true)" + if [[ -z $GCOVR ]]; then + echo "gcovr not found; install it to produce a coverage report" + else + "$GCOVR" --version + # This script configures with `cmake ..`, so the build dir it was started from is the object + # directory and the source root is one level up. Both are passed explicitly: gcovr otherwise + # walks the current directory and trips over in-tree build directories left by other tooling, + # whose stale .gcno files cannot be resolved. --config is named too, so the filters apply no + # matter what directory gcovr happens to start from. + GCOVR_ARGS=("." --root "$SRC_ROOT" --object-directory "." --config "$SRC_ROOT/gcovr.cfg") + "$GCOVR" "${GCOVR_ARGS[@]}" --txt --decisions + "$GCOVR" "${GCOVR_ARGS[@]}" --html-details coverage-details.html --html-title "Cockatrice line coverage" + "$GCOVR" "${GCOVR_ARGS[@]}" --lcov -o coverage.lcov + "$GCOVR" "${GCOVR_ARGS[@]}" --cobertura-pretty -o coverage.xml + "$GCOVR" "${GCOVR_ARGS[@]}" --json-summary-pretty -o coverage-summary.json + echo "Coverage artifacts written to coverage-details.html, coverage.lcov, coverage.xml and coverage-summary.json" + fi echo "::endgroup::" fi @@ -280,4 +401,153 @@ if [[ $MAKE_PACKAGE ]]; then BUILD_DIR="$BUILD_DIR" .ci/name_build.sh "$PACKAGE_SUFFIX" echo "::endgroup::" fi + + if [[ $RUNNER_OS == Windows ]]; then + echo "::group::Check installer for build-tree artifacts" + cd "$BUILD_DIR" + package="$(find . -maxdepth 1 -type f -name 'Cockatrice-*.exe' -print -quit)" + if [[ ! $package ]]; then + echo "::error file=$0::Could not find installer to inspect" + exit 1 + fi + seven_zip="$(command -v 7z || true)" + if [[ ! $seven_zip ]]; then + seven_zip="/c/Program Files/7-Zip/7z.exe" + fi + if [[ ! -f $seven_zip ]]; then + echo "::warning file=$0::7-Zip not found, skipping installer content check" + else + echo "Inspecting $package" + # Fail the build if the installer contains any path left behind by the MSBuild or + # Qt AUTOMOC tooling (build-tree artifacts must live in the build dir, not the install) + if "$seven_zip" l "$package" | + grep -E "_autogen|\.dir[\\/]|\.tlog|(^|[\\/])x64[\\/]|(^|[\\/])\.qt[\\/]|(^|[\\/])\.qsb[\\/]|(^|[\\/])\.lupdate[\\/]|CMakeFiles"; then + echo "::error file=$0::Installer contains build-tree artifacts" + exit 1 + fi + echo "Installer content is clean" + + # Fail the build if the installer does not carry the runtime the + # applications link against - a file that must be there, or a DLL that must + # not be. The runtime DLLs reach the package by globbing every *.dll out of + # the build output directories, so anything that build produced can ride + # along and nothing else in the build objects to it: a Qt module the client + # links statically (Qt6Multimedia.dll, Qt6QuickWidgets.dll) can be left out, + # and the test framework's gtest.dll can be shipped. Either only surfaces on + # a user's machine after an update has already replaced their install, as + # "The code execution cannot proceed because Qt6Multimedia.dll was not + # found" or "abseil_dll.dll is either not designed to run on Windows or it + # contains an error". required-runtime.txt is written by the top level + # CMakeLists.txt from the same module lists the targets are built against + # plus the vcpkg bin directory the app-local copies come from. + if [[ ! -f required-runtime.txt ]]; then + echo "::error file=$0::required-runtime.txt not found in the build dir, cannot verify the runtime" + exit 1 + fi + # 7-Zip writes CRLF on Windows, so the \r has to come off before any + # whole-line comparison below. Normalize separators to /, and strip the + # install-root prefixes an NSIS listing may carry, so the entries can be + # compared as exact relative paths. -xF keeps a '.' in a DLL name from + # being a wildcard; -i because that is how the installed tree behaves - + # the uninstaller clears both $INSTDIR\plugins and $INSTDIR\Plugins. + # The archive side and the manifest side have to agree on line endings. + # CMake writes required-runtime.txt with the platform's line ending and + # 7-Zip writes CRLF on Windows, so a trailing \r survives on $entry and + # defeats the whole-line match - every entry then reads as missing even + # though the listing holds it verbatim. + # [$] rather than \$ so the literal dollar is a bracket expression and the + # linter does not read these as unexpanded shell variables (SC2016). + installer_paths="$("$seven_zip" l -slt "$package" | tr -d '\r' | + sed -n 's/^Path = //p' | + sed -e 's|\\|/|g' -e 's|^[$]INSTDIR/||' -e 's|^[$]OUTDIR/||' -e 's|^[$]PLUGINSDIR/||' -e 's|^\./||' -e 's|^/||')" + manifest="$(tr -d '\r' sets the name of the docker image, these correspond to directories in .ci +# +# usage: source