diff --git a/Containerfile.full b/Containerfile.full index c8ccd05..354776a 100644 --- a/Containerfile.full +++ b/Containerfile.full @@ -51,33 +51,32 @@ ARG TPV_INSTALLER_URL=https://virtual.trainingpeaks.com/TPVirtual-Installer_v6.e ENV DEBIAN_FRONTEND=noninteractive # The TPV installer is PE32, so WoW64 needs the i386 architecture enabled. +# One apt index for the base, Mesa backports and WineHQ together: the repos are +# added before the single update, so the package lists are fetched once and +# dropped once rather than three times. +ARG MESA_SUITE=trixie-backports RUN dpkg --add-architecture i386 \ - && apt-get update && apt-get install -y --no-install-recommends \ - ca-certificates curl gnupg tar zstd cabextract xvfb procps iproute2 \ + && apt-get update && apt-get install -y --no-install-recommends ca-certificates curl \ + && mkdir -p /etc/apt/keyrings \ + && curl -fsSL https://dl.winehq.org/wine-builds/winehq.key \ + -o /etc/apt/keyrings/winehq-archive.key \ + && echo "deb [signed-by=/etc/apt/keyrings/winehq-archive.key] https://dl.winehq.org/wine-builds/debian/ trixie main" \ + > /etc/apt/sources.list.d/winehq.list \ + && echo "deb http://deb.debian.org/debian ${MESA_SUITE} main" \ + > /etc/apt/sources.list.d/mesa.list \ + && apt-get update \ + && apt-get install -y --no-install-recommends \ + zstd xvfb procps iproute2 \ avahi-utils \ libqt5bluetooth5 libqt5charts5 libqt5core5t64 libqt5dbus5 libqt5gui5 \ libqt5location5 libqt5multimedia5 libqt5network5 libqt5networkauth5 \ libqt5positioning5 libqt5qml5 libqt5quick5 libqt5sql5 libqt5sql5-sqlite \ libqt5texttospeech5 libqt5websockets5 libqt5widgets5 libqt5xml5 \ qml-module-qtquick2 \ - libvulkan1 libvulkan1:i386 vulkan-tools \ + libvulkan1 libvulkan1:i386 \ libglib2.0-bin \ - && rm -rf /var/lib/apt/lists/* - -# Stable's Mesa 25.0.7 is the prime suspect for mid-ride i915 GPU hangs (#5). -ARG MESA_SUITE=trixie-backports -RUN echo "deb http://deb.debian.org/debian ${MESA_SUITE} main" \ - > /etc/apt/sources.list.d/mesa.list \ - && apt-get update && apt-get install -y --no-install-recommends -t "${MESA_SUITE}" \ + && apt-get install -y --no-install-recommends -t "${MESA_SUITE}" \ mesa-vulkan-drivers mesa-vulkan-drivers:i386 libgl1-mesa-dri libgl1-mesa-dri:i386 \ - && rm -rf /var/lib/apt/lists/* - -RUN mkdir -p /etc/apt/keyrings \ - && curl -fsSL https://dl.winehq.org/wine-builds/winehq.key \ - -o /etc/apt/keyrings/winehq-archive.key \ - && echo "deb [signed-by=/etc/apt/keyrings/winehq-archive.key] https://dl.winehq.org/wine-builds/debian/ trixie main" \ - > /etc/apt/sources.list.d/winehq.list \ - && apt-get update \ && apt-get install -y --install-recommends "winehq-stable=${WINE_VERSION}" \ && rm -rf /var/lib/apt/lists/* @@ -87,16 +86,15 @@ RUN curl -fsSL "https://github.com/doitsujin/dxvk/releases/download/v${DXVK_VERS && curl -fsSL "https://github.com/HansKristian-Work/vkd3d-proton/releases/download/v${VKD3D_VERSION}/vkd3d-proton-${VKD3D_VERSION}.tar.zst" \ | tar --zstd -x -C /opt -COPY --from=qzbuild /build/qdomyos-zwift /opt/qz/qdomyos-zwift - # uid 1000 so podman --userns=keep-id maps the host user onto this account and # the prefix and volumes stay writable. RUN useradd -m -u 1000 -s /bin/bash tpv \ && mkdir -p /opt/tpv && chown tpv:tpv /opt/tpv -COPY scripts/ /opt/scripts/ -# Wine opens links via xdg-open; this one forwards them to the host browser. -RUN chmod +x /opt/scripts/*.sh \ - && ln -s /opt/scripts/xdg-open-host.sh /usr/local/bin/xdg-open + +# Only the two scripts the prefix build needs. Copying the rest here would make +# any script edit re-run the TPV installer below. +COPY scripts/build-prefix.sh scripts/xvfb-lib.sh /opt/scripts/ +RUN chmod +x /opt/scripts/*.sh USER tpv ENV WINEPREFIX=/opt/tpv/wine-template \ @@ -107,6 +105,15 @@ RUN /opt/scripts/build-prefix.sh USER root RUN chown -R tpv:tpv /opt/tpv + +# Everything below is cheap to rebuild, so it sits after the prefix: bumping +# QZ_COMMIT or editing a script no longer reruns the Wine prefix build. +COPY --from=qzbuild /build/qdomyos-zwift /opt/qz/qdomyos-zwift +COPY scripts/ /opt/scripts/ +# Wine opens links via xdg-open; this one forwards them to the host browser. +RUN chmod +x /opt/scripts/*.sh \ + && ln -sf /opt/scripts/xdg-open-host.sh /usr/local/bin/xdg-open + USER tpv # See README: Qt resolves bluetoothd's path from /proc then execs it inside the diff --git a/docs/adr/0008-order-the-image-build-around-the-wine-prefix.md b/docs/adr/0008-order-the-image-build-around-the-wine-prefix.md new file mode 100644 index 0000000..57e20ad --- /dev/null +++ b/docs/adr/0008-order-the-image-build-around-the-wine-prefix.md @@ -0,0 +1,121 @@ +# 0008. Order the image build around the Wine prefix, and drop packages nothing uses + +- **Status:** Accepted +- **Date:** 2026-09-20 +- **Deciders:** Ben Atkinson +- **Feature / area:** container-build +- **Builds on:** none — first ADR for container-build +- **Supersedes / Superseded by:** none + +## What problem were we trying to solve? + +`Containerfile.full` is expensive: a full build is **438 s** and the image is +**6.69 GB**. Most of that is inherent (compiling qdomyos-zwift, Wine, Mesa, +DXVK, VKD3D and a template WINEPREFIX), but the *everyday* cost was not. + +The single most expensive layer is `RUN /opt/scripts/build-prefix.sh`, which +runs `wineboot`, installs DXVK and VKD3D, then downloads the proprietary TPV +installer and drives Inno Setup under Xvfb with a 900 s timeout. Two unrelated +things sat **above** it and invalidated it: + +| Step | Before | After | +|---|---|---| +| `COPY --from=qzbuild` (QZ binary) | 12 | 18 | +| `COPY scripts/` (all scripts) | 14 | 19 | +| `RUN build-prefix.sh` | **18** | **15** | + +So editing any script — `entrypoint.sh`, `xdg-open-host.sh`, `verify.sh` — or +bumping `QZ_COMMIT` re-ran the Wine prefix build and re-downloaded the +installer. Measured on a one-line edit to `scripts/entrypoint.sh`, the log +showed it really happening: + +``` +==> wineboot (initialising /opt/tpv/wine-template) +==> DXVK +==> TPV launcher (Inno Setup; /VERYSILENT - WiX flags are ignored) +==> TPV launcher installed into the image: TPVirtual-Launcher.exe +``` + +## What did we try? + +### Attempt 1: build the container in CI to catch this ❌ rejected earlier + +Covered in ADR-0007: too slow, and it covers almost none of the diffs. Not +revisited here. + +### Attempt 2: drop i386 multiarch ⏸ deferred, not rejected + +The largest single saving available: **77 MB of downloads** (and considerably +more installed) for `mesa-vulkan-drivers:i386`, `libgl1-mesa-dri:i386` and +`libvulkan1:i386`. The Containerfile says i386 is enabled because the TPV +installer is PE32, which is a *build-time* need. If the launcher and the game +are both 64-bit it is dead weight at runtime. + +Not taken here, because proving it needs a build **and an actual ride**. If any +32-bit code runs, the ride breaks and only riding finds out. It belongs in its +own change that can be tested that way. + +### Attempt 3: `debian:trixie` → `trixie-slim` ⏸ deferred + +A measured 43 MB (124 MB → 81 MB), but slim drops packages this image may rely +on indirectly. Same reasoning: separable, and worth testing on its own. + +### Attempt 4: reorder around the prefix, and remove unused packages ✅ chosen + +## What did we land on, and why? + +**Reordering.** Only `build-prefix.sh` and `xvfb-lib.sh` are copied before the +prefix build. The QZ binary and the remaining scripts are copied after it, +where they are cheap to rebuild. Nothing about the resulting image changes; +only the order of the layers does. + +**Packages removed** from the runtime stage, after checking every script for +the binaries they invoke: + +| Package | Why it can go | +|---|---| +| `gnupg` | 7.1 MB, **26 packages**. Verified unnecessary: trixie's apt verifies the `signed-by` WineHQ key through Sequoia, and `apt-get update` resolves `winehq-stable` with neither `gnupg` nor `gpgv` installed | +| `vulkan-tools` | 1.8 MB, 9 packages. Referenced nowhere in the repo | +| `cabextract` | Referenced nowhere | +| `tar` | Already Essential in the base image; listing it installed nothing | + +**Packages deliberately kept**, because grepping found real uses that a size +pass would otherwise have removed: + +- **`curl`** — not just build-time. `scripts/entrypoint.sh` re-downloads the TPV + installer at **runtime** when the launcher is missing from the volume. +- **`zstd`** — `tar --zstd` extracts the VKD3D-Proton release at build time. +- **`procps`** — no script calls `ps`, but it is 1 MB and Wine's process + handling is not worth the risk. + +**One apt index.** The three `apt-get update` runs became one: the WineHQ and +Mesa repositories are added before the update, so the lists are fetched once +and dropped once. + +**Measured results:** + +| | Before | After | Change | +|---|---|---|---| +| Image size | 6.69 GB | 6.63 GB | −60 MB | +| Full build | 438 s | 385 s | −53 s (−12%) | +| **Rebuild after a script edit** | **67 s** | **3 s** | **−95%** | +| Cached layers on that rebuild | 19 | 26 | | +| Prefix build re-ran? | **yes** | no (`Using cache`) | | + +Both full builds ran with only 3 and 5 cached layers, so that comparison is +fair rather than an artefact of a warm cache. + +## What does this cost us? + +- **The apt layer is coarser.** One `RUN` now installs the base packages, Mesa + and Wine, so changing any of them rebuilds all three. They change rarely, and + the single index fetch is worth more than the granularity. +- **The QZ bump case is argued, not measured.** The step ordering proves the + binary now lands after the prefix, but bumping `QZ_COMMIT` to time it would + mean a full recompile on both trees. The reordering is structural, so the + measured script-edit case exercises the same mechanism. +- **The two big size levers are still on the table**, and this ADR does not + take them: i386 (77 MB of downloads) and `trixie-slim` (43 MB). Both need a + real ride to validate, so they stay separable. +- **`procps` is kept on suspicion, not evidence.** If a later pass wants the + 1 MB, check whether Wine or `wineserver` shells out to `ps` first.