about summary refs log tree commit diff
path: root/pkgs/development/interpreters/python
diff options
context:
space:
mode:
authorMartin Weinelt <hexa@darmstadt.ccc.de>2023-11-27 19:14:35 +0100
committerMartin Weinelt <hexa@darmstadt.ccc.de>2023-12-20 20:10:22 +0100
commit8f3162f83fcccf26bf50c55ff960fd2dee5264b2 (patch)
tree99e18c6ae853002a20552ddd5aa538da04ba189c /pkgs/development/interpreters/python
parenta648bdeede3883aeb2d9ecda43ad0f2455879abe (diff)
downloadnixlib-8f3162f83fcccf26bf50c55ff960fd2dee5264b2.tar
nixlib-8f3162f83fcccf26bf50c55ff960fd2dee5264b2.tar.gz
nixlib-8f3162f83fcccf26bf50c55ff960fd2dee5264b2.tar.bz2
nixlib-8f3162f83fcccf26bf50c55ff960fd2dee5264b2.tar.lz
nixlib-8f3162f83fcccf26bf50c55ff960fd2dee5264b2.tar.xz
nixlib-8f3162f83fcccf26bf50c55ff960fd2dee5264b2.tar.zst
nixlib-8f3162f83fcccf26bf50c55ff960fd2dee5264b2.zip
python3.pkgs.pythonRuntimeDepsCheckHook: init
Implements a hook, that checks whether all dependencies, as specified by
the wheel manifest, are present in the current environment.

Complains about missing packages, as well as version specifier
mismatches.
Diffstat (limited to 'pkgs/development/interpreters/python')
-rw-r--r--pkgs/development/interpreters/python/hooks/default.nix10
-rw-r--r--pkgs/development/interpreters/python/hooks/python-runtime-deps-check-hook.py97
-rw-r--r--pkgs/development/interpreters/python/hooks/python-runtime-deps-check-hook.sh20
-rw-r--r--pkgs/development/interpreters/python/mk-python-derivation.nix8
4 files changed, 135 insertions, 0 deletions
diff --git a/pkgs/development/interpreters/python/hooks/default.nix b/pkgs/development/interpreters/python/hooks/default.nix
index 35116b38f810..83c6be2f5382 100644
--- a/pkgs/development/interpreters/python/hooks/default.nix
+++ b/pkgs/development/interpreters/python/hooks/default.nix
@@ -172,6 +172,16 @@ in {
       };
     } ./python-remove-tests-dir-hook.sh) {};
 
