buildHomeAssistantComponent: Increase manifest check robustness

- Find and check all manifest files
- Allow ignoring too tight version constraints
This commit is contained in:
Martin Weinelt 2024-12-12 01:39:09 +01:00
parent ba5ed7f373
commit b2d6597ee4
No known key found for this signature in database
GPG Key ID: 87C1E9888F856759
5 changed files with 86 additions and 35 deletions

View File

@ -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)
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 "requirements" in manifest:
for requirement in manifest["requirements"]:
ok &= check_requirement(requirement)
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>")
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)

View File

@ -42,7 +42,6 @@ home-assistant.python.pkgs.buildPythonPackage (
nativeCheckInputs =
with home-assistant.python.pkgs;
[
importlib-metadata
manifestRequirementsCheckHook
packaging
]

View File

@ -4,7 +4,7 @@
}:
makeSetupHook {
name = "manifest-requirements-check-hook";
name = "manifest-check-hook";
substitutions = {
pythonCheckInterpreter = python.interpreter;
checkManifest = ./check_manifest.py;

View File

@ -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

View File

@ -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"
];
```
`