M Makefile.am => Makefile.am +1 -0
@@ 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/python/sitecustomize.py \
gnu/packages/aux-files/run-in-namespace.c
A gnu/packages/aux-files/python/sanity-check.py => gnu/packages/aux-files/python/sanity-check.py +91 -0
@@ 0,0 1,91 @@
+# -*- coding: utf-8 -*-
+# 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 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
+
+sys.exit(ret)
M guix/build-system/python.scm => guix/build-system/python.scm +8 -0
@@ 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)
M guix/build/python-build-system.scm => guix/build/python-build-system.scm +16 -10
@@ 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.
;;;
@@ 132,6 133,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?)
@@ 209,8 219,7 @@ 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)
(define (list-of-files dir)
@@ 244,8 253,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."
@@ 257,8 265,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."
@@ 266,8 273,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
@@ 278,8 284,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,
@@ 301,6 306,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)
M tests/builders.scm => tests/builders.scm +83 -3
@@ 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.
;;;
@@ 23,15 24,15 @@
#:use-module (guix build-system gnu)
#:use-module (guix build gnu-build-system)
#:use-module (guix build utils)
+ #: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 (ice-9 textual-ports)
@@ 111,4 112,83 @@
(call-with-input-file name get-string-all))))))))
compressors)
+
+;;;
+;;; Test the sanity-check phase of the Python build system.
+;;;
+
+(define* (make-python-dummy name #:key (setup-py-extra "")
+ (init-py "") (use-setuptools? #t))
+ (dummy-package (string-append "python-dummy-" name)
+ (version "0.1")
+ (build-system python-build-system)
+ (arguments
+ `(#:tests? #f
+ #:use-setuptools? ,use-setuptools?
+ #:phases
+ (modify-phases %standard-phases
+ (replace 'unpack
+ (lambda _
+ (mkdir-p "dummy")
+ (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))))))))))
+
+(define python-dummy-ok
+ (make-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 python-dummy-fail-requirements
+ (make-python-dummy "fail-requirements"
+ #:setup-py-extra "install_requires=['nonexistent'],"))
+
+(define python-dummy-fail-import
+ (make-python-dummy "fail-import" #:init-py "import nonexistent"))
+
+(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 (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))
+ (for-each (lambda (p) (check-build-failure store p))
+ (list
+ python-dummy-fail-requirements
+ python-dummy-fail-import
+ python-dummy-fail-console-script)))
+
(test-end "builders")