mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 02:56:17 -10:00
cargo-horizon-mod: prebuilt binaries for cargo-binstall
`cargo install --git ...` compiles the tool and its workspace, which for a mod developer on a slow machine is a long wait to write one block (#2120). `cargo binstall` skips the build when the release holds a binary for the platform. - crates/cargo-horizon-mod/Cargo.toml gains [package.metadata.binstall]: one `cargo-horizon-mod-v<version>` release, one tar.gz per Rust target, the binary at the archive's root; a `.sha256` beside each. - .github/workflows/cargo-horizon-mod.yml builds those targets (Linux x86_64 and aarch64, Windows x86_64, macOS x86_64 and arm64), starts each binary it can run, and publishes them on the tag. A pull request builds them without publishing. - tools/horizon_mod_binstall.py is the one list of targets: the workflow's matrix comes from it, and it checks the built archives and the tag against the manifest before anything is published. - docs/mods.md shows the binstall command first, the source build after. Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
This commit is contained in:
1 parent
fc02103818
commit
ab49fe48b7
5 files changed
+268
No files matched your search
@@ -0,0 +1,107 @@
|
||||
name: cargo-horizon-mod
|
||||
# `cargo binstall --git https://github.com/horizon-rust/horizon cargo-horizon-mod` (#2120): binstall reads
|
||||
# [package.metadata.binstall] from crates/cargo-horizon-mod/Cargo.toml and downloads the archive this workflow
|
||||
# attaches to the tag `cargo-horizon-mod-v<version>`, whose version must be the manifest's. The targets come from
|
||||
# tools/horizon_mod_binstall.py, the same file the manifest's template is checked against, so a platform added there
|
||||
# is built here and one nobody publishes is caught before it ships. A pull request builds the archives and publishes
|
||||
# nothing.
|
||||
on:
|
||||
push:
|
||||
tags: ['cargo-horizon-mod-v*']
|
||||
pull_request:
|
||||
paths:
|
||||
- 'crates/cargo-horizon-mod/**'
|
||||
- 'tools/horizon_mod_binstall.py'
|
||||
- '.github/workflows/cargo-horizon-mod.yml'
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
plan:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
outputs:
|
||||
platforms: ${{ steps.platforms.outputs.platforms }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- id: platforms
|
||||
run: echo "platforms=$(python3 -c 'import json, tools.horizon_mod_binstall as m; print(json.dumps(m.PLATFORMS))')" >> "$GITHUB_OUTPUT"
|
||||
build:
|
||||
needs: plan
|
||||
name: build ${{ matrix.target }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include: ${{ fromJSON(needs.plan.outputs.platforms) }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
- name: The aarch64 Linux linker
|
||||
if: matrix.target == 'aarch64-unknown-linux-gnu'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y gcc-aarch64-linux-gnu
|
||||
echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" >> "$GITHUB_ENV"
|
||||
- name: Build
|
||||
run: cargo build --release --locked -p cargo-horizon-mod --target ${{ matrix.target }}
|
||||
- name: It starts
|
||||
if: matrix.run
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
binary="target/${{ matrix.target }}/release/cargo-horizon-mod"
|
||||
[ -f "$binary.exe" ] && binary="$binary.exe"
|
||||
out=$("$binary" 2>&1 || true)
|
||||
case "$out" in
|
||||
*"usage: cargo horizon-mod"*) ;;
|
||||
*) echo "$out"; echo "::error::$binary does not start"; exit 1 ;;
|
||||
esac
|
||||
- name: Package
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
binary="target/${{ matrix.target }}/release/cargo-horizon-mod"
|
||||
[ -f "$binary.exe" ] && binary="$binary.exe"
|
||||
mkdir -p dist
|
||||
tar -czf "dist/$(basename "$binary" | sed 's/\.exe$//')-${{ matrix.target }}.tar.gz" -C "$(dirname "$binary")" "$(basename "$binary")"
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cargo-horizon-mod-${{ matrix.target }}
|
||||
path: dist/*.tar.gz
|
||||
if-no-files-found: error
|
||||
retention-days: 3
|
||||
publish:
|
||||
needs: build
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: cargo-horizon-mod-*
|
||||
path: dist
|
||||
merge-multiple: true
|
||||
- name: The tag and the manifest name the same release and archives
|
||||
run: python3 tools/horizon_mod_binstall.py dist "$GITHUB_REF_NAME"
|
||||
- name: Checksums
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cd dist
|
||||
for file in *.tar.gz; do sha256sum "$file" | cut -d' ' -f1 > "$file.sha256"; done
|
||||
- name: Publish
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
notes="\`cargo binstall --git https://github.com/horizon-rust/horizon cargo-horizon-mod\`"
|
||||
if gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1; then
|
||||
gh release upload "$GITHUB_REF_NAME" dist/* --clobber
|
||||
else
|
||||
gh release create "$GITHUB_REF_NAME" dist/* --title "cargo-horizon-mod ${GITHUB_REF_NAME#cargo-horizon-mod-v}" --notes "$notes"
|
||||
fi
|
||||
@@ -6,6 +6,14 @@ license.workspace = true
|
||||
rust-version = "1.86"
|
||||
description = "cargo horizon-mod new/build/run/check: make Horizon content mods (docs/mods.md)"
|
||||
|
||||
# `cargo binstall --git https://github.com/horizon-rust/horizon cargo-horizon-mod` (#2120) reads this: the archive
|
||||
# `cargo-horizon-mod`'s workflow attaches to the `cargo-horizon-mod-v<version>` release for the runner's target,
|
||||
# holding the binary at its root. tools/horizon_mod_binstall.py expands it for the workflow's targets.
|
||||
[package.metadata.binstall]
|
||||
pkg-url = "https://github.com/horizon-rust/horizon/releases/download/cargo-horizon-mod-v{ version }/{ name }-{ target }.tar.gz"
|
||||
bin-dir = "{ bin }{ binary-ext }"
|
||||
pkg-fmt = "tgz"
|
||||
|
||||
[dependencies]
|
||||
horizon-mods = { path = "../horizon-mods", default-features = false, features = ["index"] }
|
||||
serde_json = "1.0.151"
|
||||
|
||||
@@ -32,6 +32,9 @@ Benchmarks (`--benchmark`) never load mods.
|
||||
## Making a content mod
|
||||
|
||||
```sh
|
||||
# A prebuilt binary for Linux x86_64/aarch64, macOS x86_64/arm64 or Windows x86_64 (#2120):
|
||||
cargo binstall --git https://github.com/horizon-rust/horizon cargo-horizon-mod
|
||||
# Or build it from source, without cargo-binstall:
|
||||
cargo install --git https://github.com/horizon-rust/horizon cargo-horizon-mod
|
||||
cargo horizon-mod new my-mod # a content mod: manifest, one block with its model, loot and recipe
|
||||
cd my-mod
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
"""`cargo binstall` finds every cargo-horizon-mod archive the release ships (#2120).
|
||||
|
||||
`[package.metadata.binstall]` and `.github/workflows/cargo-horizon-mod.yml` are two halves of one contract: the
|
||||
manifest names a file per target, the workflow uploads it. These check the halves still fit, without a network.
|
||||
"""
|
||||
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
import tools.horizon_mod_binstall as binstall
|
||||
|
||||
WORKFLOW = binstall.ROOT / ".github/workflows/cargo-horizon-mod.yml"
|
||||
|
||||
|
||||
class BinstallTests(unittest.TestCase):
|
||||
def test_the_manifest_names_one_archive_per_platform_in_this_release(self):
|
||||
for platform in binstall.PLATFORMS:
|
||||
with self.subTest(target=platform["target"]):
|
||||
url = binstall.url(platform["target"])
|
||||
self.assertIn(f"/releases/download/{binstall.tag()}/", url)
|
||||
self.assertTrue(url.endswith("/" + binstall.asset(platform["target"])))
|
||||
|
||||
def test_a_placeholder_binstall_cannot_fill_fails_here(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
manifest = Path(directory, "Cargo.toml")
|
||||
manifest.write_text(
|
||||
'[package]\nname = "cargo-horizon-mod"\nversion = "1.2.3"\n'
|
||||
'[package.metadata.binstall]\npkg-url = "https://example.invalid/{ name }-{ arch }.tar.gz"\n'
|
||||
)
|
||||
with self.assertRaises(KeyError):
|
||||
binstall.asset("x86_64-unknown-linux-gnu", manifest)
|
||||
|
||||
def test_a_release_missing_an_archive_or_held_by_another_tag_is_reported(self):
|
||||
names = [binstall.asset(platform["target"]) for platform in binstall.PLATFORMS]
|
||||
self.assertEqual(len(names), len(set(names)), "two targets would overwrite one asset")
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
for name in names:
|
||||
Path(directory, name).touch()
|
||||
self.assertEqual(binstall.check(directory, binstall.tag()), [])
|
||||
self.assertEqual(binstall.check(directory), [])
|
||||
|
||||
renamed = Path(directory, names[0])
|
||||
renamed.rename(renamed.with_name("cargo-horizon-mod-mystery.tar.gz"))
|
||||
problems = binstall.check(directory)
|
||||
self.assertTrue(any(names[0] in problem for problem in problems), problems)
|
||||
self.assertTrue(any("mystery" in problem for problem in problems), problems)
|
||||
|
||||
self.assertTrue(any("not" in problem for problem in binstall.check(directory, "cargo-horizon-mod-v9.9.9")))
|
||||
|
||||
def test_the_workflow_builds_this_list_and_checks_it_before_publishing(self):
|
||||
workflow = WORKFLOW.read_text()
|
||||
self.assertIn("tools.horizon_mod_binstall", workflow)
|
||||
self.assertIn("fromJSON(needs.plan.outputs.platforms)", workflow)
|
||||
self.assertIn("tools/horizon_mod_binstall.py dist", workflow)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,91 @@
|
||||
"""Where `cargo binstall` finds `cargo-horizon-mod`'s prebuilt archives (#2120).
|
||||
|
||||
`[package.metadata.binstall]` in `crates/cargo-horizon-mod/Cargo.toml` names one release asset per Rust
|
||||
target, and `.github/workflows/cargo-horizon-mod.yml` builds [`PLATFORMS`] and attaches them to the tag
|
||||
[`tag`]. Both sides read this file, so renaming an archive here cannot leave `cargo binstall` asking for a
|
||||
file no release holds.
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
MANIFEST = ROOT / "crates/cargo-horizon-mod/Cargo.toml"
|
||||
|
||||
# One entry per archive the release holds: the Rust target, the runner that builds it, and whether that
|
||||
# runner can start the binary (a cross build cannot).
|
||||
PLATFORMS = [
|
||||
{"target": "x86_64-unknown-linux-gnu", "os": "ubuntu-22.04", "run": True},
|
||||
{"target": "aarch64-unknown-linux-gnu", "os": "ubuntu-22.04", "run": False},
|
||||
{"target": "x86_64-pc-windows-msvc", "os": "windows-latest", "run": True},
|
||||
{"target": "x86_64-apple-darwin", "os": "macos-latest", "run": True},
|
||||
{"target": "aarch64-apple-darwin", "os": "macos-latest", "run": True},
|
||||
]
|
||||
|
||||
PLACEHOLDER = re.compile(r"\{\s*([a-z-]+)\s*\}")
|
||||
|
||||
|
||||
def _package(manifest=MANIFEST):
|
||||
with open(manifest, "rb") as handle:
|
||||
return tomllib.load(handle)["package"]
|
||||
|
||||
|
||||
def version(manifest=MANIFEST):
|
||||
return _package(manifest)["version"]
|
||||
|
||||
|
||||
def tag(manifest=MANIFEST):
|
||||
"""The GitHub release holding the archives: `cargo-horizon-mod-v<version>`."""
|
||||
return f"cargo-horizon-mod-v{version(manifest)}"
|
||||
|
||||
|
||||
def url(target, manifest=MANIFEST):
|
||||
"""The download URL `binstall` builds for `target`, from the manifest's `pkg-url`.
|
||||
|
||||
Raises `KeyError` for a placeholder `binstall` would not fill in, so a typo fails here instead of at
|
||||
every mod developer's download.
|
||||
"""
|
||||
template = _package(manifest)["metadata"]["binstall"]["pkg-url"]
|
||||
fields = {
|
||||
"name": "cargo-horizon-mod",
|
||||
"version": version(manifest),
|
||||
"target": target,
|
||||
"binary-ext": ".exe" if "windows" in target else "",
|
||||
"archive-format": "tar.gz",
|
||||
}
|
||||
|
||||
def fill(match):
|
||||
return fields[match.group(1)]
|
||||
|
||||
return PLACEHOLDER.sub(fill, template)
|
||||
|
||||
|
||||
def asset(target, manifest=MANIFEST):
|
||||
"""The archive `binstall` downloads for `target`: the file name of its download URL."""
|
||||
return url(target, manifest).rsplit("/", 1)[-1]
|
||||
|
||||
|
||||
def check(directory, expected_tag=None):
|
||||
"""What is wrong with the archives in `directory` for this manifest: `[]` when binstall finds them all."""
|
||||
problems = []
|
||||
if expected_tag is not None and expected_tag != tag():
|
||||
problems.append(f"the tag is {expected_tag}, not {tag()}")
|
||||
wanted = {asset(platform["target"]) for platform in PLATFORMS}
|
||||
found = {path.name for path in Path(directory).glob("*.tar.gz")}
|
||||
problems += [f"the release has no {name}" for name in sorted(wanted - found)]
|
||||
problems += [f"{name} is not an archive binstall asks for" for name in sorted(found - wanted)]
|
||||
return problems
|
||||
|
||||
|
||||
def main(argv):
|
||||
directory = argv[0] if argv else "dist"
|
||||
problems = check(directory, argv[1] if len(argv) > 1 else None)
|
||||
for problem in problems:
|
||||
print(f"::error::{problem}")
|
||||
return 1 if problems else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv[1:]))
|
||||
Reference in new issue
Block a user