diff --git a/.github/workflows/homebrew.yml b/.github/workflows/homebrew.yml index 5847011d..75468ae6 100644 --- a/.github/workflows/homebrew.yml +++ b/.github/workflows/homebrew.yml @@ -112,6 +112,7 @@ jobs: env: GH_TOKEN: ${{ github.token }} RELEASE_TAG: ${{ steps.tag.outputs.tag }} + EVENT_NAME: ${{ github.event_name }} shell: bash run: | set -euo pipefail @@ -130,11 +131,32 @@ jobs: exit 1 fi done - python3 scripts/render_homebrew_formula.py \ - --version "$VERSION" \ - --sha256sums SHA256SUMS \ - --write \ + RENDER_ARGS=( + --version "$VERSION" + --sha256sums SHA256SUMS + --write --out /tmp/numan.rb + ) + # Linux ARM is required for current releases; omit only via explicit + # workflow_dispatch recovery of pre-ARM tags. + linux_arm_asset="numan-${VERSION}-aarch64-unknown-linux-gnu.tar.gz" + linux_arm_ok="$( + gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --json isDraft,assets \ + --jq ".isDraft == false and any(.assets[]; .name == \"${linux_arm_asset}\" and .state == \"uploaded\")" + )" + if [[ "$linux_arm_ok" == "true" ]]; then + echo "Found Linux ARM archive ${linux_arm_asset}" + else + # Missing Linux ARM asset is only acceptable during explicit manual recovery. + if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then + echo "No Linux ARM archive on ${RELEASE_TAG}; using --legacy-pre-linux-arm for manual recovery" + RENDER_ARGS+=(--legacy-pre-linux-arm) + else + echo "::error::Release ${RELEASE_TAG} is missing required Linux ARM asset ${linux_arm_asset}; automated releases must include all platform assets" + exit 1 + fi + fi + python3 scripts/render_homebrew_formula.py "${RENDER_ARGS[@]}" cp /tmp/numan.rb packaging/homebrew/numan.rb - name: Push formula to homebrew-numan tap diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d5073bcd..c32ca353 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -100,9 +100,15 @@ jobs: - os: ubuntu-latest target: x86_64-unknown-linux-gnu bin_name: numan + - os: ubuntu-24.04-arm + target: aarch64-unknown-linux-gnu + bin_name: numan - os: windows-latest target: x86_64-pc-windows-msvc bin_name: numan.exe + - os: windows-11-arm + target: aarch64-pc-windows-msvc + bin_name: numan.exe - os: macos-latest target: aarch64-apple-darwin bin_name: numan diff --git a/.github/workflows/winget.yml b/.github/workflows/winget.yml index 52e22674..1ba13158 100644 --- a/.github/workflows/winget.yml +++ b/.github/workflows/winget.yml @@ -66,23 +66,25 @@ jobs: --jq 'any(.artifacts[]; .name == "winget-release-ready" and .expired == false)' \ | grep -Fx true - - name: Verify published Windows release asset + - name: Verify published Windows release assets env: GH_TOKEN: ${{ github.token }} shell: bash run: | set -euo pipefail - expected_asset="numan-${RELEASE_TAG#v}-x86_64-pc-windows-msvc.zip" - gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --json isDraft,assets \ - | jq -e --arg expected_asset "$expected_asset" \ - '.isDraft == false and any(.assets[]; .name == $expected_asset and .state == "uploaded")' \ - >/dev/null + for triple in x86_64-pc-windows-msvc aarch64-pc-windows-msvc; do + expected_asset="numan-${RELEASE_TAG#v}-${triple}.zip" + gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --json isDraft,assets \ + | jq -e --arg expected_asset "$expected_asset" \ + '.isDraft == false and any(.assets[]; .name == $expected_asset and .state == "uploaded")' \ + >/dev/null + done - name: Publish manifest to WinGet uses: vedantmgoyal9/winget-releaser@b3a5dae0047c6180023acba3f548c55fdf6b7193 with: identifier: tonythethompson.numan fork-user: tonythethompson - installers-regex: '\.zip$' + installers-regex: '-(x86_64|aarch64)-pc-windows-msvc\.zip$' release-tag: ${{ env.RELEASE_TAG }} token: ${{ secrets.WINGET_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md index c9d0afa0..ded50fc3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- **Release assets** for `aarch64-unknown-linux-gnu` and `aarch64-pc-windows-msvc` (native ARM runners); `numan update --self` maps those triples +- **Homebrew** Linux ARM archive support; **winget** verifies and submits both Windows x64 and ARM64 zips + ### Fixed - **`numan setup nu`**: official Nushell 0.114.x release archives exceed the old diff --git a/README.md b/README.md index 5917d7bf..1cb12fb5 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,9 @@ Download the latest archive for your platform from [GitHub Releases](https://git | Platform | Archive | Binary | |----------|---------|--------| | Linux (x86_64) | `numan--x86_64-unknown-linux-gnu.tar.gz` | `numan` | +| Linux (aarch64) | `numan--aarch64-unknown-linux-gnu.tar.gz` | `numan` | | Windows (x86_64) | `numan--x86_64-pc-windows-msvc.zip` | `numan.exe` | +| Windows (ARM64) | `numan--aarch64-pc-windows-msvc.zip` | `numan.exe` | | macOS (Apple Silicon) | `numan--aarch64-apple-darwin.tar.gz` | `numan` | **Linux / macOS** @@ -106,7 +108,7 @@ install -m 755 numan-VERSION-TARGET/numan ~/.local/bin/numan **Windows (PowerShell)** ```powershell -Expand-Archive numan-VERSION-x86_64-pc-windows-msvc.zip -DestinationPath . +Expand-Archive numan-VERSION-TARGET.zip -DestinationPath . # Add the extracted folder to your PATH, or copy numan.exe into a directory already on PATH ``` diff --git a/docs/PACKAGING.md b/docs/PACKAGING.md index a4dc5be1..2af7b44d 100644 --- a/docs/PACKAGING.md +++ b/docs/PACKAGING.md @@ -47,12 +47,12 @@ Release archives extract to `numan--/` containing the `numan` ## Installation coverage -| Installation channel | Linux x86_64 | macOS Apple Silicon | Windows x86_64 | Windows ARM64 | -|-----------------------|---------------|---------------------|----------------|---------------| -| GitHub release archive | yes | yes | yes | no | -| `cargo install numan-cli` | source build | source build | source build | not validated | -| Homebrew tap | yes | yes | — | — | -| winget | — | — | yes | — | +| Installation channel | Linux x86_64 | Linux aarch64 | macOS Apple Silicon | Windows x86_64 | Windows ARM64 | +|-----------------------|---------------|---------------|---------------------|----------------|---------------| +| GitHub release archive | yes | yes | yes | yes | yes | +| `cargo install numan-cli` | source build | source build | source build | source build | not validated | +| Homebrew tap | yes | yes | yes | — | — | +| winget | — | — | — | yes | yes | ## Secrets diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 48f417fc..c0d04efe 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -47,7 +47,7 @@ Then: ``` 6. The [Release workflow](https://github.com/tonythethompson/numan/actions/workflows/release.yml) waits for green CI on the tagged commit, runs preflight checks, then builds archives and publishes. -7. Confirm platform archives, `SHA256SUMS`, and `SHA256SUMS.sig` on GitHub Releases. +7. Confirm platform archives (`x86_64-unknown-linux-gnu`, `aarch64-unknown-linux-gnu`, `x86_64-pc-windows-msvc`, `aarch64-pc-windows-msvc`, `aarch64-apple-darwin`), `SHA256SUMS`, and `SHA256SUMS.sig` on GitHub Releases. 8. Confirm the **Publish to crates.io** job succeeds (requires Trusted Publishing / OIDC on crates.io). 9. Confirm the [`Publish to WinGet`](../.github/workflows/winget.yml) workflow verifies the `winget-release-ready` artifact and published Windows release asset, then opens the update PR after the `v*.*.*` tag-triggered Release workflow completes (manual recovery: dispatch with required `release_tag`). 10. Confirm the [`Publish to Homebrew tap`](../.github/workflows/homebrew.yml) workflow verifies the `homebrew-release-ready` artifact and pushes `Formula/numan.rb` to [`tonythethompson/homebrew-numan`](https://github.com/tonythethompson/homebrew-numan) (requires `HOMEBREW_TAP_TOKEN`; manual recovery: dispatch with required `release_tag`). diff --git a/packaging/homebrew/README.md b/packaging/homebrew/README.md index 5e1c178b..a286eaa0 100644 --- a/packaging/homebrew/README.md +++ b/packaging/homebrew/README.md @@ -1,8 +1,15 @@ # Homebrew packaging -Canonical formula source lives at [`numan.rb`](numan.rb). The published tap is +The formula **shape** (bottle platforms, install layout) is defined by +[`scripts/render_homebrew_formula.py`](../../scripts/render_homebrew_formula.py). +[`numan.rb`](numan.rb) is the last generated snapshot checked into this repo for +review; digests and bottle stanzas may lag until the next tagged release whose +`SHA256SUMS` include every required archive. Today it still matches the +pre-Linux-ARM contract (macOS ARM + Linux x86_64 only) because no published +release yet ships `aarch64-unknown-linux-gnu`. The live tap is [`tonythethompson/homebrew-numan`](https://github.com/tonythethompson/homebrew-numan) -(`brew tap tonythethompson/numan`). +(`brew tap tonythethompson/numan`), updated by the publish workflow after each +`v*.*.*` release. ## Install @@ -21,20 +28,36 @@ Prefer the automated path: after a `v*.*.*` GitHub Release, the downloads `SHA256SUMS`, regenerates the formula, and pushes `Formula/numan.rb` to the tap. -Manual / dry-run: +Manual / dry-run (current formula: all three Homebrew archives, including Linux +ARM). Use a tag that ships `aarch64-unknown-linux-gnu` (the first such release +after the ARM platform expansion). Pre-ARM tags such as `v0.1.5` need +`--legacy-pre-linux-arm`: ```bash +# ARM-enabled release (preferred): +gh release download vX.Y.Z --pattern SHA256SUMS +python3 scripts/render_homebrew_formula.py \ + --version X.Y.Z \ + --sha256sums SHA256SUMS \ + --write + +# Pre-Linux-ARM recovery only (e.g. v0.1.5): gh release download v0.1.5 --pattern SHA256SUMS python3 scripts/render_homebrew_formula.py \ --version 0.1.5 \ --sha256sums SHA256SUMS \ + --legacy-pre-linux-arm \ --write ``` -Required release assets: +Required release assets (current): - `numan--aarch64-apple-darwin.tar.gz` - `numan--x86_64-unknown-linux-gnu.tar.gz` +- `numan--aarch64-unknown-linux-gnu.tar.gz` + +Legacy (`--legacy-pre-linux-arm`) omits the Linux ARM archive and the formula +`on_linux` / `on_arm` bottle block. Intel Mac (`x86_64-apple-darwin`) is not shipped; the formula fails with a clear `odie` on Intel Hardware. diff --git a/packaging/homebrew/numan.rb b/packaging/homebrew/numan.rb index a65d602b..23f078ba 100644 --- a/packaging/homebrew/numan.rb +++ b/packaging/homebrew/numan.rb @@ -10,6 +10,10 @@ # Generated by scripts/render_homebrew_formula.py — do not hand-edit digests. # The Publish to Homebrew workflow updates this file and the homebrew-numan tap # after each v*.*.* GitHub Release. +# +# Snapshot note: this checked-in copy may lag the renderer. It currently omits +# the Linux ARM (on_linux/on_arm) stanza until a release publishes +# aarch64-unknown-linux-gnu digests; see packaging/homebrew/README.md. class Numan < Formula desc "Cross-platform package manager for Nushell" diff --git a/packaging/winget/manifests/t/tonythethompson/Numan/0.1.3/tonythethompson.Numan.installer.yaml b/packaging/winget/manifests/t/tonythethompson/Numan/0.1.3/tonythethompson.Numan.installer.yaml index 79245abd..d52e9d4c 100644 --- a/packaging/winget/manifests/t/tonythethompson/Numan/0.1.3/tonythethompson.Numan.installer.yaml +++ b/packaging/winget/manifests/t/tonythethompson/Numan/0.1.3/tonythethompson.Numan.installer.yaml @@ -4,13 +4,13 @@ PackageIdentifier: tonythethompson.numan PackageVersion: 0.1.3 InstallerType: zip NestedInstallerType: portable -NestedInstallerFiles: - - RelativeFilePath: numan-0.1.3-x86_64-pc-windows-msvc\numan.exe - PortableCommandAlias: numan Commands: - numan Installers: - Architecture: x64 + NestedInstallerFiles: + - RelativeFilePath: numan-0.1.3-x86_64-pc-windows-msvc\numan.exe + PortableCommandAlias: numan InstallerUrl: https://github.com/tonythethompson/numan/releases/download/v0.1.3/numan-0.1.3-x86_64-pc-windows-msvc.zip InstallerSha256: 90F659E665836F26E2FD45AD86ED8F73C1B6BCB24BE87CF3760EC000E094ABA1 ManifestType: installer diff --git a/packaging/winget/manifests/t/tonythethompson/Numan/0.1.4/tonythethompson.Numan.installer.yaml b/packaging/winget/manifests/t/tonythethompson/Numan/0.1.4/tonythethompson.Numan.installer.yaml index bf77870b..274eed9d 100644 --- a/packaging/winget/manifests/t/tonythethompson/Numan/0.1.4/tonythethompson.Numan.installer.yaml +++ b/packaging/winget/manifests/t/tonythethompson/Numan/0.1.4/tonythethompson.Numan.installer.yaml @@ -4,13 +4,13 @@ PackageIdentifier: tonythethompson.numan PackageVersion: 0.1.4 InstallerType: zip NestedInstallerType: portable -NestedInstallerFiles: - - RelativeFilePath: numan-0.1.4-x86_64-pc-windows-msvc\numan.exe - PortableCommandAlias: numan Commands: - numan Installers: - Architecture: x64 + NestedInstallerFiles: + - RelativeFilePath: numan-0.1.4-x86_64-pc-windows-msvc\numan.exe + PortableCommandAlias: numan InstallerUrl: https://github.com/tonythethompson/numan/releases/download/v0.1.4/numan-0.1.4-x86_64-pc-windows-msvc.zip InstallerSha256: F843A717BD7FD13F2D86BF727D2292DCF9831F2544A3DF88987104299229EF9D ManifestType: installer diff --git a/packaging/winget/manifests/t/tonythethompson/numan/0.1.3/tonythethompson.numan.installer.yaml b/packaging/winget/manifests/t/tonythethompson/numan/0.1.3/tonythethompson.numan.installer.yaml index 79245abd..d52e9d4c 100644 --- a/packaging/winget/manifests/t/tonythethompson/numan/0.1.3/tonythethompson.numan.installer.yaml +++ b/packaging/winget/manifests/t/tonythethompson/numan/0.1.3/tonythethompson.numan.installer.yaml @@ -4,13 +4,13 @@ PackageIdentifier: tonythethompson.numan PackageVersion: 0.1.3 InstallerType: zip NestedInstallerType: portable -NestedInstallerFiles: - - RelativeFilePath: numan-0.1.3-x86_64-pc-windows-msvc\numan.exe - PortableCommandAlias: numan Commands: - numan Installers: - Architecture: x64 + NestedInstallerFiles: + - RelativeFilePath: numan-0.1.3-x86_64-pc-windows-msvc\numan.exe + PortableCommandAlias: numan InstallerUrl: https://github.com/tonythethompson/numan/releases/download/v0.1.3/numan-0.1.3-x86_64-pc-windows-msvc.zip InstallerSha256: 90F659E665836F26E2FD45AD86ED8F73C1B6BCB24BE87CF3760EC000E094ABA1 ManifestType: installer diff --git a/packaging/winget/manifests/t/tonythethompson/numan/0.1.4/tonythethompson.numan.installer.yaml b/packaging/winget/manifests/t/tonythethompson/numan/0.1.4/tonythethompson.numan.installer.yaml index bf77870b..274eed9d 100644 --- a/packaging/winget/manifests/t/tonythethompson/numan/0.1.4/tonythethompson.numan.installer.yaml +++ b/packaging/winget/manifests/t/tonythethompson/numan/0.1.4/tonythethompson.numan.installer.yaml @@ -4,13 +4,13 @@ PackageIdentifier: tonythethompson.numan PackageVersion: 0.1.4 InstallerType: zip NestedInstallerType: portable -NestedInstallerFiles: - - RelativeFilePath: numan-0.1.4-x86_64-pc-windows-msvc\numan.exe - PortableCommandAlias: numan Commands: - numan Installers: - Architecture: x64 + NestedInstallerFiles: + - RelativeFilePath: numan-0.1.4-x86_64-pc-windows-msvc\numan.exe + PortableCommandAlias: numan InstallerUrl: https://github.com/tonythethompson/numan/releases/download/v0.1.4/numan-0.1.4-x86_64-pc-windows-msvc.zip InstallerSha256: F843A717BD7FD13F2D86BF727D2292DCF9831F2544A3DF88987104299229EF9D ManifestType: installer diff --git a/scripts/render_homebrew_formula.py b/scripts/render_homebrew_formula.py index 47644bf1..13d177fd 100644 --- a/scripts/render_homebrew_formula.py +++ b/scripts/render_homebrew_formula.py @@ -2,8 +2,9 @@ """Render packaging/homebrew/numan.rb from a release version + SHA256SUMS. Usage: - python scripts/render_homebrew_formula.py --version 0.1.5 --sha256sums SHA256SUMS - python scripts/render_homebrew_formula.py --version 0.1.5 --sha256sums SHA256SUMS --write + python scripts/render_homebrew_formula.py --version 0.2.2 --sha256sums SHA256SUMS + python scripts/render_homebrew_formula.py --version 0.2.2 --sha256sums SHA256SUMS --write + python scripts/render_homebrew_formula.py --version 0.1.5 --sha256sums SHA256SUMS --legacy-pre-linux-arm python scripts/render_homebrew_formula.py --check-url-layout # verify known asset names """ @@ -17,22 +18,34 @@ REPO_ROOT = Path(__file__).resolve().parents[1] DEFAULT_OUT = REPO_ROOT / "packaging" / "homebrew" / "numan.rb" +# Pre-Linux-ARM Homebrew contract (macOS ARM + Linux x86_64 only). +LEGACY_REQUIRED_ASSETS = { + "aarch64-apple-darwin": "macos_arm", + "x86_64-unknown-linux-gnu": "linux_intel", +} + +# Current Homebrew contract adds Linux aarch64. +LINUX_ARM_ASSET = "aarch64-unknown-linux-gnu" + # Release asset basename suffix -> Homebrew bottle platform key used in formula. # Intel Mac (x86_64-apple-darwin) is intentionally unsupported for shipping. REQUIRED_ASSETS = { - "aarch64-apple-darwin": "macos_arm", - "x86_64-unknown-linux-gnu": "linux_intel", + **LEGACY_REQUIRED_ASSETS, + LINUX_ARM_ASSET: "linux_arm", } ASSET_RE = re.compile( r"^([0-9a-fA-F]{64})\s+numan-(?P.+)-(?P" - r"aarch64-apple-darwin|x86_64-unknown-linux-gnu" + r"aarch64-apple-darwin|x86_64-unknown-linux-gnu|aarch64-unknown-linux-gnu" r")\.(?Ptar\.gz|zip)$" ) -def parse_sha256sums(text: str, version: str) -> dict[str, str]: +def parse_sha256sums( + text: str, version: str, *, legacy_pre_linux_arm: bool = False +) -> dict[str, str]: """Map triple -> lowercase sha256 for this version's archives.""" + required = LEGACY_REQUIRED_ASSETS if legacy_pre_linux_arm else REQUIRED_ASSETS found: dict[str, str] = {} for line in text.splitlines(): line = line.strip() @@ -46,17 +59,25 @@ def parse_sha256sums(text: str, version: str) -> dict[str, str]: if match.group("ext") != "tar.gz": continue found[match.group("triple")] = match.group(1).lower() - missing = [t for t in REQUIRED_ASSETS if t not in found] + missing = [t for t in required if t not in found] if missing: raise SystemExit( f"SHA256SUMS missing required .tar.gz assets for v{version}: {', '.join(missing)}" ) - return found + return {t: found[t] for t in required} def render_formula(version: str, digests: dict[str, str]) -> str: mac_arm = digests["aarch64-apple-darwin"] linux_intel = digests["x86_64-unknown-linux-gnu"] + linux_arm = digests.get(LINUX_ARM_ASSET) + linux_arm_block = "" + if linux_arm is not None: + linux_arm_block = f""" + on_arm do + url "https://github.com/tonythethompson/numan/releases/download/v#{{version}}/numan-#{{version}}-aarch64-unknown-linux-gnu.tar.gz" + sha256 "{linux_arm}" + end""" return f"""# typed: false # frozen_string_literal: true @@ -90,7 +111,7 @@ class Numan < Formula on_intel do url "https://github.com/tonythethompson/numan/releases/download/v#{{version}}/numan-#{{version}}-x86_64-unknown-linux-gnu.tar.gz" sha256 "{linux_intel}" - end + end{linux_arm_block} end def install @@ -125,6 +146,14 @@ def main(argv: list[str] | None = None) -> int: default=DEFAULT_OUT, help="Output formula path", ) + parser.add_argument( + "--legacy-pre-linux-arm", + action="store_true", + help=( + "Accept the pre-Linux-ARM Homebrew asset set " + "(macOS ARM + Linux x86_64 only; omit on_linux/on_arm)" + ), + ) parser.add_argument( "--check-url-layout", action="store_true", @@ -136,12 +165,19 @@ def main(argv: list[str] | None = None) -> int: print("Required release assets for Homebrew:") for triple in REQUIRED_ASSETS: print(f" numan--{triple}.tar.gz") + print("Legacy (--legacy-pre-linux-arm) requires:") + for triple in LEGACY_REQUIRED_ASSETS: + print(f" numan--{triple}.tar.gz") return 0 if not args.version or not args.sha256sums: parser.error("--version and --sha256sums are required unless --check-url-layout") - digests = parse_sha256sums(args.sha256sums.read_text(encoding="utf-8"), args.version) + digests = parse_sha256sums( + args.sha256sums.read_text(encoding="utf-8"), + args.version, + legacy_pre_linux_arm=args.legacy_pre_linux_arm, + ) text = render_formula(args.version, digests) if args.write: args.out.parent.mkdir(parents=True, exist_ok=True) diff --git a/scripts/test_render_homebrew_formula.py b/scripts/test_render_homebrew_formula.py index 5335ec18..1ba7c6ab 100644 --- a/scripts/test_render_homebrew_formula.py +++ b/scripts/test_render_homebrew_formula.py @@ -31,18 +31,27 @@ def test_parse_and_render(self): 4d8fa065b5bc7fcce30af3ca7d5c3cd943701bee168d78fae6120a12689738b8 numan-0.1.5-aarch64-apple-darwin.tar.gz 2d855b3b8a9bb3c568051024b8aae9771a63c427cb3edfd3ac3aa3aa6d78468d numan-0.1.5-x86_64-pc-windows-msvc.zip 0b113361c189a2062ef6e1fca36795d2347b925c1862b42c4ddeb54773e00ae3 numan-0.1.5-x86_64-unknown-linux-gnu.tar.gz +bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb numan-0.1.5-aarch64-unknown-linux-gnu.tar.gz """.strip() digests = self.mod.parse_sha256sums(sums, "0.1.5") self.assertEqual( digests["aarch64-apple-darwin"], "4d8fa065b5bc7fcce30af3ca7d5c3cd943701bee168d78fae6120a12689738b8", ) + self.assertEqual( + digests["aarch64-unknown-linux-gnu"], + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + ) text = self.mod.render_formula("0.1.5", digests) self.assertIn('version "0.1.5"', text) self.assertIn("numan-#{version}-aarch64-apple-darwin.tar.gz", text) + self.assertIn("numan-#{version}-aarch64-unknown-linux-gnu.tar.gz", text) self.assertIn( "4d8fa065b5bc7fcce30af3ca7d5c3cd943701bee168d78fae6120a12689738b8", text ) + self.assertIn( + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", text + ) self.assertIn('bin.install "numan"', text) self.assertNotIn("arch_dir", text) self.assertNotIn("expected numan-* directory", text) @@ -51,26 +60,53 @@ def test_parse_and_render(self): self.assertNotIn("x86_64-apple-darwin.tar.gz", text) self.assertNotIn("windows-msvc", text) + def test_legacy_pre_linux_arm_omits_linux_arm_bottle(self): + sums = """ +4d8fa065b5bc7fcce30af3ca7d5c3cd943701bee168d78fae6120a12689738b8 numan-0.1.5-aarch64-apple-darwin.tar.gz +0b113361c189a2062ef6e1fca36795d2347b925c1862b42c4ddeb54773e00ae3 numan-0.1.5-x86_64-unknown-linux-gnu.tar.gz +""".strip() + digests = self.mod.parse_sha256sums( + sums, "0.1.5", legacy_pre_linux_arm=True + ) + self.assertNotIn("aarch64-unknown-linux-gnu", digests) + text = self.mod.render_formula("0.1.5", digests) + self.assertIn("x86_64-unknown-linux-gnu.tar.gz", text) + self.assertNotIn("aarch64-unknown-linux-gnu.tar.gz", text) + def test_parse_prerelease_version(self): sums = """ 4D8FA065B5BC7FCCE30AF3CA7D5C3CD943701BEE168D78FAE6120A12689738B8 numan-0.2.0-beta.1-aarch64-apple-darwin.tar.gz 2d855b3b8a9bb3c568051024b8aae9771a63c427cb3edfd3ac3aa3aa6d78468d numan-0.2.0-beta.1-x86_64-unknown-linux-gnu.tar.gz +bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb numan-0.2.0-beta.1-aarch64-unknown-linux-gnu.tar.gz """.strip() digests = self.mod.parse_sha256sums(sums, "0.2.0-beta.1") self.assertEqual( digests["aarch64-apple-darwin"], "4d8fa065b5bc7fcce30af3ca7d5c3cd943701bee168d78fae6120a12689738b8", ) + self.assertEqual( + digests["aarch64-unknown-linux-gnu"], + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + ) def test_missing_asset_fails(self): sums = "abcd numan-0.1.5-aarch64-apple-darwin.tar.gz\n" with self.assertRaises(SystemExit): self.mod.parse_sha256sums(sums, "0.1.5") + def test_missing_linux_arm_fails_without_legacy(self): + sums = """ +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa numan-0.2.0-aarch64-apple-darwin.tar.gz +cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc numan-0.2.0-x86_64-unknown-linux-gnu.tar.gz +""".strip() + with self.assertRaises(SystemExit): + self.mod.parse_sha256sums(sums, "0.2.0") + def test_write_roundtrip(self): sums = """ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa numan-0.2.0-aarch64-apple-darwin.tar.gz cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc numan-0.2.0-x86_64-unknown-linux-gnu.tar.gz +bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb numan-0.2.0-aarch64-unknown-linux-gnu.tar.gz """.strip() with tempfile.TemporaryDirectory() as tmp: sums_path = Path(tmp) / "SHA256SUMS" @@ -83,6 +119,7 @@ def test_write_roundtrip(self): body = out.read_text(encoding="utf-8") self.assertEqual(body, text) self.assertIn('version "0.2.0"', body) + self.assertIn("aarch64-unknown-linux-gnu.tar.gz", body) def test_cli_check_url_layout(self): buf = io.StringIO() @@ -92,12 +129,15 @@ def test_cli_check_url_layout(self): stdout = buf.getvalue() self.assertIn("numan--aarch64-apple-darwin.tar.gz", stdout) self.assertIn("numan--x86_64-unknown-linux-gnu.tar.gz", stdout) + self.assertIn("numan--aarch64-unknown-linux-gnu.tar.gz", stdout) + self.assertIn("--legacy-pre-linux-arm", stdout) self.assertNotIn("x86_64-apple-darwin", stdout) def test_cli_full_write(self): sums = """ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa numan-0.2.0-aarch64-apple-darwin.tar.gz cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc numan-0.2.0-x86_64-unknown-linux-gnu.tar.gz +bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb numan-0.2.0-aarch64-unknown-linux-gnu.tar.gz """.strip() with tempfile.TemporaryDirectory() as tmp: tmp_path = Path(tmp) @@ -121,8 +161,35 @@ def test_cli_full_write(self): self.assertTrue(out_path.is_file()) text = out_path.read_text(encoding="utf-8") self.assertIn('version "0.2.0"', text) + self.assertIn("aarch64-unknown-linux-gnu.tar.gz", text) self.assertIn("Wrote ", buf.getvalue()) + def test_cli_legacy_write(self): + sums = """ +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa numan-0.1.5-aarch64-apple-darwin.tar.gz +cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc numan-0.1.5-x86_64-unknown-linux-gnu.tar.gz +""".strip() + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + sums_path = tmp_path / "SHA256SUMS" + out_path = tmp_path / "numan.rb" + sums_path.write_text(sums + "\n", encoding="utf-8") + code = self.mod.main( + [ + "--version", + "0.1.5", + "--sha256sums", + str(sums_path), + "--legacy-pre-linux-arm", + "--write", + "--out", + str(out_path), + ] + ) + self.assertEqual(0, code) + text = out_path.read_text(encoding="utf-8") + self.assertNotIn("aarch64-unknown-linux-gnu", text) + def test_cli_missing_required_args_fails(self): err = io.StringIO() with redirect_stderr(err), self.assertRaises(SystemExit) as cm: diff --git a/src/cmd/self_update.rs b/src/cmd/self_update.rs index 465e5e99..4540a1c6 100644 --- a/src/cmd/self_update.rs +++ b/src/cmd/self_update.rs @@ -92,13 +92,16 @@ pub fn release_asset_name(version: &str, platform: &Platform) -> Result let version = version.trim().trim_start_matches('v'); let (triple, ext) = match (platform.os, platform.arch, platform.env) { (Os::Linux, Arch::X86_64, Env::Gnu) => ("x86_64-unknown-linux-gnu", "tar.gz"), + (Os::Linux, Arch::Aarch64, Env::Gnu) => ("aarch64-unknown-linux-gnu", "tar.gz"), (Os::Windows, Arch::X86_64, Env::Msvc) => ("x86_64-pc-windows-msvc", "zip"), + (Os::Windows, Arch::Aarch64, Env::Msvc) => ("aarch64-pc-windows-msvc", "zip"), (Os::Macos, Arch::Aarch64, Env::Darwin) => ("aarch64-apple-darwin", "tar.gz"), _ => bail!( "No GitHub Release asset is published for platform triple '{}'. \ Install or upgrade via Homebrew, winget, or cargo instead. \ Supported self-update targets: x86_64-unknown-linux-gnu, \ - x86_64-pc-windows-msvc, aarch64-apple-darwin.", + aarch64-unknown-linux-gnu, x86_64-pc-windows-msvc, \ + aarch64-pc-windows-msvc, aarch64-apple-darwin.", platform.triple ), }; @@ -647,6 +650,20 @@ mod tests { ); } + #[test] + fn release_asset_name_linux_aarch64() { + let p = Platform { + os: Os::Linux, + arch: Arch::Aarch64, + env: Env::Gnu, + triple: "aarch64-unknown-linux-gnu".into(), + }; + assert_eq!( + release_asset_name("0.2.0", &p).unwrap(), + "numan-0.2.0-aarch64-unknown-linux-gnu.tar.gz" + ); + } + #[test] fn release_asset_name_windows() { let p = Platform { @@ -661,6 +678,20 @@ mod tests { ); } + #[test] + fn release_asset_name_windows_aarch64() { + let p = Platform { + os: Os::Windows, + arch: Arch::Aarch64, + env: Env::Msvc, + triple: "aarch64-pc-windows-msvc".into(), + }; + assert_eq!( + release_asset_name("0.2.0", &p).unwrap(), + "numan-0.2.0-aarch64-pc-windows-msvc.zip" + ); + } + #[test] fn release_asset_name_macos_arm() { let p = Platform {