From 69bd0e11b9a054837e1733858490f0aec3830eca Mon Sep 17 00:00:00 2001 From: Lars-Dominik Braun Date: Sun, 3 Jan 2021 10:30:29 +0100 Subject: [PATCH v2 01/16] 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-script): Add script. (validate-loadable): New phase. (%standard-phases): Use it. * tests/builders.scm (make-python-dummy): Add test package generator. (check-build-{success,failure}): Add build helper functions. (python-dummy-*): Add test packages. ("python-build-system: …"): Add tests. --- guix/build/python-build-system.scm | 80 +++++++++++++++++++++ tests/builders.scm | 108 ++++++++++++++++++++++++++++- 2 files changed, 185 insertions(+), 3 deletions(-) diff --git a/guix/build/python-build-system.scm b/guix/build/python-build-system.scm index 09bd8465c8..3c29efea8b 100644 --- a/guix/build/python-build-system.scm +++ b/guix/build/python-build-system.scm @@ -148,6 +148,85 @@ (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 top level modules. This should not have any side-effects. + try: + metalines = dist.get_metadata_lines('top_level.txt') + except KeyError: + # distutils (i.e. #:use-setuptools? #f) will not install any metadata. + print('WARNING: cannot determine top-level modules') + continue + for name in metalines: + # Only available on Python 3. + if PathFinder and PathFinder.find_spec(name) is None: + # Ignore unavailable modules, often C modules, which were not + # installed at the top-level. 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 + + # 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', 'gui_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 + +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 +346,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..929d1a906e 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,106 @@ (test-assert "gnu-build-system" (build-system? gnu-build-system)) + +(define* (make-python-dummy name #:key (setup-py-extra "") (init-py "") (use-setuptools? #t)) + (package + (name (string-append "python-dummy-" name)) + (version "0.1") + (source #f) ; source is generated in 'unpack + (build-system python-build-system) + (arguments + `(#:tests? #f + #:use-setuptools? ,use-setuptools? + #:phases + (modify-phases %standard-phases + (replace 'unpack + (lambda _ + (mkdir-p "src/dummy") + (chdir "src") + (with-output-to-file "dummy/__init__.py" + (lambda _ + (display ,init-py))) + (with-output-to-file "setup.py" + (lambda _ + (format #t "\ +~a +setup( + name='dummy-~a', + version='0.1', + packages=['dummy'], + ~a + )" + (if ,use-setuptools? + "from setuptools import setup" + "from distutils.core import setup") + ,name ,setup-py-extra))) + #t))))) + (home-page #f) + (synopsis #f) + (description #f) + (license #f))) + +(define python-dummy-ok + (make-python-dummy "ok")) + +(define python2-dummy-ok + (package-with-python2 python-dummy-ok)) + +;; distutil won’t install any metadata, so make sure our script does not fail +;; on a otherwise fine package. +(define python-dummy-no-setuptools + (make-python-dummy + "no-setuptools" #:use-setuptools? #f)) + +(define python2-dummy-no-setuptools + (package-with-python2 python-dummy-no-setuptools)) + +(define python-dummy-fail-requirements + (make-python-dummy "fail-requirements" + #:setup-py-extra "install_requires=['nonexistent'],")) + +(define python2-dummy-fail-requirements + (package-with-python2 python-dummy-fail-requirements)) + +(define python-dummy-fail-import + (make-python-dummy "fail-import" #:init-py "import nonexistent")) + +(define python2-dummy-fail-import + (package-with-python2 python-dummy-fail-import)) + +(define python-dummy-fail-console-script + (make-python-dummy "fail-console-script" + #:setup-py-extra (string-append "entry_points={'console_scripts': " + "['broken = dummy:nonexistent']},"))) + +(define python2-dummy-fail-console-script + (package-with-python2 python-dummy-fail-console-script)) + +(define (check-build-success store p) + (unless store (test-skip 1)) + (test-assert (string-append "python-build-system: " (package-name p)) + (let* ((drv (package-derivation store p))) + (build-derivations store (list drv))))) + +(define (check-build-failure store p) + (unless store (test-skip 1)) + (test-assert (string-append "python-build-system: " (package-name p)) + (not (false-if-exception (package-derivation store python-dummy-fail-requirements))))) + +(with-external-store store + (for-each (lambda (p) (check-build-success store p)) + (list + python-dummy-ok + python-dummy-no-setuptools + python2-dummy-ok + python2-dummy-no-setuptools)) + (for-each (lambda (p) (check-build-failure store p)) + (list + python-dummy-fail-requirements + python-dummy-fail-import + python-dummy-fail-console-script + python2-dummy-fail-requirements + python2-dummy-fail-import + python2-dummy-fail-console-script))) + (test-end "builders") -- 2.26.2