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
9 from subprocess
import check_output
, run
10 from typing
import Dict
, Final
, List
, Optional
, Union
13 from aiohttp
import ClientSession
14 from packaging
.version
import Version
16 ROOT
: Final
= check_output([
23 def run_sync(cmd
: List
[str]) -> None:
24 print(f
"$ {' '.join(cmd)}")
27 if process
.returncode
!= 0:
31 async def check_async(cmd
: List
[str]) -> str:
32 print(f
"$ {' '.join(cmd)}")
33 process
= await asyncio
.create_subprocess_exec(
35 stdout
=asyncio
.subprocess
.PIPE
,
36 stderr
=asyncio
.subprocess
.PIPE
38 stdout
, stderr
= await process
.communicate()
40 if process
.returncode
!= 0:
41 error
= stderr
.decode()
42 raise RuntimeError(f
"{cmd[0]} failed: {error}")
44 return stdout
.decode().strip()
47 async def run_async(cmd
: List
[str]):
48 print(f
"$ {' '.join(cmd)}")
50 process
= await asyncio
.create_subprocess_exec(
52 stdout
=asyncio
.subprocess
.PIPE
,
53 stderr
=asyncio
.subprocess
.PIPE
,
55 stdout
, stderr
= await process
.communicate()
57 print(stdout
.decode())
59 if process
.returncode
!= 0:
60 error
= stderr
.decode()
61 raise RuntimeError(f
"{cmd[0]} failed: {error}")
65 def __init__(self
, path
: str):
66 self
.path
= os
.path
.join(ROOT
, path
)
69 with
open(self
.path
, "r") as handle
:
70 self
.text
= handle
.read()
73 def get_exact_match(self
, attr
: str, value
: str):
75 rf
'{re.escape(attr)}\s+=\s+\"?{re.escape(value)}\"?',
81 raise ValueError(f
"multiple occurrences found for {attr}={value}")
85 raise ValueError(f
"no occurrence found for {attr}={value}")
87 def substitute(self
, attr
: str, old_value
: str, new_value
: str) -> None:
88 old_line
= self
.get_exact_match(attr
, old_value
)
89 new_line
= old_line
.replace(old_value
, new_value
)
90 self
.text
= self
.text
.replace(old_line
, new_line
)
91 print(f
"Substitute `{attr}` value `{old_value}` with `{new_value}`")
93 def __exit__(self
, exc_type
, exc_val
, exc_tb
):
94 with
open(self
.path
, "w") as handle
:
95 handle
.write(self
.text
)
99 async def prefetch(cls
, url
: str, version
: str, *extra_args
: str) -> str:
106 cmd
.extend(extra_args
)
107 return await check_async(cmd
)
114 "--extra-experimental-features", "nix-command"
118 async def _run(cls
, args
: List
[str]) -> Optional
[str]:
119 return await check_async(cls
.base_cmd
+ args
)
122 async def eval(cls
, expr
: str) -> Union
[List
, Dict
, int, float, str, bool]:
123 response
= await cls
._run
([
125 "-f", f
"{ROOT}/default.nix",
130 raise RuntimeError("Nix eval expression returned no response")
132 return json
.loads(response
)
133 except (TypeError, ValueError):
134 raise RuntimeError("Nix eval response could not be parsed from JSON")
137 async def hash_to_sri(cls
, algorithm
: str, value
: str) -> Optional
[str]:
138 return await cls
._run
([
147 def __init__(self
, session
: ClientSession
):
148 self
._session
= session
150 async def get_latest_core_version(
152 owner
: str = "home-assistant",
155 async with self
._session
.get(
156 f
"https://api.github.com/repos/{owner}/{repo}/releases/latest"
158 document
= await response
.json()
160 return str(document
.get("name"))
162 raise RuntimeError("No tag name in response document")
165 async def get_latest_frontend_version(
169 async with self
._session
.get(
170 f
"https://raw.githubusercontent.com/home-assistant/core/{core_version}/homeassistant/components/frontend/manifest.json"
172 document
= await response
.json(content_type
="text/plain")
176 for requirement
in document
.get("requirements", [])
177 if requirement
.startswith("home-assistant-frontend==")
180 if len(requirements
) > 1:
182 "Found more than one version specifier for the frontend package"
184 elif len(requirements
) == 1:
185 requirement
= requirements
.pop()
186 _
, version
= requirement
.split("==", maxsplit
=1)
190 "Found no version specifier for frontend package"
194 async def update_core(self
, old_version
: str, new_version
: str) -> None:
195 old_sdist_hash
= str(await Nix
.eval("home-assistant.sdist.outputHash"))
196 new_sdist_hash
= await Nurl
.prefetch("https://pypi.org/project/homeassistant/", new_version
)
197 print(f
"sdist: {old_sdist_hash} -> {new_sdist_hash}")
199 old_git_hash
= str(await Nix
.eval("home-assistant.src.outputHash"))
200 new_git_hash
= await Nurl
.prefetch("https://github.com/home-assistant/core/", new_version
)
201 print(f
"git: {old_git_hash} -> {new_git_hash}")
203 with
File("pkgs/servers/home-assistant/default.nix") as file:
204 file.substitute("hassVersion", old_version
, new_version
)
205 file.substitute("hash", old_sdist_hash
, new_sdist_hash
)
206 file.substitute("hash", old_git_hash
, new_git_hash
)
208 async def update_frontend(self
, old_version
: str, new_version
: str) -> None:
209 old_hash
= str(await Nix
.eval("home-assistant.frontend.src.outputHash"))
210 new_hash
= await Nurl
.prefetch(
211 "https://pypi.org/project/home_assistant_frontend/",
213 "-A", "format", "wheel",
215 "-A", "python", "py3"
217 print(f
"frontend: {old_hash} -> {new_hash}")
219 with
File("pkgs/servers/home-assistant/frontend.nix") as file:
220 file.substitute("version", old_version
, new_version
)
221 file.substitute("hash", old_hash
, new_hash
)
223 async def update_components(self
):
225 f
"{ROOT}/pkgs/servers/home-assistant/update-component-packages.py"
231 if token
:= os
.environ
.get("GITHUB_TOKEN", None):
232 headers
.update({"GITHUB_TOKEN": token
})
234 async with aiohttp
.ClientSession(headers
=headers
) as client
:
235 hass
= HomeAssistant(client
)
237 core_current
= str(await Nix
.eval("home-assistant.version"))
238 core_latest
= await hass
.get_latest_core_version()
240 if Version(core_latest
) > Version(core_current
):
241 print(f
"New Home Assistant version {core_latest} is available")
242 await hass
.update_core(str(core_current
), str(core_latest
))
244 frontend_current
= str(await Nix
.eval("home-assistant.frontend.version"))
245 frontend_latest
= await hass
.get_latest_frontend_version(str(core_latest
))
247 if Version(frontend_latest
) > Version(frontend_current
):
248 await hass
.update_frontend(str(frontend_current
), str(frontend_latest
))
250 await hass
.update_components()
253 print(f
"Home Assistant {core_current} is still the latest version.")
255 # wait for async client sessions to close
256 # https://docs.aiohttp.org/en/stable/client_advanced.html#graceful-shutdown
257 await asyncio
.sleep(0)
259 if __name__
== "__main__":
260 run_sync(["pyright", __file__
])
261 run_sync(["ruff", "check", "--ignore=E501", __file__
])
262 run_sync(["isort", __file__
])