+  pythonRuntimeDepsCheckHook = callPackage ({ makePythonHook, packaging }:
+    makePythonHook {
+      name = "python-runtime-deps-check-hook.sh";
+      propagatedBuildInputs = [ packaging ];
+      substitutions = {
+        inherit pythonInterpreter pythonSitePackages;
+        hook = ./python-runtime-deps-check-hook.py;
+      };
+    } ./python-runtime-deps-check-hook.sh) {};
+
   setuptoolsBuildHook = callPackage ({ makePythonHook, setuptools, wheel }:
     makePythonHook {
       name = "setuptools-setup-hook";
diff --git a/pkgs/development/interpreters/python/hooks/python-runtime-deps-check-hook.py b/pkgs/development/interpreters/python/hooks/python-runtime-deps-check-hook.py
new file mode 100644
index 000000000000..5a3a91939175
--- /dev/null
+++ b/pkgs/development/interpreters/python/hooks/python-runtime-deps-check-hook.py
@@ -0,0 +1,97 @@
+#!/usr/bin/env python3
+"""
+The runtimeDependenciesHook validates, that all dependencies specified
+in wheel metadata are available in the local environment.
+
+In case that does not hold, it will print missing dependencies and
+violated version constraints.
+"""
+
+
+import importlib.metadata
+import re
+import sys
+import tempfile
+from argparse import ArgumentParser
+from zipfile import ZipFile
+
+from packaging.metadata import Metadata, parse_email
+from packaging.requirements import Requirement
+
+argparser = ArgumentParser()
+argparser.add_argument("wheel", help="Path to the .whl file to test")
+
+
+def error(msg: str) -> None:
+    print(f"  - {msg}", file=sys.stderr)
+
+
+def normalize_name(name: str) -> str:
+    """
+    Normalize package names according to PEP503
+    """
+    return re.sub(r"[-_.]+", "-", name).lower()
+
+
+def get_manifest_text_from_wheel(wheel: str) -> str:
+    """
+    Given a path to a wheel, this function will try to extract the
+    METADATA file in the wheels .dist-info directory.
+    """
+    with ZipFile(wheel) as zipfile:
+        for zipinfo in zipfile.infolist():
+            if zipinfo.filename.endswith(".dist-info/METADATA"):
+                with tempfile.TemporaryDirectory() as tmp:
+                    path = zipfile.extract(zipinfo, path=tmp)
+                    with open(path, encoding="utf-8") as fd:
+                        return fd.read()
+
+    raise RuntimeError("No METADATA file found in wheel")
+
+
+def get_metadata(wheel: str) -> Metadata:
+    """
+    Given a path to a wheel, returns a parsed Metadata object.
+    """
+    text = get_manifest_text_from_wheel(wheel)
+    raw, _ = parse_email(text)
+    metadata = Metadata.from_raw(raw)
+
+    return metadata
+
+
+def test_requirement(requirement: Requirement) -> bool:
+    """
+    Given a requirement specification, tests whether the dependency can
+    be resolved in the local environment, and whether it satisfies the
+    specified version constraints.
+    """
+    if requirement.marker and not requirement.marker.evaluate():
+        # ignore requirements with incompatible markers
+        return True
+
+    package_name = normalize_name(requirement.name)
+
+    try:
+        package = importlib.metadata.distribution(requirement.name)
+    except importlib.metadata.PackageNotFoundError:
+        error(f"{package_name} not installed")
+        return False
+
+    if package.version not in requirement.specifier:
+        error(
+            f"{package_name}{requirement.specifier} not satisfied by version {package.version}"
+        )
+        return False
+
+    return True
+
+
+if __name__ == "__main__":
+    args = argparser.parse_args()
+
+    metadata = get_metadata(args.wheel)
+    tests = [test_requirement(requirement) for requirement in metadata.requires_dist]
+
+    if not all(tests):
+        sys.exit(1)
diff --git a/pkgs/development/interpreters/python/hooks/python-runtime-deps-check-hook.sh b/pkgs/development/interpreters/python/hooks/python-runtime-deps-check-hook.sh
new file mode 100644
index 000000000000..43a2f9b88745
--- /dev/null
+++ b/pkgs/development/interpreters/python/hooks/python-runtime-deps-check-hook.sh
@@ -0,0 +1,20 @@
+# Setup hook for PyPA installer.
+echo "Sourcing python-runtime-deps-check-hook"
+
+pythonRuntimeDepsCheckHook() {
+    echo "Executing pythonRuntimeDepsCheck"
+
+    export PYTHONPATH="$out/@pythonSitePackages@:$PYTHONPATH"
+
+    for wheel in dist/*.whl; do
+        echo "Checking runtime dependencies for $(basename $wheel)"
+        @pythonInterpreter@ @hook@ "$wheel"
+    done
+
+    echo "Finished executing pythonRuntimeDepsCheck"
+}
+
+if [ -z "${dontCheckRuntimeDeps-}" ]; then
+    echo "Using pythonRuntimeDepsCheckHook"
+    preInstallPhases+=" pythonRuntimeDepsCheckHook"
+fi
diff --git a/pkgs/development/interpreters/python/mk-python-derivation.nix b/pkgs/development/interpreters/python/mk-python-derivation.nix
index c14c6bc096fd..e6f9087de866 100644
--- a/pkgs/development/interpreters/python/mk-python-derivation.nix
+++ b/pkgs/development/interpreters/python/mk-python-derivation.nix
@@ -19,6 +19,7 @@
 , pythonOutputDistHook
 , pythonRemoveBinBytecodeHook
 , pythonRemoveTestsDirHook
+, pythonRuntimeDepsCheckHook
 , setuptoolsBuildHook
 , setuptoolsCheckHook
 , wheelUnpackHook
@@ -229,6 +230,13 @@ let
         }
       else
         pypaBuildHook
+    ) (
+      if isBootstrapPackage then
+        pythonRuntimeDepsCheckHook.override {
+          inherit (python.pythonOnBuildForHost.pkgs.bootstrap) packaging;
+        }
+      else
+        pythonRuntimeDepsCheckHook
     )] ++ lib.optionals (format' == "wheel") [
       wheelUnpackHook
     ] ++ lib.optionals (format' == "egg") [