From fefde42033eff53bb158bb9385be091e8d243c5c Mon Sep 17 00:00:00 2001 From: utophii Date: Tue, 3 Nov 2026 20:32:47 +0300 Subject: [PATCH] feat: add musl linux release build --- .github/workflows/game-build.yml | 23 ++++++++-- .github/workflows/nightly-build.yml | 3 +- docs/release-flow.md | 8 ++++ tests/test_check_imports.py | 68 +++++++++++++++++++++++++++++ tools/check_imports.py | 19 +++++--- tools/ci/build-musl.sh | 39 +++++++++++++++++ 6 files changed, 151 insertions(+), 9 deletions(-) create mode 100644 tests/test_check_imports.py create mode 100644 tools/ci/build-musl.sh diff --git a/.github/workflows/game-build.yml b/.github/workflows/game-build.yml index c7f56bb020..f30a48defa 100644 --- a/.github/workflows/game-build.yml +++ b/.github/workflows/game-build.yml @@ -94,6 +94,11 @@ jobs: # The server build too: packaging/server/install.sh installs it on Linux. server_dir: horizon-server-linux-x86_64 server_asset: horizon-server-linux-x86_64.tar.gz + - name: linux-musl + os: ubuntu-22.04 + musl: true + dir: horizon-linux-musl-x86_64 + asset: horizon-linux-musl-x86_64.tar.gz - name: windows os: windows-latest dir: horizon-windows-x64 @@ -141,17 +146,19 @@ jobs: with: targets: ${{ matrix.target }} - uses: Swatinem/rust-cache@v2 - if: runner.environment == 'github-hosted' + if: runner.environment == 'github-hosted' && !matrix.musl with: # A release restores only what a release saved: an untrusted build has its own key and saves nothing. shared-key: ${{ inputs.trusted && 'release' || 'nightly' }}-${{ matrix.name }} save-if: ${{ inputs.trusted }} - name: ALSA and udev headers - if: runner.os == 'Linux' + if: runner.os == 'Linux' && !matrix.musl run: sudo apt-get update && sudo apt-get install -y --no-install-recommends libasound2-dev libudev-dev - name: Build the client, horizon-ctl, horizon-prepare and horizon-server + if: ${{ !matrix.musl }} run: cargo build --release --locked ${{ matrix.target && format('--target {0}', matrix.target) || '' }} -p horizon-client -p horizon-client-lib --bin horizon-client --bin horizon-ctl -p horizon-prepare --bin horizon-prepare -p horizon-server --bin horizon-server - name: Every program the release folder takes was built + if: ${{ !matrix.musl }} run: | for binary in horizon-client horizon-ctl horizon-prepare horizon-server; do test -f "$BIN/$binary${{ matrix.exe }}" || { echo "::error::$BIN/$binary${{ matrix.exe }} is missing"; exit 1; } @@ -173,6 +180,7 @@ jobs: codesign --verify --strict --verbose=2 "$BIN/$binary" done - name: Only system libraries + if: ${{ !matrix.musl }} # Windows: no non-system DLL (a "DLL not found" dialog on some players' machines). Linux: the standard # loader and libraries every desktop distribution has. macOS: only the system's own libraries. run: | @@ -188,13 +196,14 @@ jobs: - name: Baseline x86-64 CPUs only # No POPCNT or LZCNT at all (the script says why only those): a C library's count intrinsic killed the # game before main on a Core 2 (#7264). Not the Mac builds: every Intel Mac that runs macOS 11 has both. - if: runner.os != 'macOS' + if: runner.os != 'macOS' && !matrix.musl run: | set -euo pipefail rustup component add llvm-tools ${{ runner.os == 'Windows' && 'python' || 'python3' }} tools/check_cpu_baseline.py \ $(for binary in horizon-client horizon-ctl horizon-prepare horizon-server; do echo "$BIN/$binary${{ matrix.exe }}"; done) - name: Every program starts + if: ${{ !matrix.musl }} run: | set -euo pipefail bin=$BIN @@ -250,7 +259,15 @@ jobs: with: name: original-exporters path: exporters + - name: Build, check and package for musl in Alpine + if: matrix.musl + # Run Docker on the Ubuntu host: JavaScript actions themselves require glibc. + run: | + docker run --rm -v "$PWD:/work" -w /work \ + -e HORIZON_VERSION -e HORIZON_NIGHTLY \ + rust:alpine3.22 sh tools/ci/build-musl.sh - name: Package + if: ${{ !matrix.musl }} run: | set -euo pipefail cargo run --locked -p xtask -- package "$BIN/horizon-client${{ matrix.exe }}" "dist/${{ matrix.dir }}" diff --git a/.github/workflows/nightly-build.yml b/.github/workflows/nightly-build.yml index c93024e42f..e7871dbe8c 100644 --- a/.github/workflows/nightly-build.yml +++ b/.github/workflows/nightly-build.yml @@ -173,7 +173,7 @@ jobs: run: | bash tools/ci/sign_release.sh nightly dist/horizon-linux-x86_64.tar.gz dist/horizon-windows-x64.zip \ dist/horizon-macos-arm64.zip dist/horizon-macos-x64.zip dist/horizon-server-linux-x86_64.tar.gz \ - dist/horizon-source.tar.gz + dist/horizon-source.tar.gz dist/horizon-linux-musl-x86_64.tar.gz - uses: actions/upload-artifact@v4 with: name: signature @@ -235,6 +235,7 @@ jobs: gh release create nightly --prerelease --latest=false --target "$SHA" \ --title "Nightly $DATE ยท ${SHA:0:7}" --notes-file dist/nightly-notes.md \ "dist/horizon-linux-x86_64.tar.gz#Game files the launcher installs (Nightly): Linux (horizon-linux-x86_64.tar.gz)" \ + "dist/horizon-linux-musl-x86_64.tar.gz#Game files for musl Linux (horizon-linux-musl-x86_64.tar.gz)" \ "dist/horizon-windows-x64.zip#Game files the launcher installs (Nightly): Windows (horizon-windows-x64.zip)" \ "dist/horizon-macos-arm64.zip#Game files the launcher installs (Nightly): macOS on Apple Silicon (horizon-macos-arm64.zip)" \ "dist/horizon-macos-x64.zip#Game files the launcher installs (Nightly): macOS on Intel (horizon-macos-x64.zip)" \ diff --git a/docs/release-flow.md b/docs/release-flow.md index e5fbae7927..dac9293c1c 100644 --- a/docs/release-flow.md +++ b/docs/release-flow.md @@ -277,6 +277,7 @@ the notes. | Archive | Runs on | Checks | | --- | --- | --- | | `horizon-linux-x86_64.tar.gz` | GitHub `ubuntu-22.04` (an older glibc, so it runs on any recent distribution) | the standard loader and libraries only (`tools/check_imports.py`) | +| `horizon-linux-musl-x86_64.tar.gz` | Alpine 3.22 in Docker on GitHub `ubuntu-22.04`, targeting `x86_64-unknown-linux-musl` | musl loader and libraries only (`tools/check_imports.py --musl`), CPU baseline and every program's startup checked inside Alpine | | `horizon-server-linux-x86_64.tar.gz` | the same Linux job, after the client archive: `horizon-server`, `horizon-prepare` and the game built with `--no-default-features` as `horizon-engine` (docs/server.md) | as above, and neither `libasound.so.2` nor `libudev.so.1` (`tools/check_imports.py --server`) | | `horizon-windows-x64.zip` | GitHub `windows-latest`, C runtime linked in | system DLLs only (`tools/check_imports.py`) | | `horizon-macos-arm64.zip` | The Mac mini's runner (`vars.MAC_RUNNER=macos-latest` sends it to GitHub) | system libraries only (`otool -L`); a 1500-frame offscreen run of a new world (about 12 s; 300 frames end before the world has loaded) with resources prepared from the runner's own Minecraft (`--offscreen --world smoke --frames 1500 --smoke-report`: frames drawn, chunks generated and meshed, a player, no error) | @@ -287,6 +288,13 @@ each one (`--help`, `--version`), packages the folder with `cargo run -p xtask - GitHub jobs keep their Cargo caches (`Swatinem/rust-cache`); the Mac mini keeps its `target/`. Only the Mac runs the game: GitHub's runners have neither a GPU worth the name nor the game files. +The musl archive is a manual download for musl-based Linux desktops. The launcher still selects the glibc +archive. It includes the same client and tools, with audio and controllers enabled. It links musl dynamically +(`-C target-feature=-crt-static`) so graphics drivers and window libraries can be loaded at runtime. Install +musl, libgcc, ALSA, libudev and the desktop's graphics drivers (on Alpine: `musl libgcc alsa-lib eudev-libs`, +plus the appropriate Mesa/Vulkan driver). This is not a fully static binary. The Nightly signs and publishes +both Linux archives. The dedicated-server archive remains the existing glibc build. + ### Notes `tools/ci/nightly.py pick` writes them: what reached main since the previous Nightly (`### New`, label `feature`, diff --git a/tests/test_check_imports.py b/tests/test_check_imports.py new file mode 100644 index 0000000000..9494fc595d --- /dev/null +++ b/tests/test_check_imports.py @@ -0,0 +1,68 @@ +import contextlib +import io +from pathlib import Path +import struct +import tempfile +import unittest + +from tools import check_imports + + +def elf(interpreter, libraries, runpath=None): + data = bytearray(4096) + data[:7] = b"\x7fELF\x02\x01\x01" + struct.pack_into(" "$HORIZON_HOME/client-help" +grep -q -- '--resources' "$HORIZON_HOME/client-help" +"$bin/horizon-ctl" --help > /dev/null +"$bin/horizon-prepare" --help > "$HORIZON_HOME/prepare-help" +grep -q -- '--minecraft-dir' "$HORIZON_HOME/prepare-help" +for binary in horizon-prepare horizon-server; do + test "$("$bin/$binary" --version)" = "$binary $HORIZON_VERSION" +done + +cargo run --locked --target "$target" -p xtask -- package \ + "$bin/horizon-client" dist/horizon-linux-musl-x86_64 +tar -C dist -czf dist/horizon-linux-musl-x86_64.tar.gz horizon-linux-musl-x86_64