# ARM64 variant (Raspberry Pi 5, Ampere, Apple silicon under Linux). # # Architecture translation strategy: # * x86_64 Windows PE (Plutonium/IW4x/T7x .exe) -> Wine 11 thunked WoW64 + # libarm64ecfex.dll (FEX as CPU plug-in, shipped by Hangover 11.9). # * Linux helper CLIs (plutonium-updater, iw4x-launcher) -> built natively # for aarch64 from source. Upstream ships no aarch64-linux release, so the # alternative would be running their x86_64 binaries under box64; building # native instead drops the user-space x86 emulator from the runtime image. # # Base: Debian 13 trixie. Hangover publishes official .deb bundles for this # exact distro/arch, so Wine 11 is consumed pre-built. # --------------------------------------------------------------------------- # Stage 1: build plutonium-updater (Rust) natively for aarch64-linux. # --------------------------------------------------------------------------- FROM --platform=linux/arm64 rust:1-trixie AS rust-builder RUN apt-get update && apt-get install -y --no-install-recommends \ git ca-certificates pkg-config libssl-dev \ && rm -rf /var/lib/apt/lists/* WORKDIR /build RUN git clone --depth 1 https://github.com/mxve/plutonium-updater.rs.git WORKDIR /build/plutonium-updater.rs RUN cargo build --release # --------------------------------------------------------------------------- # Stage 2: build iw4x-launcher (C++ via build2) natively for aarch64-linux. # --------------------------------------------------------------------------- FROM --platform=linux/arm64 debian:trixie-slim AS cpp-builder ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates curl wget git \ g++ gcc make \ tar xz-utils \ && rm -rf /var/lib/apt/lists/* # build2 ships no aarch64-linux binaries; the install script compiles the # toolchain from source. The installer uses curl internally to fetch the # source tarball (see check_cmd curl in build2-install-*.sh). --trust yes # accepts the cppget.org repo cert without manual fingerprint entry # (acceptable inside a one-shot build container). RUN curl -fL -o build2-install.sh https://download.build2.org/0.18.1/build2-install-0.18.1.sh \ && sh build2-install.sh --trust yes --no-check --jobs $(nproc) --yes /usr/local \ && rm build2-install.sh WORKDIR /build RUN git clone --depth 1 https://github.com/iw4x/launcher.git iw4x-launcher WORKDIR /build/iw4x-launcher # bindist-gcc-static.sh prompts for confirmation; -y answers yes. The script # emits a flattened .tar.xz under dist-output/ containing only the binary. # # Best-effort by design. Upstream publishes no aarch64 launcher binary, so this # is the only way to get one, and it is periodically broken by upstream changes # we cannot pin around from here (currently iw4x/launcher#76: their pregenerated # ODB sources are stale relative to their own libodb constraint). Failing the # whole arm64 image over it would take Plutonium T4/T5/T6/IW5 and Alterware T7x # down with it, none of which need this binary. So record the failure and let # iw4xentry.sh refuse only iw4x, at runtime, with an explanation. # IW4X_LAUNCHER_REF only exists to key the layer cache on upstream's HEAD. The # fetch below still uses #main; without this the RUN's command string never # changes, so BuildKit reuses the cached layer and upstream fixes are invisible # until the cache is evicted. That matters more now than it looks: the `|| {}` # below makes this step exit 0 even when the compile fails, so a *failed* # launcher build gets cached as a success and would keep shipping the # .unavailable marker long after upstream is fixed. CI passes the resolved # commit SHA; the `main` default only affects manual local builds. ARG IW4X_LAUNCHER_REF=main RUN echo "upstream iw4x/launcher ref: ${IW4X_LAUNCHER_REF}" \ && { sh bindist-gcc-static.sh -y -j $(nproc) "launcher@https://github.com/iw4x/launcher.git#main" \ || { echo "WARNING: iw4x-launcher failed to build — this image will NOT support PLUTAINER_GAME=iw4x"; \ touch /build/iw4x-launcher-build-failed; }; } # /out must never be empty: stage 3 copies the directory (not the file) so that # a missing binary does not break the COPY, and BuildKit is happier copying a # directory that has something in it. RUN mkdir -p /out \ && if [ -f /build/iw4x-launcher-build-failed ]; then \ echo "iw4x-launcher could not be built for this architecture. See iw4x/launcher#76." \ > /out/iw4x-launcher.unavailable; \ else \ tar -xJf dist-output/*.tar.xz -C /out \ && find /out -type f -name 'launcher' -exec mv {} /out/iw4x-launcher \; ; \ fi \ && ls -la /out # --------------------------------------------------------------------------- # Stage 3: runtime image. # --------------------------------------------------------------------------- FROM --platform=linux/arm64 debian:trixie-slim ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates \ wget \ tar \ xz-utils \ unzip \ jq \ python3 \ procps \ findutils \ && rm -rf /var/lib/apt/lists/* # Hangover 11.9 = Wine 11 + FEX + box64 as Wine CPU plug-ins (the in-Wine # box64 is unrelated to a Linux-user-space box64; we do not need the latter # because the helper CLIs are native aarch64). RUN HANGOVER_TAG="hangover-11.9" \ && HANGOVER_TAR="hangover_11.9_debian13_trixie_arm64.tar" \ && wget -q "https://github.com/AndreRH/hangover/releases/download/${HANGOVER_TAG}/${HANGOVER_TAR}" \ && mkdir hangover-pkg \ && tar -xf "${HANGOVER_TAR}" -C hangover-pkg \ && apt-get update \ && apt-get install -y --no-install-recommends ./hangover-pkg/*.deb \ && rm -rf hangover-pkg "${HANGOVER_TAR}" /var/lib/apt/lists/* RUN useradd -m plutainer ENV WINEDLLOVERRIDES="mscoree,mshtml=" USER plutainer WORKDIR /home/plutainer/.plutainer # No X server anywhere in the image: every server we run is headless. t7x needs # `-headless` for that (see alterentry.sh); wineboot only warns about the # missing display driver and still initialises the prefix. RUN wineboot -u && wineserver -w COPY --from=rust-builder --chown=plutainer:plutainer \ /build/plutonium-updater.rs/target/release/plutonium-updater \ /home/plutainer/.plutainer/plutonium-updater # Directory copy, not a file copy: when the launcher build failed there is no # iw4x-launcher to copy and a file COPY would abort the image build, which is # exactly the coupling we are removing. /out then holds the .unavailable # marker instead and iw4xentry.sh reports it at startup. COPY --from=cpp-builder --chown=plutainer:plutainer \ /out/ \ /home/plutainer/.plutainer/ # Community config seeds for first-run scaffolding. Entrypoint copies these # into the bind mount on start with cp -n (never overwrites user files). # Disable per-stack via PLUTAINER_SKIP_SEED. # # Vendored, not fetched — see the equivalent block in Dockerfile for why, and # keep the two in sync. tools/refresh-seeds.sh is the only thing that should # rewrite seed-configs/; each game's SOURCE file records its upstream commit. COPY --chown=plutainer:plutainer seed-configs/ seed-configs/ COPY --chown=plutainer:plutainer scripts/ . # Everything executable, rather than a list that silently rots as scripts move # between directories. RUN find . -type f -name '*.sh' -exec chmod +x {} + && chmod +x rcon-cli USER root RUN ln -s /home/plutainer/.plutainer/rcon-cli /usr/local/bin/rcon-cli USER plutainer # No SteamCMD here: Valve ships x86_64 binaries only. steamentry.sh finds it # missing and refuses with an explanation — a capability check, so the day an # arm64 SteamCMD exists, this starts working with no code change. STOPSIGNAL SIGKILL HEALTHCHECK --interval=1m --timeout=10s --start-period=5m --retries=3 \ CMD ./healthcheck.sh ENTRYPOINT ["./entrypoint.sh"]