diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-31 23:17:39 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-31 23:17:39 +0300 |
| commit | 8a667f99d5a025106690c10858b5b71af15a47fc (patch) | |
| tree | f07e1d233b247a7e4b40e79b1a761f7018a585fa | |
| parent | 018cbfb84ad9516be3121d2264c61d9a4a3aebc1 (diff) | |
fix: use cargo lock for nix packaging
| -rw-r--r-- | AGENTS.md | 7 | ||||
| -rw-r--r-- | justfile | 23 | ||||
| -rw-r--r-- | nix/package.nix | 5 | ||||
| -rw-r--r-- | scripts/nix_cargo_hash.py | 172 | ||||
| -rw-r--r-- | scripts/test_nix_cargo_hash.py | 92 |
5 files changed, 17 insertions, 282 deletions
@@ -76,13 +76,12 @@ Default release flow: ```bash just check -just release-prepare 0.x.y -just release-publish 0.x.y +just release 0.x.y ``` -`just release-prepare 0.x.y` prepares the changelog entry, bumps `Cargo.toml`, updates `Cargo.lock`, refreshes `nix/package.nix` `cargoHash`, runs tests, and commits the release. `just release-publish 0.x.y` verifies the prepared clean tree, rechecks the committed `cargoHash` without patching, pushes the release commit to `origin/master` if needed, then tags and pushes the tag. `just release 0.x.y` runs both steps. GitHub Actions builds the binaries after the tag is pushed, creates the GitHub release, uploads all four binary assets, then updates `website/latest.json` on `master` automatically. +`just release 0.x.y` prepares the changelog entry, bumps `Cargo.toml`, updates `Cargo.lock`, runs tests, commits the release, pushes `master`, tags, and pushes the tag. GitHub Actions builds the binaries after the tag is pushed, creates the GitHub release, uploads all four binary assets, then updates `website/latest.json` on `master` automatically. -If the release will change `Cargo.lock` or the package version, refresh `nix/package.nix` after the release version bump and before tagging. `just release-prepare` runs `just refresh-nix-cargo-hash`, which forces `cargoHash = lib.fakeHash`, captures the `got:` hash from Nix, writes it back to `nix/package.nix`, and reruns the Nix flake checks. A stale hash fails both the `Nix` workflow and the release workflow's `flake-check` job with a fixed-output derivation mismatch. +`nix/package.nix` imports `Cargo.lock` directly with `cargoLock.lockFile`, so release version bumps do not require a separate Nix cargo hash update. If Cargo git dependencies are added later, add the required `cargoLock.outputHashes` entries as part of that dependency change. The release workflow must publish these four assets: @@ -20,7 +20,7 @@ ci: lint # Check formatting + run unit tests + maintenance script tests check: ci - python3 -m unittest scripts.test_changelog scripts.test_nix_cargo_hash scripts.test_vendor_libghostty_vt + python3 -m unittest scripts.test_changelog scripts.test_vendor_libghostty_vt @echo "docs reminder: if this changes user-facing behavior, make sure the relevant release docs are updated or called out before release." # Install repo-local git hooks @@ -75,20 +75,17 @@ release-docs-check: fi; \ done -# Refresh nix/package.nix cargoHash after Cargo.toml or Cargo.lock changes -refresh-nix-cargo-hash: - python3 scripts/nix_cargo_hash.py refresh - -# Check nix/package.nix cargoHash without patching -check-nix-cargo-hash: - python3 scripts/nix_cargo_hash.py check - # Prepare the release commit without tagging or pushing (usage: just release-prepare 0.1.1) release-prepare version: + @printf '%s\n' '{{version}}' | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$' || { \ + echo "error: version must look like 0.6.6 without a v prefix"; \ + exit 1; \ + } @if [ -n "$(git status --porcelain)" ]; then \ echo "error: commit your changes first"; \ exit 1; \ fi + @git fetch origin master --tags @if git rev-parse "v{{version}}" >/dev/null 2>&1; then \ echo "error: tag v{{version}} already exists"; \ exit 1; \ @@ -98,14 +95,17 @@ release-prepare version: cp CHANGELOG.md docs/next/CHANGELOG.md sed -i.bak 's/^version = ".*"/version = "{{version}}"/' Cargo.toml && rm -f Cargo.toml.bak cargo update -p herdr --offline - just refresh-nix-cargo-hash just check - git add CHANGELOG.md docs/next/CHANGELOG.md Cargo.toml Cargo.lock nix/package.nix + git add CHANGELOG.md docs/next/CHANGELOG.md Cargo.toml Cargo.lock git diff --cached --quiet || git commit -m "release: v{{version}}" @echo "v{{version}} release commit prepared. Review it, then run: just release-publish {{version}}" # Tag and push an already-prepared release commit (usage: just release-publish 0.1.1) release-publish version: + @printf '%s\n' '{{version}}' | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$' || { \ + echo "error: version must look like 0.6.6 without a v prefix"; \ + exit 1; \ + } @if [ -n "$(git status --porcelain)" ]; then \ echo "error: working tree must be clean before publishing"; \ exit 1; \ @@ -128,7 +128,6 @@ release-publish version: just release-docs-check python3 scripts/changelog.py extract --version {{version}} --output /tmp/herdr-release-notes-check.md rm -f /tmp/herdr-release-notes-check.md - just check-nix-cargo-hash @local_head="$(git rev-parse HEAD)"; \ remote_head="$(git rev-parse origin/master)"; \ if ! git merge-base --is-ancestor "$remote_head" "$local_head"; then \ diff --git a/nix/package.nix b/nix/package.nix index 06887149..87d70c92 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -42,8 +42,9 @@ rustPlatform.buildRustPackage { ); }; - cargoHash = "sha256-YseT5O69ld88SoZYPgMR/qz7djfKCdyuJkOGkHdv97A="; - cargoDepsName = "herdr"; + cargoLock = { + lockFile = ../Cargo.lock; + }; nativeBuildInputs = [ git diff --git a/scripts/nix_cargo_hash.py b/scripts/nix_cargo_hash.py deleted file mode 100644 index 1e9a28f6..00000000 --- a/scripts/nix_cargo_hash.py +++ /dev/null @@ -1,172 +0,0 @@ -#!/usr/bin/env python3 -from __future__ import annotations - -import argparse -import re -import shutil -import subprocess -import sys -from pathlib import Path - - -class NixCargoHashError(ValueError): - pass - - -CARGO_HASH_RE = re.compile( - r"(?m)^(\s*cargoHash\s*=\s*)(\"sha256-[^\"]+\"|lib\.fakeHash)(;\s*)$" -) -GOT_HASH_RE = re.compile(r"\bgot:\s*(sha256-[A-Za-z0-9+/=]+)") - - -def find_cargo_hash_assignment(package_text: str) -> str: - matches = list(CARGO_HASH_RE.finditer(package_text)) - if len(matches) != 1: - raise NixCargoHashError( - f"expected exactly one cargoHash assignment, found {len(matches)}" - ) - return matches[0].group(2) - - -def replace_cargo_hash_assignment(package_text: str, replacement: str) -> str: - matches = list(CARGO_HASH_RE.finditer(package_text)) - if len(matches) != 1: - raise NixCargoHashError( - f"expected exactly one cargoHash assignment, found {len(matches)}" - ) - return CARGO_HASH_RE.sub( - lambda match: f"{match.group(1)}{replacement}{match.group(3)}", - package_text, - ) - - -def parse_got_hash(output: str) -> str: - hashes = GOT_HASH_RE.findall(output) - if len(hashes) != 1: - raise NixCargoHashError( - f"expected exactly one got: sha256-... hash, found {len(hashes)}" - ) - return hashes[0] - - -def run_capture(command: list[str]) -> subprocess.CompletedProcess[str]: - return subprocess.run( - command, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - text=True, - check=False, - ) - - -def run_checked(command: list[str]) -> None: - subprocess.run(command, check=True) - - -def discover_cargo_hash(package_path: Path) -> tuple[str, str]: - if shutil.which("nix") is None: - raise NixCargoHashError("nix is required to refresh nix/package.nix cargoHash") - if not package_path.exists(): - raise NixCargoHashError(f"{package_path} does not exist") - - original_text = package_path.read_text(encoding="utf-8") - original_assignment = find_cargo_hash_assignment(original_text) - discovered_hash: str | None = None - - try: - package_path.write_text( - replace_cargo_hash_assignment(original_text, "lib.fakeHash"), - encoding="utf-8", - ) - - system = run_capture( - ["nix", "eval", "--impure", "--raw", "--expr", "builtins.currentSystem"] - ) - if system.returncode != 0: - raise NixCargoHashError( - system.stdout.strip() or "failed to read Nix currentSystem" - ) - - build_attr = f".#packages.{system.stdout.strip()}.herdr" - print(f"building {build_attr} with lib.fakeHash to discover cargoHash") - result = run_capture(["nix", "build", build_attr, "--no-link", "--print-build-logs"]) - if result.returncode == 0: - raise NixCargoHashError( - "nix build succeeded with lib.fakeHash; could not discover cargoHash" - ) - - discovered_hash = parse_got_hash(result.stdout) - finally: - package_path.write_text(original_text, encoding="utf-8") - - return discovered_hash, original_assignment - - -def refresh_cargo_hash(package_path: Path) -> None: - discovered_hash, original_assignment = discover_cargo_hash(package_path) - - new_assignment = f'"{discovered_hash}"' - if new_assignment == original_assignment: - print(f"nix/package.nix cargoHash is unchanged: {discovered_hash}") - else: - print(f"updating nix/package.nix cargoHash to {discovered_hash}") - package_path.write_text( - replace_cargo_hash_assignment(package_path.read_text(encoding="utf-8"), new_assignment), - encoding="utf-8", - ) - - print("verifying Nix flake checks") - run_checked(["nix", "flake", "check", "--print-build-logs"]) - run_checked(["nix", "flake", "check", "--all-systems", "--no-build", "--print-build-logs"]) - print("nix/package.nix cargoHash is ready") - - -def check_cargo_hash(package_path: Path) -> None: - discovered_hash, current_assignment = discover_cargo_hash(package_path) - expected_assignment = f'"{discovered_hash}"' - if current_assignment != expected_assignment: - raise NixCargoHashError( - "nix/package.nix cargoHash is stale: " - f"expected {expected_assignment}, found {current_assignment}" - ) - print(f"nix/package.nix cargoHash matches discovered hash: {discovered_hash}") - - -def cmd_refresh(args: argparse.Namespace) -> int: - refresh_cargo_hash(Path(args.package)) - return 0 - - -def cmd_check(args: argparse.Namespace) -> int: - check_cargo_hash(Path(args.package)) - return 0 - - -def build_parser() -> argparse.ArgumentParser: - parser = argparse.ArgumentParser(description="Refresh nix/package.nix cargoHash") - subparsers = parser.add_subparsers(dest="command", required=True) - - check = subparsers.add_parser("check", help="Check cargoHash without patching") - check.add_argument("--package", default="nix/package.nix") - check.set_defaults(func=cmd_check) - - refresh = subparsers.add_parser("refresh", help="Refresh cargoHash using Nix") - refresh.add_argument("--package", default="nix/package.nix") - refresh.set_defaults(func=cmd_refresh) - - return parser - - -def main() -> int: - parser = build_parser() - args = parser.parse_args() - - try: - return args.func(args) - except (NixCargoHashError, subprocess.CalledProcessError) as exc: - print(f"error: {exc}", file=sys.stderr) - return 1 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/test_nix_cargo_hash.py b/scripts/test_nix_cargo_hash.py deleted file mode 100644 index c00b1a6c..00000000 --- a/scripts/test_nix_cargo_hash.py +++ /dev/null @@ -1,92 +0,0 @@ -from __future__ import annotations - -import unittest - -from scripts.nix_cargo_hash import ( - NixCargoHashError, - check_cargo_hash, - find_cargo_hash_assignment, - parse_got_hash, - replace_cargo_hash_assignment, -) - - -PACKAGE_TEXT = """ -rustPlatform.buildRustPackage { - pname = "herdr"; - cargoHash = "sha256-oldHash="; -} -""" - - -class NixCargoHashTests(unittest.TestCase): - def test_find_cargo_hash_assignment_reads_single_hash(self) -> None: - self.assertEqual(find_cargo_hash_assignment(PACKAGE_TEXT), '"sha256-oldHash="') - - def test_replace_cargo_hash_assignment_updates_single_hash(self) -> None: - updated = replace_cargo_hash_assignment(PACKAGE_TEXT, '"sha256-newHash="') - - self.assertIn('cargoHash = "sha256-newHash=";', updated) - self.assertNotIn("sha256-oldHash", updated) - - def test_replace_cargo_hash_assignment_accepts_fake_hash(self) -> None: - updated = replace_cargo_hash_assignment(PACKAGE_TEXT, "lib.fakeHash") - - self.assertIn("cargoHash = lib.fakeHash;", updated) - - def test_replace_cargo_hash_assignment_rejects_missing_hash(self) -> None: - with self.assertRaisesRegex(NixCargoHashError, "found 0"): - replace_cargo_hash_assignment('pname = "herdr";\n', "lib.fakeHash") - - def test_replace_cargo_hash_assignment_rejects_multiple_hashes(self) -> None: - text = PACKAGE_TEXT + PACKAGE_TEXT - - with self.assertRaisesRegex(NixCargoHashError, "found 2"): - replace_cargo_hash_assignment(text, "lib.fakeHash") - - def test_parse_got_hash_reads_nix_hash_mismatch(self) -> None: - output = """ - specified: sha256-oldHash= - got: sha256-newHash123+/= - """ - - self.assertEqual(parse_got_hash(output), "sha256-newHash123+/=") - - def test_parse_got_hash_rejects_missing_hash(self) -> None: - with self.assertRaisesRegex(NixCargoHashError, "found 0"): - parse_got_hash("error: no hash here") - - def test_parse_got_hash_rejects_multiple_hashes(self) -> None: - output = "got: sha256-one=\ngot: sha256-two=\n" - - with self.assertRaisesRegex(NixCargoHashError, "found 2"): - parse_got_hash(output) - - def test_check_cargo_hash_restores_file_on_failure(self) -> None: - import contextlib - import io - import tempfile - from pathlib import Path - from unittest import mock - - with tempfile.TemporaryDirectory() as temp_dir: - package = Path(temp_dir) / "package.nix" - package.write_text(PACKAGE_TEXT, encoding="utf-8") - - with mock.patch("scripts.nix_cargo_hash.shutil.which", return_value="/nix/bin/nix"): - with mock.patch("scripts.nix_cargo_hash.run_capture") as run_capture: - run_capture.side_effect = [ - mock.Mock(returncode=0, stdout="x86_64-linux"), - mock.Mock(returncode=1, stdout="error without got hash"), - ] - - with contextlib.redirect_stdout(io.StringIO()): - with self.assertRaisesRegex(NixCargoHashError, "found 0"): - check_cargo_hash(package) - - self.assertIn("--no-link", run_capture.call_args_list[1].args[0]) - self.assertEqual(package.read_text(encoding="utf-8"), PACKAGE_TEXT) - - -if __name__ == "__main__": - unittest.main() |
