docker/Dockerfile.test-client
Rama master · réplica de lectura del repositorio original.
# Dockerfile for containerized game test client
# Used by E2E tests to run multiple game client instances
#
# Build: make build-test-client
# Usage: started by the test broker via Testcontainers (see tests/JunimoServer.Tests/Containers/GameClientContainer.cs)
#
# NOTE: This Dockerfile duplicates the game-downloader and smapi-builder stages of the main
# Dockerfile: the game DLLs build the test-client mod, and the SMAPI build is what a client
# installs into the shared game volume when it boots before the server.
# Global build arguments
ARG SMAPI_VERSION=4.5.2
# ============================================================================
# Stage 1: Build steam-service downloader (same as main Dockerfile)
# ============================================================================
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS steam-service-builder
WORKDIR /src
# Repo-level build props + code-style rules apply inside the image build too
COPY ./Directory.Build.props ./CodeStyle.props ./.editorconfig ./
COPY ./tools/steam-service/*.csproj ./
RUN dotnet restore
COPY ./tools/steam-service/ .
RUN dotnet publish -c Release -o /app --no-restore
# ============================================================================
# Stage 2: Download game files (same as main Dockerfile)
# ============================================================================
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS game-downloader
COPY --from=steam-service-builder /app /steam-service
WORKDIR /steam-service
# Download game (skip SDK during build)
RUN --mount=type=secret,id=steam_username \
--mount=type=secret,id=steam_password \
--mount=type=secret,id=steam_refresh_token \
if [ -f /run/secrets/steam_username ]; then export STEAM_USERNAME="$(cat /run/secrets/steam_username)"; fi && \
if [ -f /run/secrets/steam_password ]; then export STEAM_PASSWORD="$(cat /run/secrets/steam_password)"; fi && \
if [ -f /run/secrets/steam_refresh_token ]; then export STEAM_REFRESH_TOKEN="$(cat /run/secrets/steam_refresh_token)"; fi && \
export GAME_DIR=/game && \
dotnet SteamService.dll download --skip-sdk
# ============================================================================
# Stage 3: Build SMAPI from source with the patch series applied (same as main Dockerfile)
# ============================================================================
# The client shares the server's game volume, so it must install the same SMAPI build when it
# boots onto an empty volume (init_smapi in docker/rootfs-test-client/startapp.sh).
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS smapi-builder
ARG SMAPI_VERSION
# zip keeps the Unix permission bits inside install.dat (the SDK image has git but no zip)
RUN apt-get update && apt-get install -y --no-install-recommends zip && rm -rf /var/lib/apt/lists/*
COPY --from=game-downloader /game /game
RUN git clone --depth 1 --branch "${SMAPI_VERSION}" https://github.com/Pathoschild/SMAPI.git /smapi
# A patch that no longer applies to the pinned upstream fails the build here, not silently at runtime
COPY patches/smapi /smapi-patches
RUN for patch in /smapi-patches/*.patch; do \
echo "Applying ${patch}" && git -C /smapi apply --check "${patch}" && git -C /smapi apply "${patch}"; \
done
COPY --chmod=0755 docker/smapi/build-installer.sh /smapi-build.sh
RUN --mount=type=cache,target=/root/.nuget/packages \
/smapi-build.sh /smapi /game /smapi-patches "${SMAPI_VERSION}" /out
# ============================================================================
# Stage 4: Build test-client mod using game DLLs
# ============================================================================
# SDK 10.0 (not 6.0): ModBuildConfig 4.4.0's analyzer targets Roslyn 4.9, which
# the 6.0 SDK's 4.3 compiler rejects (CS9057). The net6.0 TFM still builds fine.
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS mod-builder
ARG BUILD_CONFIGURATION=Release
# Mod compilation resolves StardewModdingAPI.dll from the game folder, so install our build there
COPY --from=game-downloader /game /game
COPY --from=smapi-builder /out /tmp/smapi
RUN /tmp/smapi/internal/linux/SMAPI.Installer --no-prompt --install --game-path /game && \
rm -rf /tmp/smapi
# Copy project files first for better layer caching
# (.editorconfig: code-style rules enforced at build via EnforceCodeStyleInBuild)
COPY ./Directory.Build.props ./CodeStyle.props ./.editorconfig /src/
COPY ./tests/test-client/JunimoTestClient.csproj /src/tests/test-client/
COPY ./mod/JunimoServer.Shared/JunimoServer.Shared.csproj /src/mod/JunimoServer.Shared/
# Restore dependencies
RUN --mount=type=cache,target=/root/.nuget/packages \
dotnet restore /src/tests/test-client/JunimoTestClient.csproj
# Copy source and build
COPY ./tests/test-client /src/tests/test-client
COPY ./mod/JunimoServer.Shared /src/mod/JunimoServer.Shared
RUN --mount=type=cache,target=/root/.nuget/packages \
dotnet build /src/tests/test-client/JunimoTestClient.csproj \
--configuration ${BUILD_CONFIGURATION} \
/p:GamePath=/game \
/p:EnableModDeploy=false \
-o /output/JunimoTestClient \
&& cp /src/tests/test-client/manifest.json /output/JunimoTestClient/manifest.json
# ============================================================================
# Stage 5: Final runtime image
# ============================================================================
FROM jlesage/baseimage-gui:debian-13-v4.13 AS client
ENV APP_NAME="Stardew Test Client" \
USER_ID=1000 \
GROUP_ID=1000 \
HOME=/config \
LANG=en_US.UTF-8 \
LANGUAGE=en_US:en \
LC_ALL=en_US.UTF-8 \
ACCEPT_EULA=Y \
DEBIAN_FRONTEND=noninteractive \
TERMINAL=xterm \
DARK_MODE=1 \
# Display
DISPLAY=:0 \
DISPLAY_HEIGHT=720 \
DISPLAY_WIDTH=1280 \
CLIENT_FPS=0 \
# SMAPI
SMAPI_USE_CURRENT_SHELL=true \
SMAPI_MODS_PATH=/data/Mods \
# Test client API port (can be overridden per container)
JUNIMO_TEST_PORT=5123
# add-pkg (base image helper), not raw apt-get — see docker/Dockerfile for why: the base's
# /var/log symlink breaks postinst scripts, and recommends drag in systemd. Cleans apt itself.
RUN add-pkg \
ca-certificates \
curl \
libicu76 \
locales \
procps \
scrot \
x11-utils \
xauth \
xz-utils \
mesa-utils \
&& echo "en_US.UTF-8 UTF-8" > /etc/locale.gen \
&& locale-gen
# Install ffmpeg 8.1.2 (BtbN build of the stable 8.1 branch) rather than the distro package, whose
# x11grab timing bugs break the test-recording pipeline. Pin month-end autobuilds only: BtbN prunes
# daily builds after 14 days (month-end builds live two years), and the "latest" tag drifts.
# Must match the server image's ffmpeg pin so both ends of the recording pipeline behave identically.
RUN curl -fL -o /tmp/ffmpeg.tar.xz https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2026-06-30-13-34/ffmpeg-n8.1.2-21-gce3c09c101-linux64-gpl-8.1.tar.xz && \
tar -xf /tmp/ffmpeg.tar.xz -C /tmp && \
install -m 0755 /tmp/ffmpeg-n8.1.2-21-gce3c09c101-linux64-gpl-8.1/bin/ffmpeg /usr/local/bin/ffmpeg && \
install -m 0755 /tmp/ffmpeg-n8.1.2-21-gce3c09c101-linux64-gpl-8.1/bin/ffprobe /usr/local/bin/ffprobe && \
rm -rf /tmp/ffmpeg*
# System cleanup
RUN \
# Fix stuck dbus statoverride
rm -f /var/lib/dpkg/statoverride && \
# Prevent unnecessary logs
rm -f /etc/cont-init.d/89-info.sh && \
# Prevent overriding OpenGL drivers
rm -rf /etc/cont-env.d/LIBGL_DRIVERS_PATH && \
# Pin the runtime HOME so Steam's ~/.steam lookup matches where startapp.sh links
# steamclient.so (${HOME}/.steam/sdk64) — see docker/Dockerfile. /config is owned by USER_ID
# at startup, so it stays writable for any configured non-root user; set-cont-env is used
# because cont-env overrides container env, so HOME can't be supplied from compose.
set-cont-env HOME /config
# Our SMAPI build, installed into the game volume by startapp.sh
COPY --from=smapi-builder /out /opt/smapi
# Copy SMAPI config (client-specific settings)
COPY docker/rootfs-test-client/data/smapi-config.json /data/smapi-config.json
# Declare the xrdb one-shot as expected-to-exit-immediately (0 = no floor, matching the
# app/min_running_time the base image ships — verified in the built image's /etc/services.d).
# cinit treats a service exiting before min_running_time
# (default 500ms) as a failed start and tears the whole container down (exit 1) — and xrdb
# (load X resources, exit) only outlives 500ms when it happens to spend that long waiting on
# a still-warming Xvnc, a timing race a one-shot must not have to win.
# --chmod=0644: cinit READS this value file; an exec bit makes cinit try to run it (fails 126).
COPY --chmod=0644 docker/rootfs-test-client/etc/services.d/xrdb/min_running_time /etc/services.d/xrdb/min_running_time
# Copy startup script
COPY docker/rootfs-test-client/startapp.sh /startapp.sh
# Root init hook: takes ownership of the volumes/paths the non-root app user writes, before the
# app service drops privileges (runs as root, alphabetically, ahead of services).
COPY docker/rootfs-test-client/etc/cont-init.d/50-client-init.sh /etc/cont-init.d/50-client-init.sh
RUN chmod +x /startapp.sh /etc/cont-init.d/50-client-init.sh
# Copy test-client mod from build stage
COPY --from=mod-builder /output/JunimoTestClient /data/Mods/JunimoTestClient
# Game files provided via volume mount (shared with server)
VOLUME [ "/data/game" ]
# Expose test client API port
EXPOSE 5123
# Health check - wait for test client API to respond
HEALTHCHECK --interval=2s --timeout=2s --start-period=60s --retries=60 \
CMD curl -sf http://localhost:${JUNIMO_TEST_PORT}/ping || exit 1
WORKDIR /data