diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1abc322..8334dd8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,7 +1,7 @@ # Lander CI, shaped after Augmentinel and blast_radius. # -# Every PR builds and tests on Windows and macOS, runs a smoke test, and -# uploads the game as a downloadable artifact (Lander-windows, Lander-macos). +# Every PR builds and tests on Windows and macOS, runs a smoke test, checks the +# game needs nothing installed, and uploads the Windows build as an artifact. # Tag pushes (v*) do the same, sign the Windows exe when the eSigner secrets # exist, and publish it to itch.io. macOS signing, notarisation and publishing # run locally (scripts/build_release.sh). See the Releasing section of README.md. @@ -30,9 +30,6 @@ concurrency: group: ci-${{ github.ref }} cancel-in-progress: true -env: - SDL2_VERSION: 2.32.10 - jobs: # Emit the build matrix from the dispatch input. setup: @@ -76,23 +73,15 @@ jobs: steps: - uses: actions/checkout@v5 - # The official SDL2 development package for MSVC (x64), with its CMake - # config; SDL2.dll ships beside lander.exe. - - name: Install SDL2 (Windows) + # Builds match what ships: SDL2 built from source and linked statically, + # with the static C runtime on Windows and a universal binary on macOS + - name: Configure (Windows) if: runner.os == 'Windows' - shell: pwsh - run: | - $zip = Join-Path $env:RUNNER_TEMP "SDL2.zip" - Invoke-WebRequest "https://github.com/libsdl-org/SDL/releases/download/release-$env:SDL2_VERSION/SDL2-devel-$env:SDL2_VERSION-VC.zip" -OutFile $zip - Expand-Archive $zip -DestinationPath $env:RUNNER_TEMP -Force - "CMAKE_PREFIX_PATH=$env:RUNNER_TEMP\SDL2-$env:SDL2_VERSION" | Out-File -FilePath $env:GITHUB_ENV -Append + run: cmake -B build -DLANDER_VENDOR_SDL2=ON - - name: Install SDL2 (macOS) + - name: Configure (macOS, universal) if: runner.os == 'macOS' - run: brew install sdl2 - - - name: Configure - run: cmake -B build -DCMAKE_BUILD_TYPE=Release + run: cmake -B build -DCMAKE_BUILD_TYPE=Release -DLANDER_VENDOR_SDL2=ON "-DCMAKE_OSX_ARCHITECTURES=arm64;x86_64" - name: Build run: cmake --build build --config Release -j @@ -126,6 +115,30 @@ jobs: path: ${{ runner.temp }}/smoke/smoke.png if-no-files-found: ignore + # Players must not need anything installed (such as the Visual C++ + # redistributable): lander.exe may only import DLLs that ship with Windows + - name: Check dependencies (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + $vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" + $dumpbin = & $vswhere -latest -find "VC\Tools\MSVC\**\bin\Hostx64\x64\dumpbin.exe" | Select-Object -First 1 + $imports = & $dumpbin /dependents build\Release\lander.exe | Where-Object { $_ -match '^\s+\S+\.dll\s*$' } | ForEach-Object { $_.Trim() } + Write-Host "lander.exe imports: $($imports -join ', ')" + $bad = $imports | Where-Object { $_ -match '^(vcruntime|msvcp|ucrtbase|api-ms-win-crt|sdl)' } + if ($bad) { Write-Error "lander.exe depends on DLLs players won't have: $($bad -join ', ')"; exit 1 } + + - name: Check dependencies (macOS) + if: runner.os == 'macOS' + run: | + lipo -archs build/lander + for a in arm64 x86_64; do + lipo -archs build/lander | tr ' ' '\n' | grep -qx "$a" || { echo "ERROR: missing the $a slice"; exit 1; } + done + echo "minos $(otool -l build/lander | awk '/minos/ { print $2; exit }')" + bad="$(otool -L -arch all build/lander | awk 'NR > 1 && !/architecture/ { print $1 }' | grep -vE '^(/System/|/usr/lib/)' || true)" + [ -z "$bad" ] || { echo "ERROR: lander links libraries players won't have:"; echo "$bad"; exit 1; } + # The .rc embeds the version block; fail if it is missing. - name: Check version resource (Windows) if: runner.os == 'Windows' @@ -195,14 +208,14 @@ jobs: Write-Host "Signature status: $($sig.Status); signer: $($sig.SignerCertificate.Subject)" if ($sig.Status -ne 'Valid') { Write-Error "lander.exe signature is not Valid: $($sig.Status)"; exit 1 } - # lander.exe needs SDL2.dll and the sounds beside it. + # lander.exe is self-contained apart from the sounds beside it. - name: Package (Windows) if: runner.os == 'Windows' shell: pwsh run: | $out = "dist\Lander-windows" New-Item -ItemType Directory -Force -Path $out | Out-Null - foreach ($item in "lander.exe", "SDL2.dll", "sounds") { + foreach ($item in "lander.exe", "sounds") { Copy-Item "build\Release\$item" $out -Recurse } Get-ChildItem $out -Recurse -File | ForEach-Object { Write-Host $_.FullName.Substring((Resolve-Path $out).Path.Length + 1) } diff --git a/CMakeLists.txt b/CMakeLists.txt index 5face0a..d53eca3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,17 @@ cmake_minimum_required(VERSION 3.16) -project(lander VERSION 1.2.0 LANGUAGES CXX) + +# Release builds build SDL2 from source and link it statically, so the game +# needs nothing installed: a single exe on Windows, a self-contained app on +# macOS. Development builds can use an installed SDL2 (e.g. Homebrew's). +option(LANDER_VENDOR_SDL2 "Build SDL2 from source and link it statically" OFF) + +# Self-contained macOS builds run on macOS 11 and later (this must be set +# before project() to take effect) +if(APPLE AND LANDER_VENDOR_SDL2 AND NOT CMAKE_OSX_DEPLOYMENT_TARGET) + set(CMAKE_OSX_DEPLOYMENT_TARGET "11.0" CACHE STRING "Minimum macOS version") +endif() + +project(lander VERSION 1.2.1 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -10,6 +22,29 @@ if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE) endif() +# Link the C/C++ runtime statically, so players don't need the Visual C++ +# redistributable +set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") + +if(LANDER_VENDOR_SDL2) + include(FetchContent) + set(SDL_SHARED OFF CACHE BOOL "" FORCE) + set(SDL_STATIC ON CACHE BOOL "" FORCE) + set(SDL_TEST OFF CACHE BOOL "" FORCE) + set(SDL_FORCE_STATIC_VCRT ON CACHE BOOL "" FORCE) + set(SDL2_DISABLE_INSTALL ON CACHE BOOL "" FORCE) + FetchContent_Declare(SDL2 + URL https://github.com/libsdl-org/SDL/releases/download/release-2.32.10/SDL2-2.32.10.tar.gz + URL_HASH SHA256=5f5993c530f084535c65a6879e9b26ad441169b3e25d789d83287040a9ca5165 + DOWNLOAD_EXTRACT_TIMESTAMP TRUE) + # Before our warning flags, which are for our own code + FetchContent_MakeAvailable(SDL2) + set(LANDER_SDL2_LIBRARIES SDL2::SDL2main SDL2::SDL2) +else() + find_package(SDL2 REQUIRED) + set(LANDER_SDL2_LIBRARIES ${SDL2_LIBRARIES}) +endif() + if(MSVC) add_compile_options(/W3) # M_PI in , and portable C stdio without "unsafe" warnings @@ -22,8 +57,6 @@ else() add_compile_options(-fwrapv) endif() -find_package(SDL2 REQUIRED) - # ============================================================================= # Game # ============================================================================= @@ -51,7 +84,7 @@ add_library(lander_core STATIC src/settings.cpp ) target_include_directories(lander_core PUBLIC src ${SDL2_INCLUDE_DIRS}) -target_link_libraries(lander_core PUBLIC ${SDL2_LIBRARIES}) +target_link_libraries(lander_core PUBLIC ${LANDER_SDL2_LIBRARIES}) add_executable(lander src/main.cpp) target_link_libraries(lander PRIVATE lander_core) @@ -71,8 +104,8 @@ if(WIN32) target_sources(lander PRIVATE ${CMAKE_BINARY_DIR}/generated/lander.rc) endif() -if(MSVC) - # Windows: Copy SDL2.dll to output directory +if(MSVC AND NOT LANDER_VENDOR_SDL2) + # Windows with an installed SDL2: copy SDL2.dll to the output directory add_custom_command(TARGET lander POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different $ diff --git a/README.md b/README.md index 341e50e..71eeb95 100644 --- a/README.md +++ b/README.md @@ -145,12 +145,20 @@ ctest --output-on-failure The version comes from `project(lander VERSION ...)` in `CMakeLists.txt`; the release scripts stamp it into the app bundle and `lander.exe`. -**macOS** (`scripts/build_release.sh`): builds `dist/Lander.app`, signs it with -the Developer ID certificate (hardened runtime, timestamped), notarises it with -the `lineage-notary` notarytool profile, staples the ticket and zips it to -`dist/Lander-macOS.zip`. Override with `SIGN_IDENTITY` / `NOTARY_PROFILE`, or -use `SKIP_NOTARIZE=1` for a quick signed-only build. Without the certificate -it produces an ad-hoc signed `Lander-macOS-unsigned.zip`. +**Self-contained builds:** releases build SDL2 from source and link it +statically (`-DLANDER_VENDOR_SDL2=ON`), so players need nothing installed. On +Windows that is a single `lander.exe` with the C runtime linked in too; on +macOS a universal (Apple Silicon and Intel) app for macOS 11 or later. +Everyday builds can use an installed SDL2, such as Homebrew's. + +**macOS** (`scripts/build_release.sh`): builds `dist/Lander.app` self-contained +and universal, checks it links only system libraries, signs it with the +Developer ID certificate (hardened runtime, timestamped), launches it once as a +smoke test, notarises it with the `lineage-notary` notarytool profile, staples +the ticket and zips it to `dist/Lander-macOS.zip`. Override with +`SIGN_IDENTITY` / `NOTARY_PROFILE`, or use `SKIP_NOTARIZE=1` for a quick +signed-only build. Without the certificate it produces an ad-hoc signed +`Lander-macOS-unsigned.zip`. **Windows** (GitHub Actions, `.github/workflows/ci.yml`): every PR builds, tests and smoke-tests on Windows and macOS. Pushing a `vX.Y.Z` tag (matching diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index aa2da51..a177bd8 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -72,6 +72,7 @@ | - | Authentic controls: exact port of mouse polar conversion, original steering damping per physics step, original friction | 3c15e3b | | - | Authentic rocks and gravity: rocks drop in front of the ship as in the original; gravity rises with score | 1346b21 | | - | v1.2.0 release: GitHub Actions CI (build, test, smoke test, Windows signing and itch.io publishing) | 7a4c13d | +| - | v1.2.1 release: self-contained builds (static SDL2 and C runtime; universal macOS 11+ app) | bbe3f22 | ## Summary diff --git a/scripts/build_release.sh b/scripts/build_release.sh index d55a26d..834eead 100755 --- a/scripts/build_release.sh +++ b/scripts/build_release.sh @@ -61,12 +61,24 @@ rm -rf "$ICONSET_DIR" echo "==> Build release binary (fresh configure, so a stale cache can't leak in)" rm -rf "$BUILD_DIR/CMakeCache.txt" "$BUILD_DIR/CMakeFiles" -cmake -S "$PROJECT_DIR" -B "$BUILD_DIR" -DCMAKE_BUILD_TYPE=Release +# SDL2 is built from source and linked statically, as a universal binary for +# Apple Silicon and Intel Macs running macOS 11 or later +cmake -S "$PROJECT_DIR" -B "$BUILD_DIR" -DCMAKE_BUILD_TYPE=Release \ + -DLANDER_VENDOR_SDL2=ON "-DCMAKE_OSX_ARCHITECTURES=arm64;x86_64" cmake --build "$BUILD_DIR" --target lander -j "$(sysctl -n hw.ncpu)" +echo "==> Check the binary is self-contained" +for arch in arm64 x86_64; do + lipo -archs "$BUILD_DIR/lander" | tr ' ' '\n' | grep -qx "$arch" \ + || { echo "ERROR: lander is missing the $arch slice"; exit 1; } +done +NON_SYSTEM="$(otool -L -arch all "$BUILD_DIR/lander" | awk 'NR > 1 && !/architecture/ { print $1 }' \ + | grep -vE '^(/System/|/usr/lib/)' || true)" +[ -z "$NON_SYSTEM" ] || { echo "ERROR: lander links libraries players won't have:"; echo "$NON_SYSTEM"; exit 1; } + echo "==> Assemble app bundle" rm -rf "$APP_BUNDLE" -mkdir -p "$APP_BUNDLE/Contents/MacOS" "$APP_BUNDLE/Contents/Resources" "$APP_BUNDLE/Contents/Frameworks" +mkdir -p "$APP_BUNDLE/Contents/MacOS" "$APP_BUNDLE/Contents/Resources" BINARY="$APP_BUNDLE/Contents/MacOS/lander" cp "$BUILD_DIR/lander" "$BINARY" cp "$DIST_DIR/lander.icns" "$APP_BUNDLE/Contents/Resources/" @@ -74,7 +86,7 @@ cp -R "$PROJECT_DIR/sounds" "$APP_BUNDLE/Contents/Resources/" printf 'APPL????' > "$APP_BUNDLE/Contents/PkgInfo" # Info.plist takes its version from CMakeLists.txt, and its minimum macOS -# version from the binary (Homebrew SDL2 targets the build machine's macOS) +# version from the binary PLIST="$APP_BUNDLE/Contents/Info.plist" cp "$PROJECT_DIR/macos/Info.plist" "$PLIST" MIN_MACOS="$(otool -l "$BINARY" | awk '/minos/ { print $2; exit }')" @@ -84,43 +96,13 @@ MIN_MACOS="$(otool -l "$BINARY" | awk '/minos/ { print $2; exit }')" -c "Set :LSMinimumSystemVersion $MIN_MACOS" \ "$PLIST" -echo "==> Bundle SDL2" -SDL2_PATH="$(otool -L "$BINARY" | grep -o '/.*libSDL2.*\.dylib' | head -1)" -if [ -z "$SDL2_PATH" ]; then - echo "ERROR: Could not find SDL2 library path" - exit 1 -fi -SDL2_DYLIB="$APP_BUNDLE/Contents/Frameworks/$(basename "$SDL2_PATH")" -cp "$SDL2_PATH" "$SDL2_DYLIB" -chmod u+w "$SDL2_DYLIB" -install_name_tool -change "$SDL2_PATH" "@executable_path/../Frameworks/$(basename "$SDL2_PATH")" "$BINARY" -install_name_tool -id "@executable_path/../Frameworks/$(basename "$SDL2_PATH")" "$SDL2_DYLIB" - -# Homebrew's sdl2 is now sdl2-compat, which implements SDL2 on top of SDL3 and -# loads SDL3 when it starts, looking first for libSDL3.dylib beside itself. -# Without it the app shows an error dialog and never starts. -SDL3_DYLIB="" -if grep -qa "sdl2-compat" "$SDL2_DYLIB"; then - echo "==> Bundle SDL3 (SDL2 is sdl2-compat)" - SDL3_PATH="$(brew --prefix sdl3)/lib/libSDL3.0.dylib" - [ -f "$SDL3_PATH" ] || { echo "ERROR: sdl2-compat needs SDL3, not found at $SDL3_PATH"; exit 1; } - SDL3_DYLIB="$APP_BUNDLE/Contents/Frameworks/libSDL3.dylib" - cp "$SDL3_PATH" "$SDL3_DYLIB" - chmod u+w "$SDL3_DYLIB" - install_name_tool -id "@loader_path/libSDL3.dylib" "$SDL3_DYLIB" -fi -FRAMEWORK_DYLIBS=("$SDL2_DYLIB") -[ -n "$SDL3_DYLIB" ] && FRAMEWORK_DYLIBS+=("$SDL3_DYLIB") - -# Inner code first, then the bundle. Hardened runtime and a secure timestamp -# are required for notarisation; no entitlements are needed. +# Hardened runtime and a secure timestamp are required for notarisation; no +# entitlements are needed if [ "$UNSIGNED" = "1" ]; then echo "==> Codesign (ad-hoc, UNSIGNED build)" - codesign --force --sign - "${FRAMEWORK_DYLIBS[@]}" codesign --force --sign - "$APP_BUNDLE" else echo "==> Codesign ($SIGN_IDENTITY)" - codesign --force --options runtime --timestamp --sign "$SIGN_IDENTITY" "${FRAMEWORK_DYLIBS[@]}" codesign --force --options runtime --timestamp --sign "$SIGN_IDENTITY" "$APP_BUNDLE" fi codesign --verify --deep --strict --verbose=2 "$APP_BUNDLE"