diff options
Diffstat (limited to 'guix/build')
-rw-r--r-- | guix/build/python-build-system.scm | 150 |
1 files changed, 126 insertions, 24 deletions
diff --git a/guix/build/python-build-system.scm b/guix/build/python-build-system.scm index 9109fb4ac7..3f280b0ac0 100644 --- a/guix/build/python-build-system.scm +++ b/guix/build/python-build-system.scm @@ -3,6 +3,7 @@ ;;; Copyright © 2013 Andreas Enge <andreas@enge.fr> ;;; Copyright © 2013 Nikita Karetnikov <nikita@karetnikov.org> ;;; Copyright © 2015 Mark H Weaver <mhw@netris.org> +;;; Copyright © 2016 Hartmut Goebel <h.goebel@crazy-compilers.com> ;;; ;;; This file is part of GNU Guix. ;;; @@ -27,31 +28,119 @@ #:use-module (srfi srfi-1) #:use-module (srfi srfi-26) #:export (%standard-phases + add-installed-pythonpath + site-packages python-build)) ;; Commentary: ;; ;; Builder-side code of the standard Python package build procedure. ;; -;; Code: +;; +;; Backgound about the Python installation methods +;; +;; In Python there are different ways to install packages: distutils, +;; setuptools, easy_install and pip. All of these are sharing the file +;; setup.py, introduced with distutils in Python 2.0. The setup.py file can be +;; considered as a kind of Makefile accepting targets (or commands) like +;; "build" and "install". As of autumn 2016 the recommended way to install +;; Python packages is using pip. +;; +;; For both distutils and setuptools, running "python setup.py install" is the +;; way to install Python packages. With distutils the "install" command +;; basically copies all packages into <prefix>/lib/pythonX.Y/site-packages. +;; +;; Some time later "setuptools" was established to enhance distutils. To use +;; setuptools, the developer imports setuptools in setup.py. When importing +;; setuptools, the original "install" command gets overwritten by setuptools' +;; "install" command. +;; +;; The command-line tools easy_install and pip are both capable of finding and +;; downloading the package source from PyPI (the Python Package Index). Both +;; of them import setuptools and execute the "setup.py" file under their +;; control. Thus the "setup.py" behaves as if the developer had imported +;; setuptools within setup.py - even is still using only distutils. +;; +;; Setuptools' "install" command (to be more precise: the "easy_install" +;; command which is called by "install") will put the path of the currently +;; installed version of each package and it's dependencies (as declared in +;; setup.py) into an "easy-install.pth" file. In Guix each packages gets its +;; own "site-packages" directory and thus an "easy-install.pth" of its own. +;; To avoid conflicts, the python build system renames the file to +;; <packagename>.pth in the phase rename-pth-file. To ensure that Python will +;; process the .pth file, easy_install also creates a basic "site.py" in each +;; "site-packages" directory. The file is the same for all packages, thus +;; there is no need to rename it. For more information about .pth files and +;; the site module, please refere to +;; https://docs.python.org/3/library/site.html. +;; +;; The .pth files contain the file-system paths (pointing to the store) of all +;; dependencies. So the dependency is hidden in the .pth file but is not +;; visible in the file-system. Now if packages A and B both required packages +;; P, but in different versions, Guix will not detect this when installing +;; both A and B to a profile. (For details and example see +;; https://lists.gnu.org/archive/html/guix-devel/2016-10/msg01233.html.) +;; +;; Pip behaves a bit different then easy_install: it always executes +;; "setup.py" with the option "--single-version-externally-managed" set. This +;; makes setuptools' "install" command run the original "install" command +;; instead of the "easy_install" command, so no .pth file (and no site.py) +;; will be created. The "site-packages" directory only contains the package +;; and the related .egg-info directory. +;; +;; This is exactly what we need for Guix and this is what we mimic in the +;; install phase below. +;; +;; As a draw back, the magic of the .pth file of linking to the other required +;; packages is gone and these packages have now to be declared as +;; "propagated-inputs". +;; +;; Note: Importing setuptools also adds two sub-commands: "install_egg_info" +;; and "install_scripts". These sub-commands are executed even if +;; "--single-version-externally-managed" is set, thus the .egg-info directory +;; and the scripts defined in entry-points will always be created. + +(define setuptools-shim + ;; Run setup.py with "setuptools" being imported, which will patch + ;; "distutils". This is needed for packages using "distutils" instead of + ;; "setuptools" since the former does not understand the + ;; "--single-version-externally-managed" flag. + ;; Python code taken from pip 9.0.1 pip/utils/setuptools_build.py + (string-append + "import setuptools, tokenize;__file__='setup.py';" + "f=getattr(tokenize, 'open', open)(__file__);" + "code=f.read().replace('\\r\\n', '\\n');" + "f.close();" + "exec(compile(code, __file__, 'exec'))")) -(define (call-setuppy command params) +(define (call-setuppy command params use-setuptools?) (if (file-exists? "setup.py") (begin (format #t "running \"python setup.py\" with command ~s and parameters ~s~%" command params) - (zero? (apply system* "python" "setup.py" command params))) + (if use-setuptools? + (zero? (apply system* "python" "-c" setuptools-shim + command params)) + (zero? (apply system* "python" "./setup.py" command params)))) (error "no setup.py found"))) -(define* (build #:rest empty) +(define* (build #:key use-setuptools? #:allow-other-keys) "Build a given Python package." - (call-setuppy "build" '())) + (call-setuppy "build" '() use-setuptools?)) -(define* (check #:key tests? test-target #:allow-other-keys) +(define* (check #:key tests? test-target use-setuptools? #:allow-other-keys) "Run the test suite of a given Python package." (if tests? - (call-setuppy test-target '()) + ;; Running `setup.py test` creates an additional .egg-info directory in + ;; build/lib in some cases, e.g. if the source is in a sub-directory + ;; (given with `package_dir`). This will by copied to the output, too, + ;; so we need to remove. + (let ((before (find-files "build" "\\.egg-info$" #:directories? #t))) + (call-setuppy test-target '() use-setuptools?) + (let* ((after (find-files "build" "\\.egg-info$" #:directories? #t)) + (inter (lset-difference eqv? after before))) + (for-each delete-file-recursively inter))) #t)) (define (get-python-version python) @@ -60,25 +149,36 @@ (major+minor (take components 2))) (string-join major+minor "."))) -(define* (install #:key outputs inputs (configure-flags '()) +(define (site-packages inputs outputs) + "Return the path of the current output's Python site-package." + (let* ((out (assoc-ref outputs "out")) + (python (assoc-ref inputs "python"))) + (string-append out "/lib/python" + (get-python-version python) + "/site-packages/"))) + +(define (add-installed-pythonpath inputs outputs) + "Prepend the Python site-package of OUTPUT to PYTHONPATH. This is useful +when running checks after installing the package." + (let ((old-path (getenv "PYTHONPATH")) + (add-path (site-packages inputs outputs))) + (setenv "PYTHONPATH" + (string-append add-path + (if old-path (string-append ":" old-path) ""))) + #t)) + +(define* (install #:key outputs (configure-flags '()) use-setuptools? #:allow-other-keys) "Install a given Python package." (let* ((out (assoc-ref outputs "out")) (params (append (list (string-append "--prefix=" out)) - configure-flags)) - (python-version (get-python-version (assoc-ref inputs "python"))) - (old-path (getenv "PYTHONPATH")) - (add-path (string-append out "/lib/python" python-version - "/site-packages/"))) - ;; create the module installation directory and add it to PYTHONPATH - ;; to make setuptools happy - (mkdir-p add-path) - (setenv "PYTHONPATH" - (string-append (if old-path - (string-append old-path ":") - "") - add-path)) - (call-setuppy "install" params))) + (if use-setuptools? + ;; distutils does not accept these flags + (list "--single-version-externally-managed" + "--root=/") + '()) + configure-flags))) + (call-setuppy "install" params use-setuptools?))) (define* (wrap #:key inputs outputs #:allow-other-keys) (define (list-of-files dir) @@ -112,6 +212,9 @@ (define* (rename-pth-file #:key name inputs outputs #:allow-other-keys) "Rename easy-install.pth to NAME.pth to avoid conflicts between packages installed with setuptools." + ;; Even if the "easy-install.pth" is not longer created, we kept this phase. + ;; There still may be packages creating an "easy-install.pth" manually for + ;; some good reason. (let* ((out (assoc-ref outputs "out")) (python (assoc-ref inputs "python")) (site-packages (string-append out "/lib/python" @@ -137,8 +240,7 @@ installed with setuptools." #t)) (define %standard-phases - ;; 'configure' and 'build' phases are not needed. Everything is done during - ;; 'install'. + ;; 'configure' phase is not needed. (modify-phases gnu:%standard-phases (add-after 'unpack 'ensure-no-mtimes-pre-1980 ensure-no-mtimes-pre-1980) (delete 'configure) |