From a2fb95e57bfc5dc9f9e63c6d19a321b8b454e3cf Mon Sep 17 00:00:00 2001 From: Matthias Ladkau Date: Tue, 11 Aug 2026 05:14:13 +0200 Subject: [PATCH] Add Windows cross-compilation via MinGW, and fix Docker build permission issues on vboxsf-mounted checkouts New `make dist-win`/`package-win` targets (folded into `dist`/`package` alongside the renamed `dist-linux`/`package-linux`) cross-compile systemshock.exe via MinGW, using prebuilt SDL2/SDL2_mixer/GLEW/ fluidsynth-lite baked into the build-image - no Windows machine or Wine needed to build it, confirmed working and playable on a real Windows machine. dist-win/ ships DLLs flat alongside the exe plus a new res/run.bat launcher, packaged into a .zip the same way dist/ becomes a .tar.gz. Also fixes three build-image bugs hit while testing on a VirtualBox vboxsf-mounted checkout: build-engine.sh/docker-entrypoint.sh losing their execute bit (chmod +x on restrictive source perms), the container user missing access to /workspace's supplementary vboxsf group, and cp -a failing on symlink/hard-link creation (vboxsf doesn't support either) - now falls back to dereferencing copies when detected. Also adds Docker/zip/etc. prerequisites to the README for both the desktop and Quest builds. --- .gitattributes | 2 + .gitea/workflows/build.yml | 29 +++++--- .gitignore | 7 +- Makefile | 99 ++++++++++++++++++++++---- NOTICE.txt | 16 +++++ README.md | 85 ++++++++++++++++++---- build-image/Dockerfile | 116 ++++++++++++++++++++++++++++++- build-image/build-engine-win.sh | 106 ++++++++++++++++++++++++++++ build-image/build-engine.sh | 27 +++++-- build-image/docker-entrypoint.sh | 13 ++++ res/run.bat | 4 ++ 11 files changed, 457 insertions(+), 47 deletions(-) create mode 100644 .gitattributes create mode 100644 build-image/build-engine-win.sh create mode 100644 res/run.bat diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..efdba87 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +* text=auto +*.sh text eol=lf diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml index 633a486..3b20d06 100644 --- a/.gitea/workflows/build.yml +++ b/.gitea/workflows/build.yml @@ -1,14 +1,16 @@ name: build -# Builds a versioned Linux release tarball (see `make package`) on every -# push/PR, plus on-demand via the Gitea "Run workflow" button. Runs +# Builds versioned Linux and Windows release packages (see `make +# package`, which builds both - a .tar.gz and a .zip) on every push/PR, +# plus on-demand via the Gitea "Run workflow" button. Runs # inside the build-image (see ../../build-image/Dockerfile, built/pushed # via ../../build-image.sh and ../../upload-image.sh), which bundles -# every dependency engine/ needs to compile - no network access needed -# at job runtime. Since the job's container already *is* the build-image -# (QUESTSHOCK_BUILD_IMAGE=1), `make package`'s `engine` prerequisite -# compiles directly instead of trying to docker-run it again, which -# wouldn't work here (no nested docker). +# every dependency engine/ needs to compile (for both platforms, +# including the Windows cross-toolchain) - no network access needed at +# job runtime. Since the job's container already *is* the build-image +# (QUESTSHOCK_BUILD_IMAGE=1), `make package`'s `engine`/`engine-win` +# prerequisites compile directly instead of trying to docker-run it +# again, which wouldn't work here (no nested docker). on: push: pull_request: @@ -50,17 +52,22 @@ jobs: # comment) - openxr was added to this image in the same # Android/Quest layer, so it's an equally good marker of that. [ -d /opt/prebuilt/android/openxr ] || { echo "PREFLIGHT FAIL: /opt/prebuilt/android/openxr missing - runner is using a build-image older than the Android/Quest layer" >&2; exit 1; } + [ -d /opt/prebuilt/win/sdl2 ] || { echo "PREFLIGHT FAIL: /opt/prebuilt/win/sdl2 missing - runner is using a build-image older than the Windows cross-build layer" >&2; exit 1; } + command -v x86_64-w64-mingw32-gcc >/dev/null 2>&1 || { echo "PREFLIGHT FAIL: x86_64-w64-mingw32-gcc not in PATH" >&2; exit 1; } + command -v zip >/dev/null 2>&1 || { echo "PREFLIGHT FAIL: zip not in PATH (needed by make package-win)" >&2; exit 1; } - name: Build package run: make package - - name: Upload build artifact + - name: Upload build artifacts # v4 uses the newer @actions/artifact backend, which this Gitea # instance's artifact storage doesn't support (GHESNotSupportedError) - v3 works. uses: actions/upload-artifact@v3 with: name: shockolate - path: dist/shockolate-*-linux-*.tar.gz + path: | + dist/shockolate-*-linux-*.tar.gz + dist/shockolate-*-windows-*.zip - name: Build Quest APK # QUESTSHOCK_BUILD_IMAGE is already set (see Preflight above), so @@ -78,13 +85,14 @@ jobs: path: dist/questshock-*-android-*.apk - name: Publish to dl.ladkau.de - # Uploads the tarball and APK over SFTP instead of using + # Uploads the tarball, zip, and APK over SFTP instead of using # actions/upload-artifact (whose zip wrapping can't be disabled). # Only runs on push so PR builds don't publish. if: gitea.event_name == 'push' run: | set -euo pipefail TARBALL="$(ls dist/shockolate-*-linux-*.tar.gz)" + ZIP="$(ls dist/shockolate-*-windows-*.zip)" APK="$(ls dist/questshock-*-android-*.apk)" mkdir -p ~/.ssh echo "${{ secrets.DL_SFTP_KEY }}" > ~/.ssh/dl_sftp_key @@ -94,5 +102,6 @@ jobs: uploader@dl.ladkau.de <-linux-.tar.gz - everything needed to # run except the proprietary game assets (res/GET_ASSETS.txt explains how @@ -72,7 +114,7 @@ dist: engine assets # tag to drive a release. Override with `make package VERSION=1.2.3`, or # just run it untagged for a local dev build (gets a 0.0.0-dev+ # placeholder version, with a warning). -package: engine +package-linux: engine @git config --global --add safe.directory "$$(pwd)" 2>/dev/null || true @V="$$(VERSION="$(VERSION)" ./build-image/version.sh)"; \ PKG_NAME="shockolate-$$V-linux-$(ARCH)"; \ @@ -95,6 +137,33 @@ package: engine rm -rf "$(BUILD_DIR)/package"; \ echo "Wrote $(DIST_DIR)/$$PKG_NAME.tar.gz" +# Windows counterpart of package-linux: dist/shockolate--windows- +# x86_64.zip. Same versioning (build-image/version.sh) and VERSION= +# override. Needs `zip` on whatever host runs `make package-win` itself +# (unlike engine-win's own build, packaging isn't run inside the +# build-image) - see README's Desktop build prerequisites. +package-win: engine-win + @git config --global --add safe.directory "$$(pwd)" 2>/dev/null || true + @command -v zip >/dev/null 2>&1 || { echo "PREFLIGHT FAIL: zip not found in PATH" >&2; exit 1; } + @V="$$(VERSION="$(VERSION)" ./build-image/version.sh)"; \ + PKG_NAME="shockolate-$$V-windows-x86_64"; \ + PKG_STAGE="$(BUILD_DIR)/package/$$PKG_NAME"; \ + echo "Packaging $$PKG_NAME"; \ + rm -rf "$$PKG_STAGE"; \ + mkdir -p "$$PKG_STAGE/res"; \ + cp "$(ENGINE_OUT_WIN)/systemshock.exe" "$(ENGINE_OUT_WIN)"/*.dll "$$PKG_STAGE/"; \ + cp -a engine/shaders "$$PKG_STAGE/shaders"; \ + cp "$(ENGINE_OUT_WIN)/soundfont.sf2" "$$PKG_STAGE/res/"; \ + cp res/assets/GET_ASSETS.txt "$$PKG_STAGE/res/GET_ASSETS.txt"; \ + cp res/run.bat "$$PKG_STAGE/run.bat"; \ + cp LICENSE "$$PKG_STAGE/LICENSE"; \ + cp engine/LICENSE "$$PKG_STAGE/LICENSE.Shockolate"; \ + cp NOTICE.txt "$$PKG_STAGE/NOTICE.txt"; \ + mkdir -p "$(DIST_DIR)"; \ + (cd "$(BUILD_DIR)/package" && zip -rq "$(CURDIR)/$(DIST_DIR)/$$PKG_NAME.zip" "$$PKG_NAME"); \ + rm -rf "$(BUILD_DIR)/package"; \ + echo "Wrote $(DIST_DIR)/$$PKG_NAME.zip" + # Builds the Quest APK (see build-image/build-apk.sh and # android/engine-patches/ - engine/ itself is never modified; a patch is # applied to a scratch copy at build time instead). Same @@ -124,7 +193,7 @@ android-studio: ./run-image.sh bash build-image/prepare-android-project.sh --host-paths clean: - rm -rf "$(DIST_DIR)" "$(BUILD_DIR)" "$(ENGINE_OUT)" \ + rm -rf "$(DIST_DIR)" "$(DIST_WIN_DIR)" "$(BUILD_DIR)" "$(ENGINE_OUT)" "$(ENGINE_OUT_WIN)" \ engine/build_ext engine/CMakeCache.txt engine/CMakeFiles \ engine/cmake_install.cmake engine/Makefile engine/systemshock \ engine/src/Libraries/CMakeFiles \ diff --git a/NOTICE.txt b/NOTICE.txt index a0e4f3d..1466bd3 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -16,3 +16,19 @@ The Android build additionally bundles: - The Khronos Group's OpenXR-SDK loader (https://github.com/KhronosGroup/OpenXR-SDK), prebuilt unmodified as lib/arm64-v8a/libopenxr_loader.so. It is Apache 2.0-licensed. + +The Windows build (cross-compiled via MinGW - see README's "4.2. Windows +cross-build") additionally bundles: + +- GLEW (http://glew.sourceforge.net/), unmodified, as glew32.dll - used + for OpenGL extension loading, which Windows' own opengl32.dll doesn't + provide past OpenGL 1.1 (Linux instead gets this straight from Mesa's + headers, needing no separate loader library - see + engine/src/MacSrc/OpenGL.cc). It is licensed under a combination of the + Modified BSD License, the MIT License, and the Khronos License, all + permissive. +- The MinGW-w64 runtime's winpthreads library, as libwinpthread-1.dll. It + is MIT-licensed. (GCC's own runtime, libgcc/libstdc++, is linked + statically into systemshock.exe instead of shipped as a DLL, under the + GCC Runtime Library Exception - this doesn't subject the rest of the + binary to the GPL.) diff --git a/README.md b/README.md index 02e61f0..9d58381 100644 --- a/README.md +++ b/README.md @@ -92,10 +92,30 @@ below. ## 4. Desktop build -Builds and runs Questshock natively on Linux (your dev machine, or any -Linux box) - useful for local development and testing without a VR -headset at all. For the VR-headset build, see "5. Android / Quest build" -below instead. +Builds and runs Questshock natively on the desktop (your dev machine, or +any Linux/Windows box) - useful for local development and testing +without a VR headset at all. For the VR-headset build, see "5. Android / +Quest build" below instead. + +Two platforms: Linux (native) and Windows (x86_64, cross-compiled via +MinGW - see "4.2. Windows cross-build" below for how). `make dist`/`make +package` build **both** by default; use `make dist-linux`/`make +dist-win` (or `package-linux`/`package-win`) to build just one. + +**Prerequisites:** + +- **Docker** - `build-image.sh`/`run-image.sh` build and run the engine + build-image; no other build tool touches the host directly. The + Windows cross-toolchain (MinGW) is baked into the same build-image, so + building `dist-win` needs nothing extra beyond Docker itself. +- **`innoextract` and `unzip`** - optional, only needed by + `res/assets/extract_assets.sh` (see "3. Game assets" above) to unpack + the GOG installer locally; if either is missing, that script falls + back to running the extraction inside a throwaway Docker container + instead. +- **`zip`** - only needed on the host for `make package`/`package-win` + (the Windows release archive); `make dist`/`dist-win` and + `package-linux` don't need it. ```sh # 1. Build the engine build-image (once, or after build-image/ changes) @@ -104,11 +124,12 @@ below instead. # 2. Get your own copy of the game data (see "3. Game assets" above), then: res/assets/extract_assets.sh -# 3. Compile the engine and assemble dist/ +# 3. Compile the engine and assemble dist/ (Linux) and dist-win/ (Windows) make dist # 4. Play -dist/run.sh +dist/run.sh # Linux +dist-win/run.bat # Windows (or systemshock.exe directly) ``` `make dist` always recompiles the engine from the current `engine/` @@ -117,14 +138,38 @@ source (via `run-image.sh`), so a fresh build-image plus a re-run of ### 4.1. Packaging a distributable build -`make package` builds `dist/shockolate--linux-.tar.gz`: the -compiled binary, its runtime libraries, shaders, a default MIDI -soundfont, license information, and `res/GET_ASSETS.txt` in place of the -actual game data (which the tarball never includes). Version comes from -the current git tag (push a `vX.Y.Z` tag to drive a release); without one -it builds an untagged `0.0.0-dev+` placeholder. `make apk` (see -"5. Android / Quest build" below) is versioned identically, via the same -`build-image/version.sh`. +`make package` builds both `dist/shockolate--linux-.tar.gz` +and `dist/shockolate--windows-x86_64.zip`: the compiled +binary/DLLs, shaders, a default MIDI soundfont, license information, and +`res/GET_ASSETS.txt` in place of the actual game data (which neither +archive ever includes). Version comes from the current git tag (push a +`vX.Y.Z` tag to drive a release); without one it builds an untagged +`0.0.0-dev+` placeholder. `make apk` (see "5. Android / Quest build" +below) is versioned identically, via the same `build-image/version.sh`. + +### 4.2. Windows cross-build + +`make dist-win`/`make package-win` cross-compile a native Windows x86_64 +build via MinGW (`x86_64-w64-mingw32-gcc`/`g++`, baked into the +build-image alongside the Linux toolchain - see +`build-image/build-engine-win.sh` and the "Windows cross-compile" section +of `build-image/Dockerfile`) - no Windows machine, Wine, or VM involved +in building it. `engine/CMakeLists.txt` already has working `WIN32`/ +`MINGW` branches from Shockolate's own upstream Windows build (built +natively via Git Bash/MinGW on a real Windows machine - see +`engine/build_win64.sh`/`engine/appveyor.yml`), so unlike the Quest +build's `android/engine-patches/`, no source patching is needed here - +this cross-compiles those same branches offline and reproducibly, from +Linux, via Docker. + +`dist-win/`/the packaged `.zip` ship `systemshock.exe` with its DLLs +(SDL2, SDL2_mixer, GLEW, fluidsynth-lite, plus the MinGW pthread runtime) +sitting flat alongside it, rather than in a `lib/` subdirectory like +`dist/` - Windows' default DLL search order already checks the +executable's own directory first, so no `PATH`/library-path setup is +needed the way `dist/run.sh` needs `LD_LIBRARY_PATH`. + +Confirmed working (and playable) on a real Windows machine. A Gitea Actions workflow (`.gitea/workflows/build.yml`) builds this package on every push, using the build-image as its container (so no @@ -133,6 +178,18 @@ resulting tarball to dl.ladkau.de. ## 5. Android / Quest build +**Prerequisites:** + +- **Docker** - same build-image as the desktop build (see "4. Desktop + build" above), plus network access at build time for Gradle/AGP's own + dependency resolution (the one target that isn't fully offline). +- **SideQuest or `adb`** (Android Platform Tools) - to sideload the built + APK onto the headset, and to enable/verify USB debugging; see "5.1. + Installing and playing" below. +- Building/debugging natively in Android Studio instead of via `make + apk` needs its own separate toolchain - see "5.2. Building natively in + Android Studio" below. + `make apk` builds `dist/questshock--android-arm64.apk` - an immersive OpenXR app (see `android/app/src/main/cpp/xr_session.c`) that can be sideloaded onto any Android-based VR headset with OpenXR support diff --git a/build-image/Dockerfile b/build-image/Dockerfile index 22bdbc3..898bbd4 100644 --- a/build-image/Dockerfile +++ b/build-image/Dockerfile @@ -18,6 +18,13 @@ ARG FLUIDSYNTH_LITE_REF=c539a8d9270ba5a3f7d6e460606483fc2ab1eb61 # Soundfont used for MIDI music, matching what engine/build_deps.sh itself # fetches (a free substitute for the Windows default GM soundfont). ARG SOUNDFONT_URL=http://rancid.kapsi.fi/windows.sf2 +# GLEW, for the Windows/MinGW cross-build only (engine/CMakeLists.txt's +# WIN32 branch - Windows' own opengl32.dll only exposes OpenGL 1.1, so +# anything newer needs GLEW's runtime extension loading; Linux instead +# gets modern prototypes straight from Mesa's headers, no loader needed - +# see engine/src/MacSrc/OpenGL.cc). Matches the version engine/'s own +# upstream Windows build script (build_win64.sh) used. +ARG GLEW_VERSION=2.1.0 # Gitea/GitHub Actions' JS-based actions (actions/checkout, # actions/upload-artifact, ...) need a node binary in the container job's # PATH - this image is otherwise pure C toolchain, so it isn't pulled in @@ -99,6 +106,15 @@ ENV DEBIAN_FRONTEND=noninteractive # openssh-client: the CI workflow's `sftp` publish step. # openjdk-17-jdk-headless: Gradle/AGP's own minimum JDK for the APK build. # unzip: extracts the Android cmdline-tools zip below. +# mingw-w64: the x86_64-w64-mingw32-{gcc,g++,windres,ar,...} cross +# toolchain for the Windows desktop build (see the "Windows cross-compile" +# section below). Ubuntu ships both a win32-thread-model and a +# posix-thread-model variant behind update-alternatives; the default +# (win32) is fine here since nothing in engine/ uses std::thread. +# zip: `make package-win`'s Windows release archive - a CI job's own +# `make package` runs inside this image as its container (see +# .gitea/workflows/build.yml), so it needs to be baked in here, not just +# available on a local dev machine's own host (see README). # # All apt installs deliberately live in this one RUN, first, so editing # anything below it (in particular the Android cross-compile steps, the @@ -111,7 +127,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ libgl1-mesa-dev libglx-dev libxext-dev libx11-dev libxrandr-dev \ libxi-dev libxfixes-dev libxss-dev libxinerama-dev libxcursor-dev \ libogg-dev libvorbis-dev libasound2-dev openssh-client \ - openjdk-17-jdk-headless unzip \ + openjdk-17-jdk-headless unzip mingw-w64 zip \ && rm -rf /var/lib/apt/lists/* WORKDIR /opt/prebuilt @@ -150,10 +166,96 @@ RUN git clone https://github.com/EtherTyper/fluidsynth-lite.git \ && rm -rf .git # General MIDI soundfont for fluidsynth playback - engine/build_deps.sh -# fetches the same file and drops it into engine/res/. +# fetches the same file and drops it into engine/res/. Shared by both the +# Linux and Windows builds below. RUN mkdir -p soundfont \ && curl -sSL -o soundfont/default.sf2 "${SOUNDFONT_URL}" +# CMake toolchain file for the Windows/MinGW cross-build below, reused at +# container-run time by build-image/build-engine-win.sh (see +# MINGW_TOOLCHAIN_FILE) to cross-compile engine/ itself the same way. GCC's +# runtime (libgcc/libstdc++) is linked statically so only the SDL2/ +# SDL2_mixer/GLEW/fluidsynth-lite DLLs (plus libwinpthread, which isn't +# safe to static-link the same way) need shipping alongside systemshock.exe. +ENV MINGW_TOOLCHAIN_FILE=/opt/mingw-toolchain.cmake +RUN printf '%s\n' \ + 'set(CMAKE_SYSTEM_NAME Windows)' \ + 'set(CMAKE_SYSTEM_PROCESSOR x86_64)' \ + 'set(CMAKE_C_COMPILER x86_64-w64-mingw32-gcc)' \ + 'set(CMAKE_CXX_COMPILER x86_64-w64-mingw32-g++)' \ + 'set(CMAKE_RC_COMPILER x86_64-w64-mingw32-windres)' \ + 'set(CMAKE_FIND_ROOT_PATH /usr/x86_64-w64-mingw32)' \ + '# LIBRARY/INCLUDE/PACKAGE deliberately left at CMake'"'"'s own' \ + '# cross-compiling default (BOTH) - engine/CMakeLists.txt'"'"'s' \ + '# BUNDLED SDL2/SDL2_mixer/FluidSynth find_library() calls point at' \ + '# build_ext/ (outside this sysroot entirely), which ONLY would' \ + '# refuse to search.' \ + 'set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)' \ + 'set(CMAKE_EXE_LINKER_FLAGS_INIT "-static-libgcc -static-libstdc++")' \ + > "${MINGW_TOOLCHAIN_FILE}" + +# SDL2 and SDL2_mixer for Windows: unlike the desktop build above, these +# are libsdl.org's own official prebuilt MinGW devel packages (headers + +# import libs + DLLs for x86_64-w64-mingw32), not built from source here - +# SDL2's autotools setup targets *nix; upstream ships MinGW builds +# pre-made instead, same as engine/'s own upstream Windows build script +# (build_win64.sh) uses. Same version pins as the Linux build above, so +# both desktop builds ship the same SDL2/SDL2_mixer release. +RUN curl -sSLO "https://www.libsdl.org/release/SDL2-devel-${SDL2_VERSION}-mingw.tar.gz" \ + && tar xf "SDL2-devel-${SDL2_VERSION}-mingw.tar.gz" \ + && mkdir -p /opt/prebuilt/win \ + && mv "SDL2-${SDL2_VERSION}/x86_64-w64-mingw32" /opt/prebuilt/win/sdl2 \ + && rm -rf "SDL2-${SDL2_VERSION}" "SDL2-devel-${SDL2_VERSION}-mingw.tar.gz" + +RUN curl -sSLO "https://www.libsdl.org/projects/SDL_mixer/release/SDL2_mixer-devel-${SDL2_MIXER_VERSION}-mingw.tar.gz" \ + && tar xf "SDL2_mixer-devel-${SDL2_MIXER_VERSION}-mingw.tar.gz" --exclude=Xcode \ + && mv "SDL2_mixer-${SDL2_MIXER_VERSION}/x86_64-w64-mingw32" /opt/prebuilt/win/sdl2_mixer \ + && rm -rf "SDL2_mixer-${SDL2_MIXER_VERSION}" "SDL2_mixer-devel-${SDL2_MIXER_VERSION}-mingw.tar.gz" + +# GLEW for Windows: compiled directly instead of via GLEW's own bundled +# cross-compile Makefile configs (config/Makefile.linux-mingw64 et al) - +# those pass raw `-soname`/`--out-implib` straight to whatever $(LD) is +# set to, which only works if LD is the real `ld` binary, not gcc-as- +# linker-driver (and that config also hardcodes a 32-bit `i686-w64- +# mingw32` host despite the "64" in its name) - simpler and more robust +# to just compile+link GLEW's one source file ourselves. Produces +# glew32.dll (to ship alongside systemshock.exe) and libglew32.dll.a (the +# MinGW import library engine/CMakeLists.txt's WIN32 branch links +# against). +RUN curl -sSL -o "glew-${GLEW_VERSION}.tgz" \ + "https://sourceforge.net/projects/glew/files/glew/${GLEW_VERSION}/glew-${GLEW_VERSION}.tgz/download" \ + && tar xf "glew-${GLEW_VERSION}.tgz" \ + && cd "glew-${GLEW_VERSION}" \ + && mkdir -p /opt/prebuilt/win/glew/include/GL /opt/prebuilt/win/glew/lib \ + && x86_64-w64-mingw32-gcc -DGLEW_NO_GLU -O2 -Iinclude -c src/glew.c -o glew.o \ + && x86_64-w64-mingw32-gcc -shared \ + -Wl,--out-implib,/opt/prebuilt/win/glew/lib/libglew32.dll.a \ + -o /opt/prebuilt/win/glew/lib/glew32.dll \ + glew.o -lopengl32 -lgdi32 -luser32 -lkernel32 \ + && cp include/GL/glew.h include/GL/wglew.h /opt/prebuilt/win/glew/include/GL/ \ + && cd .. && rm -rf "glew-${GLEW_VERSION}" "glew-${GLEW_VERSION}.tgz" + +# fluidsynth-lite for Windows: same source/ref/DLL-mode patch as the +# desktop build above, cross-compiled via the MinGW toolchain file. WIN32 +# skips fluidsynth-lite's own pthread dependency (see its CMakeLists.txt), +# so no libwinpthread linkage to worry about here. +RUN git clone https://github.com/EtherTyper/fluidsynth-lite.git fluidsynth-lite-win \ + && cd fluidsynth-lite-win \ + && git checkout "${FLUIDSYNTH_LITE_REF}" \ + && sed -i 's/DLL"\ off/DLL"\ on/' CMakeLists.txt \ + && rm -rf .git \ + && cd .. \ + && cmake -S fluidsynth-lite-win -B build-fluidsynth-win \ + -DCMAKE_TOOLCHAIN_FILE="${MINGW_TOOLCHAIN_FILE}" \ + && cmake --build build-fluidsynth-win -j"$(nproc)" \ + && mkdir -p /opt/prebuilt/win/fluidsynth-lite/lib /opt/prebuilt/win/fluidsynth-lite/include \ + && cp build-fluidsynth-win/src/*.dll build-fluidsynth-win/src/*.dll.a \ + /opt/prebuilt/win/fluidsynth-lite/lib/ \ + && cp -a fluidsynth-lite-win/include/. /opt/prebuilt/win/fluidsynth-lite/include/ \ + && cp build-fluidsynth-win/include/fluidsynth/version.h \ + /opt/prebuilt/win/fluidsynth-lite/include/fluidsynth/version.h \ + && rm -rf fluidsynth-lite-win build-fluidsynth-win + # Node.js: needed only so Gitea/GitHub Actions' JS-based actions can run # when this image is used as a CI job's container - see NODE_VERSION above. RUN curl -sSL -o /tmp/node.tar.xz \ @@ -283,7 +385,15 @@ RUN git clone --branch "release-${ANDROID_OPENXR_VERSION}" --depth 1 \ COPY build-image/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh COPY build-image/build-engine.sh /usr/local/bin/build-engine.sh -RUN chmod +x /usr/local/bin/docker-entrypoint.sh /usr/local/bin/build-engine.sh +# 755, not +x: the entrypoint drops to a `builder` user matching the +# *host's* UID/GID (see docker-entrypoint.sh), which is never in these +# root-owned files' group, so it needs the image's own explicit +# world-read+execute here - `chmod +x` alone only adds execute bits on +# top of whatever "other" permissions the source file happened to have +# (which depends on the host's umask at checkout, e.g. a restrictive +# 0007 umask yields unreadable-by-other files, which then round-trip +# into the image and cause a Permission denied at container run time). +RUN chmod 755 /usr/local/bin/docker-entrypoint.sh /usr/local/bin/build-engine.sh # Marks a shell as already running inside this image (with every engine # build dependency prebuilt above) - lets the Makefile's `engine` target diff --git a/build-image/build-engine-win.sh b/build-image/build-engine-win.sh new file mode 100644 index 0000000..485c876 --- /dev/null +++ b/build-image/build-engine-win.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +# Cross-compiles engine/ (the vendored Shockolate snapshot) for Windows +# (x86_64) via MinGW, against the dependencies prebuilt into this image at +# /opt/prebuilt/win - no network access needed. engine/CMakeLists.txt +# already has working WIN32/MINGW branches (the upstream Shockolate +# project's own build_win64.sh/appveyor.yml build the same way, natively +# on Windows via Git Bash/MinGW; this cross-compiles the same branches +# fully offline and reproducibly from Linux instead), so no source +# patching is needed, unlike the Quest build's android/engine-patches/. +# +# Builds from a scratch copy of engine/ (like android/engine-patches/ +# does for the Quest build) rather than engine/ itself: its BUNDLED +# dependency paths (build_ext/built_sdl etc.) and in-source CMake cache +# are relative to/inside the source tree, and would otherwise collide +# with the Linux desktop build's own build_ext/CMakeCache.txt (see +# build-engine.sh) if both are built from the same checkout, as `make +# dist` (dist-linux + dist-win) does. +# +# Must be run with the repo root as the working directory - either via +# ../run-image.sh, or directly when already inside this image (see the +# Makefile's `engine-win` target, which picks whichever of these +# applies, same as `engine`). +# +# Output lands in engine/.build-output-win/: systemshock.exe, the DLLs +# it needs at runtime, and a default MIDI soundfont - everything the +# root Makefile needs to assemble dist-win/. +set -euo pipefail + +REPO_ROOT="$(pwd)" +SCRATCH_ENGINE="$REPO_ROOT/build/win-engine" +OUT_DIR="$REPO_ROOT/engine/.build-output-win" + +echo "== Preparing a scratch copy of engine/ for the Windows cross-build ==" +rm -rf "$SCRATCH_ENGINE" +mkdir -p "$(dirname "$SCRATCH_ENGINE")" + +# See build-engine.sh for why this detection is needed (e.g. a VirtualBox +# vboxsf shared folder refusing to create symlinks or preserve hard-link +# relationships) - applied here too since build/ is under the same +# bind-mounted repo checkout. +CP_FLAGS=(-a) +mkdir -p "$(dirname "$SCRATCH_ENGINE")/.symlink-test-dir" +if ! ln -s test-target "$(dirname "$SCRATCH_ENGINE")/.symlink-test-dir/test" 2>/dev/null; then + echo "(destination filesystem doesn't support symlinks - copying real file content instead)" + CP_FLAGS=(-a --dereference --no-preserve=links) +fi +rm -rf "$(dirname "$SCRATCH_ENGINE")/.symlink-test-dir" + +cp "${CP_FLAGS[@]}" "$REPO_ROOT/engine" "$SCRATCH_ENGINE" +cd "$SCRATCH_ENGINE" + +# If engine/ has ever been built in-source directly (`make dist`/`make +# engine` - see build-engine.sh), that leftover CMakeCache.txt etc. just +# got copied along verbatim - and it still points at the real engine/ +# path, not this scratch copy, which CMake refuses to configure against +# ("directory is different than the directory where CMakeCache.txt was +# created"). Same artifact list as .gitignore's "Engine build artifacts" +# section. +rm -rf build_ext CMakeCache.txt CMakeFiles cmake_install.cmake Makefile \ + systemshock src/Libraries/CMakeFiles + +echo "== Wiring up prebuilt SDL2/SDL2_mixer/GLEW/fluidsynth-lite (Windows/MinGW) from the image ==" +rm -rf build_ext +mkdir -p build_ext +cp "${CP_FLAGS[@]}" /opt/prebuilt/win/sdl2 build_ext/built_sdl +cp "${CP_FLAGS[@]}" /opt/prebuilt/win/sdl2_mixer build_ext/built_sdl_mixer +cp "${CP_FLAGS[@]}" /opt/prebuilt/win/glew build_ext/built_glew +# engine/CMakeLists.txt's BUNDLED FluidSynth mode hardcodes +# build_ext/fluidsynth-lite/src as the library search path (matching the +# desktop Linux in-source build's own output layout) - same lib/ -> src/ +# remapping build-image/prepare-android-project.sh does for the Quest +# build's own prebuilt fluidsynth-lite. +mkdir -p build_ext/fluidsynth-lite/src build_ext/fluidsynth-lite/include +cp "${CP_FLAGS[@]}" /opt/prebuilt/win/fluidsynth-lite/lib/. build_ext/fluidsynth-lite/src/ +cp "${CP_FLAGS[@]}" /opt/prebuilt/win/fluidsynth-lite/include/. build_ext/fluidsynth-lite/include/ + +echo "== Configuring (CMake, MinGW cross-compile, BUNDLED SDL2/SDL2_mixer/FluidSynth) ==" +cmake -DCMAKE_TOOLCHAIN_FILE="$MINGW_TOOLCHAIN_FILE" \ + -DENABLE_SDL2=BUNDLED -DENABLE_SOUND=BUNDLED -DENABLE_FLUIDSYNTH=BUNDLED . + +echo "== Compiling ==" +make -j"$(nproc)" systemshock + +echo "== Assembling engine/.build-output-win ==" +rm -rf "$OUT_DIR" +mkdir -p "$OUT_DIR" +cp systemshock.exe "$OUT_DIR/" +# Only SDL2.dll/SDL2_mixer.dll themselves - not SDL2_mixer's own bundled +# codec DLLs (libvorbis, libmodplug, libopus, ...), matching what +# engine/'s own upstream Windows build script (build_win64.sh) ships: +# Shockolate only ever calls Mix_LoadWAV_RW/Mix_HookMusic (same as the +# Quest build - see build-image/Dockerfile's Android layer comment), +# never loading the OGG/MOD/MP3 game data those codecs would be for. +cp build_ext/built_sdl/bin/SDL2.dll build_ext/built_sdl_mixer/bin/SDL2_mixer.dll "$OUT_DIR/" +cp build_ext/built_glew/lib/glew32.dll "$OUT_DIR/" +cp build_ext/fluidsynth-lite/src/*.dll "$OUT_DIR/" +# libgcc/libstdc++ are statically linked (see the Dockerfile's +# MINGW_TOOLCHAIN_FILE), but fluidsynth-lite/SDL2 still pull in the +# MinGW pthread emulation dynamically. +cp /usr/x86_64-w64-mingw32/lib/libwinpthread-1.dll "$OUT_DIR/" +cp /opt/prebuilt/soundfont/default.sf2 "$OUT_DIR/soundfont.sf2" + +echo "== Done ==" +echo "Binary: $OUT_DIR/systemshock.exe" +echo "DLLs: $OUT_DIR/*.dll" +echo "Soundfont: $OUT_DIR/soundfont.sf2" diff --git a/build-image/build-engine.sh b/build-image/build-engine.sh index 0d3825e..b649568 100755 --- a/build-image/build-engine.sh +++ b/build-image/build-engine.sh @@ -22,9 +22,28 @@ cd "$ENGINE_DIR" echo "== Wiring up prebuilt SDL2/SDL2_mixer/fluidsynth-lite from the image ==" rm -rf build_ext mkdir -p build_ext -cp -a /opt/prebuilt/built_sdl build_ext/ -cp -a /opt/prebuilt/built_sdl_mixer build_ext/ -cp -a /opt/prebuilt/fluidsynth-lite build_ext/ + +# Some filesystems the repo might be checked out on (e.g. a VirtualBox +# vboxsf shared folder) refuse to create symlinks at all ("Operation not +# permitted"), which plain `cp -a` needs for SDL2/SDL2_mixer's +# libFoo.so -> libFoo.so.N -> libFoo.so.N.M dev-symlink chain. Detect +# that up front, once, and copy real file content instead of recreating +# links if so, rather than failing partway through. `-a` implies +# --preserve=all (which includes hard-link relationships between files, +# not just symlinks - SDL2's install hard-links the two most-specific +# version files together), so --no-preserve=links is needed alongside +# --dereference to avoid that too. +CP_FLAGS=(-a) +if ! ln -s test-target build_ext/.symlink-test 2>/dev/null; then + echo "(destination filesystem doesn't support symlinks - copying real file content instead)" + CP_FLAGS=(-a --dereference --no-preserve=links) +else + rm -f build_ext/.symlink-test +fi + +cp "${CP_FLAGS[@]}" /opt/prebuilt/built_sdl build_ext/ +cp "${CP_FLAGS[@]}" /opt/prebuilt/built_sdl_mixer build_ext/ +cp "${CP_FLAGS[@]}" /opt/prebuilt/fluidsynth-lite build_ext/ echo "== Configuring (CMake, BUNDLED SDL2/SDL2_mixer/FluidSynth) ==" rm -f CMakeCache.txt @@ -38,7 +57,7 @@ rm -rf "$OUT_DIR" mkdir -p "$OUT_DIR/lib" cp systemshock "$OUT_DIR/" find build_ext/built_sdl/lib build_ext/built_sdl_mixer/lib build_ext/fluidsynth-lite/src \ - -name '*.so*' -not -name '*.la' -exec cp -a {} "$OUT_DIR/lib/" \; + -name '*.so*' -not -name '*.la' -exec cp "${CP_FLAGS[@]}" {} "$OUT_DIR/lib/" \; cp /opt/prebuilt/soundfont/default.sf2 "$OUT_DIR/soundfont.sf2" echo "== Done ==" diff --git a/build-image/docker-entrypoint.sh b/build-image/docker-entrypoint.sh index 810de83..c5af2e9 100755 --- a/build-image/docker-entrypoint.sh +++ b/build-image/docker-entrypoint.sh @@ -11,4 +11,17 @@ USER_GID="${HOST_GID:-1000}" groupadd -g "$USER_GID" builder 2>/dev/null || true useradd -u "$USER_UID" -g "$USER_GID" -m -s /bin/bash builder 2>/dev/null || true +# /workspace (the bind-mounted repo) may actually be owned by a group the +# host user only has via *supplementary* membership rather than their +# primary GID above - e.g. a VirtualBox vboxsf shared folder, which shows +# up as root:vboxsf on the host and would otherwise be inaccessible to +# `builder` here (falls through to "other", which vboxsf's default mode +# leaves with no permissions at all). Join whatever group actually owns +# /workspace too, if it differs. +WORKSPACE_GID="$(stat -c %g /workspace 2>/dev/null || true)" +if [ -n "$WORKSPACE_GID" ] && [ "$WORKSPACE_GID" != "$USER_GID" ]; then + groupadd -g "$WORKSPACE_GID" workspace 2>/dev/null || true + usermod -aG "$WORKSPACE_GID" builder 2>/dev/null || true +fi + exec gosu builder "$@" diff --git a/res/run.bat b/res/run.bat new file mode 100644 index 0000000..f411ba0 --- /dev/null +++ b/res/run.bat @@ -0,0 +1,4 @@ +@echo off +cd /d "%~dp0" +systemshock.exe %* +exit /b %errorlevel%