Cockatrice/.ci/compile.sh
BruebachL 1e1f79db0d
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 26 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions
[Windows] Wait for uninstaller, rework .dll bundling, add logging (#7407)
* [Windows] Purge every runtime DLL on upgrade instead of a named few

A user updating from 3.0.2 to 3.1 got "abseil_dll.dll is either not designed
to run on Windows or it contains an error" and could not start the client.

abseil_dll.dll comes from vcpkg, not Qt. It rides into the install directory
on the "*.dll" glob in cockatrice/CMakeLists.txt, alongside libprotobuf.dll,
liblzma.dll and z.dll, and none of those were named in the upgrade purge. So
on an upgrade the old uninstaller's RMDir /r skipped whichever of them was
locked, the purge did not look for them, HasStaleRuntimeFiles did not look
for them either, and the installer reported success - having written the new
executables next to the previous version's protobuf runtime. Windows then
refused to load it.

The mask list was also already wrong where it existed: it matches zlib*.dll
while vcpkg ships z.dll.

Delete every top-level DLL instead of naming library families. The payload is
written after the purge, so a DLL the new build still needs is put straight
back; what the purge has to guarantee is only that nothing from the previous
build is left to be loaded next to the new executables. Enumerated families
cannot do that - every transitive dependency that arrives between two
releases is one more name to forget, and the three previous rounds of this
work each reasoned about Qt because that is what the reports named at the
time.

HasStaleRuntimeFiles collapses to the matching *.dll probe, so the abort
fires on any survivor rather than only on the families it was told about.

IsExistingCockatriceInstall stays on Qt6*.dll on purpose: it decides whether a
directory is ours at all, and a folder that merely holds some DLLs of its own
must not be purged. Qt6*.dll is enough even when only the protobuf runtime
survived, because the whole Qt set is mapped by the same process that maps
abseil_dll.dll and so is locked or free with it.

* [CI] Assert the whole Windows runtime, not just the Qt modules

required-qt-runtime.txt listed only the Qt modules COCKATRICE_QT_MODULES
names, so it could not see any other runtime go missing. The three previous
rounds of installer work all reasoned about Qt*.dll because that was all the
build knew how to ask about, and the one runtime that actually broke the
reported upgrade - abseil_dll.dll - was outside the set entirely.

- Rename it to required-runtime.txt and take the third party runtime names
  from the vcpkg bin directory the app-local copies come from, so a
  dependency bump cannot leave the list behind and a versioned name such as
  libcrypto-3-x64.dll becoming libcrypto-4-x64.dll cannot either
- Union the three applications' module lists rather than the client's alone:
  Servatrice contributes Qt6Sql.dll, servatrice.exe is installed next to
  cockatrice.exe and cannot start without it, and nothing asserted it
- Add Core to the client's module list. It is the one module nothing can do
  without and it only ever arrived transitively, so Qt6Core.dll was shipped
  and never checked - the new unknown-runtime check flagged it on the first
  run
- Fail the build on a missing runtime, as before, and warn on a runtime
  nothing requires. Only the test framework is a hard failure there: "no
  shipped executable links gtest" is a fact, whereas a runtime missing from
  the manifest may be one the manifest failed to derive, and a check that has
  never run before should not be able to block a release

The warning is meant to be read. Qt6::Quick pulls in Qml, QmlModels,
QmlWorkerScript, QuickBasic, QuickTemplates2 and OpenGL transitively and none
are named, so the manifest is known to be a partial list; the first Windows
run reports the real one, which is evidence rather than a guess about what Qt
links. Fold that back in when it lands.

* [CI] Stop deriving the required runtime from the vcpkg bin directory

The first Windows run of this check failed, and the seven files it named as
missing are not runtime at all: gmock.dll, gmock_main.dll, legacy.dll,
libprotobuf-lite.dll, libprotoc.dll and pthreadVC3/VCE3/VSE3.dll. None of them
is linked by any shipped executable, so a correct package leaves them out.

vcpkg_installed/x64-windows/bin is everything vcpkg installed, not everything
the applications import. The protoc compiler, the GoogleMock library, a port
tool and three CMake FindThreads probes sit beside the genuine runtimes, and a
directory listing cannot tell them apart. Asserting it fails the build over a
package that is right - which is what happened, and it is the same mistake as
the one that produced this bug report: reasoning about a list of names instead
of about the build.

The real source of truth is the three executables' import tables, which
fixup_bundle already walks to decide what to copy. Until CMake can read that
result, the Qt half is what can be derived honestly, from the same module lists
the targets are built against. So assert that, and report the rest rather than
require it:

- Drop the vcpkg bin glob, its gtest filter and the vcpkg-runtime-known marker.
  All three existed only to support the glob.
- Report top level DLLs the manifest does not name without failing. These are
  expected: protobuf, Abseil, OpenSSL, liblzma, ffmpeg and the Qt debug DLLs all
  arrive on the build output glob because nothing in the build declares them by
  name. The list is a report of what the package carries, not a verdict, and it
  is how the missing Qt6Qml/Quick/OpenGL entries above became visible.
- Report a leaked gtest or gmock instead of failing on it. "No shipped
  executable links this" is a fact, but a check that has never passed on a real
  package has not earned the right to block a release, and the first run of an
  enforcing version is exactly when its own wrong assumptions show up.
- Skip DLLs under a subdirectory, which are platform plugins rather than
  imported runtimes, and the NSIS plugins, which extract to $PLUGINSDIR and so
  reach the comparison looking like payload.

Verified against the paths 7-Zip actually listed in the failing package: the
check passes on it, still fails when Qt6Sql.dll or the qwindows plugin is
removed, and still reports gtest.dll and gmock.dll if either is added.

The purge in cmake/NSIS.template.in covers the runtimes the manifest does not
name. That fix is independent of any list and is what makes the reported update
failure go away.

* [Installer] Fix the /R upgrade aborting before it does anything

WaitForLauncherToExit read the wrong registers. System.dll writes its
.rN outputs to $N and its .RN outputs to $RN, but the wait used
p.r0/i.r1 while testing $R1. $R1 therefore stayed empty, and the "did
not exit" test compared "" with "0" - true every time. Every in-app
update passes /PID=, so every upgrade hit AbortInstall before writing
anything; because /R also sets silent, the override just vanished with
exit code 1 and no message. The bug came in with #7372 and is inherited
by this branch, not introduced by it.

Use the uppercase .RN forms, and treat a null or plugin-failed
OpenProcess as the launcher already being gone - the case the old
${Errors} check never caught, because System.dll does not set the NSIS
error flag from the callee's return value.

Also restore the abort dialog for the in-app updater: AbortInstall and
the two inline warnings gated their message box on ${IfNot} ${Silent},
which is false for a /R upgrade, so any abort on that path was equally
silent. The unattended case is a /S install without /R, not /R, so show
the box for $ReinstallMode = 1 or an interactive install. The
stale-runtime abort now goes through AbortInstall so that decision
lives in one place.

* [Installer] Log every upgrade step to %TEMP%\Cockatrice-installer.log

* [Installer] Actually append to the upgrade log (FileSeek END)

* [Installer] Wait for the old uninstaller before replacing files

An NSIS uninstaller copies itself to %TEMP% and runs from there so that it
can delete its own directory; the process the installer started exits
immediately. A plain ExecWait therefore returns before the uninstall has
run, and the temp copy's "RMDir /r $INSTDIR" then executes concurrently
with the install section, deleting files the payload just wrote. That is
why an upgrade could leave Qt6Core/Gui/Multimedia and the other
alphabetically-first DLLs missing while the tail of the directory survived:
a recursive delete marching from the front while the final listing ran.

Pass _?=$INSTDIR to the old uninstaller so it runs in place and ExecWait
actually blocks until the uninstall completes. _?= must be the last
argument and unquoted, even for paths with spaces. Applied to both the
silent /R path (AutoUninstallIfNeeded) and the interactive path
(componentsPagePre).

* [Installer] Tighten the wording of the upgrade comments

Drop the dash-heavy phrasing from the comments this PR adds, and say plainly
in the _?= comment that ExecWait returns before the old uninstaller has run
and the temp copy then deletes the payload the section is still writing.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-09 20:09:34 +02:00

479 lines
18 KiB
Bash
Executable file

#!/bin/bash
# This script is to be used by the ci environment from the project root directory, do not use it from somewhere else.
# Compiles cockatrice inside of a ci environment
# --install runs make install
# --package [<package type>] runs make package, optionally force the type
# --suffix <suffix> renames package with this suffix, requires arg
# --server compiles servatrice
# --test runs tests
# --debug or --release sets the build type ie CMAKE_BUILD_TYPE
# --ccache [<size>] uses ccache and shows stats, optionally provide size
# --evict-ccache <age> runs ccache eviction based on given age after build
# --dir <dir> sets the name of the build dir, default is "build"
# --cmake-generator <generator> sets CMAKE_GENERATOR as used by cmake
# --target-macos-version <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 CCACHE_EVICTION_AGE BUILD_DIR CMAKE_GENERATOR TARGET_MACOS_VERSION
# (correspond to args: --debug/--release --install --package <package type> --suffix <suffix> --server --test --ccache <ccache_size> --dir <dir>)
# exitcode: 1 for failure, 3 for invalid arguments
# Read arguments
while [[ $# != 0 ]]; do
case "$1" in
'--')
shift
;;
'--install')
MAKE_INSTALL=1
shift
;;
'--package')
MAKE_PACKAGE=1
shift
if [[ $# != 0 && ${1:0:1} != - ]]; then
PACKAGE_TYPE="$1"
shift
fi
;;
'--suffix')
shift
if [[ $# == 0 ]]; then
echo "::error file=$0::--suffix expects an argument"
exit 3
fi
PACKAGE_SUFFIX="$1"
shift
;;
'--server')
MAKE_SERVER=1
shift
;;
'--no-client')
MAKE_NO_CLIENT=1
shift
;;
'--test')
MAKE_TEST=1
shift
;;
'--debug')
BUILDTYPE="Debug"
shift
;;
'--release')
BUILDTYPE="Release"
shift
;;
'--ccache')
USE_CCACHE=1
shift
if [[ $# != 0 && ${1:0:1} != - ]]; then
CCACHE_SIZE="$1"
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
;;
'--dir')
shift
if [[ $# == 0 ]]; then
echo "::error file=$0::--dir expects an argument"
exit 3
fi
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
echo "::error file=$0::--target-macos-version expects an argument"
exit 3
fi
TARGET_MACOS_VERSION="$1"
shift
;;
*)
echo "::error file=$0::unrecognized option: $1"
exit 3
;;
esac
done
set -e
# Setup
./servatrice/check_schema_version.sh
if [[ ! $BUILDTYPE ]]; then
BUILDTYPE=Release
fi
if [[ ! $BUILD_DIR ]]; then
BUILD_DIR="build"
fi
mkdir -p "$BUILD_DIR"
cd "$BUILD_DIR"
# Set minimum CMake Version
export CMAKE_POLICY_VERSION_MINIMUM=3.10
# Add cmake flags
flags=("-DCMAKE_BUILD_TYPE=$BUILDTYPE")
if [[ $MAKE_SERVER ]]; then
flags+=("-DWITH_SERVER=1")
fi
if [[ $MAKE_NO_CLIENT ]]; then
flags+=("-DWITH_CLIENT=0" "-DWITH_ORACLE=0")
fi
if [[ $MAKE_TEST ]]; then
flags+=("-DTEST=1")
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"
fi
fi
if [[ $PACKAGE_TYPE ]]; then
flags+=("-DCPACK_GENERATOR=$PACKAGE_TYPE")
fi
if [[ $USE_VCPKG ]]; then
flags+=("-DUSE_VCPKG=1")
flags+=("-DVCPKG_INSTALL_OPTIONS=--x-abi-tools-use-exact-versions")
fi
# Add cmake --build flags
buildflags=(--config "$BUILDTYPE")
function ccachestatsverbose() {
# note, verbose only works on newer ccache, discard the error
local got
if got="$(ccache --show-stats --verbose 2>/dev/null)"; then
echo "$got"
else
ccache --show-stats
fi
}
# 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
flags+=("-DCMAKE_OSX_DEPLOYMENT_TARGET=$TARGET_MACOS_VERSION")
# 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::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
security default-keychain -s build.keychain
security set-keychain-settings -t 3600 -l build.keychain
security unlock-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain
security import certificate.p12 -k build.keychain -P "$MACOS_CERTIFICATE_PWD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_CI_KEYCHAIN_PWD" build.keychain
echo "macOS signing certificate successfully imported and keychain configured."
else
echo "No signing certificate configured. Skipping set up of keychain in macOS environment."
fi
echo "::endgroup::"
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
i=0
while ! hdiutil "$@"; do
if (( ++i >= 10 )); then
echo "Error: hdiutil failed $i times!" >&2
break
fi
sleep 1
done' >"$hdiutil_script"
chmod +x "$hdiutil_script"
flags+=(-DCPACK_COMMAND_HDIUTIL="$hdiutil_script")
fi
elif [[ $RUNNER_OS == Windows ]]; then
# 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 ]]; then
echo "::group::Show ccache stats"
ccachestatsverbose
echo "::endgroup::"
fi
echo "::group::Configure cmake"
cmake --version
echo "Running cmake with flags: ${flags[*]}"
cmake .. "${flags[@]}"
echo "::endgroup::"
echo "::group::Build project"
echo "Running cmake --build with flags: ${buildflags[*]}"
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; do
binary="$GITHUB_WORKSPACE/build/$app/$app.app/Contents/MacOS/$app"
echo "Inspecting $app..."
vtool -show-build "$binary"
file "$binary"
lipo -info "$binary"
echo ""
done
echo "::endgroup::"
fi
if [[ $MAKE_TEST ]]; then
echo "::group::Run tests"
ctest -C "$BUILDTYPE" --output-on-failure
echo "::endgroup::"
fi
if [[ $MAKE_INSTALL ]]; then
echo "::group::Install"
cmake --build . --target install --config "$BUILDTYPE"
echo "::endgroup::"
fi
if [[ $MAKE_PACKAGE ]]; then
echo "::group::Create package"
cmake --build . --target package --config "$BUILDTYPE"
echo "::endgroup::"
if [[ $PACKAGE_SUFFIX ]]; then
echo "::group::Update package name"
cd ..
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' <required-runtime.txt)"
missing=""
checked=0
while IFS= read -r entry; do
if [[ -z $entry || $entry == \#* ]]; then
continue
fi
checked=$((checked + 1))
if ! grep -qxiF "$entry" <<<"$installer_paths"; then
missing+=" $entry"
fi
done <<<"$manifest"
if [[ -n $missing ]]; then
echo "::error file=$0::Installer is missing required runtime files:$missing"
# Print both sides of the comparison. A mismatch here has twice been
# caused by the shape of 7-Zip's output rather than by a missing file,
# and a bare "missing" line cannot tell those apart.
echo "required-runtime.txt asked for $checked path(s); 7-Zip listed $(grep -c '' <<<"$installer_paths") path(s)"
# Indent with parameter expansion rather than sed 's/^/ /', which the
# linter reports as SC2001.
echo "--- required ---"
echo "${manifest//$'\n'/$'\n '}"
echo "--- first 40 paths 7-Zip lists in the installer ---"
listed_head="$(head -40 <<<"$installer_paths")"
echo "${listed_head//$'\n'/$'\n '}"
exit 1
fi
echo "Installer carries the Qt runtime of every module the three applications link ($checked files)"
# Now the other direction: a top level DLL in the package that the manifest
# does not name. The missing check above cannot see a runtime that was
# never expected.
#
# Two things are reported here and neither may fail the build.
#
# The test framework is a fact rather than a guess - no shipped executable
# links gtest or gmock, and the build output directories the "*.dll" glob
# reads are shared with the test targets, so gtest.dll and gmock.dll are
# one filter away from shipping in every release. Even so it is reported
# instead of enforced: a check that has never passed on a real package has
# not earned the right to block a release, and the first Windows run of an
# enforcing version is exactly when its own wrong assumptions show up.
#
# Everything else is expected to be there. required-runtime.txt names the
# Qt modules the three applications link, and the protobuf, Abseil,
# OpenSSL, liblzma, ffmpeg and Qt debug DLLs in the package come in on the
# build output glob instead, because nothing in the build declares them by
# name. So this list is a report of what the package carries, not a verdict.
# Read it when a dependency is added; do not fail on it.
forbidden=""
unexpected=""
while IFS= read -r entry; do
[[ -z $entry ]] && continue
# Top level only. A DLL under Plugins/ or a nested image format plugin is
# a plugin the platform loads by name, not a runtime the executable
# imports, and requiring one here would list all of them every run.
[[ $entry == */* ]] && continue
if grep -qxiF "$entry" <<<"$manifest"; then
continue
fi
# NSIS extracts its plugins to $PLUGINSDIR, which the path normalization
# above strips, so they arrive here looking like payload files. They are
# part of the installer, not of the installed tree.
if [[ $entry == [Nn][Ss][Dd][Ii][Aa][Ll][Oo][Gg][Ss].dll ||
$entry == [Nn][Ss][Ee][Xx][Ee][Cc].dll ||
$entry == [Ss][Yy][Ss][Tt][Ee][Mm].dll ||
$entry == [Uu][Ss][Ee][Rr][Ii][Nn][Ff][Oo].dll ||
$entry == [Ii][Nn][Ee][Tt].dll ]]; then
continue
fi
if [[ $entry == [Gg][Tt][Ee][Ss][Tt]* || $entry == [Gg][Mm][Oo][Cc][Kk]* ]]; then
forbidden+=" $entry"
else
unexpected+=" $entry"
fi
done < <(grep -i '\.dll$' <<<"$installer_paths")
if [[ -n $forbidden ]]; then
echo "::warning file=$0::Installer contains test framework DLLs that no shipped executable links:$forbidden"
echo "A build output directory shared with the test targets has leaked into the package."
fi
if [[ -n $unexpected ]]; then
echo "Top level DLLs in the package beyond the linked Qt modules:$unexpected"
echo "These arrive on the build output glob, not from required-runtime.txt, so they are reported and not required."
fi
if [[ -z $forbidden && -z $unexpected ]]; then
echo "Installer carries no runtime files beyond the linked Qt modules"
fi
fi
echo "::endgroup::"
fi
fi