Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 31 additions & 24 deletions Containerfile.full
Original file line number Diff line number Diff line change
Expand Up @@ -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/*

Expand All @@ -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 \
Expand All @@ -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
Expand Down
121 changes: 121 additions & 0 deletions docs/adr/0008-order-the-image-build-around-the-wine-prefix.md
Original file line number Diff line number Diff line change
@@ -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.
Loading