874b6f20c2
Основа — Horizon (MIT). История апстрима не переносится: этот репозиторий ведёт свою.
292 lines
15 KiB
YAML
292 lines
15 KiB
YAML
name: game build
|
|
# Builds the game for every platform from `ref`: the Java exporters, then Linux and the Linux server and Windows on
|
|
# GitHub, macOS on the Mac mini's runner with a 1500-frame offscreen run of a new world (smoke-test.json). Called by
|
|
# release.yml (the releases) and nightly-build.yml (the opt-in Nightly), always from main's copy of this file: the
|
|
# caller's commit, never the one being built. It holds no secrets: signing the checksums is the callers' own job,
|
|
# which never runs what this one built. See docs/release-flow.md.
|
|
on:
|
|
workflow_call:
|
|
inputs:
|
|
ref:
|
|
description: The commit to build
|
|
type: string
|
|
required: true
|
|
version:
|
|
description: The version built in (HORIZON_VERSION), e.g. 0.2.81 or nightly-2026-10-28
|
|
type: string
|
|
required: true
|
|
nightly:
|
|
description: A nightly's "<date> <short sha>" (HORIZON_NIGHTLY), shown by the game; empty for a release
|
|
type: string
|
|
default: ''
|
|
trusted:
|
|
# Only release.yml says true: it builds main. A nightly can hold pull requests nobody reviewed yet, whose
|
|
# build scripts and tests run here and could rewrite target/, the cargo home or a compiler cache. So an
|
|
# untrusted build never runs on the release runner, never saves the Rust cache (its key is its own anyway),
|
|
# and a trusted one never reads the machine's shared sccache, which pull requests' jobs write
|
|
# (tests/test_nightly.py pins all of it).
|
|
description: The source is main (a release); false for anything else (a nightly)
|
|
type: boolean
|
|
default: false
|
|
developer_id:
|
|
description: Sign the macOS programs with Developer ID and notarize the zip (the runner's keychain)
|
|
type: boolean
|
|
default: true
|
|
permissions:
|
|
contents: read
|
|
jobs:
|
|
exporters:
|
|
# This runner holds the maintainer's own pinned Minecraft jars. Only our classes leave it. An untrusted build
|
|
# goes to the Mac mini runner that already runs pull requests' parity jobs (macOS + horizon-game:
|
|
# ludovics-mac-mini), never the release runner (MAC_RUNNER: ludovics-mac-mini-release) nor parity-main's.
|
|
runs-on: ${{ inputs.trusted && vars.MAC_RUNNER || fromJSON('["self-hosted", "macOS", "horizon-game"]') }}
|
|
# Three full preparations (two isolated, one batched) besides the compile.
|
|
timeout-minutes: 60
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
with:
|
|
ref: ${{ inputs.ref }}
|
|
persist-credentials: false
|
|
clean: true
|
|
- uses: dtolnay/rust-toolchain@stable
|
|
- run: bash tools/ci/runner-env.sh
|
|
env:
|
|
HORIZON_NO_SCCACHE: ${{ inputs.trusted && '1' || '' }}
|
|
- run: bash tools/ci/vanilla-data.sh
|
|
- name: Compile and validate original exporters against the pinned jars
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
# target/ is gone after the clean checkout, and vanilla-data.sh builds the preparer only when it
|
|
# prepares afresh.
|
|
cargo build --release --locked -p horizon-prepare
|
|
# Compiled here, never taken from the runner's shared preparations: pull requests' parity jobs
|
|
# write those (vanilla-data.sh links .local/exporters to them).
|
|
rm -rf .local/exporters
|
|
bundle=$(target/release/horizon-prepare compile-exporters --root "$PWD" | tail -1)
|
|
mkdir -p exporters
|
|
cp -R "$bundle/server" "$bundle/client" "$bundle/manifest.json" exporters/
|
|
# Only our classes and their manifest leave this runner.
|
|
stray=$(find exporters -type f ! -path exporters/manifest.json \
|
|
! \( -name '*.class' \( -path 'exporters/server/dev/horizonlab/parity/*' -o -path 'exporters/client/dev/horizonlab/parity/*' \) \))
|
|
[ -z "$stray" ] || { echo "::error::not one of our classes: $stray"; exit 1; }
|
|
python3 tools/check_prepare_exporters.py --binary target/release/horizon-prepare
|
|
- uses: actions/upload-artifact@v4
|
|
with:
|
|
name: original-exporters
|
|
path: exporters/
|
|
if-no-files-found: error
|
|
retention-days: 3
|
|
|
|
build:
|
|
needs: exporters
|
|
name: build ${{ matrix.name }}
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- name: linux
|
|
os: ubuntu-22.04
|
|
dir: horizon-linux-x86_64
|
|
asset: horizon-linux-x86_64.tar.gz
|
|
# 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: windows
|
|
os: windows-latest
|
|
dir: horizon-windows-x64
|
|
asset: horizon-windows-x64.zip
|
|
exe: .exe
|
|
# The C runtime linked in, so the game runs without the Visual C++ redistributable.
|
|
rustflags: -C target-feature=+crt-static
|
|
- name: macos
|
|
mac: true
|
|
dir: horizon-macos-arm64
|
|
asset: horizon-macos-arm64.zip
|
|
runs-on: ${{ matrix.mac && (inputs.trusted && vars.MAC_RUNNER || fromJSON('["self-hosted", "macOS", "horizon-game"]')) || matrix.os }}
|
|
# Notarizing the zip can take a few minutes.
|
|
timeout-minutes: ${{ matrix.mac && 60 || 40 }}
|
|
env:
|
|
RUSTFLAGS: ${{ matrix.rustflags }}
|
|
# The release's version, built in (horizon_core::release): --version, user agents, crash reports and
|
|
# telemetry say it. No version-bump commits.
|
|
HORIZON_VERSION: ${{ inputs.version }}
|
|
# A nightly's date and commit ("2026-10-28 0c02067"), which the game shows; empty for a release.
|
|
HORIZON_NIGHTLY: ${{ inputs.nightly }}
|
|
defaults:
|
|
run:
|
|
shell: bash
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
with:
|
|
ref: ${{ inputs.ref }}
|
|
persist-credentials: false
|
|
# Always from a clean checkout, the Mac mini's runner too: its work folder is shared with pull requests'
|
|
# parity jobs, and nothing they leave (target/, an untracked .cargo/config.toml, a build script's output)
|
|
# may end up in a release. target/ is rebuilt from scratch; .local/ is relinked by vanilla-data.sh, which
|
|
# keeps its preparations outside the checkout.
|
|
clean: true
|
|
- uses: dtolnay/rust-toolchain@stable
|
|
- uses: Swatinem/rust-cache@v2
|
|
if: runner.environment == 'github-hosted'
|
|
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'
|
|
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
|
|
# `horizon-client/mods`: every released game runs code mods (the feature is off in dev builds).
|
|
run: cargo build --release --locked --features horizon-client/mods -p horizon-client --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
|
|
run: |
|
|
for binary in horizon-client horizon-ctl horizon-prepare horizon-server; do
|
|
test -f "target/release/$binary${{ matrix.exe }}" || { echo "::error::target/release/$binary${{ matrix.exe }} is missing"; exit 1; }
|
|
done
|
|
- name: The signing keychain, unlocked
|
|
if: runner.os == 'macOS' && vars.MACOS_SIGN_IDENTITY != '' && inputs.developer_id
|
|
env:
|
|
MACOS_SIGN_IDENTITY: ${{ vars.MACOS_SIGN_IDENTITY }}
|
|
run: packaging/macos/keychain.sh unlock
|
|
- name: Signed with Developer ID and the hardened runtime
|
|
# Before the checks below, so they run the programs as players will.
|
|
if: runner.os == 'macOS' && vars.MACOS_SIGN_IDENTITY != '' && inputs.developer_id
|
|
env:
|
|
MACOS_SIGN_IDENTITY: ${{ vars.MACOS_SIGN_IDENTITY }}
|
|
run: |
|
|
for binary in horizon-client horizon-ctl horizon-prepare horizon-server; do
|
|
codesign --force --options runtime --timestamp --keychain "$MACOS_SIGN_KEYCHAIN" \
|
|
--identifier "dev.horizon.${binary#horizon-}" --sign "$MACOS_SIGN_IDENTITY" "target/release/$binary"
|
|
codesign --verify --strict --verbose=2 "target/release/$binary"
|
|
done
|
|
- name: Only system libraries
|
|
# 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: |
|
|
for binary in horizon-client horizon-ctl horizon-prepare horizon-server; do
|
|
file="target/release/$binary${{ matrix.exe }}"
|
|
if [[ "$RUNNER_OS" == macOS ]]; then
|
|
others=$(otool -L "$file" | tail -n +2 | awk '{print $1}' | grep -v -E '^(/usr/lib/|/System/Library/)' || true)
|
|
[[ -z "$others" ]] || { echo "::error::$binary links $others"; exit 1; }
|
|
else
|
|
${{ runner.os == 'Windows' && 'python' || 'python3' }} tools/check_imports.py "$file"
|
|
fi
|
|
done
|
|
- name: Every program starts
|
|
run: |
|
|
set -euo pipefail
|
|
bin=target/release
|
|
"$bin/horizon-client${{ matrix.exe }}" --help | grep -q -- '--resources'
|
|
"$bin/horizon-ctl${{ matrix.exe }}" --help > /dev/null
|
|
"$bin/horizon-prepare${{ matrix.exe }}" --help | grep -q -- '--minecraft-dir'
|
|
# The version is the release's.
|
|
for binary in horizon-prepare horizon-server; do
|
|
out=$("$bin/$binary${{ matrix.exe }}" --version | tr -d '\r')
|
|
echo "$out"
|
|
[[ "$out" == "$binary $HORIZON_VERSION" ]] || { echo "::error::expected $binary $HORIZON_VERSION"; exit 1; }
|
|
done
|
|
- name: Tools from the runner
|
|
if: runner.environment == 'self-hosted'
|
|
env:
|
|
HORIZON_NO_SCCACHE: ${{ inputs.trusted && '1' || '' }}
|
|
run: bash tools/ci/runner-env.sh
|
|
- name: Resources prepared from the runner's own Minecraft
|
|
if: runner.environment == 'self-hosted'
|
|
run: bash tools/ci/vanilla-data.sh
|
|
- name: The game runs (offscreen, a new world, 1500 frames)
|
|
if: runner.environment == 'self-hosted'
|
|
run: |
|
|
set -euo pipefail
|
|
# From a scratch folder: the game keeps its saves and options in the folder it starts in.
|
|
home="$RUNNER_TEMP/horizon-smoke"
|
|
rm -rf "$home" && mkdir -p "$home"
|
|
version=$(sed -n 's/.*"version": *"\([^"]*\)".*/\1/p' research-lock.json | head -1)
|
|
client="$PWD/target/release/horizon-client"
|
|
resources="$PWD/.local/horizon-client/$version"
|
|
(cd "$home" && HORIZON_HOME="$home" HORIZON_NO_AUDIO=1 "$client" --resources "$resources" --offscreen \
|
|
--world smoke --seed 1 --render-distance 4 --world-frames 1500 --timeout-seconds 180 \
|
|
--smoke-report "$home/smoke.json" > "$home/client.log" 2>&1) || { tail -40 "$home/client.log"; exit 1; }
|
|
python3 - "$home/smoke.json" <<'EOF'
|
|
import json, sys
|
|
report = json.load(open(sys.argv[1]))
|
|
print(json.dumps(report))
|
|
if report.get("error") is not None or report.get("frames", 0) < 1500 or not report.get("chunks") \
|
|
or not report.get("meshed_sections") or not report.get("player"):
|
|
sys.exit("the smoke run did not draw a world")
|
|
EOF
|
|
# Published with the release; the publish job checks it again before it publishes anything.
|
|
cp "$home/smoke.json" "$RUNNER_TEMP/smoke-test.json"
|
|
- uses: actions/upload-artifact@v4
|
|
if: runner.environment == 'self-hosted'
|
|
with:
|
|
name: smoke-test.json
|
|
path: ${{ runner.temp }}/smoke-test.json
|
|
if-no-files-found: error
|
|
retention-days: 3
|
|
- uses: actions/download-artifact@v4
|
|
with:
|
|
name: original-exporters
|
|
path: exporters
|
|
- name: Package
|
|
run: |
|
|
set -euo pipefail
|
|
cargo run --locked -p xtask -- package "target/release/horizon-client${{ matrix.exe }}" "dist/${{ matrix.dir }}"
|
|
cd dist
|
|
rm -f "${{ matrix.asset }}"
|
|
case "${{ matrix.asset }}" in
|
|
*.tar.gz) tar czf "${{ matrix.asset }}" "${{ matrix.dir }}" ;;
|
|
*) if [[ "$RUNNER_OS" == Windows ]]; then 7z a -tzip "${{ matrix.asset }}" "${{ matrix.dir }}" > /dev/null
|
|
else
|
|
# Without resource forks and extended attributes: plain ditto adds an
|
|
# AppleDouble `._` file beside every file that has any.
|
|
ditto -c -k --norsrc --noextattr --noqtn --keepParent "${{ matrix.dir }}" "${{ matrix.asset }}"
|
|
if unzip -Z1 "${{ matrix.asset }}" | grep -E '(^|/)(\._|__MACOSX/)'; then
|
|
echo "::error::macOS metadata files in ${{ matrix.asset }}"; exit 1
|
|
fi
|
|
fi ;;
|
|
esac
|
|
ls -l "${{ matrix.asset }}"
|
|
- name: Notarized
|
|
# A zip of programs can't be stapled: Gatekeeper asks Apple the first time each one runs.
|
|
if: runner.os == 'macOS' && vars.MACOS_SIGN_IDENTITY != '' && inputs.developer_id
|
|
run: |
|
|
for binary in horizon-client horizon-ctl horizon-prepare horizon-server; do
|
|
codesign --verify --strict "dist/${{ matrix.dir }}/$binary"
|
|
done
|
|
packaging/macos/notarize.sh "dist/${{ matrix.asset }}"
|
|
- uses: actions/upload-artifact@v4
|
|
with:
|
|
name: ${{ matrix.asset }}
|
|
path: dist/${{ matrix.asset }}
|
|
if-no-files-found: error
|
|
retention-days: 3
|
|
- name: The server build (the game without sound or controllers, as horizon-engine)
|
|
if: matrix.server_asset
|
|
run: |
|
|
set -euo pipefail
|
|
# After the client is packaged: target/release/horizon-client is rebuilt without the audio and
|
|
# controllers features, so it needs neither libasound nor libudev (minimal server images lack them),
|
|
# and with code mods like the client.
|
|
cargo build --release --locked -p horizon-client --no-default-features --features mods --bin horizon-client
|
|
cargo run --locked -p xtask -- package-server target/release/horizon-client "dist/${{ matrix.server_dir }}"
|
|
folder="dist/${{ matrix.server_dir }}"
|
|
python3 tools/check_imports.py --server "$folder/horizon-engine" "$folder/horizon-server" "$folder/horizon-prepare"
|
|
"$folder/horizon-engine" --help | grep -q -- '--resources'
|
|
[[ "$("$folder/horizon-server" --version)" == "horizon-server $HORIZON_VERSION" ]]
|
|
cd dist
|
|
rm -f "${{ matrix.server_asset }}"
|
|
tar czf "${{ matrix.server_asset }}" "${{ matrix.server_dir }}"
|
|
ls -l "${{ matrix.server_asset }}"
|
|
- uses: actions/upload-artifact@v4
|
|
if: matrix.server_asset
|
|
with:
|
|
name: ${{ matrix.server_asset }}
|
|
path: dist/${{ matrix.server_asset }}
|
|
if-no-files-found: error
|
|
retention-days: 3
|
|
- name: The signing keychain, locked again
|
|
if: always() && runner.os == 'macOS' && vars.MACOS_SIGN_IDENTITY != '' && inputs.developer_id
|
|
run: packaging/macos/keychain.sh lock
|
|
|