aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-05-31 23:17:39 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-05-31 23:17:39 +0300
commit8a667f99d5a025106690c10858b5b71af15a47fc (patch)
treef07e1d233b247a7e4b40e79b1a761f7018a585fa
parent018cbfb84ad9516be3121d2264c61d9a4a3aebc1 (diff)
fix: use cargo lock for nix packaging
-rw-r--r--AGENTS.md7
-rw-r--r--justfile23
-rw-r--r--nix/package.nix5
-rw-r--r--scripts/nix_cargo_hash.py172
-rw-r--r--scripts/test_nix_cargo_hash.py92
5 files changed, 17 insertions, 282 deletions
diff --git a/AGENTS.md b/AGENTS.md
index 4d27c244..c70366b3 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -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:
diff --git a/justfile b/justfile
index 1f5a475a..abd976f6 100644
--- a/justfile
+++ b/justfile
@@ -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()