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.master
parent
8a15ecf0e3
commit
09448c0994
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
|
@ -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.
|
||||
;;;
|
||||
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
|
Reference in New Issue