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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ Configure via environment:
| `QZ_HR_BELT` | `TRACKR HR 7DC2` | BLE name of your HR strap |
| `TPV_GPU` | `auto` | `intel`, `nvidia`, `none` |
| `STATE` | `~/.local/share/tpv-full` | Where the Wine prefix and game data live |
| `TZ` | host zone (`timedatectl`) | Time zone for TPV's HUD clock; set to override |

`verify.sh` checks the five things that actually matter: the Bluetooth backend
chosen, the trainer connection, the DIRCON listeners, whether the mDNS advert
Expand Down
53 changes: 53 additions & 0 deletions docs/adr/0003-pass-the-host-timezone-into-the-container.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# 0003. Pass the host timezone into the container and refresh the Wine prefix when it changes

- **Status:** Accepted
- **Date:** 2026-09-17
- **Deciders:** Ben Atkinson
- **Feature / area:** container-timezone
- **Builds on:** none — first ADR for container-timezone
- **Supersedes / Superseded by:** none

## What problem were we trying to solve?

TPV's HUD clock showed UTC instead of local time: an hour behind on a
`Europe/London` host during BST (#2). The container never received the host's
zone (`TZ` unset, `/etc/localtime` → `Etc/UTC`), and Wine derives the Windows
time zone from the Linux one. The fix had to follow the host automatically,
handle DST, still allow an explicit `TZ=` override, and work for existing users
whose Wine prefix was already created under UTC.

## What did we try?

### Option 1: pass `TZ` only ❌ considered, not enough on its own

`TZ` alone relies on the image's zoneinfo and a zone name `timedatectl` can
report. Some hosts don't have `timedatectl`, so there has to be a fallback.

### Option 2: bind-mount `/etc/localtime` only ❌ considered, not enough on its own

This gives the right offset, including DST, from the host's own zone file, but
no zone *name* for anything that reads `TZ`, and no simple override.

### Attempt: fix the environment and leave the prefix alone ❌ failed in practice

Even with the container's clock correct, a prefix created under UTC kept an
empty `TimeZoneKeyName`. Wine only rewrites it when the prefix is updated, so
existing installs would still show UTC.

## What did we land on, and why?

- **`run-tpv.sh`:** passes `TZ` (an explicit `TZ` override, else `timedatectl`'s
zone) **and** bind-mounts `/etc/localtime` read-only. Together they cover
zone-name and zone-file consumers.
- **`scripts/entrypoint.sh`:** `sync_timezone` keeps a marker file in the
prefix (`TZ` plus a checksum of `/etc/localtime`). When the marker changes, it
runs `wineboot -u` once and waits for `wineserver`. Existing UTC-era prefixes
pick up the zone on the next start, without a `wineboot` on every launch.

## What does this cost us?

- **Slower start after a zone change:** the first start after a zone change (or
on an old prefix) runs a `wineboot -u` prefix update.
- **One more host file mounted** into the container: `/etc/localtime`, read-only.
- **Not yet verified here:** that the HUD shows local time including the
BST/GMT switch, and that `TZ=...` overrides the detected zone.
7 changes: 7 additions & 0 deletions run-tpv.sh
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@ if [ ${#DISPLAY_ARGS[@]} -eq 0 ]; then
exit 1
fi

TIMEZONE=()
HOST_TZ="${TZ:-$(timedatectl show -p Timezone --value 2>/dev/null || true)}"
[ -n "$HOST_TZ" ] && TIMEZONE+=(-e "TZ=${HOST_TZ}")
[ -e /etc/localtime ] && TIMEZONE+=(-v /etc/localtime:/etc/localtime:ro)
echo "==> timezone: ${HOST_TZ:-host /etc/localtime}"

INHIBIT=()
if [ "$MODE" != qz ] && command -v gnome-session-inhibit >/dev/null; then
INHIBIT=(gnome-session-inhibit --inhibit idle:suspend
Expand All @@ -76,6 +82,7 @@ exec "${INHIBIT[@]}" podman run --rm --name tpv \
--network=host \
"${GPU[@]}" "${AUDIO[@]}" \
"${DISPLAY_ARGS[@]}" \
"${TIMEZONE[@]}" \
-e "QZ_TRAINER=${TRAINER}" \
-e "QZ_HR_BELT=${HR_BELT}" \
-e "TPV_DIRECT=${TPV_DIRECT:-0}" \
Expand Down
13 changes: 13 additions & 0 deletions scripts/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,22 @@ wait_for_dircon() {
echo " WARNING: DIRCON never came up; TPV will find no trainer." >&2
}

sync_timezone() {
# Wine rewrites TimeZoneKeyName only on a prefix update, so a UTC-era prefix needs wineboot -u.
local marker="$WINEPREFIX/.tpv-timezone"
local current
current="${TZ:-}:$(cksum < /etc/localtime 2>/dev/null || true)"
[ -f "$marker" ] && [ "$(cat "$marker")" = "$current" ] && return 0
echo "==> timezone changed (${TZ:-/etc/localtime}); updating the Wine prefix"
wineboot -u >/dev/null 2>&1 || true
wineserver -w
printf '%s' "$current" > "$marker"
}

run_tpv() {
seed_prefix
ensure_tpv
sync_timezone
# TPV refuses a direct launch of the game exe ("must be launched using the
# TPVirtual-Launcher"), so the launcher is the only supported path. Do not
# exec it: it hands off to the game and exits, which would take the
Expand Down