From b2d6597ee47d0de82f59ff8df7f4d5f8b9589fc4 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Thu, 12 Dec 2024 01:39:09 +0100 Subject: [PATCH] buildHomeAssistantComponent: Increase manifest check robustness - Find and check all manifest files - Allow ignoring too tight version constraints --- .../build-custom-component/check_manifest.py | 77 ++++++++++++------- .../build-custom-component/default.nix | 1 - .../manifest-requirements-check-hook.nix | 2 +- .../manifest-requirements-check-hook.sh | 23 ++++-- .../custom-components/README.md | 18 +++++ 5 files changed, 86 insertions(+), 35 deletions(-) diff --git a/pkgs/servers/home-assistant/build-custom-component/check_manifest.py b/pkgs/servers/home-assistant/build-custom-component/check_manifest.py index a8fd9505e52f..c2a41584effc 100644 --- a/pkgs/servers/home-assistant/build-custom-component/check_manifest.py +++ b/pkgs/servers/home-assistant/build-custom-component/check_manifest.py @@ -1,19 +1,31 @@ #!/usr/bin/env python3 +import argparse import json import os import sys -import importlib_metadata +import importlib.metadata +from typing import Dict, List from packaging.requirements import InvalidRequirement, Requirement -def error(msg: str, ret: bool = False) -> None: +def error(msg: str, ret: bool = False) -> bool: print(f" - {msg}", file=sys.stderr) return ret -def check_requirement(req: str): +def check_derivation_name(manifest: Dict) -> bool: + derivation_domain = os.environ.get("domain") + manifest_domain = manifest["domain"] + if derivation_domain != manifest_domain: + return error( + f"Derivation attribute domain ({derivation_domain}) should match manifest domain ({manifest_domain})" + ) + return True + + +def test_requirement(req: str, ignore_version_requirement: List[str]) -> bool: # https://packaging.pypa.io/en/stable/requirements.html try: requirement = Requirement(req) @@ -21,44 +33,55 @@ def check_requirement(req: str): return error(f"{req} could not be parsed", ret=True) try: - version = importlib_metadata.distribution(requirement.name).version - except importlib_metadata.PackageNotFoundError: - return error(f"{requirement.name}{requirement.specifier} not present") + version = importlib.metadata.distribution(requirement.name).version + except importlib.metadata.PackageNotFoundError: + return error(f"{requirement.name}{requirement.specifier} not installed") # https://packaging.pypa.io/en/stable/specifiers.html - if version not in requirement.specifier: + if ( + requirement.name not in ignore_version_requirement + and version not in requirement.specifier + ): return error( - f"{requirement.name}{requirement.specifier} expected, but got {version}" + f"{requirement.name}{requirement.specifier} not satisfied by version {version}" ) return True -def check_manifest(manifest_file: str): - with open(manifest_file) as fd: - manifest = json.load(fd) - +def check_requirements(manifest: Dict, ignore_version_requirement: List[str]): ok = True - derivation_domain = os.environ.get("domain") - manifest_domain = manifest["domain"] - if derivation_domain != manifest_domain: - ok = False - error( - f"Derivation attribute domain ({derivation_domain}) must match manifest domain ({manifest_domain})" - ) + for requirement in manifest.get("requirements", []): + ok &= test_requirement(requirement, ignore_version_requirement) - if "requirements" in manifest: - for requirement in manifest["requirements"]: - ok &= check_requirement(requirement) + return ok + + +def main(args): + ok = True + + manifests = [] + for fd in args.manifests: + manifests.append(json.load(fd)) + + # At least one manifest should match the component name + ok &= any(check_derivation_name(manifest) for manifest in manifests) + + # All requirements need to match, use `ignoreRequirementVersion` to ignore too strict version constraints + ok &= all( + check_requirements(manifest, args.ignore_version_requirement) + for manifest in manifests + ) if not ok: - error("Manifest check failed.") sys.exit(1) if __name__ == "__main__": - if len(sys.argv) < 2: - raise RuntimeError(f"Usage {sys.argv[0]} ") - manifest_file = sys.argv[1] - check_manifest(manifest_file) + parser = argparse.ArgumentParser() + parser.add_argument("manifests", type=argparse.FileType("r"), nargs="+") + parser.add_argument("--ignore-version-requirement", action="append", default=[]) + args = parser.parse_args() + + main(args) diff --git a/pkgs/servers/home-assistant/build-custom-component/default.nix b/pkgs/servers/home-assistant/build-custom-component/default.nix index b413da8bd91c..b951b076b75f 100644 --- a/pkgs/servers/home-assistant/build-custom-component/default.nix +++ b/pkgs/servers/home-assistant/build-custom-component/default.nix @@ -42,7 +42,6 @@ home-assistant.python.pkgs.buildPythonPackage ( nativeCheckInputs = with home-assistant.python.pkgs; [ - importlib-metadata manifestRequirementsCheckHook packaging ] diff --git a/pkgs/servers/home-assistant/build-custom-component/manifest-requirements-check-hook.nix b/pkgs/servers/home-assistant/build-custom-component/manifest-requirements-check-hook.nix index ada031db22e6..757fcf33a633 100644 --- a/pkgs/servers/home-assistant/build-custom-component/manifest-requirements-check-hook.nix +++ b/pkgs/servers/home-assistant/build-custom-component/manifest-requirements-check-hook.nix @@ -4,7 +4,7 @@ }: makeSetupHook { - name = "manifest-requirements-check-hook"; + name = "manifest-check-hook"; substitutions = { pythonCheckInterpreter = python.interpreter; checkManifest = ./check_manifest.py; diff --git a/pkgs/servers/home-assistant/build-custom-component/manifest-requirements-check-hook.sh b/pkgs/servers/home-assistant/build-custom-component/manifest-requirements-check-hook.sh index 1fb366d13b43..cfe61f4308a6 100644 --- a/pkgs/servers/home-assistant/build-custom-component/manifest-requirements-check-hook.sh +++ b/pkgs/servers/home-assistant/build-custom-component/manifest-requirements-check-hook.sh @@ -1,17 +1,28 @@ +# shellcheck shell=bash + # Setup hook to check HA manifest requirements -echo "Sourcing manifest-requirements-check-hook" +echo "Sourcing manifest-check-hook" function manifestCheckPhase() { echo "Executing manifestCheckPhase" runHook preCheck - manifests=$(shopt -s nullglob; echo $out/custom_components/*/manifest.json) + args="" + # shellcheck disable=SC2154 + for package in "${ignoreVersionRequirement[@]}"; do + args+=" --ignore-version-requirement ${package}" + done - if [ ! -z "$manifests" ]; then - echo Checking manifests $manifests - @pythonCheckInterpreter@ @checkManifest@ $manifests + readarray -d '' manifests < <(find . -type f -name "manifest.json" -print0) + + if [ "${#manifests[@]}" -gt 0 ]; then + # shellcheck disable=SC2068 + echo Checking manifests ${manifests[@]} + # shellcheck disable=SC2068,SC2086 + @pythonCheckInterpreter@ @checkManifest@ ${manifests[@]} $args else - echo "No custom component manifests found in $out" >&2 + # shellcheck disable=SC2154 + echo "No component manifests found in $out" >&2 exit 1 fi diff --git a/pkgs/servers/home-assistant/custom-components/README.md b/pkgs/servers/home-assistant/custom-components/README.md index 38b2346a4eab..f868502b3261 100644 --- a/pkgs/servers/home-assistant/custom-components/README.md +++ b/pkgs/servers/home-assistant/custom-components/README.md @@ -72,3 +72,21 @@ and manifest agree about the domain name. There shouldn't be a need to disable this hook, but you can set `dontCheckManifest` to `true` in the derivation to achieve that. + +### Too narrow version constraints + +Every once in a while a dependency constraint is more narrow than it +needs to be. Instead of applying brittle substitions the version constraint +can be ignored on a per requirement basis. + +```nix + dependencies = [ + pyemvue + ]; + + # don't check the version constraint of pyemvue + ignoreVersionRequirement = [ + "pyemvue" + ]; +``` +`