1import json
2import os
3import pathlib
4import requests
5import shutil
6import subprocess
7import sys
8import tempfile
9
10
11def replace_in_file(file_path, replacements):
12 file_contents = pathlib.Path(file_path).read_text()
13 for old, new in replacements.items():
14 if old == new:
15 continue
16 updated_file_contents = file_contents.replace(old, new)
17 # A dumb way to check that we’ve actually replaced the string.
18 if file_contents == updated_file_contents:
19 print(f"no string to replace: {old} → {new}", file=sys.stderr)
20 sys.exit(1)
21 file_contents = updated_file_contents
22 with tempfile.NamedTemporaryFile(mode="w") as t:
23 t.write(file_contents)
24 t.flush()
25 shutil.copyfile(t.name, file_path)
26
27
28def nix_hash_to_sri(hash):
29 return subprocess.run(
30 [
31 "nix",
32 "--extra-experimental-features", "nix-command",
33 "hash",
34 "to-sri",
35 "--type", "sha256",
36 "--",
37 hash,
38 ],
39 stdout=subprocess.PIPE,
40 text=True,
41 check=True,
42 ).stdout.rstrip()
43
44
45nixpkgs_path = "."
46attr_path = os.getenv("UPDATE_NIX_ATTR_PATH", "prowlarr")
47
48package_attrs = json.loads(subprocess.run(
49 [
50 "nix",
51 "--extra-experimental-features", "nix-command",
52 "eval",
53 "--json",
54 "--file", nixpkgs_path,
55 "--apply", """p: {
56 dir = builtins.dirOf p.meta.position;
57 version = p.version;
58 sourceHash = p.src.src.outputHash;
59 yarnHash = p.yarnOfflineCache.outputHash;
60 }""",
61 "--",
62 attr_path,
63 ],
64 stdout=subprocess.PIPE,
65 text=True,
66 check=True,
67).stdout)
68
69old_version = package_attrs["version"]
70new_version = old_version
71
72# Note that we use Prowlarr API instead of GitHub to fetch latest stable release.
73# This corresponds to the Updates tab in the web UI. See also
74# https://github.com/Prowlarr/Prowlarr/blob/7d813ef97a01af0f36a2beaec32e9cd854fc67f3/src/NzbDrone.Core/Update/UpdatePackageProvider.cs
75# https://github.com/Prowlarr/Prowlarr/blob/7d813ef97a01af0f36a2beaec32e9cd854fc67f3/src/NzbDrone.Common/Cloud/ProwlarrCloudRequestBuilder.cs
76version_update = requests.get(
77 f"https://prowlarr.servarr.com/v1/update/master?version={old_version}&includeMajorVersion=true",
78).json()
79if version_update["available"]:
80 new_version = version_update["updatePackage"]["version"]
81
82if new_version == old_version:
83 sys.exit()
84
85source_nix_hash, source_store_path = subprocess.run(
86 [
87 "nix-prefetch-url",
88 "--name", "source",
89 "--unpack",
90 "--print-path",
91 f"https://github.com/Prowlarr/Prowlarr/archive/v{new_version}.tar.gz",
92 ],
93 stdout=subprocess.PIPE,
94 text=True,
95 check=True,
96).stdout.rstrip().split("\n")
97
98old_source_hash = package_attrs["sourceHash"]
99new_source_hash = nix_hash_to_sri(source_nix_hash)
100
101package_dir = package_attrs["dir"]
102package_file_name = "package.nix"
103deps_file_name = "deps.json"
104
105# To update deps.nix, we copy the package to a temporary directory and run
106# passthru.fetch-deps script there.
107with tempfile.TemporaryDirectory() as work_dir:
108 package_file = os.path.join(work_dir, package_file_name)
109 deps_file = os.path.join(work_dir, deps_file_name)
110
111 shutil.copytree(package_dir, work_dir, dirs_exist_ok=True)
112
113 replace_in_file(package_file, {
114 # NB unlike hashes, versions are likely to be used in code or comments.
115 # Try to be more specific to avoid false positive matches.
116 f"version = \"{old_version}\"": f"version = \"{new_version}\"",
117 old_source_hash: new_source_hash,
118 })
119
120 # We need access to the patched and updated src to get the patched
121 # `yarn.lock`.
122 patched_src = os.path.join(work_dir, "patched-src")
123 subprocess.run(
124 [
125 "nix",
126 "--extra-experimental-features", "nix-command",
127 "build",
128 "--impure",
129 "--nix-path", "",
130 "--include", f"nixpkgs={nixpkgs_path}",
131 "--include", f"package={package_file}",
132 "--expr", "(import <nixpkgs> { }).callPackage <package> { }",
133 "--out-link", patched_src,
134 "src",
135 ],
136 check=True,
137 )
138 old_yarn_hash = package_attrs["yarnHash"]
139 new_yarn_hash = nix_hash_to_sri(subprocess.run(
140 [
141 "prefetch-yarn-deps",
142 # does not support "--" separator :(
143 # Also --verbose writes to stdout, yikes.
144 os.path.join(patched_src, "yarn.lock"),
145 ],
146 stdout=subprocess.PIPE,
147 text=True,
148 check=True,
149 ).stdout.rstrip())
150
151 replace_in_file(package_file, {
152 old_yarn_hash: new_yarn_hash,
153 })
154
155 # Generate nuget-to-json dependency lock file.
156 fetch_deps = os.path.join(work_dir, "fetch-deps")
157 subprocess.run(
158 [
159 "nix",
160 "--extra-experimental-features", "nix-command",
161 "build",
162 "--impure",
163 "--nix-path", "",
164 "--include", f"nixpkgs={nixpkgs_path}",
165 "--include", f"package={package_file}",
166 "--expr", "(import <nixpkgs> { }).callPackage <package> { }",
167 "--out-link", fetch_deps,
168 "passthru.fetch-deps",
169 ],
170 check=True,
171 )
172 subprocess.run(
173 [
174 fetch_deps,
175 deps_file,
176 ],
177 stdout=subprocess.DEVNULL,
178 check=True,
179 )
180
181 shutil.copy(deps_file, os.path.join(package_dir, deps_file_name))
182 shutil.copy(package_file, os.path.join(package_dir, package_file_name))