From 2df41c3fb476822efac1aa8dac8368e91a0e360a Mon Sep 17 00:00:00 2001
From: Lars-Dominik Braun <lars@6xq.net>
Date: Sun, 3 Jan 2021 10:30:29 +0100
Subject: [PATCH] build-system/python: Add a sanity check phase.
Add a new phase validating the usability of installed Python packages.
* gnu/packages/aux-files/python/sanity-check.py: New file.
* Makefile.am (AUX_FILES): Register it.
* guix/build-system/python.scm (sanity-check.py): New variable.
(lower): Add the script as an implicit input.
* guix/build/python-build-system.scm: Remove trailing #t.
(sanity-check): New phase.
(%standard-phases): Use it.
* tests/builders.scm (test-build-package): New syntax.
("python-build-system: dummy-ok")
("python-build-system: dummy-fail-requirements")
("python-build-system: dummy-fail-import")
("python-build-system: dummy-fail-console-script"): Add tests.
---
Makefile.am | 1 +
gnu/packages/aux-files/python/sanity-check.py | 85 ++++++++++++++
guix/build-system/python.scm | 8 ++
guix/build/python-build-system.scm | 26 +++--
tests/builders.scm | 106 +++++++++++++++++-
5 files changed, 212 insertions(+), 14 deletions(-)
create mode 100644 gnu/packages/aux-files/python/sanity-check.py
@@ -380,6 +380,7 @@ AUX_FILES = \
gnu/packages/aux-files/linux-libre/4.4-i686.conf \
gnu/packages/aux-files/linux-libre/4.4-x86_64.conf \
gnu/packages/aux-files/pack-audit.c \
+ gnu/packages/aux-files/python/sanity-check.py \
gnu/packages/aux-files/run-in-namespace.c
# Templates, examples.
new file mode 100644
@@ -0,0 +1,85 @@
+# GNU Guix --- Functional package management for GNU
+# Copyright © 2021 Lars-Dominik Braun <lars@6xq.net>
+#
+# This file is part of GNU Guix.
+#
+# GNU Guix is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or (at
+# your option) any later version.
+#
+# GNU Guix is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import print_function # Python 2 support.
+import importlib
+import pkg_resources
+import sys
+import 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)
@@ -2,6 +2,7 @@
;;; Copyright © 2013, 2014, 2015, 2016, 2017 Ludovic Courtès <ludo@gnu.org>
;;; Copyright © 2013 Andreas Enge <andreas@enge.fr>
;;; Copyright © 2013 Nikita Karetnikov <nikita@karetnikov.org>
+;;; Copyright © 2021 Lars-Dominik Braun <lars@6xq.net>
;;;
;;; This file is part of GNU Guix.
;;;
@@ -19,6 +20,8 @@
;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
(define-module (guix build-system python)
+ #:use-module ((gnu packages) #:select (search-auxiliary-file))
+ #:use-module (guix gexp)
#:use-module (guix store)
#:use-module (guix utils)
#:use-module (guix memoization)
@@ -70,6 +73,10 @@ extension, such as '.tar.gz'."
(let ((python (resolve-interface '(gnu packages python))))
(module-ref python 'python-2)))
+(define sanity-check.py
+ ;; The script used to validate the installation of a Python package.
+ (search-auxiliary-file "python/sanity-check.py"))
+
(define* (package-with-explicit-python python old-prefix new-prefix
#:key variant-property)
"Return a procedure of one argument, P. The procedure creates a package with
@@ -156,6 +163,7 @@ pre-defined variants."
;; Keep the standard inputs of 'gnu-build-system'.
,@(standard-packages)))
(build-inputs `(("python" ,python)
+ ("sanity-check.py" ,(local-file sanity-check.py))
,@native-inputs))
(outputs outputs)
(build python-build)
@@ -9,6 +9,7 @@
;;; Copyright © 2019, 2020, 2021 Maxim Cournoyer <maxim.cournoyer@gmail.com>
;;; Copyright © 2020 Jakub Kądziołka <kuba@kadziolka.net>
;;; Copyright © 2020 Efraim Flashner <efraim@flashner.co.il>
+;;; Copyright © 2021 Lars-Dominik Braun <lars@6xq.net>
;;;
;;; This file is part of GNU Guix.
;;;
@@ -134,6 +135,15 @@
(apply invoke "python" "./setup.py" command params)))
(error "no setup.py found")))
+(define* (sanity-check #: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."
+ (let ((sanity-check.py (assoc-ref inputs "sanity-check.py")))
+ ;; Make sure the working directory is empty (i.e. no Python modules in it)
+ (with-directory-excursion "/tmp"
+ (invoke "python" sanity-check.py (site-packages inputs outputs)))))
+
(define* (build #:key use-setuptools? #:allow-other-keys)
"Build a given Python package."
(call-setuppy "build" '() use-setuptools?)
@@ -225,8 +235,7 @@ useful when running checks after installing the package."
;; '--invalidation-mode' option, do not generate any.
(unless <3.7?
(invoke "python" "-m" "compileall" "--invalidation-mode=unchecked-hash"
- out))
- #t))
+ out))))
(define* (wrap #:key inputs outputs #:allow-other-keys)
(let ((pythonpath (guix-pythonpath inputs)))
@@ -262,8 +271,7 @@ installed with setuptools."
(easy-install-pth (string-append site-packages "/easy-install.pth"))
(new-pth (string-append site-packages "/" name ".pth")))
(when (file-exists? easy-install-pth)
- (rename-file easy-install-pth new-pth))
- #t))
+ (rename-file easy-install-pth new-pth))))
(define* (ensure-no-mtimes-pre-1980 #:rest _)
"Ensure that there are no mtimes before 1980-01-02 in the source tree."
@@ -275,8 +283,7 @@ installed with setuptools."
(ftw "." (lambda (file stat flag)
(unless (<= early-1980 (stat:mtime stat))
(utime file early-1980 early-1980))
- #t))
- #t))
+ #t))))
(define* (enable-bytecode-determinism #:rest _)
"Improve determinism of pyc files."
@@ -284,8 +291,7 @@ installed with setuptools."
(setenv "PYTHONHASHSEED" "0")
;; Prevent Python from creating .pyc files when loading modules (such as
;; when running a test suite).
- (setenv "PYTHONDONTWRITEBYTECODE" "1")
- #t)
+ (setenv "PYTHONDONTWRITEBYTECODE" "1"))
(define* (ensure-no-cythonized-files #:rest _)
"Check the source code for @code{.c} files which may have been pre-generated
@@ -296,8 +302,7 @@ by Cython."
(string-append (string-drop-right file 3) "c")))
(when (file-exists? generated-file)
(format #t "Possible Cythonized file found: ~a~%" generated-file))))
- (find-files "." "\\.pyx$"))
- #t)
+ (find-files "." "\\.pyx$")))
(define %standard-phases
;; The build phase only builds C extensions and copies the Python sources,
@@ -319,6 +324,7 @@ by Cython."
(add-after 'install 'wrap wrap)
(add-before 'check 'add-install-to-pythonpath add-install-to-pythonpath)
(add-before 'check 'add-install-to-path add-install-to-path)
+ (add-after 'check 'sanity-check sanity-check)
(add-before 'strip 'rename-pth-file rename-pth-file)))
(define* (python-build #:key inputs (phases %standard-phases)
@@ -1,5 +1,6 @@
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2012, 2013, 2014, 2015, 2019 Ludovic Courtès <ludo@gnu.org>
+;;; Copyright © 2021 Lars-Dominik Braun <lars@6xq.net>
;;;
;;; This file is part of GNU Guix.
;;;
@@ -17,19 +18,19 @@
;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
-(define-module (test-builders)
+(define-module (tests builders)
#: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 +79,101 @@
(test-assert "gnu-build-system"
(build-system? gnu-build-system))
+
+;;;
+;;; Test the sanity-check phase of the Python build system.
+;;;
+
+(define-syntax-rule (test-build-package name expect-failure? package)
+ "Return a test named NAME, building PACKAGE in the external store."
+ (with-external-store store
+ (unless store (test-skip 1))
+ (let ((build (lambda (p)
+ (build-derivations
+ store (list (package-derivation store p))))))
+ (if expect-failure?
+ (test-error name
+ (store-protocol-error?)
+ (build package))
+ (test-assert name (build package))))))
+
+(test-build-package "python-build-system: dummy-ok" #f
+ (dummy-package "python-dummy-ok"
+ (build-system python-build-system)
+ (arguments
+ `(#:phases
+ (modify-phases %standard-phases
+ (replace 'unpack
+ (lambda _
+ (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',
+ packages=['dummy'])"))))))))))
+
+(test-build-package "python-build-system: dummy-fail-requirements" #t
+ (dummy-package "python-dummy-fail-requirements"
+ (build-system python-build-system)
+ (arguments
+ `(#:tests? #f
+ #:phases
+ (modify-phases %standard-phases
+ (replace 'unpack
+ (lambda _
+ (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',
+ packages=['dummy'],
+ install_requires=['nonexistent'])"))))))))))
+
+(test-build-package "python-build-system: dummy-fail-import" #t
+ (dummy-package "python-dummy-fail-import"
+ (build-system python-build-system)
+ (arguments
+ `(#:tests? #f
+ #:phases
+ (modify-phases %standard-phases
+ (replace 'unpack
+ (lambda _
+ (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',
+ packages=['dummy'])"))))))))))
+
+(test-build-package "python-build-system: dummy-fail-console-script" #f
+ (dummy-package "python-dummy-fail-console-script"
+ (build-system python-build-system)
+ (arguments
+ `(#:tests? #f
+ #:phases
+ (modify-phases %standard-phases
+ (replace 'unpack
+ (lambda _
+ (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',
+ packages=['dummy'],
+ entry_points={'console_scripts': ['broken = dummy:nonexistent']})"))))))))))
+
(test-end "builders")
--
2.30.0