1 #!/usr/bin/env nix-shell
2 #!nix-shell -I nixpkgs=channel:nixpkgs-unstable -i python3 -p "python3.withPackages (ps: with ps; [ aiohttp packaging ])" -p git nurl pyright ruff isort
10 from subprocess
import check_output
, run
11 from typing
import Dict
, Final
, List
, Optional
, Union
14 from aiohttp
import ClientSession
15 from packaging
.version
import Version
17 ROOT
: Final
= check_output([
24 def run_sync(cmd
: List
[str]) -> None:
25 print(f
"$ {' '.join(cmd)}")
28 if process
.returncode
!= 0:
32 async def check_async(cmd
: List
[str]) -> str:
33 print(f
"$ {' '.join(cmd)}")
34 process
= await asyncio
.create_subprocess_exec(
36 stdout
=asyncio
.subprocess
.PIPE
,
37 stderr
=asyncio
.subprocess
.PIPE
39 stdout
, stderr
= await process
.communicate()
41 if process
.returncode
!= 0:
42 error
= stderr
.decode()
43 raise RuntimeError(f
"{cmd[0]} failed: {error}")
45 return stdout
.decode().strip()
48 async def run_async(cmd
: List
[str]):
49 print(f
"$ {' '.join(cmd)}")
51 process
= await asyncio
.create_subprocess_exec(
53 stdout
=asyncio
.subprocess
.PIPE
,
54 stderr
=asyncio
.subprocess
.PIPE
,
56 stdout
, stderr
= await process
.communicate()
58 print(stdout
.decode())
60 if process
.returncode
!= 0:
61 error
= stderr
.decode()
62 raise RuntimeError(f
"{cmd[0]} failed: {error}")
66 def __init__(self
, path
: str):
67 self
.path
= os
.path
.join(ROOT
, path
)
70 with
open(self
.path
, "r") as handle
:
71 self
.text
= handle
.read()
74 def get_exact_match(self
, attr
: str, value
: str):
76 rf
'{re.escape(attr)}\s+=\s+\"?{re.escape(value)}\"?',
82 raise ValueError(f
"multiple occurrences found for {attr}={value}")
86 raise ValueError(f
"no occurrence found for {attr}={value}")
88 def substitute(self
, attr
: str, old_value
: str, new_value
: str) -> None:
89 old_line
= self
.get_exact_match(attr
, old_value
)
90 new_line
= old_line
.replace(old_value
, new_value
)
91 self
.text
= self
.text
.replace(old_line
, new_line
)
92 print(f
"Substitute `{attr}` value `{old_value}` with `{new_value}`")
94 def __exit__(self
, exc_type
, exc_val
, exc_tb
):
95 with
open(self
.path
, "w") as handle
:
96 handle
.write(self
.text
)
100 async def prefetch(cls
, url
: str, version
: str, *extra_args
: str) -> str:
107 cmd
.extend(extra_args
)
108 return await check_async(cmd
)
115 "--extra-experimental-features", "nix-command"
119 async def _run(cls
, args
: List
[str]) -> Optional
[str]:
120 return await check_async(cls
.base_cmd
+ args
)
123 async def eval(cls
, expr
: str) -> Union
[List
, Dict
, int, float, str, bool]:
124 response
= await cls
._run
([
126 "-f", f
"{ROOT}/default.nix",
131 raise RuntimeError("Nix eval expression returned no response")
133 return json
.loads(response
)
134 except (TypeError, ValueError):
135 raise RuntimeError("Nix eval response could not be parsed from JSON")
138 async def hash_to_sri(cls
, algorithm
: str, value
: str) -> Optional
[str]:
139 return await cls
._run
([
148 def __init__(self
, session
: ClientSession
):
149 self
._session
= session
151 async def get_latest_core_version(
153 owner
: str = "home-assistant",
156 async with self
._session
.get(
157 f
"https://api.github.com/repos/{owner}/{repo}/releases/latest"
159 document
= await response
.json()
161 return str(document
.get("name"))
163 raise RuntimeError("No tag name in response document")
166 async def get_latest_frontend_version(
170 async with self
._session
.get(
171 f
"https://raw.githubusercontent.com/home-assistant/core/{core_version}/homeassistant/components/frontend/manifest.json"
173 document
= await response
.json(content_type
="text/plain")
177 for requirement
in document
.get("requirements", [])
178 if requirement
.startswith("home-assistant-frontend==")
181 if len(requirements
) > 1:
183 "Found more than one version specifier for the frontend package"
185 elif len(requirements
) == 1:
186 requirement
= requirements
.pop()
187 _
, version
= requirement
.split("==", maxsplit
=1)
191 "Found no version specifier for frontend package"
195 async def update_core(self
, old_version
: str, new_version
: str) -> None:
196 old_sdist_hash
= str(await Nix
.eval("home-assistant.sdist.outputHash"))
197 new_sdist_hash
= await Nurl
.prefetch("https://pypi.org/project/homeassistant/", new_version
)
198 print(f
"sdist: {old_sdist_hash} -> {new_sdist_hash}")
200 old_git_hash
= str(await Nix
.eval("home-assistant.src.outputHash"))
201 new_git_hash
= await Nurl
.prefetch("https://github.com/home-assistant/core/", new_version
)
202 print(f
"git: {old_git_hash} -> {new_git_hash}")
204 with
File("pkgs/servers/home-assistant/default.nix") as file:
205 file.substitute("hassVersion", old_version
, new_version
)
206 file.substitute("hash", old_sdist_hash
, new_sdist_hash
)
207 file.substitute("hash", old_git_hash
, new_git_hash
)
209 async def update_frontend(self
, old_version
: str, new_version
: str) -> None:
210 old_hash
= str(await Nix
.eval("home-assistant.frontend.src.outputHash"))
211 new_hash
= await Nurl
.prefetch(
212 "https://pypi.org/project/home_assistant_frontend/",
214 "-A", "format", "wheel",
216 "-A", "python", "py3"
218 print(f
"frontend: {old_hash} -> {new_hash}")
220 with
File("pkgs/servers/home-assistant/frontend.nix") as file:
221 file.substitute("version", old_version
, new_version
)
222 file.substitute("hash", old_hash
, new_hash
)
224 async def update_components(self
):
226 f
"{ROOT}/pkgs/servers/home-assistant/update-component-packages.py"
230 async def main(target_version
: Optional
[str] = None):
232 if token
:= os
.environ
.get("GITHUB_TOKEN", None):
233 headers
.update({"GITHUB_TOKEN": token
})
235 async with aiohttp
.ClientSession(headers
=headers
) as client
:
236 hass
= HomeAssistant(client
)
238 core_current
= str(await Nix
.eval("home-assistant.version"))
239 core_latest
= target_version
or await hass
.get_latest_core_version()
241 if Version(core_latest
) > Version(core_current
):
242 print(f
"New Home Assistant version {core_latest} is available")
243 await hass
.update_core(str(core_current
), str(core_latest
))
245 frontend_current
= str(await Nix
.eval("home-assistant.frontend.version"))
246 frontend_latest
= await hass
.get_latest_frontend_version(str(core_latest
))
248 if Version(frontend_latest
) > Version(frontend_current
):
249 await hass
.update_frontend(str(frontend_current
), str(frontend_latest
))
251 await hass
.update_components()
254 print(f
"Home Assistant {core_current} is still the latest version.")
256 # wait for async client sessions to close
257 # https://docs.aiohttp.org/en/stable/client_advanced.html#graceful-shutdown
258 await asyncio
.sleep(0)
260 if __name__
== "__main__":
261 parser
= argparse
.ArgumentParser()
262 parser
.add_argument("version", nargs
="?")
263 args
= parser
.parse_args()
265 run_sync(["pyright", __file__
])
266 run_sync(["ruff", "check", "--ignore=E501", __file__
])
267 run_sync(["isort", __file__
])
269 asyncio
.run(main(args
.version
))