Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f65cd2faa | ||
|
|
01abc78c0f | ||
|
|
63daa89bcb | ||
|
|
748c65d592 | ||
|
|
91ad350543 | ||
|
|
e281e4024a | ||
|
|
3a84022d8f | ||
|
|
c16b061fe7 | ||
|
|
bc8fc88849 | ||
|
|
e349124fc4 | ||
|
|
93a328a007 | ||
|
|
597e112e2b | ||
|
|
b0406b82f1 | ||
|
|
b10af101ed | ||
|
|
635cdd6c2c | ||
|
|
91b1cef4b2 | ||
|
|
1858aa143c | ||
|
|
93fbabcb60 | ||
|
|
aff9324c5c | ||
|
|
da49d87193 | ||
|
|
83886de13c | ||
|
|
df3e843284 | ||
|
|
5798ba209e | ||
|
|
8984945062 | ||
|
|
b8778d8a89 | ||
|
|
5b2524fe4d | ||
|
|
6f3babddd3 | ||
|
|
6134bd3735 | ||
|
|
b44db64e1e | ||
|
|
9d9bfd868d | ||
|
|
fb19e0fa84 | ||
|
|
88ab8d85cb | ||
|
|
e49f05cd1d | ||
|
|
573e129dee | ||
|
|
c01152b3fc | ||
|
|
62ef8d3be8 | ||
|
|
29bdf93a39 | ||
|
|
0f0f7be986 | ||
|
|
81d6eba98b | ||
|
|
08ba86bf79 | ||
|
|
8e9427d775 | ||
|
|
54c59cfd8a | ||
|
|
4d5cb3f2a4 | ||
|
|
77d41657aa | ||
|
|
5a5b8d2bd2 | ||
|
|
d2de4c18de | ||
|
|
037298b5a7 | ||
|
|
68bd653d9c | ||
|
|
c7432500fc | ||
|
|
d0d3e68ec4 | ||
|
|
0da4e7d52e | ||
|
|
ef6ef9e6a2 | ||
|
|
aad3b35a3a | ||
|
|
dc20a5e020 | ||
|
|
d0cf2b0811 | ||
|
|
6e79b7f740 | ||
|
|
e736186084 | ||
|
|
56930b532a | ||
|
|
1bf7fa9614 | ||
|
|
25ab2d7ea1 | ||
|
|
a1198c687e | ||
|
|
aa6d96e803 | ||
|
|
b49e1fd97d | ||
|
|
8a6980d33b | ||
|
|
8514f5774b | ||
|
|
32d977bf99 | ||
|
|
d0bd59f735 | ||
|
|
21155c06fb | ||
|
|
e0ac4551c2 | ||
|
|
e799b04b82 | ||
|
|
c2d42eba1b | ||
|
|
070bddc417 | ||
|
|
f3d5967ae6 | ||
|
|
72c0912c7b | ||
|
|
89020e2062 | ||
|
|
a617775941 | ||
|
|
b9e03dbb7b | ||
|
|
96d7f7673b | ||
|
|
a2a18e41c3 | ||
|
|
e2fcb0f5c2 | ||
|
|
ed1a281c9c | ||
|
|
9844193547 | ||
|
|
e9d402b60c | ||
|
|
71323383d8 | ||
|
|
3b34bef27e | ||
|
|
23402771cf | ||
|
|
6480bf8b8d | ||
|
|
f64587952e | ||
|
|
ad5e3b12b7 | ||
|
|
f1cdfcb3e8 | ||
|
|
4931f3a819 | ||
|
|
7b74fe93b1 | ||
|
|
4ccc44d612 | ||
|
|
08e63434d8 | ||
|
|
40dc440cbb | ||
|
|
7581c129bb | ||
|
|
259d022945 | ||
|
|
e44816b65a | ||
|
|
b300e7efd4 | ||
|
|
78853c19a6 | ||
|
|
e53ad8220b | ||
|
|
43fb2513a2 | ||
|
|
c909eea98f | ||
|
|
1f1240298a | ||
|
|
dfd2102274 | ||
|
|
35ce93db53 | ||
|
|
3f3103d070 | ||
|
|
ed4aa85442 | ||
|
|
43010d3eb2 | ||
|
|
546deac561 | ||
|
|
2eca4bc2ec | ||
|
|
a2ae910123 | ||
|
|
88b20af247 | ||
|
|
6a3454953c | ||
|
|
ceb5b2275d | ||
|
|
e980583b96 | ||
|
|
5c094254e5 | ||
|
|
d8a3327ee3 | ||
|
|
239ce420fa | ||
|
|
13d6b9fbaa | ||
|
|
ee1620f438 | ||
|
|
a0dba26666 | ||
|
|
82e29462a9 | ||
|
|
725ff7a410 | ||
|
|
64b5237f7c | ||
|
|
7d17c4d63a | ||
|
|
aab9aa4c22 | ||
|
|
6d9a7492e2 | ||
|
|
96c7c46e41 | ||
|
|
09f69374d9 | ||
|
|
4e8a65e584 | ||
|
|
bba4bda2d7 | ||
|
|
6263473f81 | ||
|
|
1fc4bd47d5 | ||
|
|
7558c0945c | ||
|
|
467b17bf38 | ||
|
|
3d113f473a | ||
|
|
792fa822f4 | ||
|
|
7b65e9b10f | ||
|
|
cd1f1c3970 | ||
|
|
be266234cb | ||
|
|
cf1f608745 | ||
|
|
f1f5aba33a | ||
|
|
0062a1e27a | ||
|
|
e79b92ff2c | ||
|
|
ce14e90beb | ||
|
|
93b3f04390 | ||
|
|
bc40e7bafe | ||
|
|
5628be3e6b | ||
|
|
58fbad4a85 | ||
|
|
7645804495 | ||
|
|
e5d840dabf |
No files matched your search
@@ -4,3 +4,9 @@
|
||||
* @ludovic111 @horizon-rust/horizon-team
|
||||
/.github/ @ludovic111
|
||||
/revoked.toml @ludovic111
|
||||
# What decides which builds the native-builds environment signs, and under which key (native-build.yml,
|
||||
# horizon#11859): the signing key's owner reviews it.
|
||||
/scripts/native_build.py @ludovic111
|
||||
/scripts/test_native_build.py @ludovic111
|
||||
/prebuilt.toml @ludovic111
|
||||
/.github/workflows/native-build.yml @ludovic111
|
||||
@@ -29,13 +29,9 @@ jobs:
|
||||
with:
|
||||
path: head
|
||||
persist-credentials: false
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin
|
||||
~/.cargo/horizon-rev
|
||||
key: cargo-horizon-mod-${{ env.HORIZON_REV }}
|
||||
# main's scripts, not the pull request's.
|
||||
# main's scripts, not the pull request's. No actions/cache: this job decides whether a submission passes,
|
||||
# and main's cache entries can be written by runs that build other people's code (native-build.yml), so
|
||||
# the tool is built fresh and the files are checked against their SHA-256 (fetch.sh).
|
||||
- run: base/scripts/install-tool.sh
|
||||
- name: Who submitted it
|
||||
id: author
|
||||
@@ -60,13 +56,8 @@ jobs:
|
||||
| tee -a "$GITHUB_STEP_SUMMARY" || status=1
|
||||
echo '```' >> "$GITHUB_STEP_SUMMARY"
|
||||
exit $status
|
||||
# main's files, kept by publish: each download from a release counts as a
|
||||
# player's.
|
||||
- uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: files
|
||||
key: files-${{ hashFiles('head/mods/*.toml') }}
|
||||
restore-keys: files-
|
||||
# Published files come from the registry-files release publish keeps: downloads from a mod's own
|
||||
# release would count as a player's.
|
||||
- name: Download its files
|
||||
run: base/scripts/fetch.sh head
|
||||
- name: Check each new file
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
# Prebuilt native builds (horizon#11859): the game compiled for one exact set of registry native mods, signed, so
|
||||
# players' launchers download it instead of compiling for 30 to 60 minutes.
|
||||
# - Every day after the Nightly, `dispatch` starts one run per set of prebuilt.toml on the newest Nightly. A run
|
||||
# builds ONE set, and only a set prebuilt.toml lists (`plan` and `sign` both refuse any other).
|
||||
# - On GitHub's runners only:
|
||||
# plan (no secrets): resolves the Nightly and each mod against the published index (newest unrevoked version,
|
||||
# not broken_on that commit), computes each target's request key as the launcher does, leaves out what is
|
||||
# published, downloads the mods' files and the launcher release named by vars.NATIVE_BUILD_LAUNCHER and
|
||||
# checks that release's signed SHA256SUMS against Horizon's release keys (pinned below, by SHA-256).
|
||||
# build (no secrets, a token that can only read, no cache): runs that launcher headless
|
||||
# (`native-build --prebuilt-out`): its pinned Rust, the Nightly's source, the release profile, the same
|
||||
# pins, hook points and left-out mods as a player's build. A mod crate's build script runs here, so this
|
||||
# job is UNTRUSTED: with the runner's own token it can replace any artifact of the run (`plan` included)
|
||||
# and write cache entries on main.
|
||||
# sign (environment `native-builds`, main only): trusts nothing a build job could touch. What it signs comes
|
||||
# from the plan job's OUTPUTS only (no later job can change them); it recomputes each key from the base,
|
||||
# the mods and the target, derives every file name from key and target, downloads `build-<target>` by
|
||||
# its exact name and refuses anything in it but that one archive. It runs only Horizon's
|
||||
# tools/ci/sign_native_build.sh (sha256sum and OpenSSL) at a pinned commit, checked by SHA-256 before
|
||||
# the key is in reach. A build can only ever be signed as its own set's.
|
||||
# Caches: no job here restores one, and no job of horizon-mods that holds a write token may restore one either
|
||||
# (publish.yml, native-compat.yml): the build job above can write cache entries on main, and restoring an entry
|
||||
# unpacks a tar anywhere on the runner.
|
||||
name: native-build
|
||||
on:
|
||||
schedule:
|
||||
- cron: "30 19 * * *"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
mods:
|
||||
description: "One set of prebuilt.toml, exactly as listed (`id` or `id@version`, space-separated). Empty: every set (one run each)"
|
||||
default: ""
|
||||
base:
|
||||
description: "The Nightly: `latest` or nightly-<date>-<short>"
|
||||
default: latest
|
||||
targets:
|
||||
description: "`all`, or target triples separated by commas"
|
||||
default: all
|
||||
permissions:
|
||||
contents: read
|
||||
concurrency:
|
||||
group: native-build-${{ inputs.mods || 'dispatch' }}-${{ inputs.base || 'latest' }}
|
||||
cancel-in-progress: false
|
||||
env:
|
||||
# A launcher release with `native-build --prebuilt-out` (Horizon #11859), e.g. [email protected].
|
||||
LAUNCHER: ${{ vars.NATIVE_BUILD_LAUNCHER }}
|
||||
# The Horizon commit this workflow takes its signer and keys from: its own pin, never vars.HORIZON_REV (shared with
|
||||
# publish and the checks) and never a branch. Each file is checked against its SHA-256 here before it is used.
|
||||
# Moving the pin means reviewing the files' diff and updating the three hashes (`sha256sum`).
|
||||
SIGN_REV: c794b87841b58f72ade9842b46757d843246054c
|
||||
SIGN_SCRIPT_SHA256: 95ebefe4b0beeb1b4282a09352260c48f1793b13e370414e91175aa789fc2278 # tools/ci/sign_native_build.sh
|
||||
SIGN_KEYS_SHA256: f75dc6fe23343a4e97fe2453853e67f20ddcce1bc1f2c240a56b8b6d6681f611 # crates/horizon-release-sign/native-build-keys.txt
|
||||
RELEASE_KEYS_SHA256: bb1f43a3eb14a2c0d89d7d823a3352dd4b7ac01f50d55451e282ea178fb61f6f # crates/horizon-release-sign/release-keys.txt
|
||||
# The largest archive signed: the launcher refuses bigger ones before checking (launcher/src/native_prebuilt.rs).
|
||||
MAX_ARCHIVE: "536870912"
|
||||
jobs:
|
||||
dispatch:
|
||||
# Idle until a launcher with native-build --prebuilt-out is released and named.
|
||||
if: (github.event_name == 'schedule' || inputs.mods == '') && vars.NATIVE_BUILD_LAUNCHER != ''
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: One run per set
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
BASE: ${{ inputs.base || 'latest' }}
|
||||
run: |
|
||||
python3 scripts/native_build.py sets | while read -r set; do
|
||||
echo "native-build: $set"
|
||||
gh workflow run native-build.yml --repo "$GITHUB_REPOSITORY" --ref main -f mods="$set" -f base="$BASE"
|
||||
done
|
||||
plan:
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.mods != ''
|
||||
runs-on: ubuntu-latest
|
||||
# The sign job reads these, never the `plan` artifact: build jobs can replace an artifact, not a job's outputs.
|
||||
outputs:
|
||||
matrix: ${{ steps.plan.outputs.matrix }}
|
||||
any: ${{ steps.plan.outputs.any }}
|
||||
tag: ${{ steps.plan.outputs.tag }}
|
||||
base: ${{ steps.plan.outputs.base }}
|
||||
mods: ${{ steps.plan.outputs.mods }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- run: python3 -m unittest scripts/test_native_build.py
|
||||
- id: plan
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
MODS: ${{ inputs.mods }}
|
||||
BASE: ${{ inputs.base }}
|
||||
TARGETS: ${{ inputs.targets }}
|
||||
run: python3 scripts/native_build.py plan --mods "$MODS" --base "$BASE" --targets "$TARGETS" --out plan
|
||||
- name: The launcher, checked against Horizon's release keys
|
||||
if: steps.plan.outputs.any == 'true'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
[ -n "$LAUNCHER" ] || { echo "::error::vars.NATIVE_BUILD_LAUNCHER names no launcher release"; exit 1; }
|
||||
mkdir -p plan/launcher && cd plan/launcher
|
||||
base="https://github.com/horizon-rust/horizon/releases/download/$LAUNCHER"
|
||||
curl -sSfLO "$base/SHA256SUMS" && curl -sSfLO "$base/SHA256SUMS.sig"
|
||||
for asset in $(jq -r '.builds[].launcher' ../plan.json | sort -u); do curl -sSfLO "$base/$asset"; done
|
||||
curl -sSfL -o release-keys.txt \
|
||||
"https://raw.githubusercontent.com/horizon-rust/horizon/$SIGN_REV/crates/horizon-release-sign/release-keys.txt"
|
||||
echo "$RELEASE_KEYS_SHA256 release-keys.txt" | sha256sum -c
|
||||
grep -v '^#' release-keys.txt | grep . > keys
|
||||
{ printf 'horizon release %s\n' "$LAUNCHER"; cat SHA256SUMS; } > message
|
||||
ok=
|
||||
while read -r key; do
|
||||
{ printf '\x30\x2a\x30\x05\x06\x03\x2b\x65\x70\x03\x21\x00'; echo "$key" | base64 -d; } > pub.der
|
||||
while read -r sig; do
|
||||
echo "$sig" | base64 -d > sig.bin
|
||||
openssl pkeyutl -verify -rawin -pubin -keyform DER -inkey pub.der -in message -sigfile sig.bin >/dev/null 2>&1 && ok=1
|
||||
done < SHA256SUMS.sig
|
||||
done < keys
|
||||
[ -n "$ok" ] || { echo "::error::$LAUNCHER: SHA256SUMS isn't signed by a Horizon release key"; exit 1; }
|
||||
sha256sum --ignore-missing -c SHA256SUMS
|
||||
rm -f message pub.der sig.bin keys release-keys.txt SHA256SUMS SHA256SUMS.sig
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: plan
|
||||
path: plan
|
||||
build:
|
||||
needs: plan
|
||||
if: needs.plan.outputs.any == 'true'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include: ${{ fromJson(needs.plan.outputs.matrix) }}
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 180
|
||||
permissions:
|
||||
contents: read
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: plan
|
||||
path: plan
|
||||
- if: runner.os == 'Linux'
|
||||
# The game's ALSA and udev headers; the launcher links xkbcommon and xcb even headless.
|
||||
run: sudo apt-get update && sudo apt-get install -y libasound2-dev libudev-dev pkg-config libxkbcommon-x11-0 libxcb1
|
||||
- name: Build the set as a player's launcher would
|
||||
env:
|
||||
TARGET: ${{ matrix.target }}
|
||||
KEY: ${{ matrix.key }}
|
||||
LAUNCHER_ASSET: ${{ matrix.launcher }}
|
||||
ARCHIVE: ${{ matrix.archive }}
|
||||
TAG: ${{ needs.plan.outputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
scratch="$RUNNER_TEMP/native"
|
||||
mkdir -p "$scratch/launcher" "$scratch/out"
|
||||
case "$LAUNCHER_ASSET" in
|
||||
*.tar.gz) tar xzf "plan/launcher/$LAUNCHER_ASSET" -C "$scratch/launcher"; launcher="$scratch/launcher/horizon-launcher" ;;
|
||||
*macos*) unzip -q "plan/launcher/$LAUNCHER_ASSET" -d "$scratch/launcher"; launcher="$scratch/launcher/Horizon.app/Contents/MacOS/horizon-launcher" ;;
|
||||
*) unzip -q "plan/launcher/$LAUNCHER_ASSET" -d "$scratch/launcher"; launcher="$scratch/launcher/horizon-launcher.exe" ;;
|
||||
esac
|
||||
export HORIZON_LAUNCHER_HOME="$scratch/home" HORIZON_HOME="$scratch/game" XDG_DATA_HOME="$scratch/data" \
|
||||
XDG_CONFIG_HOME="$scratch/config" HORIZON_LAUNCHER_NO_SELF_UPDATE=1 HORIZON_NO_AUDIO=1
|
||||
py=$(command -v python3 || command -v python) # Windows runners name it python
|
||||
"$launcher" install "$TAG"
|
||||
"$launcher" native-build --prebuilt-out "$scratch/out/build" --version "$TAG" --profile release \
|
||||
--trust-native-code plan/hnmods
|
||||
got=$("$py" -c 'import hashlib,sys; print(hashlib.sha256(open(sys.argv[1],"rb").read()).hexdigest())' "$scratch/out/build/request.txt")
|
||||
[ "$got" = "$KEY" ] || { echo "::error::the launcher built request $got, the plan asked for $KEY"; exit 1; }
|
||||
mkdir -p dist
|
||||
tar czf "dist/$ARCHIVE" -C "$scratch/out/build" .
|
||||
ls -l dist
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build-${{ matrix.target }}
|
||||
path: dist/${{ matrix.archive }}
|
||||
if-no-files-found: error
|
||||
sign:
|
||||
needs: [plan, build]
|
||||
if: always() && needs.plan.outputs.any == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
environment: native-builds
|
||||
permissions:
|
||||
contents: write
|
||||
actions: read
|
||||
env:
|
||||
# Job outputs of plan, the only input of this job besides the archives themselves.
|
||||
BASE: ${{ needs.plan.outputs.base }}
|
||||
TAG: ${{ needs.plan.outputs.tag }}
|
||||
MODS: ${{ needs.plan.outputs.mods }}
|
||||
MATRIX: ${{ needs.plan.outputs.matrix }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: |
|
||||
scripts/native_build.py
|
||||
prebuilt.toml
|
||||
sparse-checkout-cone-mode: false
|
||||
persist-credentials: false
|
||||
path: registry
|
||||
- name: The signer, at its pinned commit and hashes
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p "$RUNNER_TEMP/signer" && cd "$RUNNER_TEMP/signer"
|
||||
raw="https://raw.githubusercontent.com/horizon-rust/horizon/$SIGN_REV"
|
||||
curl -sSfL -o sign_native_build.sh "$raw/tools/ci/sign_native_build.sh"
|
||||
curl -sSfL -o native-build-keys.txt "$raw/crates/horizon-release-sign/native-build-keys.txt"
|
||||
printf '%s %s\n' "$SIGN_SCRIPT_SHA256" sign_native_build.sh "$SIGN_KEYS_SHA256" native-build-keys.txt | sha256sum -c
|
||||
- name: What to sign, recomputed from the plan job's outputs
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Checks the mods are a prebuilt.toml set and every row's key is that set's for its target; prints
|
||||
# "<key> <target>". Fails the job on any mismatch.
|
||||
python3 -I registry/scripts/native_build.py signable > "$RUNNER_TEMP/rows"
|
||||
cat "$RUNNER_TEMP/rows"
|
||||
- name: Each target's build, by its exact artifact name
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
while read -r key target <&3; do
|
||||
name="${key:0:16}-$target.tar.gz"
|
||||
dir="$RUNNER_TEMP/builds/$target"
|
||||
mkdir -p "$dir"
|
||||
if ! gh run download "$GITHUB_RUN_ID" --name "build-$target" --dir "$dir"; then
|
||||
echo "::warning::$target: no build"
|
||||
continue
|
||||
fi
|
||||
# Exactly one regular file, named from the key and target; anything else in the artifact is refused.
|
||||
listing=$(cd "$dir" && find . -mindepth 1 -printf '%y %P\n')
|
||||
if [ "$listing" != "f $name" ]; then
|
||||
echo "::error::$target: build-$target holds something other than $name alone:"; echo "$listing"; exit 1
|
||||
fi
|
||||
size=$(stat -c %s "$dir/$name")
|
||||
[ "$size" -le "$MAX_ARCHIVE" ] || { echo "::error::$target: $name is $size bytes, over $MAX_ARCHIVE"; exit 1; }
|
||||
echo "$target: $name, $size bytes, sha256 $(sha256sum < "$dir/$name" | cut -d' ' -f1)"
|
||||
done 3< "$RUNNER_TEMP/rows"
|
||||
- name: Sign, then publish
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
NATIVE_BUILD_SIGNING_KEY: ${{ secrets.NATIVE_BUILD_SIGNING_KEY }}
|
||||
NATIVE_BUILD_SIGNING_KEY_NEXT: ${{ secrets.NATIVE_BUILD_SIGNING_KEY_NEXT }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
signer="$RUNNER_TEMP/signer"
|
||||
# Checked again in the step that holds the key: nothing between the steps may have changed them.
|
||||
(cd "$signer" && printf '%s %s\n' "$SIGN_SCRIPT_SHA256" sign_native_build.sh "$SIGN_KEYS_SHA256" native-build-keys.txt \
|
||||
| sha256sum -c --quiet)
|
||||
release="native-builds-${BASE:0:12}"
|
||||
if ! gh release view "$release" >/dev/null 2>&1; then
|
||||
gh release create "$release" --prerelease --latest=false --title "Prebuilt native builds on $TAG" \
|
||||
--notes "The game built with registry native mods on Horizon $BASE, signed by native-build.yml (horizon#11859). The Horizon launcher downloads and checks these; nothing here is for downloading by hand."
|
||||
fi
|
||||
missing=0
|
||||
while read -r key target <&3; do
|
||||
dir="$RUNNER_TEMP/builds/$target"
|
||||
name="${key:0:16}-$target.tar.gz"
|
||||
sig="${key:0:16}-$target.sig"
|
||||
if [ ! -f "$dir/$name" ]; then missing=1; continue; fi
|
||||
NATIVE_BUILD_KEYS="$signer/native-build-keys.txt" bash "$signer/sign_native_build.sh" "$key" "$target" "$dir/$name"
|
||||
# The signature last: the launcher looks for it first, so a half-uploaded build is never offered.
|
||||
gh release upload "$release" "$dir/$name"
|
||||
gh release upload "$release" "$dir/$sig"
|
||||
done 3< "$RUNNER_TEMP/rows"
|
||||
exit $missing
|
||||
@@ -0,0 +1,130 @@
|
||||
# Native mods on the newest Nightly, as players' launchers build them (scripts/native_compat.py):
|
||||
# - on a pull request, each new native-mod version, applied together with the most downloaded native mods
|
||||
# and compiled (`cargo check`); its support modules' names against every registry mod's;
|
||||
# - every day after the Nightly, every native mod's newest version, applied, then the rest compiled
|
||||
# (`cargo check`): the ones horizon-mixins refuses alone and the ones whose own code doesn't compile go
|
||||
# to the `native-health` release, which publish adds to the index (`broken_on`) so the launcher warns
|
||||
# before an install.
|
||||
# Both compile other people's crates, whose build scripts run: those jobs hold a token that can only read and
|
||||
# run main's script, never the pull request's. The daily result goes to the release from a separate job that
|
||||
# runs nothing of theirs and restores no cache (a build can write cache entries on main).
|
||||
name: native-compat
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
paths: ["mods/*.toml"]
|
||||
schedule:
|
||||
- cron: "40 18 * * *"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
horizon:
|
||||
description: "Horizon commit, or `latest` (the newest Nightly)"
|
||||
default: latest
|
||||
upload:
|
||||
description: "Publish the result (native-health release); `false` for a trial run"
|
||||
default: "true"
|
||||
permissions:
|
||||
contents: read
|
||||
concurrency:
|
||||
group: native-compat-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
env:
|
||||
# The Rust the launcher builds native mods with (Horizon's launcher/src/rust_toolchain.rs).
|
||||
RUST: "1.99.1"
|
||||
jobs:
|
||||
check:
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.base.sha }}
|
||||
path: base
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
path: head
|
||||
persist-credentials: false
|
||||
- name: Does it change a native mod?
|
||||
id: native
|
||||
run: |
|
||||
if [ -n "$(python3 base/scripts/native_compat.py new base head)" ]; then
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
- if: steps.native.outputs.changed == 'true'
|
||||
run: rustup toolchain install $RUST --profile minimal && rustup default $RUST
|
||||
- if: steps.native.outputs.changed == 'true'
|
||||
run: sudo apt-get update && sudo apt-get install -y libasound2-dev libudev-dev pkg-config
|
||||
- if: steps.native.outputs.changed == 'true'
|
||||
name: On the newest Nightly, with the popular native mods
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -o pipefail
|
||||
python3 base/scripts/native_compat.py check base head | tee -a "$GITHUB_STEP_SUMMARY"
|
||||
health:
|
||||
if: github.event_name != 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
# Each mod horizon-mixins refuses costs one more apply of the whole set.
|
||||
timeout-minutes: 300
|
||||
# Read only: every registry native mod's build script runs in this job.
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- run: python3 -m unittest discover -s scripts -p 'test_*.py'
|
||||
- run: rustup toolchain install $RUST --profile minimal && rustup default $RUST
|
||||
- run: sudo apt-get update && sudo apt-get install -y libasound2-dev libudev-dev pkg-config
|
||||
# Yesterday's dependencies, checked, and horizon-mixins: only Horizon's own crates and the mods compile again.
|
||||
# Safe only because this job holds nothing: it builds untrusted code anyway.
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry/index
|
||||
~/.cargo/registry/cache
|
||||
~/.cargo/git/db
|
||||
native/target/compat/cargo
|
||||
native/target/horizon-mixins
|
||||
key: native-health-${{ env.RUST }}-${{ github.run_id }}
|
||||
restore-keys: native-health-${{ env.RUST }}-
|
||||
- name: Every native mod's newest version on the newest Nightly
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
HORIZON: ${{ inputs.horizon || 'latest' }}
|
||||
run: |
|
||||
set -o pipefail
|
||||
python3 scripts/native_compat.py health --horizon "$HORIZON" --out native-health.json | tee -a "$GITHUB_STEP_SUMMARY"
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: native-health
|
||||
path: native-health.json
|
||||
if-no-files-found: error
|
||||
health-publish:
|
||||
needs: health
|
||||
if: inputs.upload != 'false'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
steps:
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: native-health
|
||||
path: health
|
||||
- name: Keep it for publish
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Data from a job that ran other people's code: only the one file, only the shape publish reads.
|
||||
[ "$(cd health && find . -mindepth 1 -printf '%y %P\n')" = "f native-health.json" ]
|
||||
jq -e '(.horizon | type == "string" and test("^[0-9a-f]{40}$"))
|
||||
and (.broken | type == "object")
|
||||
and ([.broken | to_entries[] | (.key | test("^[a-z0-9_-]+$")) and (.value.version | type == "string")] | all)' \
|
||||
health/native-health.json > /dev/null
|
||||
gh release view native-health >/dev/null 2>&1 \
|
||||
|| gh release create native-health --prerelease --title "Native mods' health" \
|
||||
--notes "native-health.json: the native mods that don't apply on the newest Nightly (native-compat.yml). publish adds them to the index as broken_on."
|
||||
gh release upload native-health health/native-health.json --clobber
|
||||
@@ -78,7 +78,9 @@ jobs:
|
||||
names = zipfile.ZipFile(path).namelist()
|
||||
manifest = json.loads(zipfile.ZipFile(path).read("horizon-native.json"))
|
||||
assert manifest["hooks"] >= 1, manifest
|
||||
assert all(n == "horizon-native.json" or (n.startswith(f"mixins/{manifest['id']}/") and n.endswith((".rs", ".toml"))) for n in names), names
|
||||
bundle = f"mixins/{manifest['id']}/"
|
||||
assert all(n == "horizon-native.json" or n == bundle + "LICENSE" or (n.startswith(bundle) and n.endswith((".rs", ".toml"))) for n in names), names
|
||||
assert bundle + "LICENSE" in names, "the mod's LICENSE travels in its package"
|
||||
print(path, manifest)
|
||||
PY
|
||||
- uses: actions/upload-artifact@v4
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# Merging publishes: each new version's file becomes a release asset
|
||||
# (<id>-<version>/<file>), then index.json and the icons go to GitHub Pages
|
||||
# (https://horizon-rust.github.io/horizon-mods/index.json), which the launcher and
|
||||
# (<id>-<version>/<file>), then index.json, the icons and the screenshots go to GitHub
|
||||
# Pages (https://horizon-rust.github.io/horizon-mods/index.json), which the launcher and
|
||||
# playhorizon.dev/mods read. Nothing is published if any entry fails its checks.
|
||||
# Every hour it runs again to refresh each project's `downloads`: GitHub's
|
||||
# download counts of its release assets.
|
||||
# Every hour it runs again to refresh each project's `downloads` (GitHub's download
|
||||
# counts of its release assets) and `stars` (its source repository's GitHub stars).
|
||||
name: publish
|
||||
on:
|
||||
push:
|
||||
@@ -30,22 +30,17 @@ jobs:
|
||||
GH_REPO: ${{ github.repository }}
|
||||
HORIZON_REV: ${{ vars.HORIZON_REV || 'main' }}
|
||||
steps:
|
||||
# No actions/cache in this job, ever: it holds a write token (releases, Pages), and any run on main can
|
||||
# write cache entries, including native-build.yml's and native-compat.yml's builds of other people's
|
||||
# code. Restoring an entry unpacks a tar anywhere on the runner, so the tool is built fresh (about two
|
||||
# minutes) and the files come from the registry-files release, each checked against its SHA-256.
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin
|
||||
~/.cargo/horizon-rev
|
||||
key: cargo-horizon-mod-${{ env.HORIZON_REV }}
|
||||
persist-credentials: false
|
||||
- run: scripts/install-tool.sh
|
||||
# Files kept from the last run: GitHub counts every download of a release
|
||||
# asset, so fetching them all again each hour would count as players.
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: files
|
||||
key: files-${{ hashFiles('mods/*.toml') }}
|
||||
restore-keys: files-
|
||||
- run: scripts/fetch.sh .
|
||||
# FILES_MIRROR_UPLOAD: what isn't in registry-files yet is fetched from its own release once, then kept
|
||||
# there, so these hourly runs don't count as players' downloads.
|
||||
- run: FILES_MIRROR_UPLOAD=1 scripts/fetch.sh .
|
||||
- name: Build the index
|
||||
run: |
|
||||
mkdir -p site
|
||||
@@ -73,9 +68,48 @@ jobs:
|
||||
| if $n > 0 then .downloads = $n else . end)' site/index.json > index.counted.json
|
||||
mv index.counted.json site/index.json
|
||||
jq -r '.projects[] | "\(.id) \(.downloads // 0)"' site/index.json
|
||||
- name: GitHub stars
|
||||
run: |
|
||||
# A project's stars are its source repository's (owner/repo of a
|
||||
# https://github.com/ source, a folder of it or .git included), asked
|
||||
# once per repository. One that can't be read is left out, not zero.
|
||||
jq -r '.projects[].source // empty' site/index.json \
|
||||
| sed -nE 's#^https://github\.com/([A-Za-z0-9-]+)/([A-Za-z0-9._-]+).*#\1/\2#p' \
|
||||
| sed -E 's/\.git$//' | tr 'A-Z' 'a-z' | sort -u \
|
||||
| while read -r repo; do
|
||||
n="$(gh api "repos/$repo" --jq .stargazers_count 2>/dev/null)" || continue
|
||||
echo "{\"key\": \"$repo\", \"value\": $n}"
|
||||
done | jq -s from_entries > stars.json
|
||||
jq --slurpfile stars stars.json '
|
||||
.projects |= map(
|
||||
(first((.source // "") | ascii_downcase
|
||||
| capture("^https://github[.]com/(?<r>[a-z0-9-]+/[a-z0-9._-]+)").r
|
||||
| sub("[.]git$"; "")) // "") as $r
|
||||
| ($stars[0][$r] // 0) as $n
|
||||
| if $n > 0 then .stars = $n else . end)' site/index.json > index.starred.json
|
||||
mv index.starred.json site/index.json
|
||||
jq -r '.projects[] | "\(.id) \(.stars // 0)"' site/index.json
|
||||
- name: Native mods that don't apply on the newest Nightly
|
||||
run: |
|
||||
# native-compat.yml's daily health: the version each names gets `broken_on`, the Horizon
|
||||
# commit horizon-mixins refused it on and its code, so the launcher warns before an install.
|
||||
if gh release download native-health --pattern native-health.json --dir health 2>/dev/null; then
|
||||
jq --slurpfile h health/native-health.json '
|
||||
$h[0] as $health
|
||||
| .projects |= map(. as $p | .versions |= map(
|
||||
($health.broken[$p.id] // null) as $b
|
||||
| if $b != null and $b.version == .version
|
||||
then .broken_on = [{horizon: $health.horizon, code: $b.code}] else . end))' \
|
||||
site/index.json > index.health.json
|
||||
mv index.health.json site/index.json
|
||||
jq -r '.projects[] | select(.versions[0].broken_on) | "\(.id) \(.versions[0].version): \(.versions[0].broken_on[0].code)"' site/index.json
|
||||
fi
|
||||
- name: The site
|
||||
run: |
|
||||
cp -R icons site/icons
|
||||
# Screenshots are served beside the icons: the index lists them at
|
||||
# <site>screenshots/<id>/<name>.
|
||||
if [ -d screenshots ]; then cp -R screenshots site/screenshots; fi
|
||||
cat > site/index.html <<'HTML'
|
||||
<!doctype html><meta charset="utf-8"><title>Horizon mods</title>
|
||||
<meta http-equiv="refresh" content="0; url=https://playhorizon.dev/mods">
|
||||
|
||||
@@ -11,9 +11,10 @@ with no file, network or clock access, and can only do what their page lists.
|
||||
Native source mods (`.hnmod`, kind `native-mod`) are built into an isolated game by
|
||||
a launcher with native-mod support. They run with the player’s account permissions.
|
||||
Their bundles pin the exact Horizon and Horizon Mixins revisions used for the build.
|
||||
Other revisions have not been validated. Registry checks inspect the source bundle without
|
||||
executing native code; reviewers must also examine the source and pinned-build
|
||||
validation. Publishing these requires the native-mod changes to `cargo-horizon-mod`
|
||||
Other revisions have not been validated. Registry checks inspect the source bundle and compile it
|
||||
on the newest Nightly with the most downloaded native mods (in a job whose token
|
||||
can only read) without running it; reviewers must also examine the source and
|
||||
pinned-build validation. Publishing these requires the native-mod changes to `cargo-horizon-mod`
|
||||
and the launcher to have shipped first.
|
||||
|
||||
**Who is responsible.** Except the projects marked Official (made by the Horizon project), everything here is made and
|
||||
@@ -65,6 +66,7 @@ why.
|
||||
| `.github/workflows/check.yml` | A pull request's checks |
|
||||
| `.github/workflows/publish.yml` | On merge: releases, then `index.json` on Pages |
|
||||
| `.github/workflows/cleanup.yml` | Weekly: staging files nobody needs any more |
|
||||
| `.github/workflows/native-compat.yml` | Native mods on the newest Nightly, as the launcher builds them: a pull request's new version applied and compiled with the most downloaded native mods, its module names against every mod's; daily, every mod's newest version, the ones that don't apply marked `broken_on` in the index (`scripts/native_compat.py`) |
|
||||
| `.github/workflows/native.yml` | Horizon's native mods: each changed one's tests, `.hnmod` and game check, all of them together, and weekly on the newest Nightly |
|
||||
| `native/<name>/` | Horizon's own native mods (Zoom, FPS Counter, Keystrokes, Minimap, Recipes, Coordinates HUD): the source of their `.hnmod` files |
|
||||
| `native/tools/` | Shared by them: apply on Horizon, check, build, package, prove (`native/tools/native.py`) |
|
||||
@@ -83,7 +85,8 @@ The index: `https://horizon-rust.github.io/horizon-mods/index.json`.
|
||||
[horizon-mixins](https://github.com/horizon-rust/horizon-mixins) bundles on Horizon's native hook points
|
||||
([docs/native-mods.md](https://github.com/horizon-rust/horizon/blob/main/docs/native-mods.md)). Each folder keeps the
|
||||
history of the repository it came from. A mod builds on any Horizon whose hook points are at least the version its
|
||||
`compatibility.json` names; `horizon_revision` is the commit it was last tested on.
|
||||
`compatibility.json` names; `horizon_revision` is the commit it was last tested on. `smp` and `voicechat-server`
|
||||
are server mods, built into the official server's engine (see their READMEs).
|
||||
|
||||
```sh
|
||||
python3 native/tools/native.py check zoom # apply on the tested Horizon and cargo check the game
|
||||
|
||||
|
After Width: | Height: | Size: 782 B |
|
After Width: | Height: | Size: 208 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 9.2 KiB |
|
After Width: | Height: | Size: 193 KiB |
|
After Width: | Height: | Size: 805 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 687 B |
|
After Width: | Height: | Size: 150 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 198 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 245 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 211 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 107 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 660 B |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 195 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 212 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 9.5 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 221 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 131 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,34 @@
|
||||
id = "accurate_block_placement"
|
||||
kind = "native-mod"
|
||||
name = "Accurate Block Placement"
|
||||
summary = "Hold right click with a block and every new spot you look at gets one, however fast you move."
|
||||
description = """
|
||||
Placing blocks fast is all about timing: vanilla places a block every four ticks while you hold right click, so moving fast skips spots and moving slowly stacks extra blocks. **Accurate Block Placement** times it for you: hold right click and every *new* spot you look at gets a block at once.
|
||||
|
||||
- **A new spot, a block**: bridging, building walls and floors, or sweeping along a row; no spot skipped, however fast you move.
|
||||
- **No extra blocks**: staring at one spot places nothing more.
|
||||
- **A single click stays a single block**: right after the click it waits as vanilla does, unless you turn the view.
|
||||
- **Building backwards**: walking backwards while placing toward yourself places one block per step.
|
||||
- Doors, chests, buttons and other blocks you click still work as usual.
|
||||
|
||||
**Settings** (Options > Mods > Accurate Block Placement): turn it on or off, bind a key to toggle it (unbound by default), and choose whether it works on servers.
|
||||
|
||||
Some servers see fast, accurate placing as a cheat: check their rules, or turn "Allowed on servers" off.
|
||||
|
||||
A port of [Accurate Block Placement: Reborn](https://modrinth.com/mod/accurate-block-placement-reborn) (MIT) by Clayborn, Flourick, schwar and KadTheHunter, rewritten in Rust for Horizon."""
|
||||
authors = ["Lutziano888"]
|
||||
owners = ["Lutziano888"]
|
||||
owner_ids = [129792568]
|
||||
license = "MIT"
|
||||
source = "https://github.com/Lutziano888/horizon-accurate-block-placement"
|
||||
icon = "icons/accurate_block_placement.png"
|
||||
categories = ["utility", "game-mechanics"]
|
||||
tags = ["building", "block-placement", "bridging", "port"]
|
||||
|
||||
[[versions]]
|
||||
version = "1.0.0"
|
||||
published = "2026-11-03"
|
||||
game = "26.3"
|
||||
file = "accurate_block_placement-1.0.0.hnmod"
|
||||
sha256 = "e631745f6d267360de6eb51995ee995bf1ab3d64393d287b7b93c332b85939ce"
|
||||
changelog = "First release."
|
||||