Compare commits

...
Author SHA1 Message Date
Horizon agentandClaude Opus 5.5 2f65cd2faa native-build: sign only what the plan job outputs; no cache restores in write-token jobs
Security review of #381 (horizon#11859):
- H1: the sign job no longer reads the `plan` artifact, which a build job can replace. It takes the
  set, base and matrix from the plan job's outputs, recomputes every key (`native_build.py
  signable`), derives the file names from key and target, downloads `build-<target>` by exact name
  and refuses anything but that one archive.
- H2: publish.yml and check.yml restore no cache (a restored entry unpacks a tar anywhere on the
  runner, and native builds run on main): the tool is built fresh, files come from a
  `registry-files` release by URL and are checked against their SHA-256 (fetch.sh), so the hourly
  runs still don't count as players' downloads. native-compat.yml's health compile now holds a
  read token; a separate job checks the JSON's shape and uploads it.
- M1: the signer and keys come from a pinned Horizon commit, checked by SHA-256, never HORIZON_REV.
- M2: native_build.py, its test, prebuilt.toml and native-build.yml are Ludovic's in CODEOWNERS.
- M4: plan and sign accept only a set prebuilt.toml lists.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 19:49:49 +01:00
Horizon agentandClaude Opus 5.5 01abc78c0f native-build: prebuilt native builds, signed, one set per run (horizon#11859)
scripts/native_build.py plans one set on one Nightly: each mod against the published index
(newest unrevoked version, not broken_on that commit), each target's request key as the
launcher computes it (same test vector as horizon_release_sign::native_build), what is already
published left out. native-build.yml: plan (no secrets; checks the launcher release's signed
SHA256SUMS against Horizon's release keys) -> build on GitHub-hosted runners (no secrets, read
token, no cache; the launcher's own native-build --prebuilt-out) -> sign in the native-builds
environment with Horizon's sign_native_build.sh, for the keys the plan names, then upload to
native-builds-<base12>. A daily dispatch starts one run per set of prebuilt.toml, idle until
vars.NATIVE_BUILD_LAUNCHER names a launcher release with --prebuilt-out.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:55:30 +01:00
levzzz 63daa89bcb Merge pull request #367 from horizon-rust/submit/right_click_crops-1.0.1
Update right_click_crops 1.0.1
2026-11-07 16:15:53 +05:00
Ludovic MarieandAyanoKaito 748c65d592 Update right_click_crops 1.0.1
Submitted by @AyanoKaito through the form.

Submission: WAZyOGCt_BUXQQkl_FVhcXCytMPmZKl59xs8ga3REfg
Co-authored-by: AyanoKaito <[email protected]>
2026-11-07 11:45:17 +01:00
Ludovic MarieandAyanoKaito 91ad350543 Update right_click_crops 1.0.1
Submitted by @AyanoKaito through the form.

Submission: WAZyOGCt_BUXQQkl_FVhcXCytMPmZKl59xs8ga3REfg
Co-authored-by: AyanoKaito <[email protected]>
2026-11-07 11:45:15 +01:00
Ludovic MarieandAyanoKaito e281e4024a Update right_click_crops 1.0.1
Submitted by @AyanoKaito through the form.

Submission: WAZyOGCt_BUXQQkl_FVhcXCytMPmZKl59xs8ga3REfg
Co-authored-by: AyanoKaito <[email protected]>
2026-11-07 11:45:13 +01:00
12 changed files with 808 additions and 49 deletions

No files matched your search

+6
View File
@@ -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
+5 -14
View File
@@ -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
+269
View 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
+35 -8
View File
@@ -5,8 +5,9 @@
# (`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.
# The check compiles the submission, whose crates may have build scripts: it runs with a token that can
# only read, and main's script, never the pull request's.
# 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:
@@ -66,17 +67,18 @@ jobs:
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: write
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
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: |
@@ -94,10 +96,35 @@ jobs:
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
if: inputs.upload != 'false'
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 native-health.json --clobber
gh release upload native-health health/native-health.json --clobber
+8 -13
View File
@@ -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
+15 -1
View File
@@ -22,7 +22,7 @@ Wheat, carrots, potatoes, beetroot, nether wart, cocoa, torchflower, pitcher cro
- Bone meal, ordinary breaking and planting, sweet berries and glow berries keep their normal behaviour.
### Compatibility
Last checked on **November 1, 2026**, for Minecraft 26.3. The release is pinned to Horizon Nightly 2026-10-31 (`9d979ca`), where single-player and LAN were tested. A compilation-only check also passed on the newest Nightly available on that date (`d91cab6`); gameplay on that newer revision was not tested.
For Minecraft 26.3. Each version is built and gameplay-tested on one exact Horizon Nightly; the launcher warns when a newer Nightly breaks it. Tested revisions and results: [Validation](https://github.com/AyanoKaito/horizon-right-click-crops/blob/main/VALIDATION.md).
This is a native mod: install only if you trust its source and accept the launcher's native-code warning.
@@ -36,8 +36,22 @@ source = "https://github.com/AyanoKaito/horizon-right-click-crops"
icon = "icons/right_click_crops.png"
categories = ["utility", "food"]
tags = ["farming", "crops", "harvest", "replant"]
screenshots = ["screenshots/right_click_crops/1.jpg", "screenshots/right_click_crops/2.jpg"]
key = "RWRwbuaByP5mM4VUm4kZEQVPiuw+3IEVJ7CZcEHIjY5fPKf9lhQKUZRy"
[[versions]]
version = "1.0.1"
published = "2026-11-07"
game = "26.3"
file = "right_click_crops-1.0.1.hnmod"
sha256 = "e369d9cb34320144d805d02f162a3ed357b9cf8bbeee457e409cedc0b7e63d0a"
signature = """
untrusted comment: signature from minisign secret key\r
RURwbuaByP5mMzuPfAJmER9wI28tgK38Mep2zAKwU82bFZt+grGVZOcHq03jTwRSPx7BxM2s6fiQSG1yBNj3fnTM254AAdMTmgY=\r
trusted comment: timestamp:1794046822\tfile:right_click_crops-1.0.1.hnmod\thashed\r
eZC0QPTQ1vVVypFSHHcn3hb29MfdbBU8aXb3q7Rzrgroq8O9q2BZzniMx9XqwvyYcRJIs+RZZxmEwsiF9ZoNAA=="""
changelog = "Compatibility patch for Horizon Nightly 2026-11-05 (`66f3981`). Horizon's block loot helper now takes the broken block's entity data; the managed plants have none, so harvesting passes none and loot is unchanged. No gameplay changes. Validated in single-player and for LAN host and guest on that exact revision."
[[versions]]
version = "1.0.0"
published = "2026-11-01"
+21
View File
@@ -0,0 +1,21 @@
# The native mod sets the registry prebuilds on each new Nightly (.github/workflows/native-build.yml, horizon#11859):
# a player whose turned-on native mods are exactly one of these sets downloads the game instead of compiling it.
# Each set is built for every target (Linux, Windows x64 and ARM64, macOS ARM and Intel) on GitHub's runners,
# about 20-30 minutes per target. A mod is named by its id (its newest version) or `id@version`.
# Keep the list short: every set costs five builds per Nightly.
[[set]]
name = "Horizon's own client mods"
mods = ["zoom", "fps_overlay", "minimap", "coords_hud", "keystrokes", "recipes"]
[[set]]
name = "Zoom"
mods = ["zoom"]
[[set]]
name = "FPS overlay"
mods = ["fps_overlay"]
[[set]]
name = "Minimap"
mods = ["minimap"]
Binary file not shown.

After

Width:  |  Height:  |  Size: 345 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 360 KiB

+42 -13
View File
@@ -1,26 +1,55 @@
#!/usr/bin/env bash
# Downloads every file the entries list into files/<sha256>: from its own release
# (<id>-<version>) once published, else from the `staging` prerelease, where the
# submission form uploads <sha256>.<extension>. Usage: scripts/fetch.sh [checkout]
# Needs `gh` (GH_TOKEN) and cargo-horizon-mod. FILES_FROM=<dir> reads <dir>/<file>
# instead, for a local trial.
# Downloads every file the entries list into files/<sha256>, each checked against its SHA-256. Sources, in order:
# - the `registry-files` prerelease: a copy of every published file named <sha256>, kept by publish, so the
# registry's own runs don't count as players' downloads (it reads it by URL, not through the API);
# - its own release (<id>-<version>) once published;
# - the `staging` prerelease, where the submission form uploads <sha256>.<extension>.
# A file already in files/ stays only if its SHA-256 is right.
# Usage: scripts/fetch.sh [checkout]. Needs `gh` (GH_TOKEN, GH_REPO), curl and cargo-horizon-mod.
# FILES_FROM=<dir> reads <dir>/<file> instead, for a local trial. FILES_MIRROR_UPLOAD=1 (publish, which can
# write releases) adds each file it had to take from elsewhere to registry-files.
set -euo pipefail
checkout="${1:-.}"
mirror=registry-files
mkdir -p files
sha_of() { sha256sum < "$1" | cut -d' ' -f1; }
cargo horizon-mod registry list "$checkout" | while read -r sha id version file; do
[ -f "files/$sha" ] && continue
[[ "$sha" =~ ^[0-9a-f]{64}$ ]] || { echo "::error::$id $version: $sha is not a SHA-256"; exit 1; }
if [ -f "files/$sha" ] && [ ! -L "files/$sha" ] && [ "$(sha_of "files/$sha")" = "$sha" ]; then
continue
fi
rm -rf "files/$sha"
ext="${file##*.}"
tmp="$(mktemp -d)"
mkdir "$tmp/dl" # gh downloads land here, apart from $tmp/file
if [ -n "${FILES_FROM:-}" ]; then
cp "$FILES_FROM/$file" "$tmp/$file"
elif gh release download "$id-$version" --pattern "$file" --dir "$tmp" 2>/dev/null; then
:
elif gh release download staging --pattern "$sha.$ext" --dir "$tmp"; then
mv "$tmp/$sha.$ext" "$tmp/$file"
cp "$FILES_FROM/$file" "$tmp/file"
from=folder
elif [ -n "${GH_REPO:-}" ] \
&& curl -sfL --max-filesize 1073741824 -o "$tmp/file" "https://github.com/$GH_REPO/releases/download/$mirror/$sha" < /dev/null \
&& [ "$(sha_of "$tmp/file")" = "$sha" ]; then
from=mirror
elif gh release download "$id-$version" --pattern "$file" --dir "$tmp/dl" < /dev/null 2>/dev/null; then
mv "$tmp/dl/$file" "$tmp/file"
from="release $id-$version"
elif gh release download staging --pattern "$sha.$ext" --dir "$tmp/dl" < /dev/null; then
mv "$tmp/dl/$sha.$ext" "$tmp/file"
from=staging
else
echo "::error::$id $version: $file is neither in release $id-$version nor in staging as $sha.$ext"
exit 1
fi
mv "$tmp/$file" "files/$sha"
rmdir "$tmp"
if [ "$(sha_of "$tmp/file")" != "$sha" ]; then
echo "::error::$id $version: $file from $from doesn't have the SHA-256 its entry lists ($sha)"
exit 1
fi
if [ "${FILES_MIRROR_UPLOAD:-}" = 1 ] && [ "$from" != mirror ] && [ "$from" != folder ]; then
gh release view "$mirror" > /dev/null 2>&1 < /dev/null \
|| gh release create "$mirror" --prerelease --latest=false --title "Registry files (for the registry's own runs)" \
--notes "Every published mod file, named by its SHA-256, for publish and the checks to read without counting as a player's download (scripts/fetch.sh). Players download from each version's own release." < /dev/null
cp "$tmp/file" "$tmp/dl/$sha"
gh release upload "$mirror" "$tmp/dl/$sha" --clobber < /dev/null
fi
mv "$tmp/file" "files/$sha"
rm -rf "$tmp"
done
+269
View File
@@ -0,0 +1,269 @@
#!/usr/bin/env python3
"""Prebuilt native builds (horizon#11859): which builds native-build.yml makes, named as the launcher names them.
python3 scripts/native_build.py sets [--sets prebuilt.toml] # one line per set: its mods
python3 scripts/native_build.py plan --mods "zoom [email protected]" [--base latest|nightly-<date>-<short>]
[--targets all|<triple>,...] --out plan/
BASE=<40 hex> MODS=<json> MATRIX=<json> python3 scripts/native_build.py signable # sign job: "key target" lines
`plan` resolves one set for one base: the Nightly (its tag for the launcher, its 40-hex commit for the key),
each mod against the published index (its newest version unless `@version` names one; refused when unknown,
revoked, or `broken_on` that commit), and each target's request key (horizon_release_sign::native_build:
the same text, so the launcher looks up what CI built). Targets already published are left out. It writes
plan/plan.json and downloads each mod's file into plan/hnmods/, checked against the index's SHA-256.
A run builds ONE set, and only a set prebuilt.toml lists (exactly its items): nobody can dispatch a signed build
of any other mix of registry mods. Every build job of a run belongs to that set, so a mod's build script, which
runs in that job, can only ever replace a build of its own set.
`signable` is what the sign job (which holds the key) signs: it reads only the plan job's outputs (job outputs,
which no later job can rewrite; never the `plan` artifact, which a build job can), checks again that the mods are a
prebuilt.toml set, recomputes every row's key from the base, the mods and the row's target, and prints
`<key> <target>` per row. The sign job derives every file name from those two.
"""
import argparse
import hashlib
import json
import os
import re
import sys
import tomllib
import urllib.error
import urllib.request
from pathlib import Path
INDEX = "https://horizon-rust.github.io/horizon-mods/index.json"
REGISTRY = "horizon-rust/horizon-mods"
HORIZON = "horizon-rust/horizon"
ROOT = Path(__file__).resolve().parents[1]
# The launcher's toolchain host for each target (Horizon's launcher/src/rust_toolchain.rs), the GitHub-hosted
# runner that builds it, and the launcher release asset that runs there.
TARGETS = {
"x86_64-unknown-linux-gnu": ("ubuntu-22.04", "horizon-launcher-linux-x86_64.tar.gz"),
"x86_64-pc-windows-gnu": ("windows-latest", "horizon-launcher-windows-x64.zip"),
"aarch64-pc-windows-gnullvm": ("windows-11-arm", "horizon-launcher-windows-arm64.zip"),
"aarch64-apple-darwin": ("macos-15", "horizon-launcher-macos-arm64.zip"),
"x86_64-apple-darwin": ("macos-15-intel", "horizon-launcher-macos-x64.zip"),
}
MAX_MODS = 64
SETS = ROOT / "prebuilt.toml"
# horizon_release_sign::native_build's field rules: what the launcher accepts in a request.
ID = re.compile(r"[a-z0-9_-]+")
VERSION = re.compile(r"[!-~]+")
HEX40 = re.compile(r"[0-9a-f]{40}")
HEX64 = re.compile(r"[0-9a-f]{64}")
def request_text(target, base, mods):
"""horizon_release_sign::native_build::Request::text: mods are (id, version, sha256), sorted by id."""
lines = ["horizon native build request 1", f"target {target}", f"base {base.lower()}"]
lines += [f"mod {i} {v} {s.lower()}" for i, v, s in sorted(mods)]
return "\n".join(lines) + "\n"
def request_key(target, base, mods):
return hashlib.sha256(request_text(target, base, mods).encode()).hexdigest()
def release_tag(base):
return f"native-builds-{base[:12]}"
def archive(key, target):
return f"{key[:16]}-{target}.tar.gz"
def signature(key, target):
return f"{key[:16]}-{target}.sig"
def get(url, token=True):
headers = {"User-Agent": "horizon-mods native-build"}
if token and os.environ.get("GITHUB_TOKEN") and url.startswith("https://api.github.com/"):
headers["Authorization"] = f"Bearer {os.environ['GITHUB_TOKEN']}"
with urllib.request.urlopen(urllib.request.Request(url, headers=headers), timeout=120) as r:
return r.read()
def exists(url):
try:
with urllib.request.urlopen(urllib.request.Request(url, method="HEAD"), timeout=60):
return True
except urllib.error.HTTPError as e:
if e.code == 404:
return False
raise
def nightly(base):
"""(launcher tag, 40-hex commit) of the Nightly `base` names: `latest`, or `nightly-<date>-<short>`. A Nightly
stays buildable after it is pruned: its permanent `nightly-source/<date>-<short>` tag keeps the commit."""
refs = json.loads(get(f"https://api.github.com/repos/{HORIZON}/git/matching-refs/tags/nightly-source/"))
names = {r["ref"].removeprefix("refs/tags/nightly-source/"): r["object"]["sha"] for r in refs}
if base == "latest":
if not names:
sys.exit("no Nightly has a nightly-source/ tag")
name = max(names)
else:
name = base.removeprefix("nightly-")
if name not in names:
sys.exit(f"{base}: no such Nightly (nightly-<date>-<short>)")
commit = json.loads(get(f"https://api.github.com/repos/{HORIZON}/commits/nightly-source/{name}"))["sha"]
return f"nightly-{name}", commit
def pick(index, wanted, commit):
"""The index version each `id[@version]` of `wanted` names, refusing what the launcher wouldn't build."""
revoked = {r.get("sha256") for r in index.get("revoked", [])}
projects = {p["id"]: p for p in index["projects"]}
picked = []
for item in wanted:
mod_id, _, version = item.partition("@")
project = projects.get(mod_id)
if not project or project.get("kind") != "native-mod":
raise ValueError(f"{mod_id}: no native mod of that id in the registry")
versions = [v for v in project["versions"] if v["file"]["sha256"] not in revoked]
found = next((v for v in versions if v["version"] == version), None) if version else (versions or [None])[0]
if not found:
raise ValueError(f"{item}: no such published, unrevoked version")
if any(b.get("horizon") == commit for b in found.get("broken_on", [])):
raise ValueError(f"{mod_id} {found['version']} doesn't build on {commit[:7]} (broken_on)")
picked.append(found | {"id": mod_id})
ids = [p["id"] for p in picked]
if len(set(ids)) != len(ids):
raise ValueError("a mod is listed twice")
if not 1 <= len(picked) <= MAX_MODS:
raise ValueError(f"a set has 1 to {MAX_MODS} mods")
return picked
def listed(wanted, sets_path=SETS):
"""`wanted` (its `id[@version]` items) if it is exactly one set of prebuilt.toml, else ValueError."""
if sorted(wanted) not in [sorted(line.split()) for line in sets(sets_path)]:
raise ValueError(f"{' '.join(wanted) or '(nothing)'}: not a set of prebuilt.toml; only those are built")
return wanted
def plan(mods, base, targets, out):
wanted = listed(mods.split())
tag, commit = nightly(base)
index = json.loads(get(INDEX, token=False))
picked = pick(index, wanted, commit)
out.mkdir(parents=True, exist_ok=True)
hnmods = out / "hnmods"
hnmods.mkdir(exist_ok=True)
for v in picked:
data = get(v["file"]["url"], token=False)
if hashlib.sha256(data).hexdigest() != v["file"]["sha256"]:
sys.exit(f"{v['file']['url']}: its SHA-256 isn't the index's")
(hnmods / f"{v['id']}-{v['version']}.hnmod").write_bytes(data)
triples = list(TARGETS) if targets == "all" else targets.split(",")
request = [(v["id"], v["version"], v["file"]["sha256"]) for v in picked]
builds = []
for target in triples:
if target not in TARGETS:
sys.exit(f"{target}: not a target the launcher builds")
key = request_key(target, commit, request)
url = f"https://github.com/{REGISTRY}/releases/download/{release_tag(commit)}/{signature(key, target)}"
if exists(url):
print(f"{target}: already published ({key[:16]})")
continue
runner, launcher = TARGETS[target]
builds.append({"target": target, "key": key, "runner": runner, "launcher": launcher,
"archive": archive(key, target), "signature": signature(key, target)})
result = {"tag": tag, "base": commit, "release": release_tag(commit),
"mods": [{"id": i, "version": v, "sha256": s} for i, v, s in sorted(request)], "builds": builds}
(out / "plan.json").write_text(json.dumps(result, indent=1) + "\n")
print(json.dumps({k: result[k] for k in ("tag", "base", "mods")}), f"{len(builds)} to build")
return result
def signable(base, mods, matrix, sets_path=SETS):
"""[(key, target)] the sign job may sign, from the plan job's outputs only (see the module's doc)."""
if not isinstance(base, str) or not HEX40.fullmatch(base):
raise ValueError(f"{base!r}: not a 40-hex commit")
if not isinstance(mods, list) or not mods:
raise ValueError("no mods")
request = []
for m in mods:
i, v, s = (m.get(k) if isinstance(m, dict) else None for k in ("id", "version", "sha256"))
if not (isinstance(i, str) and ID.fullmatch(i) and isinstance(v, str) and VERSION.fullmatch(v)
and isinstance(s, str) and HEX64.fullmatch(s)):
raise ValueError(f"{m!r}: not a mod (id, version, sha256)")
request.append((i, v, s))
ids = [i for i, _, _ in request]
if len(set(ids)) != len(ids) or len(ids) > MAX_MODS:
raise ValueError("a mod is listed twice, or too many mods")
versions = dict((i, v) for i, v, _ in request)
for line in sets(sets_path):
items = [item.partition("@") for item in line.split()]
if sorted(i for i, _, _ in items) == sorted(ids) and all(not v or versions[i] == v for i, _, v in items):
break
else:
raise ValueError(f"{' '.join(sorted(ids))}: not a set of prebuilt.toml")
if not isinstance(matrix, list):
raise ValueError("the matrix is not a list")
rows, seen = [], set()
for row in matrix:
target, key = (row.get(k) if isinstance(row, dict) else None for k in ("target", "key"))
if target not in TARGETS or target in seen:
raise ValueError(f"{target!r}: not a target, or twice")
seen.add(target)
if key != request_key(target, base, request):
raise ValueError(f"{target}: the matrix's key {key!r} isn't this set's")
rows.append((key, target))
return rows
def sets(path):
"""Each set of prebuilt.toml as one line of space-separated `id[@version]`."""
data = tomllib.loads(path.read_text())
lines = []
for s in data.get("set", []):
mods = s.get("mods", [])
if not mods or not all(isinstance(m, str) and m and " " not in m for m in mods):
sys.exit(f"{path}: set {s.get('name', '?')!r} needs a list of mod ids")
lines.append(" ".join(mods))
return lines
def main():
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
sub = parser.add_subparsers(dest="command", required=True)
s = sub.add_parser("sets")
s.add_argument("--sets", type=Path, default=ROOT / "prebuilt.toml")
p = sub.add_parser("plan")
p.add_argument("--mods", required=True)
p.add_argument("--base", default="latest")
p.add_argument("--targets", default="all")
p.add_argument("--out", type=Path, required=True)
sub.add_parser("signable")
args = parser.parse_args()
if args.command == "sets":
print("\n".join(sets(args.sets)))
return
if args.command == "signable":
try:
env = {k: json.loads(os.environ[k]) for k in ("MODS", "MATRIX")}
rows = signable(os.environ["BASE"], env["MODS"], env["MATRIX"])
except (KeyError, ValueError) as e:
sys.exit(f"signable: {e}")
print("\n".join(f"{key} {target}" for key, target in rows))
return
try:
result = plan(args.mods, args.base, args.targets, args.out)
except ValueError as e:
sys.exit(str(e))
if os.environ.get("GITHUB_OUTPUT"):
with open(os.environ["GITHUB_OUTPUT"], "a") as f:
# The sign job reads these job outputs, never the plan artifact (a build job can replace that).
f.write(f"matrix={json.dumps(result['builds'])}\n")
f.write(f"any={'true' if result['builds'] else 'false'}\n")
f.write(f"tag={result['tag']}\n")
f.write(f"base={result['base']}\n")
f.write(f"mods={json.dumps(result['mods'])}\n")
if __name__ == "__main__":
main()
+138
View File
@@ -0,0 +1,138 @@
import sys
import tempfile
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
import native_build # noqa: E402
BASE = "656064105c0e1fbd0a8f1d6e5f5d3a8b3c2e1f00"
def version(v, sha, broken=()):
return {"version": v, "file": {"url": f"https://x/{v}", "sha256": sha},
"broken_on": [{"horizon": b, "code": "E"} for b in broken]}
INDEX = {
"projects": [
{"id": "zoom", "kind": "native-mod", "versions": [version("2.1.0", "c" * 64), version("2.0.0", "b" * 64)]},
{"id": "minimap", "kind": "native-mod", "versions": [version("1.4.0", "a" * 64)]},
{"id": "broken", "kind": "native-mod", "versions": [version("1.0.0", "d" * 64, [BASE])]},
{"id": "pack", "kind": "hmod", "versions": [version("1.0.0", "e" * 64)]},
],
"revoked": [{"id": "zoom", "sha256": "c" * 64}],
}
class Key(unittest.TestCase):
def test_the_key_is_the_launchers(self):
# The same vector as horizon_release_sign::native_build's test: CI and the launcher name a build alike.
mods = [("zoom", "2.0.0", "b" * 64), ("minimap", "1.4.0", "a" * 64)]
self.assertEqual(native_build.request_key("x86_64-unknown-linux-gnu", BASE, mods),
"c07fffc296cf14c5c496a016a930f84b0d0ac028a63c667c2bdf47657788445d")
def test_names(self):
key = "0123456789abcdef" + "0" * 48
self.assertEqual(native_build.release_tag(BASE), "native-builds-656064105c0e")
self.assertEqual(native_build.archive(key, "aarch64-apple-darwin"),
"0123456789abcdef-aarch64-apple-darwin.tar.gz")
self.assertEqual(native_build.signature(key, "aarch64-apple-darwin"),
"0123456789abcdef-aarch64-apple-darwin.sig")
class Pick(unittest.TestCase):
def test_newest_unrevoked_version_unless_one_is_named(self):
picked = native_build.pick(INDEX, ["zoom", "[email protected]"], BASE)
self.assertEqual([(p["id"], p["version"]) for p in picked], [("zoom", "2.0.0"), ("minimap", "1.4.0")])
def test_what_the_launcher_would_not_build_is_refused(self):
for wanted in (["nothing"], ["pack"], ["[email protected]"], ["zoom@9"], ["broken"], ["zoom", "[email protected]"], []):
with self.assertRaises(ValueError, msg=wanted):
native_build.pick(INDEX, wanted, BASE)
def test_broken_on_another_commit_still_builds(self):
picked = native_build.pick(INDEX, ["broken"], "1" * 40)
self.assertEqual(picked[0]["version"], "1.0.0")
SETS = """
[[set]]
name = "Two"
mods = ["zoom", "[email protected]"]
[[set]]
name = "One"
mods = ["zoom"]
"""
LINUX = "x86_64-unknown-linux-gnu"
MAC = "aarch64-apple-darwin"
ZOOM = {"id": "zoom", "version": "2.0.0", "sha256": "b" * 64}
MINIMAP = {"id": "minimap", "version": "1.4.0", "sha256": "a" * 64}
def key(target, *mods):
return native_build.request_key(target, BASE, [(m["id"], m["version"], m["sha256"]) for m in mods])
class OnlyListedSets(unittest.TestCase):
def setUp(self):
tmp = tempfile.TemporaryDirectory()
self.addCleanup(tmp.cleanup)
self.sets = Path(tmp.name) / "prebuilt.toml"
self.sets.write_text(SETS)
def test_plan_takes_a_listed_set_in_any_order(self):
self.assertEqual(native_build.listed(["[email protected]", "zoom"], self.sets), ["[email protected]", "zoom"])
def test_plan_refuses_any_other_set(self):
for wanted in (["minimap"], ["zoom", "minimap"], ["zoom", "[email protected]", "keystrokes"], ["[email protected]"], []):
with self.assertRaises(ValueError, msg=wanted):
native_build.listed(wanted, self.sets)
def test_sign_recomputes_each_rows_key(self):
matrix = [{"target": LINUX, "key": key(LINUX, ZOOM, MINIMAP), "archive": "ignored"},
{"target": MAC, "key": key(MAC, ZOOM, MINIMAP)}]
self.assertEqual(native_build.signable(BASE, [ZOOM, MINIMAP], matrix, self.sets),
[(key(LINUX, ZOOM, MINIMAP), LINUX), (key(MAC, ZOOM, MINIMAP), MAC)])
def test_sign_refuses_another_sets_key(self):
# A row naming the key of `zoom` alone, in a run of the two-mod set: H1 of the review.
with self.assertRaises(ValueError):
native_build.signable(BASE, [ZOOM, MINIMAP], [{"target": LINUX, "key": key(LINUX, ZOOM)}], self.sets)
# Another target's key on this row.
with self.assertRaises(ValueError):
native_build.signable(BASE, [ZOOM], [{"target": LINUX, "key": key(MAC, ZOOM)}], self.sets)
def test_sign_refuses_mods_or_rows_outside_the_rules(self):
good = [{"target": LINUX, "key": key(LINUX, ZOOM)}]
cases = [
("1" * 39, [ZOOM], good), # not a commit
(BASE, [], good), # no mods
(BASE, [dict(ZOOM, id="zoom\nmod x")], good), # a line break in an id
(BASE, [dict(ZOOM, version="2.0 0")], good), # a space in a version
(BASE, [dict(ZOOM, sha256="B" * 64)], good), # not lowercase hex
(BASE, [MINIMAP], [{"target": LINUX, "key": key(LINUX, MINIMAP)}]), # minimap alone isn't listed
(BASE, [dict(MINIMAP, version="1.5.0"), ZOOM], # the set pins minimap 1.4.0
[{"target": LINUX, "key": key(LINUX, dict(MINIMAP, version="1.5.0"), ZOOM)}]),
(BASE, [ZOOM], good + good), # a target twice
(BASE, [ZOOM], [{"target": "x86_64-unknown-freebsd", "key": key("x86_64-unknown-freebsd", ZOOM)}]),
(BASE, [ZOOM], {"target": LINUX}), # not a list
]
for base, mods, matrix in cases:
with self.assertRaises(ValueError, msg=(base, mods, matrix)):
native_build.signable(base, mods, matrix, self.sets)
class Sets(unittest.TestCase):
def test_the_registrys_sets_are_native_mods_it_lists(self):
lines = native_build.sets(native_build.ROOT / "prebuilt.toml")
self.assertTrue(lines)
official = {p.name for p in (native_build.ROOT / "native").iterdir() if (p / "compatibility.json").is_file()}
for line in lines:
for item in line.split():
self.assertIn(item.partition("@")[0].replace("_", "-"), official, line)
if __name__ == "__main__":
unittest.main()