1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
|
#!/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())
|