From 1d12e4e09996f4bbda31987c32e66264ef8258ff Mon Sep 17 00:00:00 2001 From: EnderTheCoder Date: Sat, 12 Sep 2026 02:04:25 +0800 Subject: [PATCH] v0.2.0: clang + import std; isolated Windows cross-build (llvm-mingw + SDL3) --- .gitlab-ci.yml | 112 +++++++++++------ CMakeLists.txt | 147 ++++++++++++----------- Dockerfile | 36 +++--- Dockerfile.win | 63 ++++++++++ README.md | 72 ++++++++--- VERSION | 2 +- apps/openra3/main.cpp | 11 +- cmake/toolchains/llvm-mingw-x86_64.cmake | 43 +++++++ docs/ARCHITECTURE.md | 6 +- docs/ROADMAP.md | 4 + scripts/build-linux.sh | 14 +++ scripts/build-windows.sh | 16 +++ src/client/ra3.client.cppm | 8 +- src/core/ra3.core.cppm | 19 +-- src/fs/ra3.fs.cppm | 20 +-- src/game/ra3.game.cppm | 9 +- src/logic/ra3.logic.cppm | 14 +-- src/map/ra3.map.cppm | 15 +-- src/render/ra3.render.cppm | 17 +-- src/skirmish/ra3.skirmish.cppm | 13 +- src/ui/ra3.ui.null.cppm | 5 +- src/ui/ra3.ui.sdl.cppm | 4 +- tests/ra3_tests.cpp | 10 +- 23 files changed, 393 insertions(+), 267 deletions(-) create mode 100644 Dockerfile.win create mode 100644 cmake/toolchains/llvm-mingw-x86_64.cmake create mode 100644 scripts/build-linux.sh create mode 100644 scripts/build-windows.sh diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index fecc1a4..3130739 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -6,14 +6,11 @@ variables: DOCKER_HOST: tcp://docker:2375 DOCKER_DRIVER: overlay2 DOCKER_TLS_CERTDIR: "" - # Where to push the dev image. Leave empty to not push at all. - # Examples: "192.168.1.11:9090//openra3" or "docker.io//openra3" - IMAGE_NAME: "" + DOCKER_BUILDKIT: "1" + # apt mirror used inside both images (override per pipeline if needed) APT_MIRROR: "http://mirrors.tuna.tsinghua.edu.cn/ubuntu" -image_build: - stage: image - image: docker:latest +.dind: &dind services: - name: docker:dind # Pull docker.io images through the intranet Harbor pull-through cache; @@ -21,44 +18,79 @@ image_build: command: - --registry-mirror=http://192.168.1.11:9090/dockerhub - --insecure-registry=192.168.1.11:9090 - script: - - docker build --target dev -t openra3-dev:local --build-arg APT_MIRROR=$APT_MIRROR . - - docker save openra3-dev:local -o dev-image.tar - - | - if [ -n "$IMAGE_NAME" ]; then - # add `docker login` here if IMAGE_NAME points to a private registry - docker tag openra3-dev:local $IMAGE_NAME:$CI_COMMIT_SHORT_SHA - docker push $IMAGE_NAME:$CI_COMMIT_SHORT_SHA - fi - artifacts: - name: "dev-image-$CI_COMMIT_SHORT_SHA" - paths: - - dev-image.tar - expire_in: 1 day + +# --- build the two isolated toolchain images --------------------------------- +linux_image: + stage: image + image: docker:latest + services: *dind tags: - docker + script: + - docker build --target dev -t openra3-linux:local --build-arg APT_MIRROR=$APT_MIRROR . + - docker save openra3-linux:local -o linux-image.tar + artifacts: + name: "linux-image-$CI_COMMIT_SHORT_SHA" + paths: + - linux-image.tar + expire_in: 1 day -build_test_package: +windows_image: + stage: image + image: docker:latest + services: *dind + tags: + - docker + script: + - docker build -f Dockerfile.win --target dev -t openra3-windows:local --build-arg APT_MIRROR=$APT_MIRROR . + - docker save openra3-windows:local -o windows-image.tar + artifacts: + name: "windows-image-$CI_COMMIT_SHORT_SHA" + paths: + - windows-image.tar + expire_in: 1 day + +# --- build and test each target ---------------------------------------------- +build_linux: stage: build image: docker:latest - services: - - name: docker:dind - command: - - --registry-mirror=http://192.168.1.11:9090/dockerhub - - --insecure-registry=192.168.1.11:9090 - needs: - - image_build - before_script: - - docker load -i dev-image.tar - script: - - docker run --rm -v "$(pwd):/work" -w /work openra3-dev:local cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release - - docker run --rm -v "$(pwd):/work" -w /work openra3-dev:local cmake --build build -j - - docker run --rm -v "$(pwd):/work" -w /work openra3-dev:local ctest --test-dir build --output-on-failure - - docker run --rm -v "$(pwd):/work" -w /work openra3-dev:local bash -c "mkdir -p artifacts_bin && cp build/bin/openra3 artifacts_bin/" - artifacts: - name: "$CI_PROJECT_NAME-executables-$CI_COMMIT_SHORT_SHA" - paths: - - artifacts_bin/ - expire_in: 7 days + services: *dind tags: - docker + needs: + - linux_image + before_script: + - docker load -i linux-image.tar + script: + - docker run --rm -v "$(pwd):/work" -w /work openra3-linux:local cmake -S . -B build/linux -G Ninja -DCMAKE_BUILD_TYPE=Release + - docker run --rm -v "$(pwd):/work" -w /work openra3-linux:local cmake --build build/linux -j + - docker run --rm -v "$(pwd):/work" -w /work openra3-linux:local ctest --test-dir build/linux --output-on-failure + - docker run --rm -v "$(pwd):/work" -w /work openra3-linux:local bash -c "mkdir -p artifacts/linux && cp build/linux/bin/openra3 artifacts/linux/" + artifacts: + name: "$CI_PROJECT_NAME-linux-$CI_COMMIT_SHORT_SHA" + paths: + - artifacts/linux/ + expire_in: 7 days + +build_windows: + stage: build + image: docker:latest + services: *dind + tags: + - docker + needs: + - windows_image + before_script: + - docker load -i windows-image.tar + script: + - docker run --rm -v "$(pwd):/work" -w /work openra3-windows:local + cmake -S . -B build/windows -G Ninja + -DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/llvm-mingw-x86_64.cmake + -DCMAKE_BUILD_TYPE=Release + - docker run --rm -v "$(pwd):/work" -w /work openra3-windows:local cmake --build build/windows -j + - docker run --rm -v "$(pwd):/work" -w /work openra3-windows:local bash -c "mkdir -p artifacts/windows && cp build/windows/bin/openra3.exe build/windows/bin/SDL3.dll artifacts/windows/" + artifacts: + name: "$CI_PROJECT_NAME-windows-$CI_COMMIT_SHORT_SHA" + paths: + - artifacts/windows/ + expire_in: 7 days diff --git a/CMakeLists.txt b/CMakeLists.txt index bcda2d6..b7bc9e4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,10 +1,31 @@ -cmake_minimum_required(VERSION 3.28) +cmake_minimum_required(VERSION 3.30) -project(OpenRA3 VERSION 0.0.1 LANGUAGES CXX) +# --- C++26 modules + `import std;` (clang + libc++) ------------------------- +# CMake gates `import std` behind an experimental value documented in +# Help/dev/experimental.rst for the CMake version in use. +set(CMAKE_EXPERIMENTAL_CXX_IMPORT_STD "d0edc3af-4c50-42ea-a356-e2862fe7a444") +# Path to the standard-library module description. A cross toolchain overrides +# this before the compiler is probed; this default targets the Linux LLVM +# package. +if(NOT CMAKE_CXX_STDLIB_MODULES_JSON) + set(CMAKE_CXX_STDLIB_MODULES_JSON "/usr/lib/llvm-21/lib/libc++.modules.json") +endif() + +# libc++ must be selected before the compiler is probed: CMake builds its +# internal standard-library module target during `project()`, and that target +# only sees `CMAKE_CXX_FLAGS` (not later `add_compile_options`). The standard +# and extension settings must also be in place before `project()` so the +# internal `std.pcm` is built with the same configuration as our modules. +set(CMAKE_CXX_FLAGS_INIT "-stdlib=libc++") +set(CMAKE_EXE_LINKER_FLAGS_INIT "-stdlib=libc++") +set(CMAKE_SHARED_LINKER_FLAGS_INIT "-stdlib=libc++") set(CMAKE_CXX_STANDARD 26) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) +set(CMAKE_CXX_SCAN_FOR_MODULES ON) + +project(OpenRA3 VERSION 0.1.0 LANGUAGES CXX) if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE) @@ -15,121 +36,113 @@ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") option(OPENRA3_WERROR "Treat warnings as errors" OFF) function(openra3_target_defaults target) - target_compile_options(${target} PRIVATE -Wall -Wextra -Wpedantic) + target_compile_options(${target} PRIVATE -Wall -Wextra) + set_property(TARGET ${target} PROPERTY CXX_MODULE_STD ON) if(OPENRA3_WERROR) target_compile_options(${target} PRIVATE -Werror) endif() endfunction() -# --- engine core: fundamental types, math, strings, random, message stream --- +# --- SDL3: pkg-config on Linux, or an explicit MinGW root for Windows -------- +add_library(openra3_sdl3 INTERFACE) +set(OPENRA3_HAS_SDL3 OFF) +if(OPENRA3_SDL3_ROOT) + target_include_directories(openra3_sdl3 INTERFACE "${OPENRA3_SDL3_ROOT}/x86_64-w64-mingw32/include") + target_link_libraries(openra3_sdl3 INTERFACE "${OPENRA3_SDL3_ROOT}/x86_64-w64-mingw32/lib/libSDL3.dll.a") + set(OPENRA3_HAS_SDL3 ON) +else() + find_package(PkgConfig QUIET) + if(PkgConfig_FOUND) + pkg_check_modules(SDL3 QUIET IMPORTED_TARGET sdl3) + endif() + if(SDL3_FOUND) + target_link_libraries(openra3_sdl3 INTERFACE PkgConfig::SDL3) + set(OPENRA3_HAS_SDL3 ON) + endif() +endif() +if(OPENRA3_HAS_SDL3) + message(STATUS "OpenRA3: SDL3 found - windowed viewer enabled") +else() + message(STATUS "OpenRA3: SDL3 not found - offscreen BMP output only") +endif() + +# --- engine core ------------------------------------------------------------- add_library(ra3_core STATIC) -target_sources(ra3_core - PUBLIC FILE_SET CXX_MODULES FILES - src/core/ra3.core.cppm -) +target_sources(ra3_core PUBLIC FILE_SET CXX_MODULES FILES src/core/ra3.core.cppm) openra3_target_defaults(ra3_core) -# --- game logic: objects, players, teams, spatial partition, game loop --- +# --- game logic -------------------------------------------------------------- add_library(ra3_logic STATIC) -target_sources(ra3_logic - PUBLIC FILE_SET CXX_MODULES FILES - src/logic/ra3.logic.cppm -) +target_sources(ra3_logic PUBLIC FILE_SET CXX_MODULES FILES src/logic/ra3.logic.cppm) target_link_libraries(ra3_logic PUBLIC ra3_core) openra3_target_defaults(ra3_logic) -# --- client: headless display/input abstraction and the client facade --- +# --- client ------------------------------------------------------------------ add_library(ra3_client STATIC) -target_sources(ra3_client - PUBLIC FILE_SET CXX_MODULES FILES - src/client/ra3.client.cppm -) +target_sources(ra3_client PUBLIC FILE_SET CXX_MODULES FILES src/client/ra3.client.cppm) target_link_libraries(ra3_client PUBLIC ra3_core ra3_logic) openra3_target_defaults(ra3_client) -# --- RA3 game definitions: factions, player templates, science, special powers --- +# --- RA3 game definitions ---------------------------------------------------- add_library(ra3_game STATIC) -target_sources(ra3_game - PUBLIC FILE_SET CXX_MODULES FILES - src/game/ra3.game.cppm -) +target_sources(ra3_game PUBLIC FILE_SET CXX_MODULES FILES src/game/ra3.game.cppm) target_link_libraries(ra3_game PUBLIC ra3_core ra3_logic) openra3_target_defaults(ra3_game) -# --- filesystem: BIG4 archives, RefPack codec, local install locator --- +# --- filesystem: BIG4 + RefPack --------------------------------------------- add_library(ra3_fs STATIC) -target_sources(ra3_fs - PUBLIC FILE_SET CXX_MODULES FILES - src/fs/ra3.fs.cppm -) +target_sources(ra3_fs PUBLIC FILE_SET CXX_MODULES FILES src/fs/ra3.fs.cppm) target_link_libraries(ra3_fs PUBLIC ra3_core) openra3_target_defaults(ra3_fs) -# --- map discovery/loading (real .big data, user's local install) --- +# --- map discovery/loading --------------------------------------------------- add_library(ra3_map STATIC) -target_sources(ra3_map - PUBLIC FILE_SET CXX_MODULES FILES - src/map/ra3.map.cppm -) +target_sources(ra3_map PUBLIC FILE_SET CXX_MODULES FILES src/map/ra3.map.cppm) target_link_libraries(ra3_map PUBLIC ra3_core ra3_fs) openra3_target_defaults(ra3_map) -# --- minimal headless skirmish simulation --- +# --- minimal skirmish simulation -------------------------------------------- add_library(ra3_skirmish STATIC) -target_sources(ra3_skirmish - PUBLIC FILE_SET CXX_MODULES FILES - src/skirmish/ra3.skirmish.cppm -) +target_sources(ra3_skirmish PUBLIC FILE_SET CXX_MODULES FILES src/skirmish/ra3.skirmish.cppm) target_link_libraries(ra3_skirmish PUBLIC ra3_core ra3_logic ra3_game ra3_map) openra3_target_defaults(ra3_skirmish) -# --- software renderer: framebuffer, TGA decode, BMP encode, map compositing --- +# --- software renderer ------------------------------------------------------- add_library(ra3_render STATIC) -target_sources(ra3_render - PUBLIC FILE_SET CXX_MODULES FILES - src/render/ra3.render.cppm -) +target_sources(ra3_render PUBLIC FILE_SET CXX_MODULES FILES src/render/ra3.render.cppm) target_link_libraries(ra3_render PUBLIC ra3_core) openra3_target_defaults(ra3_render) -# --- windowed viewer: SDL3 when available, otherwise a null backend --- -find_package(PkgConfig QUIET) -if(PkgConfig_FOUND) - pkg_check_modules(SDL3 QUIET IMPORTED_TARGET sdl3) -endif() +# --- windowed viewer (SDL3 or null) ----------------------------------------- add_library(ra3_ui STATIC) -if(SDL3_FOUND) - target_sources(ra3_ui - PUBLIC FILE_SET CXX_MODULES FILES - src/ui/ra3.ui.sdl.cppm - ) - target_link_libraries(ra3_ui PUBLIC ra3_core ra3_render PkgConfig::SDL3) - message(STATUS "OpenRA3: SDL3 ${SDL3_VERSION} found - windowed viewer enabled") +if(OPENRA3_HAS_SDL3) + target_sources(ra3_ui PUBLIC FILE_SET CXX_MODULES FILES src/ui/ra3.ui.sdl.cppm) + target_link_libraries(ra3_ui PUBLIC ra3_core ra3_render openra3_sdl3) else() - target_sources(ra3_ui - PUBLIC FILE_SET CXX_MODULES FILES - src/ui/ra3.ui.null.cppm - ) + target_sources(ra3_ui PUBLIC FILE_SET CXX_MODULES FILES src/ui/ra3.ui.null.cppm) target_link_libraries(ra3_ui PUBLIC ra3_core ra3_render) - message(STATUS "OpenRA3: SDL3 not found - offscreen BMP output only") endif() openra3_target_defaults(ra3_ui) -# --- umbrella module re-exporting the whole SDK --- +# --- umbrella ----------------------------------------------------------------- add_library(ra3 STATIC) -target_sources(ra3 - PUBLIC FILE_SET CXX_MODULES FILES - src/ra3.cppm -) +target_sources(ra3 PUBLIC FILE_SET CXX_MODULES FILES src/ra3.cppm) target_link_libraries(ra3 PUBLIC ra3_core ra3_logic ra3_client ra3_game ra3_fs ra3_map ra3_skirmish ra3_render ra3_ui) openra3_target_defaults(ra3) -# --- the headless game executable --- +# --- executable --------------------------------------------------------------- add_executable(openra3 apps/openra3/main.cpp) target_link_libraries(openra3 PRIVATE ra3) openra3_target_defaults(openra3) -# --- unit tests --- +if(WIN32 AND OPENRA3_SDL3_ROOT) + add_custom_command(TARGET openra3 POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${OPENRA3_SDL3_ROOT}/x86_64-w64-mingw32/bin/SDL3.dll" + "$") +endif() + +# --- tests -------------------------------------------------------------------- enable_testing() add_executable(ra3_tests tests/ra3_tests.cpp) target_link_libraries(ra3_tests PRIVATE ra3) diff --git a/Dockerfile b/Dockerfile index 3d32a71..9449794 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,9 @@ # syntax=docker/dockerfile:1 -# dev target: full toolchain + debug tools for development and debugging. -# Base image and compiler are the latest stable (ubuntu:26.04 ships gcc/g++ 16). +# Linux build environment. Isolated from the Windows cross image (Dockerfile.win) +# so the two toolchains and their standard libraries never interfere. +# +# dev target: clang + libc++ (for `import std;`) + SDL3 + debug tools. FROM ubuntu:26.04 AS dev ENV DEBIAN_FRONTEND=noninteractive @@ -23,44 +25,44 @@ RUN --mount=type=cache,target=/var/lib/apt,sharing=locked \ sleep 5; \ done \ && apt-get -o Acquire::Retries=5 install -y --no-install-recommends \ - build-essential \ ca-certificates \ + clang-21 \ + clang-tools-21 \ cmake \ - g++-16 \ gdb \ git \ + libc++-21-dev \ + libc++abi-21-dev \ libsdl3-dev \ ninja-build \ pkg-config \ && rm -rf /var/lib/apt/lists/* -# Pin the toolchain to GCC 16 (the distro default is still GCC 15). -ENV CC=gcc-16 -ENV CXX=g++-16 -RUN update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-16 100 \ - && update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-16 100 \ - && update-alternatives --install /usr/bin/c++ c++ /usr/bin/g++-16 100 +ENV CC=clang-21 +ENV CXX=clang++-21 WORKDIR /work COPY . . -# C++26 modules need CMake >= 3.28, the Ninja generator and a module-aware gcc. -RUN cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \ - && cmake --build build -j \ - && ctest --test-dir build --output-on-failure +RUN cmake -S . -B build/linux -G Ninja -DCMAKE_BUILD_TYPE=Release \ + && cmake --build build/linux -j \ + && ctest --test-dir build/linux --output-on-failure # deploy target: runtime dependencies + final binary, minimal and fast. FROM ubuntu:26.04 AS deploy ENV DEBIAN_FRONTEND=noninteractive -RUN apt-get -o Acquire::Retries=5 update \ +RUN --mount=type=cache,target=/var/lib/apt,sharing=locked \ + --mount=type=cache,target=/var/cache/apt,sharing=locked \ + apt-get -o Acquire::Retries=5 update \ && apt-get -o Acquire::Retries=5 install -y --no-install-recommends \ + libc++1 \ + libc++abi1 \ libsdl3-0 \ - libstdc++6 \ && rm -rf /var/lib/apt/lists/* WORKDIR /app -COPY --from=dev /work/build/bin/openra3 /app/openra3 +COPY --from=dev /work/build/linux/bin/openra3 /app/openra3 ENTRYPOINT ["/app/openra3"] diff --git a/Dockerfile.win b/Dockerfile.win new file mode 100644 index 0000000..ea35604 --- /dev/null +++ b/Dockerfile.win @@ -0,0 +1,63 @@ +# syntax=docker/dockerfile:1 + +# Windows cross-build environment (x86_64), fully isolated from the Linux image +# in Dockerfile so the two toolchains and standard libraries never interfere. +# +# Toolchain: llvm-mingw (clang + libc++ + the libc++ `std` module, MSVCRT +# runtime) with the official SDL3 MinGW development package. + +FROM ubuntu:26.04 AS dev + +ENV DEBIAN_FRONTEND=noninteractive + +ARG APT_MIRROR="" +RUN if [ -n "$APT_MIRROR" ]; then \ + sed -i -e "s|http://archive.ubuntu.com/ubuntu|${APT_MIRROR}|g" \ + -e "s|http://security.ubuntu.com/ubuntu|${APT_MIRROR}|g" \ + -e "s|http://ports.ubuntu.com/ubuntu-ports|${APT_MIRROR}|g" \ + /etc/apt/sources.list /etc/apt/sources.list.d/*.sources 2>/dev/null || true; \ + fi + +RUN --mount=type=cache,target=/var/lib/apt,sharing=locked \ + --mount=type=cache,target=/var/cache/apt,sharing=locked \ + for attempt in 1 2 3 4 5; do \ + apt-get -o Acquire::Retries=5 update && break; \ + echo "apt-get update failed (attempt $attempt), retrying"; \ + sleep 5; \ + done \ + && apt-get -o Acquire::Retries=5 install -y --no-install-recommends \ + ca-certificates \ + cmake \ + curl \ + ninja-build \ + xz-utils \ + && rm -rf /var/lib/apt/lists/* + +# Pinned toolchain + SDL3 versions (override with --build-arg to bump). +ARG LLVM_MINGW_VERSION=20260908 +ARG LLVM_MINGW_FLAVOR=msvcrt +ARG SDL3_VERSION=3.4.16 + +RUN mkdir -p /opt/llvm-mingw /opt/sdl3-mingw \ + && curl -fL "https://github.com/mstorsjo/llvm-mingw/releases/download/${LLVM_MINGW_VERSION}/llvm-mingw-${LLVM_MINGW_VERSION}-${LLVM_MINGW_FLAVOR}-ubuntu-22.04-x86_64.tar.xz" \ + | tar -xJ -C /opt/llvm-mingw --strip-components=1 \ + && curl -fL "https://github.com/libsdl-org/SDL/releases/download/release-${SDL3_VERSION}/SDL3-devel-${SDL3_VERSION}-mingw.tar.gz" \ + | tar -xz -C /opt/sdl3-mingw --strip-components=1 + +ENV OPENRA3_LLVM_MINGW_ROOT=/opt/llvm-mingw +ENV OPENRA3_SDL3_ROOT=/opt/sdl3-mingw + +WORKDIR /work +COPY . . + +RUN cmake -S . -B build/windows -G Ninja \ + -DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/llvm-mingw-x86_64.cmake \ + -DCMAKE_BUILD_TYPE=Release \ + && cmake --build build/windows -j + +# package target: the .exe plus the SDL3 runtime DLL. +FROM ubuntu:26.04 AS package + +WORKDIR /app +COPY --from=dev /work/build/windows/bin/openra3.exe /app/ +COPY --from=dev /work/build/windows/bin/SDL3.dll /app/ diff --git a/README.md b/README.md index 84c54b2..5e608ea 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ # OpenRA3 A from-scratch, portable re-implementation of **Command & Conquer: Red Alert 3** -in **C++26** using **C++ modules**, built with **GCC 16**. +in **C++26** using **C++ modules** and `import std;`, built with **Clang** for +both **Linux** and **Windows**. Red Alert 3 runs SAGE 2.0. EA never released that engine's C++ source, but it did open-source the closely related SAGE 1.0 engine as @@ -17,11 +18,13 @@ Ghidra. ## Status -OpenRA3 is at **v0.0.1**. The engine compiles and runs headless, and a **minimal +OpenRA3 is at **v0.2.0**. The engine compiles and runs headless, and a **minimal skirmish** is playable: it reads a real multiplayer map out of your install, recovers the player start waypoints, and simulates two sides earning credits, training units and fighting until one base falls. The map and match state can be -**rendered** to a window (SDL3) or to an image. +**rendered** to a window (SDL3) or to an image. The whole tree builds for +**Linux** (clang + libc++) and cross-compiles to **Windows** (`openra3.exe` + +`SDL3.dll`) with llvm-mingw — both using C++26 modules and `import std;`. ```text $ openra3 skirmish --game-dir "/game" --map map_mp_2_feasel4 @@ -73,19 +76,48 @@ built-in test map so the project still builds and runs in CI. | `tests/ra3_tests.cpp` | smoke tests (run via `ctest`) | | `tools/` | reference fetch + Ghidra-driven reconstruction helpers | | `docs/` | architecture, reverse-engineering notes, roadmap | -| `Dockerfile` | `dev` (toolchain) and `deploy` (runtime) targets | -| `.gitlab-ci.yml` | build → test → package pipeline | +| `Dockerfile` | Linux build image (`dev` toolchain + `deploy` runtime) | +| `Dockerfile.win` | isolated Windows cross-build image (llvm-mingw + SDL3 MinGW) | +| `cmake/toolchains/` | `llvm-mingw-x86_64.cmake` cross toolchain | +| `scripts/` | `build-linux.sh` / `build-windows.sh` one-shot builders | +| `.gitlab-ci.yml` | build both targets → test → package pipeline | + +## Why Clang + `import std;` + +The engine never `#include`s the standard library: every module does +`import std;`. CMake's support for that (`CXX_MODULE_STD`) works today with +Clang + libc++. Linux uses the distro clang (LLVM 21); Windows cross-compiles +with [llvm-mingw](https://github.com/mstorsjo/llvm-mingw) (clang 23 + libc++ + +the libc++ `std` module). The two toolchains live in **separate images** so +their compilers and standard libraries never interfere. ## Build -The host needs no toolchain: build inside the container. +The host needs no toolchain: each target builds inside its own image. ```bash -docker build --target dev -t openra3-dev:local . +# Linux -> build/linux/bin/openra3 +scripts/build-linux.sh -docker run --rm -v "$PWD:/work" -w /work openra3-dev:local cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -docker run --rm -v "$PWD:/work" -w /work openra3-dev:local cmake --build build -j -docker run --rm -v "$PWD:/work" -w /work openra3-dev:local ctest --test-dir build --output-on-failure +# Windows -> build/windows/bin/openra3.exe (+ SDL3.dll) +scripts/build-windows.sh +``` + +Or drive Docker directly: + +```bash +docker build --target dev -t openra3-linux:local . +docker run --rm -v "$PWD:/work" -w /work openra3-linux:local \ + cmake -S . -B build/linux -G Ninja -DCMAKE_BUILD_TYPE=Release +docker run --rm -v "$PWD:/work" -w /work openra3-linux:local cmake --build build/linux -j +docker run --rm -v "$PWD:/work" -w /work openra3-linux:local ctest --test-dir build/linux --output-on-failure + +docker build -f Dockerfile.win --target dev -t openra3-windows:local . +docker run --rm -v "$PWD:/work" -w /work openra3-windows:local \ + cmake -S . -B build/windows -G Ninja \ + -DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/llvm-mingw-x86_64.cmake \ + -DCMAKE_BUILD_TYPE=Release +docker run --rm -v "$PWD:/work" -w /work openra3-windows:local cmake --build build/windows -j ``` Behind a slow or blocked mirror, pass `--build-arg APT_MIRROR=`. @@ -94,14 +126,17 @@ Behind a slow or blocked mirror, pass `--build-arg APT_MIRROR=`. ```bash # list the maps in your install -docker run --rm -v "/path/to/Red Alert 3:/game:ro" openra3-dev:local \ - ./build/bin/openra3 maps --game-dir /game +docker run --rm -v "/path/to/Red Alert 3:/game:ro" openra3-linux:local \ + /work/build/linux/bin/openra3 maps --game-dir /game # play a headless skirmish on a real map -docker run --rm -v "/path/to/Red Alert 3:/game:ro" openra3-dev:local \ - ./build/bin/openra3 skirmish --game-dir /game --map map_mp_2_feasel4 --seed 7 +docker run --rm -v "/path/to/Red Alert 3:/game:ro" openra3-linux:local \ + /work/build/linux/bin/openra3 skirmish --game-dir /game --map map_mp_2_feasel4 --seed 7 ``` +The Windows build is a native `openra3.exe` — copy it next to `SDL3.dll` and run +it from `cmd`/PowerShell, e.g. `openra3.exe skirmish --game-dir "C:\Red Alert 3"`. + `--frames N` caps the simulation length (default 15 minutes of game time at 30 Hz). The result is deterministic for a given map and seed. @@ -113,10 +148,13 @@ The renderer draws the map's own overview art, a world grid and the match state **Offscreen image** (works anywhere, no display needed): ```bash -docker run --rm -v "/path/to/Red Alert 3:/game:ro" -v "$PWD/out:/out" openra3-dev:local \ - /work/build/bin/openra3 render --game-dir /game --map map_mp_2_feasel4 --out /out/map.bmp +docker run --rm -v "/path/to/Red Alert 3:/game:ro" -v "$PWD/out:/out" openra3-linux:local \ + /work/build/linux/bin/openra3 render --game-dir /game --map map_mp_2_feasel4 --out /out/map.bmp ``` +On Windows the same command runs natively: `openra3.exe render --game-dir +"C:\Red Alert 3" --map map_mp_2_feasel4` opens an SDL3 window. + **Interactive window** (SDL3; drag to pan, wheel to zoom, Esc to quit). In a container you need an X server on the host — on Windows run [VcXsrv](https://sourceforge.net/projects/vcxsrv/) and launch it with "Disable @@ -124,7 +162,7 @@ access control", then: ```bash docker run --rm -v "/path/to/Red Alert 3:/game:ro" -e DISPLAY=host.docker.internal:0.0 \ - openra3-dev:local /work/build/bin/openra3 render --game-dir /game --map map_mp_2_feasel4 + openra3-linux:local /work/build/linux/bin/openra3 render --game-dir /game --map map_mp_2_feasel4 ``` If no display is available the viewer falls back to writing `openra3_view.bmp`. diff --git a/VERSION b/VERSION index 6e8bf73..0ea3a94 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1.0 +0.2.0 diff --git a/apps/openra3/main.cpp b/apps/openra3/main.cpp index 4d4417c..2cd0b45 100644 --- a/apps/openra3/main.cpp +++ b/apps/openra3/main.cpp @@ -1,13 +1,4 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include - +import std; import ra3; namespace { diff --git a/cmake/toolchains/llvm-mingw-x86_64.cmake b/cmake/toolchains/llvm-mingw-x86_64.cmake new file mode 100644 index 0000000..e91a711 --- /dev/null +++ b/cmake/toolchains/llvm-mingw-x86_64.cmake @@ -0,0 +1,43 @@ +# Cross-compile for Windows x86_64 with llvm-mingw: clang + libc++ + the +# libc++ `std` module, targeting the MSVCRT MinGW runtime (which is what the +# official SDL3 MinGW package links against). + +# The `import std` gate must be set before the compiler is probed; a toolchain +# file is processed earlier than the project's own top-level settings. +set(CMAKE_EXPERIMENTAL_CXX_IMPORT_STD "d0edc3af-4c50-42ea-a356-e2862fe7a444") + +set(CMAKE_SYSTEM_NAME Windows) +set(CMAKE_SYSTEM_PROCESSOR x86_64) + +set(OPENRA3_LLVM_MINGW_ROOT "/opt/llvm-mingw" CACHE PATH "llvm-mingw installation root") +if(NOT EXISTS "${OPENRA3_LLVM_MINGW_ROOT}/bin/clang++") + message(FATAL_ERROR "llvm-mingw not found at ${OPENRA3_LLVM_MINGW_ROOT} (expected bin/clang++)") +endif() + +set(CMAKE_C_COMPILER "${OPENRA3_LLVM_MINGW_ROOT}/bin/clang") +set(CMAKE_CXX_COMPILER "${OPENRA3_LLVM_MINGW_ROOT}/bin/clang++") +set(CMAKE_C_COMPILER_TARGET x86_64-w64-mingw32) +set(CMAKE_CXX_COMPILER_TARGET x86_64-w64-mingw32) +# CMake's standard-library detection does not pass COMPILER_TARGET, so without +# this it probes the host standard library. Putting the target in ARG1 makes +# the probe see libc++ (the MinGW target's library). +set(CMAKE_C_COMPILER_ARG1 "--target=x86_64-w64-mingw32") +set(CMAKE_CXX_COMPILER_ARG1 "--target=x86_64-w64-mingw32") + +# libc++ standard-library module description for the MinGW target. Must be set +# before the compiler is probed so CMake can build `import std;`. +set(CMAKE_CXX_STDLIB_MODULES_JSON "${OPENRA3_LLVM_MINGW_ROOT}/x86_64-w64-mingw32/lib/libc++.modules.json") + +# Select libc++ explicitly so CMake's `import std` detection recognises the +# standard library (llvm-mingw defaults to it, but CMake keys off -stdlib=). +set(CMAKE_CXX_FLAGS_INIT "-stdlib=libc++") +set(CMAKE_EXE_LINKER_FLAGS_INIT "-stdlib=libc++") +set(CMAKE_SHARED_LINKER_FLAGS_INIT "-stdlib=libc++") + +# Official SDL3 MinGW development package (headers + import library + DLL). +set(OPENRA3_SDL3_ROOT "/opt/sdl3-mingw" CACHE PATH "SDL3 MinGW development package root") + +set(CMAKE_FIND_ROOT_PATH "${OPENRA3_LLVM_MINGW_ROOT}/x86_64-w64-mingw32") +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index ff34d4b..36e9081 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,7 +1,9 @@ # Architecture -OpenRA3 is organised as a stack of C++ modules. Each layer may import the ones -below it, never the ones above. +OpenRA3 is organised as a stack of C++26 modules. Each layer may import the ones +below it, never the ones above. Every module uses `import std;` for the standard +library; the engine is built with Clang + libc++ on Linux and cross-compiled +with llvm-mingw for Windows. ``` ┌───────────────────────────────────────┐ diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 302bab9..402272b 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -14,6 +14,10 @@ that each one produces something that builds and runs. - [x] **v0.1.0 — map renderer + window.** Software ARGB framebuffer, TGA decoder for the map art, BMP output, map/grid/unit compositing, and an SDL3 window viewer (pan/zoom). +- [x] **v0.2.0 — dual toolchain.** Switched to Clang + libc++ with + `import std;` in every module; added an isolated Windows cross-build + (llvm-mingw + SDL3 MinGW) producing `openra3.exe` + `SDL3.dll`, alongside + the Linux build. Both images are separate to keep the toolchains apart. ## Next diff --git a/scripts/build-linux.sh b/scripts/build-linux.sh new file mode 100644 index 0000000..fb7cc61 --- /dev/null +++ b/scripts/build-linux.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# Build the Linux target inside its own isolated image (Dockerfile). +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +IMAGE="${OPENRA3_LINUX_IMAGE:-openra3-linux:local}" +BUILD_DIR="${1:-build/linux}" + +docker build --target dev -t "$IMAGE" "$ROOT" +docker run --rm -v "$ROOT:/work" -w /work "$IMAGE" cmake -S . -B "$BUILD_DIR" -G Ninja -DCMAKE_BUILD_TYPE=Release +docker run --rm -v "$ROOT:/work" -w /work "$IMAGE" cmake --build "$BUILD_DIR" -j +docker run --rm -v "$ROOT:/work" -w /work "$IMAGE" ctest --test-dir "$BUILD_DIR" --output-on-failure + +echo "Linux build: $ROOT/$BUILD_DIR/bin/openra3" diff --git a/scripts/build-windows.sh b/scripts/build-windows.sh new file mode 100644 index 0000000..b881fed --- /dev/null +++ b/scripts/build-windows.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# Build the Windows x86_64 target inside its own isolated image (Dockerfile.win). +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +IMAGE="${OPENRA3_WINDOWS_IMAGE:-openra3-windows:local}" +BUILD_DIR="${1:-build/windows}" + +docker build --target dev -f "$ROOT/Dockerfile.win" -t "$IMAGE" "$ROOT" +docker run --rm -v "$ROOT:/work" -w /work "$IMAGE" \ + cmake -S . -B "$BUILD_DIR" -G Ninja \ + -DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/llvm-mingw-x86_64.cmake \ + -DCMAKE_BUILD_TYPE=Release +docker run --rm -v "$ROOT:/work" -w /work "$IMAGE" cmake --build "$BUILD_DIR" -j + +echo "Windows build: $ROOT/$BUILD_DIR/bin/openra3.exe (+ SDL3.dll)" diff --git a/src/client/ra3.client.cppm b/src/client/ra3.client.cppm index d9c6d82..f92592f 100644 --- a/src/client/ra3.client.cppm +++ b/src/client/ra3.client.cppm @@ -1,11 +1,7 @@ -module; - -#include -#include -#include - export module ra3.client; +import std; + import ra3.core; import ra3.logic; diff --git a/src/core/ra3.core.cppm b/src/core/ra3.core.cppm index 5345f81..c330c53 100644 --- a/src/core/ra3.core.cppm +++ b/src/core/ra3.core.cppm @@ -1,20 +1,7 @@ -module; - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - export module ra3.core; +import std; + /** * Fundamental engine types shared by every OpenRA3 module. * @@ -24,7 +11,7 @@ export module ra3.core; */ export namespace ra3::core { inline constexpr int version_major = 0; - inline constexpr int version_minor = 1; + inline constexpr int version_minor = 2; inline constexpr int version_patch = 0; using real = float; diff --git a/src/fs/ra3.fs.cppm b/src/fs/ra3.fs.cppm index 1b7e7c3..3847cf2 100644 --- a/src/fs/ra3.fs.cppm +++ b/src/fs/ra3.fs.cppm @@ -1,23 +1,7 @@ -module; - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - export module ra3.fs; +import std; + export import ra3.core; /** diff --git a/src/game/ra3.game.cppm b/src/game/ra3.game.cppm index 779afdd..ff3a98f 100644 --- a/src/game/ra3.game.cppm +++ b/src/game/ra3.game.cppm @@ -1,12 +1,7 @@ -module; - -#include -#include -#include -#include - export module ra3.game; +import std; + import ra3.core; import ra3.logic; diff --git a/src/logic/ra3.logic.cppm b/src/logic/ra3.logic.cppm index 0b292cc..8ab0d8e 100644 --- a/src/logic/ra3.logic.cppm +++ b/src/logic/ra3.logic.cppm @@ -1,17 +1,7 @@ -module; - -#include -#include -#include -#include -#include -#include -#include -#include -#include - export module ra3.logic; +import std; + import ra3.core; /** diff --git a/src/map/ra3.map.cppm b/src/map/ra3.map.cppm index c9f0a00..df05c98 100644 --- a/src/map/ra3.map.cppm +++ b/src/map/ra3.map.cppm @@ -1,18 +1,7 @@ -module; - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - export module ra3.map; +import std; + export import ra3.core; export import ra3.fs; diff --git a/src/render/ra3.render.cppm b/src/render/ra3.render.cppm index 4e8cffa..2997dbe 100644 --- a/src/render/ra3.render.cppm +++ b/src/render/ra3.render.cppm @@ -1,18 +1,7 @@ -module; - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - export module ra3.render; +import std; + export import ra3.core; /** @@ -223,7 +212,7 @@ export namespace ra3::render { out[at + 2U] = static_cast(value >> 16U); out[at + 3U] = static_cast(value >> 24U); }; - auto put16 = [&out](usize at, uint16_t value) { + auto put16 = [&out](usize at, std::uint16_t value) { out[at] = static_cast(value); out[at + 1U] = static_cast(value >> 8U); }; diff --git a/src/skirmish/ra3.skirmish.cppm b/src/skirmish/ra3.skirmish.cppm index bc86928..b936d47 100644 --- a/src/skirmish/ra3.skirmish.cppm +++ b/src/skirmish/ra3.skirmish.cppm @@ -1,16 +1,7 @@ -module; - -#include -#include -#include -#include -#include -#include -#include -#include - export module ra3.skirmish; +import std; + export import ra3.core; export import ra3.map; export import ra3.game; diff --git a/src/ui/ra3.ui.null.cppm b/src/ui/ra3.ui.null.cppm index af0169a..bcc498c 100644 --- a/src/ui/ra3.ui.null.cppm +++ b/src/ui/ra3.ui.null.cppm @@ -1,9 +1,6 @@ -module; - -#include - export module ra3.ui; +import std; export import ra3.core; import ra3.render; diff --git a/src/ui/ra3.ui.sdl.cppm b/src/ui/ra3.ui.sdl.cppm index 91bead5..d795b0a 100644 --- a/src/ui/ra3.ui.sdl.cppm +++ b/src/ui/ra3.ui.sdl.cppm @@ -2,11 +2,9 @@ module; #include -#include -#include - export module ra3.ui; +import std; export import ra3.core; import ra3.render; diff --git a/tests/ra3_tests.cpp b/tests/ra3_tests.cpp index 88878ee..129f123 100644 --- a/tests/ra3_tests.cpp +++ b/tests/ra3_tests.cpp @@ -1,12 +1,4 @@ -#include -#include -#include -#include -#include -#include -#include -#include - +import std; import ra3; namespace {