From 7a9ac1ee220ec2cb3dc10da1a8455289aa5e3b99 Mon Sep 17 00:00:00 2001 From: Lars-Dominik Braun Date: Sun, 3 Jan 2021 10:30:29 +0100 Subject: [PATCH 01/15] build-system/python: Validate installed package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a new phase validating usalibity of installed Python packages. * guix/build/python-build-system.scm (validate-loadable): New phase. (%standard-phases): Use it. * tests/builders.scm (python-dummy-*) Add test packages. ("python-build-system: …"): Add tests. --- guix/build/python-build-system.scm | 70 +++++++++++++ tests/builders.scm | 161 ++++++++++++++++++++++++++++- 2 files changed, 228 insertions(+), 3 deletions(-) diff --git a/guix/build/python-build-system.scm b/guix/build/python-build-system.scm index 09bd8465c8..15d4f0c54e 100644 --- a/guix/build/python-build-system.scm +++ b/guix/build/python-build-system.scm @@ -148,6 +148,75 @@ (format #t "test suite not run~%")) #t) +(define validate-script + "from __future__ import print_function # Python 2 support. +import pkg_resources, sys, importlib, traceback +try: + from importlib.machinery import PathFinder +except ImportError: + PathFinder = None +ret = 0 +# Only check site-packages installed by this package, but not dependencies +# (which pkg_resources.working_set would include). Path supplied via argv. +ws = pkg_resources.find_distributions(sys.argv[1]) +for dist in ws: + print('validating', repr(dist.project_name), dist.location) + try: + print('...checking requirements', end=': ') + req = str(dist.as_requirement()) + # dist.activate() is not enough to actually check requirements, we have to + # .require() it. + pkg_resources.require(req) + print('OK') + except Exception as e: + print('ERROR:', req, e) + ret = 1 + continue + # Try to load entry points of console scripts too, making sure they work. They + # should be removed if they don’t. Other groups may not be safe, as they can + # depend on optional packages. + for group, v in dist.get_entry_map().items(): + if group not in {'console_scripts', }: + continue + for name, ep in v.items(): + try: + print('...trying to load endpoint', group, name, end=': ') + ep.load() + print('OK') + except Exception: + print('ERROR:') + traceback.print_exc(file=sys.stdout) + ret = 1 + continue + # And finally try to load top level modules. This should not have any + # side-effects. + for name in dist.get_metadata_lines('top_level.txt'): + # Only available on Python 3. + if PathFinder and PathFinder.find_spec(name) is None: + # Ignore unavailable modules. Cannot use ModuleNotFoundError, + # because it is raised by failed imports too. + continue + try: + print('...trying to load module', name, end=': ') + importlib.import_module(name) + print('OK') + except Exception: + print('ERROR:') + traceback.print_exc(file=sys.stdout) + ret = 1 + continue +sys.exit(ret)") + +(define* (validate-loadable #:key tests? inputs outputs #:allow-other-keys) + "Ensure packages depending on this package via setuptools work properly, +their advertised endpoints work and their top level modules are importable +without errors." + (add-installed-pythonpath inputs outputs) + ;; Make sure the working directory is empty (i.e. no Python modules in it) + (with-directory-excursion "/tmp" + (invoke "python" "-c" validate-script (site-packages inputs outputs))) + #t) + (define (python-version python) (let* ((version (last (string-split python #\-))) (components (string-split version #\.)) @@ -267,6 +336,7 @@ installed with setuptools." (replace 'install install) (add-after 'install 'check check) (add-after 'install 'wrap wrap) + (add-after 'check 'validate-loadable validate-loadable) (add-before 'strip 'rename-pth-file rename-pth-file))) (define* (python-build #:key inputs (phases %standard-phases) diff --git a/tests/builders.scm b/tests/builders.scm index fdcf38ded3..8fc0c07ee0 100644 --- a/tests/builders.scm +++ b/tests/builders.scm @@ -21,15 +21,15 @@ #:use-module (guix download) #:use-module (guix build-system) #:use-module (guix build-system gnu) + #:use-module (guix build-system python) #:use-module (guix store) + #:use-module (guix monads) #:use-module (guix utils) #:use-module (guix base32) #:use-module (guix derivations) #:use-module (gcrypt hash) #:use-module (guix tests) - #:use-module ((guix packages) - #:select (package? - package-derivation package-native-search-paths)) + #:use-module (guix packages) #:use-module (gnu packages bootstrap) #:use-module (ice-9 match) #:use-module (srfi srfi-1) @@ -78,4 +78,159 @@ (test-assert "gnu-build-system" (build-system? gnu-build-system)) + +(define python-dummy-ok + (package + (name "python-dummy-ok") + (version "0.1") + (source #f) ; source is generated in 'unpack + (build-system python-build-system) + (arguments + `(#:phases + (modify-phases %standard-phases + (replace 'unpack + (lambda _ + (mkdir-p "src") + (chdir "src") + (mkdir-p "dummy") + (invoke "touch" "dummy/__init__.py") + (with-output-to-file "setup.py" + (lambda _ + (display "from setuptools import setup +setup( + name='dummy-ok', + version='0.1', + packages=['dummy'], + ) +"))) + #t))))) + (home-page #f) + (synopsis #f) + (description #f) + (license #f))) + +(define python2-dummy-ok + (package-with-python2 python-dummy-ok)) + +(define python-dummy-fail-requirements + (package + (name "python-dummy-fail-requirements") + (version "0.1") + (source #f) ; source is generated in 'unpack + (build-system python-build-system) + (arguments + `(#:tests? #f + #:phases + (modify-phases %standard-phases + (replace 'unpack + (lambda _ + (mkdir-p "src") + (chdir "src") + (mkdir-p "dummy") + (invoke "touch" "dummy/__init__.py") + (with-output-to-file "setup.py" + (lambda _ + (display "from setuptools import setup +setup( + name='dummy-fail-requirements', + version='0.1', + packages=['dummy'], + install_requires=['nonexistent'], + ) +"))) + #t))))) + (home-page #f) + (synopsis #f) + (description #f) + (license #f))) + +(define-public python2-dummy-fail-requirements + (package-with-python2 python-dummy-fail-requirements)) + +(define-public python-dummy-fail-import + (package + (name "python-dummy-fail-import") + (version "0.1") + (source #f) ; source is generated in 'unpack + (build-system python-build-system) + (arguments + `(#:tests? #f + #:phases + (modify-phases %standard-phases + (replace 'unpack + (lambda _ + (mkdir-p "src") + (chdir "src") + (mkdir-p "dummy") + (with-output-to-file "dummy/__init__.py" + (lambda _ + (display "import nonexistent"))) + (with-output-to-file "setup.py" + (lambda _ + (display "from setuptools import setup +setup( + name='dummy-fail-import', + version='0.1', + packages=['dummy'], + ) +"))) + #t))))) + (home-page #f) + (synopsis #f) + (description #f) + (license #f))) + +(define-public python2-dummy-fail-import + (package-with-python2 python-dummy-fail-import)) + +(define-public python-dummy-fail-console-script + (package + (name "python-dummy-fail-console-script") + (version "0.1") + (source #f) ; source is generated in 'unpack + (build-system python-build-system) + (arguments + `(#:tests? #f + #:phases + (modify-phases %standard-phases + (replace 'unpack + (lambda _ + (mkdir-p "src") + (chdir "src") + (mkdir-p "dummy") + (invoke "touch" "dummy/__init__.py") + (with-output-to-file "setup.py" + (lambda _ + (display "from setuptools import setup +setup( + name='dummy-fail-console-script', + version='0.1', + packages=['dummy'], + entry_points={'console_scripts': ['broken = dummy:nonexistent']}, + ) +"))) + #t))))) + (home-page #f) + (synopsis #f) + (description #f) + (license #f))) + +(define-public python2-dummy-fail-console-script + (package-with-python2 python-dummy-fail-console-script)) + +(with-external-store store + (unless store (test-skip 1)) + (test-assert "python-build-system: dummy-ok" + (let* ((drv (package-derivation store python-dummy-ok))) + (build-derivations store (list drv)))) + (unless store (test-skip 1)) + (test-assert "python-build-system: dummy-fail-requirements" + (not (false-if-exception (package-derivation store python-dummy-fail-requirements)))) + (unless store (test-skip 1)) + (test-assert "python-build-system: dummy-fail-import" + (not (false-if-exception (package-derivation store python-dummy-fail-import)))) + (unless store (test-skip 1)) + (test-assert "python-build-system: dummy-fail-console-script" + (not (false-if-exception (package-derivation store python-dummy-fail-console-script))))) + (test-end "builders") -- 2.26.2