build/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: (make-python-dummy)
(dummy-ok, dummy-dummy-nosetuptools, dummy-fail-requirements)
(dummy-fail-import, dummy-fail-console-script): New variables.
("python-build-system: dummy-ok")
("python-build-system: dummy-dummy-nosetuptools")
("python-build-system: dummy-fail-requirements")
("python-build-system: dummy-fail-import")
("python-build-system: dummy-fail-console-script"): Add tests.
This commit is contained in:
Lars-Dominik Braun 2021-01-03 10:30:29 +01:00 committed by Maxim Cournoyer
parent 8a15ecf0e3
commit 09448c0994
No known key found for this signature in database
GPG key ID: 1260E46482E63562
5 changed files with 199 additions and 13 deletions

View file

@ -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

View file

@ -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)

View file

@ -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 @@ (define (default-python2)
(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 @@ (define private-keywords
;; 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)

View file

@ -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 @@ (define (call-setuppy command params use-setuptools?)
(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 @@ (define* (install #:key inputs outputs (configure-flags '()) use-setuptools?
;; '--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 @@ (define* (rename-pth-file #:key name inputs outputs #:allow-other-keys)
(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 @@ (define* (ensure-no-mtimes-pre-1980 #:rest _)
(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 @@ (define* (enable-bytecode-determinism #:rest _)
(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 @@ (define* (ensure-no-cythonized-files #:rest _)
(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 @@ (define %standard-phases
(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)

View file

@ -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 @@ (define-module (tests builders)
#: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 @@ (define compressors '(("gzip" . "gz")
(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")