diff --git a/pkgs/development/python-modules/roombapy/default.nix b/pkgs/development/python-modules/roombapy/default.nix index c1ee853a105c..7cbe1942e50a 100644 --- a/pkgs/development/python-modules/roombapy/default.nix +++ b/pkgs/development/python-modules/roombapy/default.nix @@ -11,7 +11,7 @@ buildPythonPackage rec { pname = "roombapy"; - version = "1.6.5"; + version = "1.6.6"; format = "pyproject"; disabled = pythonOlder "3.7"; @@ -20,7 +20,7 @@ buildPythonPackage rec { owner = "pschmitt"; repo = "roombapy"; rev = version; - sha256 = "sha256-Xjeh29U+FCzI5n/i5s6wC0B88Ktmb8pnNDdOzCiKWi4="; + hash = "sha256-dfeMd/THlj2HQYcLPmeC3AWP3vR/6+8BFU1QtSu5xg4="; }; nativeBuildInputs = [ diff --git a/pkgs/servers/home-assistant/component-packages.nix b/pkgs/servers/home-assistant/component-packages.nix index c348ca08bf9d..26c2a1f9c8d3 100644 --- a/pkgs/servers/home-assistant/component-packages.nix +++ b/pkgs/servers/home-assistant/component-packages.nix @@ -2,7 +2,7 @@ # Do not edit! { - version = "2023.3.2"; + version = "2023.3.3"; components = { "3_day_blinds" = ps: with ps; [ ]; diff --git a/pkgs/servers/home-assistant/default.nix b/pkgs/servers/home-assistant/default.nix index 1f124d153be4..51f6b41d0329 100644 --- a/pkgs/servers/home-assistant/default.nix +++ b/pkgs/servers/home-assistant/default.nix @@ -263,7 +263,7 @@ let extraBuildInputs = extraPackages python.pkgs; # Don't forget to run parse-requirements.py after updating - hassVersion = "2023.3.2"; + hassVersion = "2023.3.3"; in python.pkgs.buildPythonApplication rec { pname = "homeassistant"; @@ -279,7 +279,7 @@ in python.pkgs.buildPythonApplication rec { # Primary source is the pypi sdist, because it contains translations src = fetchPypi { inherit pname version; - hash = "sha256-I6NSVoMS3xbUqh/7BxJj/Evkk7+g3N0dZVJjEbr2pCs="; + hash = "sha256-AJJ0w66a8D3kiLHhnoFmnGRWyDJ4OCebwwKTGdprGa0="; }; # Secondary source is git for tests @@ -287,7 +287,7 @@ in python.pkgs.buildPythonApplication rec { owner = "home-assistant"; repo = "core"; rev = "refs/tags/${version}"; - hash = "sha256-Qd++/73c9VDNe4AMdiDIVJXxh4qFx2x4HDkY1An2VjE="; + hash = "sha256-KTmMA8P0MhYAiwp073Q3s60budFKHrsBnAJSqYC7zis="; }; nativeBuildInputs = with python3.pkgs; [ @@ -442,6 +442,7 @@ in python.pkgs.buildPythonApplication rec { python supportedComponentsWithTests; pythonPath = python3.pkgs.makePythonPath (componentBuildInputs ++ extraBuildInputs); + frontend = python.pkgs.home-assistant-frontend; intents = python.pkgs.home-assistant-intents; tests = { nixos = nixosTests.home-assistant; diff --git a/pkgs/servers/home-assistant/frontend.nix b/pkgs/servers/home-assistant/frontend.nix index 26ef8b7168f2..76462fc851bf 100644 --- a/pkgs/servers/home-assistant/frontend.nix +++ b/pkgs/servers/home-assistant/frontend.nix @@ -4,7 +4,7 @@ buildPythonPackage rec { # the frontend version corresponding to a specific home-assistant version can be found here # https://github.com/home-assistant/home-assistant/blob/master/homeassistant/components/frontend/manifest.json pname = "home-assistant-frontend"; - version = "20230306.0"; + version = "20230309.0"; format = "wheel"; src = fetchPypi { @@ -12,7 +12,7 @@ buildPythonPackage rec { pname = "home_assistant_frontend"; dist = "py3"; python = "py3"; - hash = "sha256-E/e1XyhwFiNMLz7+o99eG9sW2ZCCfPFnkBcu3BpCbxQ="; + hash = "sha256-gHc93xKIm0LDQrkTtlMdLv/N2smfYz5lQ6uLV+Cqj+s="; }; # there is nothing to strip in this package diff --git a/pkgs/servers/home-assistant/parse-requirements.py b/pkgs/servers/home-assistant/parse-requirements.py index 162bb4af04b9..7f0cb371769f 100755 --- a/pkgs/servers/home-assistant/parse-requirements.py +++ b/pkgs/servers/home-assistant/parse-requirements.py @@ -1,5 +1,5 @@ #! /usr/bin/env nix-shell -#! nix-shell -i python3 -p "python3.withPackages (ps: with ps; [ mypy attrs packaging rich ]) +#! nix-shell -i python3 -p "python3.withPackages (ps: with ps; [ attrs packaging rich ])" -p nodePackages.pyright ruff isort" # # This script downloads Home Assistant's source tarball. # Inside the homeassistant/components directory, each integration has an associated manifest.json, @@ -25,8 +25,9 @@ import tarfile import tempfile from functools import reduce from io import BytesIO -from typing import Dict, Optional, Set, Any +from typing import Any, Dict, List, Optional, Set from urllib.request import urlopen + from packaging import version as Version from rich.console import Console from rich.table import Table @@ -45,17 +46,21 @@ PKG_PREFERENCES = { } -def run_mypy() -> None: - cmd = ["mypy", "--ignore-missing-imports", __file__] + +def run_sync(cmd: List[str]) -> None: print(f"$ {' '.join(cmd)}") - subprocess.run(cmd, check=True) + process = subprocess.run(cmd) + + if process.returncode != 0: + sys.exit(1) -def get_version(): +def get_version() -> str: with open(os.path.dirname(sys.argv[0]) + "/default.nix") as f: # A version consists of digits, dots, and possibly a "b" (for beta) - m = re.search('hassVersion = "([\\d\\.b]+)";', f.read()) - return m.group(1) + if match := re.search('hassVersion = "([\\d\\.b]+)";', f.read()): + return match.group(1) + raise RuntimeError("hassVersion not in default.nix") def parse_components(version: str = "master"): @@ -74,7 +79,7 @@ def parse_components(version: str = "master"): components_with_tests.append(entry.name) sys.path.append(core_path) - from script.hassfest.model import Integration + from script.hassfest.model import Integration # type: ignore integrations = Integration.load_dir( pathlib.Path( os.path.join(core_path, "homeassistant/components") @@ -270,5 +275,7 @@ def main() -> None: if __name__ == "__main__": - run_mypy() + run_sync(["pyright", __file__]) + run_sync(["ruff", "--ignore=E501", __file__]) + run_sync(["isort", __file__]) main() diff --git a/pkgs/servers/home-assistant/update.py b/pkgs/servers/home-assistant/update.py new file mode 100755 index 000000000000..c914979e28bd --- /dev/null +++ b/pkgs/servers/home-assistant/update.py @@ -0,0 +1,263 @@ +#!/usr/bin/env nix-shell +#!nix-shell -I nixpkgs=channel:nixpkgs-unstable -i python3 -p "python3.withPackages (ps: with ps; [ aiohttp packaging ])" -p git nurl nodePackages.pyright ruff isort + +import asyncio +import json +import os +import re +import sys +from subprocess import check_output, run +from typing import Dict, Final, List, Optional, Union + +import aiohttp +from aiohttp import ClientSession +from packaging.version import Version + +ROOT: Final = check_output([ + "git", + "rev-parse", + "--show-toplevel", +]).decode().strip() + + +def run_sync(cmd: List[str]) -> None: + print(f"$ {' '.join(cmd)}") + process = run(cmd) + + if process.returncode != 0: + sys.exit(1) + + +async def check_async(cmd: List[str]) -> str: + print(f"$ {' '.join(cmd)}") + process = await asyncio.create_subprocess_exec( + *cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE + ) + stdout, stderr = await process.communicate() + + if process.returncode != 0: + error = stderr.decode() + raise RuntimeError(f"{cmd[0]} failed: {error}") + + return stdout.decode().strip() + + +async def run_async(cmd: List[str]): + print(f"$ {' '.join(cmd)}") + + process = await asyncio.create_subprocess_exec( + *cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + stdout, stderr = await process.communicate() + + print(stdout.decode()) + + if process.returncode != 0: + error = stderr.decode() + raise RuntimeError(f"{cmd[0]} failed: {error}") + + +class File: + def __init__(self, path: str): + self.path = os.path.join(ROOT, path) + + def __enter__(self): + with open(self.path, "r") as handle: + self.text = handle.read() + return self + + def get_exact_match(self, attr: str, value: str): + matches = re.findall( + rf'{re.escape(attr)}\s+=\s+\"?{re.escape(value)}\"?', + self.text + ) + + n = len(matches) + if n > 1: + raise ValueError(f"multiple occurrences found for {attr}={value}") + elif n == 1: + return matches.pop() + else: + raise ValueError(f"no occurrence found for {attr}={value}") + + def substitute(self, attr: str, old_value: str, new_value: str) -> None: + old_line = self.get_exact_match(attr, old_value) + new_line = old_line.replace(old_value, new_value) + self.text = self.text.replace(old_line, new_line) + print(f"Substitute `{attr}` value `{old_value}` with `{new_value}`") + + def __exit__(self, exc_type, exc_val, exc_tb): + with open(self.path, "w") as handle: + handle.write(self.text) + +class Nurl: + @classmethod + async def prefetch(cls, url: str, version: str, *extra_args: str) -> str: + cmd = [ + "nurl", + "--hash", + url, + version, + ] + cmd.extend(extra_args) + return await check_async(cmd) + + +class Nix: + base_cmd: Final = [ + "nix", + "--show-trace", + "--extra-experimental-features", "nix-command" + ] + + @classmethod + async def _run(cls, args: List[str]) -> Optional[str]: + return await check_async(cls.base_cmd + args) + + @classmethod + async def eval(cls, expr: str) -> Union[List, Dict, int, float, str, bool]: + response = await cls._run([ + "eval", + "-f", f"{ROOT}/default.nix", + "--json", + expr + ]) + if response is None: + raise RuntimeError("Nix eval expression returned no response") + try: + return json.loads(response) + except (TypeError, ValueError): + raise RuntimeError("Nix eval response could not be parsed from JSON") + + @classmethod + async def hash_to_sri(cls, algorithm: str, value: str) -> Optional[str]: + return await cls._run([ + "hash", + "to-sri", + "--type", algorithm, + value + ]) + + +class HomeAssistant: + def __init__(self, session: ClientSession): + self._session = session + + async def get_latest_core_version( + self, + owner: str = "home-assistant", + repo: str = "core" + ) -> str: + async with self._session.get( + f"https://api.github.com/repos/{owner}/{repo}/releases/latest" + ) as response: + document = await response.json() + try: + return str(document.get("name")) + except KeyError: + raise RuntimeError("No tag name in response document") + + + async def get_latest_frontend_version( + self, + core_version: str + ) -> str: + async with self._session.get( + f"https://raw.githubusercontent.com/home-assistant/core/{core_version}/homeassistant/components/frontend/manifest.json" + ) as response: + document = await response.json(content_type="text/plain") + + requirements = [ + requirement + for requirement in document.get("requirements", []) + if requirement.startswith("home-assistant-frontend==") + ] + + if len(requirements) > 1: + raise RuntimeError( + "Found more than one version specifier for the frontend package" + ) + elif len(requirements) == 1: + requirement = requirements.pop() + _, version = requirement.split("==", maxsplit=1) + return str(version) + else: + raise RuntimeError( + "Found no version specifier for frontend package" + ) + + + async def update_core(self, old_version: str, new_version: str) -> None: + old_sdist_hash = str(await Nix.eval("home-assistant.src.outputHash")) + new_sdist_hash = await Nurl.prefetch("https://pypi.org/project/homeassistant/", new_version) + print(f"sdist: {old_sdist_hash} -> {new_sdist_hash}") + + old_git_hash = str(await Nix.eval("home-assistant.gitSrc.outputHash")) + new_git_hash = await Nurl.prefetch("https://github.com/home-assistant/core/", new_version) + print(f"git: {old_git_hash} -> {new_git_hash}") + + with File("pkgs/servers/home-assistant/default.nix") as file: + file.substitute("hassVersion", old_version, new_version) + file.substitute("hash", old_sdist_hash, new_sdist_hash) + file.substitute("hash", old_git_hash, new_git_hash) + + async def update_frontend(self, old_version: str, new_version: str) -> None: + old_hash = str(await Nix.eval("home-assistant.frontend.src.outputHash")) + new_hash = await Nurl.prefetch( + "https://pypi.org/project/home_assistant_frontend/", + new_version, + "-A", "format", "wheel", + "-A", "dist", "py3", + "-A", "python", "py3" + ) + print(f"frontend: {old_hash} -> {new_hash}") + + with File("pkgs/servers/home-assistant/frontend.nix") as file: + file.substitute("version", old_version, new_version) + file.substitute("hash", old_hash, new_hash) + + async def update_components(self): + await run_async([ + f"{ROOT}/pkgs/servers/home-assistant/parse-requirements.py" + ]) + + +async def main(): + headers = {} + if token := os.environ.get("GITHUB_TOKEN", None): + headers.update({"GITHUB_TOKEN": token}) + + async with aiohttp.ClientSession(headers=headers) as client: + hass = HomeAssistant(client) + + core_current = str(await Nix.eval("home-assistant.version")) + core_latest = await hass.get_latest_core_version() + + if Version(core_latest) > Version(core_current): + print(f"New Home Assistant version {core_latest} is available") + await hass.update_core(str(core_current), str(core_latest)) + + frontend_current = str(await Nix.eval("home-assistant.frontend.version")) + frontend_latest = await hass.get_latest_frontend_version(str(core_latest)) + + if Version(frontend_latest) > Version(frontend_current): + await hass.update_frontend(str(frontend_current), str(frontend_latest)) + + await hass.update_components() + + else: + print(f"Home Assistant {core_current} is still the latest version.") + + # wait for async client sessions to close + # https://docs.aiohttp.org/en/stable/client_advanced.html#graceful-shutdown + await asyncio.sleep(0) + +if __name__ == "__main__": + run_sync(["pyright", __file__]) + run_sync(["ruff", "--ignore=E501", __file__]) + run_sync(["isort", __file__]) + asyncio.run(main()) diff --git a/pkgs/servers/home-assistant/update.sh b/pkgs/servers/home-assistant/update.sh deleted file mode 100755 index 05f2e93dfe46..000000000000 --- a/pkgs/servers/home-assistant/update.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env nix-shell -#!nix-shell -p nix -p jq -p curl -p bash -p git -p nix-update -i bash - -set -eux - -DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -cd "$DIR" - -CURRENT_VERSION=$(nix-instantiate ../../.. --eval --strict -A home-assistant.version | tr -d '"') -TARGET_VERSION=$(curl https://api.github.com/repos/home-assistant/core/releases/latest | jq -r '.name') -MANIFEST=$(curl https://raw.githubusercontent.com/home-assistant/core/${TARGET_VERSION}/homeassistant/components/frontend/manifest.json) -FRONTEND_VERSION=$(echo $MANIFEST | jq -r '.requirements[] | select(startswith("home-assistant-frontend")) | sub(".*==(?.*)"; .vers)') - -if [[ "$CURRENT_VERSION" == "$TARGET_VERSION" ]]; then - echo "home-assistant is up-to-date: ${CURRENT_VERSION}" - exit 0 -fi - - -sed -i -e "s/version =.*/version = \"${TARGET_VERSION}\";/" \ - component-packages.nix - -sed -i -e "s/hassVersion =.*/hassVersion = \"${TARGET_VERSION}\";/" \ - default.nix - -( - # update the frontend before running parse-requirements, so it doesn't get shown as outdated - cd ../../.. - nix-update --version "$FRONTEND_VERSION" home-assistant.python.pkgs.home-assistant-frontend -) - -./parse-requirements.py - -read - -( - cd ../../.. - nix-update --version "$TARGET_VERSION" --build home-assistant -) - -#git add ./component-packages.nix ./default.nix ./frontend.nix -#git commit -m "home-assistant: ${CURRENT_VERSION} -> ${TARGET_VERSION}"