summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLars-Dominik Braun <lars@6xq.net>2022-04-23 11:36:55 +0200
committerMarius Bakke <marius@gnu.org>2022-10-27 19:43:06 +0200
commit400a7a4c80efbde1905ae98a298bbb5882d46a0d (patch)
tree151375e9dab1ace78459c2e1e852991e9bdb6acb
parentb4e2effb30bdcbab00dbe1af3e2b9d4ad446897e (diff)
build-system: Add pyproject-build-system.
This is an experimental build system based on python-build-system that implements PEP 517-compliant builds. * doc/guix.texi (Build Systems): Add pyproject-build-system section. * doc/contributing.texi (Python Modules): Mention pyproject.toml and the PYTHON-TOOLCHAIN package, as well as differences to python-build-system. * guix/build-system/pyproject.scm, guix/build/pyproject-build-system.scm, gnu/packages/aux-files/python/sanity-check-next.py, gnu/packages/python-commencement.scm: New files. * Makefile.am (MODULES): Register the new build systems. * gnu/local.mk (GNU_SYSTEM_MODULES): Add python-commencement.scm. * gnu/packages/python.scm (python-sans-pip, python-sans-pip-wrapper): New variables. Co-authored-by: Marius Bakke <marius@gnu.org>
-rw-r--r--Makefile.am2
-rw-r--r--doc/contributing.texi36
-rw-r--r--doc/guix.texi29
-rw-r--r--gnu/packages/aux-files/python/sanity-check-next.py99
-rw-r--r--gnu/packages/python.scm35
-rw-r--r--guix/build-system/pyproject.scm147
-rw-r--r--guix/build/pyproject-build-system.scm393
7 files changed, 732 insertions, 9 deletions
diff --git a/Makefile.am b/Makefile.am
index 22dcc43f99d..6ccb790c118 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -166,6 +166,7 @@ MODULES = \
166 guix/build-system/maven.scm \ 166 guix/build-system/maven.scm \
167 guix/build-system/node.scm \ 167 guix/build-system/node.scm \
168 guix/build-system/perl.scm \ 168 guix/build-system/perl.scm \
169 guix/build-system/pyproject.scm \
169 guix/build-system/python.scm \ 170 guix/build-system/python.scm \
170 guix/build-system/renpy.scm \ 171 guix/build-system/renpy.scm \
171 guix/build-system/ocaml.scm \ 172 guix/build-system/ocaml.scm \
@@ -222,6 +223,7 @@ MODULES = \
222 guix/build/minetest-build-system.scm \ 223 guix/build/minetest-build-system.scm \
223 guix/build/node-build-system.scm \ 224 guix/build/node-build-system.scm \
224 guix/build/perl-build-system.scm \ 225 guix/build/perl-build-system.scm \
226 guix/build/pyproject-build-system.scm \
225 guix/build/python-build-system.scm \ 227 guix/build/python-build-system.scm \
226 guix/build/ocaml-build-system.scm \ 228 guix/build/ocaml-build-system.scm \
227 guix/build/qt-build-system.scm \ 229 guix/build/qt-build-system.scm \
diff --git a/doc/contributing.texi b/doc/contributing.texi
index 4b1eed1cb11..c3221d23e4a 100644
--- a/doc/contributing.texi
+++ b/doc/contributing.texi
@@ -786,12 +786,29 @@ for instance, the module python-dateutil is packaged under the names
786starts with @code{py} (e.g.@: @code{pytz}), we keep it and prefix it as 786starts with @code{py} (e.g.@: @code{pytz}), we keep it and prefix it as
787described above. 787described above.
788 788
789@quotation Note
790Currently there are two different build systems for Python packages in Guix:
791@var{python-build-system} and @var{pyproject-build-system}. For the
792longest time, Python packages were built from an informally specified
793@file{setup.py} file. That worked amazingly well, considering Python's
794success, but was difficult to build tooling around. As a result, a host
795of alternative build systems emerged and the community eventually settled on a
796@url{https://peps.python.org/pep-0517/, formal standard} for specifying build
797requirements. @var{pyproject-build-system} is Guix's implementation of this
798standard. It is considered ``experimental'' in that it does not yet support
799all the various PEP-517 @emph{build backends}, but you are encouraged to try
800it for new Python packages and report any problems. It will eventually be
801deprecated and merged into @var{python-build-system}.
802@end quotation
803
789@subsubsection Specifying Dependencies 804@subsubsection Specifying Dependencies
790@cindex inputs, for Python packages 805@cindex inputs, for Python packages
791 806
792Dependency information for Python packages is usually available in the 807Dependency information for Python packages is usually available in the
793package source tree, with varying degrees of accuracy: in the 808package source tree, with varying degrees of accuracy: in the
794@file{setup.py} file, in @file{requirements.txt}, or in @file{tox.ini}. 809@file{pyproject.toml} file, the @file{setup.py} file, in
810@file{requirements.txt}, or in @file{tox.ini} (the latter mostly for
811test dependencies).
795 812
796Your mission, when writing a recipe for a Python package, is to map 813Your mission, when writing a recipe for a Python package, is to map
797these dependencies to the appropriate type of ``input'' (@pxref{package 814these dependencies to the appropriate type of ``input'' (@pxref{package
@@ -802,10 +819,12 @@ following check list to determine which dependency goes where.
802@itemize 819@itemize
803 820
804@item 821@item
805We currently package Python 2 with @code{setuptools} and @code{pip} 822We currently package Python with @code{setuptools} and @code{pip}
806installed like Python 3.4 has per default. Thus you don't need to 823installed per default. This is about to change, and users are encouraged
807specify either of these as an input. @command{guix lint} will warn you 824to use @code{python-toolchain} if they want a build environment for Python.
808if you do. 825
826@command{guix lint} will warn if @code{setuptools} or @code{pip} are
827added as native-inputs because they are generally not necessary.
809 828
810@item 829@item
811Python dependencies required at run time go into 830Python dependencies required at run time go into
@@ -814,9 +833,10 @@ Python dependencies required at run time go into
814@file{requirements.txt} file. 833@file{requirements.txt} file.
815 834
816@item 835@item
817Python packages required only at build time---e.g., those listed with 836Python packages required only at build time---e.g., those listed under
818the @code{setup_requires} keyword in @file{setup.py}---or only for 837@code{build-system.requires} in @file{pyproject.toml} or with the
819testing---e.g., those in @code{tests_require}---go into 838@code{setup_requires} keyword in @file{setup.py}---or dependencies only
839for testing---e.g., those in @code{tests_require} or @file{tox.ini}---go into
820@code{native-inputs}. The rationale is that (1) they do not need to be 840@code{native-inputs}. The rationale is that (1) they do not need to be
821propagated because they are not needed at run time, and (2) in a 841propagated because they are not needed at run time, and (2) in a
822cross-compilation context, it's the ``native'' input that we'd want. 842cross-compilation context, it's the ``native'' input that we'd want.
diff --git a/doc/guix.texi b/doc/guix.texi
index 2f7ab61aecd..3bfb89bc33a 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -9313,7 +9313,36 @@ instead of the default @code{"out"} output. This is useful for packages that
9313include a Python package as only a part of the software, and thus want to 9313include a Python package as only a part of the software, and thus want to
9314combine the phases of @code{python-build-system} with another build system. 9314combine the phases of @code{python-build-system} with another build system.
9315Python bindings are a common usecase. 9315Python bindings are a common usecase.
9316@end defvr
9317
9318@defvr {Scheme Variable} pyproject-build-system
9319This is a variable exported by @code{guix build-system pyproject}. It
9320is based on @var{python-build-system}, and adds support for
9321@file{pyproject.toml} and @url{https://peps.python.org/pep-0517/, PEP 517}.
9322It also supports a variety of build backends and test frameworks.
9323
9324The API is slightly different from @var{python-build-system}:
9325@itemize
9326@item
9327@code{#:use-setuptools?} and @code{#:test-target} is removed.
9328@item
9329@code{#:build-backend} is added. It defaults to @code{#false} and will try
9330to guess the appropriate backend based on @file{pyproject.toml}.
9331@item
9332@code{#:test-backend} is added. It defaults to @code{#false} and will guess
9333an appropriate test backend based on what is available in package inputs.
9334@item
9335@code{#:test-flags} is added. The default is @code{#false}, and varies based
9336on the detected @code{#:test-backend}.
9337@end itemize
9338
9339It is considered ``experimental'' in that the implementation details are
9340not set in stone yet, however users are encouraged to try it for new
9341Python projects (even those using @file{setup.py}). The API is subject to
9342change, but any breaking changes in the Guix channel will be dealt with.
9316 9343
9344Eventually this build system will be deprecated and merged back into
9345@var{python-build-system}, probably some time in 2024.
9317@end defvr 9346@end defvr
9318 9347
9319@defvr {Scheme Variable} perl-build-system 9348@defvr {Scheme Variable} perl-build-system
diff --git a/gnu/packages/aux-files/python/sanity-check-next.py b/gnu/packages/aux-files/python/sanity-check-next.py
new file mode 100644
index 00000000000..a206b51d0a0
--- /dev/null
+++ b/gnu/packages/aux-files/python/sanity-check-next.py
@@ -0,0 +1,99 @@
1# -*- coding: utf-8 -*-
2# GNU Guix --- Functional package management for GNU
3# Copyright © 2021, 2022 Lars-Dominik Braun <lars@6xq.net>
4#
5# This file is part of GNU Guix.
6#
7# GNU Guix is free software; you can redistribute it and/or modify it
8# under the terms of the GNU General Public License as published by
9# the Free Software Foundation; either version 3 of the License, or (at
10# your option) any later version.
11#
12# GNU Guix is distributed in the hope that it will be useful, but
13# WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
19
20# This version adds a small change to accommodate missing python-setuptools.
21# TODO: Merge with sanity-check.py in the next core-updates cycle.
22
23from __future__ import print_function # Python 2 support.
24import importlib
25import sys
26import traceback
27try:
28 import pkg_resources
29except ImportError:
30 print('Warning: Skipping, because python-setuptools are not available.')
31 sys.exit(0)
32
33try:
34 from importlib.machinery import PathFinder
35except ImportError:
36 PathFinder = None
37
38ret = 0
39
40# Only check site-packages installed by this package, but not dependencies
41# (which pkg_resources.working_set would include). Path supplied via argv.
42ws = pkg_resources.find_distributions(sys.argv[1])
43
44for dist in ws:
45 print('validating', repr(dist.project_name), dist.location)
46 try:
47 print('...checking requirements: ', end='')
48 req = str(dist.as_requirement())
49 # dist.activate() is not enough to actually check requirements, we
50 # have to .require() it.
51 pkg_resources.require(req)
52 print('OK')
53 except Exception as e:
54 print('ERROR:', req, repr(e))
55 ret = 1
56 continue
57
58 # Try to load top level modules. This should not have any side-effects.
59 try:
60 metalines = dist.get_metadata_lines('top_level.txt')
61 except (KeyError, EnvironmentError):
62 # distutils (i.e. #:use-setuptools? #f) will not install any metadata.
63 # This file is also missing for packages built using a PEP 517 builder
64 # such as poetry.
65 print('WARNING: cannot determine top-level modules')
66 continue
67 for name in metalines:
68 # Only available on Python 3.
69 if PathFinder and PathFinder.find_spec(name) is None:
70 # Ignore unavailable modules, often C modules, which were not
71 # installed at the top-level. Cannot use ModuleNotFoundError,
72 # because it is raised by failed imports too.
73 continue
74 try:
75 print('...trying to load module', name, end=': ')
76 importlib.import_module(name)
77 print('OK')
78 except Exception:
79 print('ERROR:')
80 traceback.print_exc(file=sys.stdout)
81 ret = 1
82
83 # Try to load entry points of console scripts too, making sure they
84 # work. They should be removed if they don't. Other groups may not be
85 # safe, as they can depend on optional packages.
86 for group, v in dist.get_entry_map().items():
87 if group not in {'console_scripts', 'gui_scripts'}:
88 continue
89 for name, ep in v.items():
90 try:
91 print('...trying to load endpoint', group, name, end=': ')
92 ep.load()
93 print('OK')
94 except Exception:
95 print('ERROR:')
96 traceback.print_exc(file=sys.stdout)
97 ret = 1
98
99sys.exit(ret)
diff --git a/gnu/packages/python.scm b/gnu/packages/python.scm
index 00c3eb7774e..0c164d7991d 100644
--- a/gnu/packages/python.scm
+++ b/gnu/packages/python.scm
@@ -26,7 +26,7 @@
26;;; Copyright © 2016, 2017 Nikita <nikita@n0.is> 26;;; Copyright © 2016, 2017 Nikita <nikita@n0.is>
27;;; Copyright © 2016 Dylan Jeffers <sapientech@sapientech@openmailbox.org> 27;;; Copyright © 2016 Dylan Jeffers <sapientech@sapientech@openmailbox.org>
28;;; Copyright © 2016 David Craven <david@craven.ch> 28;;; Copyright © 2016 David Craven <david@craven.ch>
29;;; Copyright © 2016, 2017, 2018, 2019, 2020, 2021 Marius Bakke <marius@gnu.org> 29;;; Copyright © 2016-2022 Marius Bakke <marius@gnu.org>
30;;; Copyright © 2016, 2017 Stefan Reichör <stefan@xsteve.at> 30;;; Copyright © 2016, 2017 Stefan Reichör <stefan@xsteve.at>
31;;; Copyright © 2016, 2017 Alex Vong <alexvong1995@gmail.com> 31;;; Copyright © 2016, 2017 Alex Vong <alexvong1995@gmail.com>
32;;; Copyright © 2016, 2017, 2018 Arun Isaac <arunisaac@systemreboot.net> 32;;; Copyright © 2016, 2017, 2018 Arun Isaac <arunisaac@systemreboot.net>
@@ -60,6 +60,7 @@
60;;; Copyright © 2020, 2021 Greg Hogan <code@greghogan.com> 60;;; Copyright © 2020, 2021 Greg Hogan <code@greghogan.com>
61;;; Copyright © 2022 Philip McGrath <philip@philipmcgrath.com> 61;;; Copyright © 2022 Philip McGrath <philip@philipmcgrath.com>
62;;; Copyright © 2022 jgart <jgart@dismail.de> 62;;; Copyright © 2022 jgart <jgart@dismail.de>
63;;; Copyright © 2021 Lars-Dominik Braun <lars@6xq.net>
63;;; 64;;;
64;;; This file is part of GNU Guix. 65;;; This file is part of GNU Guix.
65;;; 66;;;
@@ -87,6 +88,7 @@
87 #:use-module (gnu packages hurd) 88 #:use-module (gnu packages hurd)
88 #:use-module (gnu packages libffi) 89 #:use-module (gnu packages libffi)
89 #:use-module (gnu packages pkg-config) 90 #:use-module (gnu packages pkg-config)
91 #:use-module (gnu packages python-build)
90 #:use-module (gnu packages readline) 92 #:use-module (gnu packages readline)
91 #:use-module (gnu packages sqlite) 93 #:use-module (gnu packages sqlite)
92 #:use-module (gnu packages tcl) 94 #:use-module (gnu packages tcl)
@@ -674,6 +676,37 @@ and the unversioned commands available.")))
674(define-public python-wrapper (wrap-python3 python)) 676(define-public python-wrapper (wrap-python3 python))
675(define-public python-minimal-wrapper (wrap-python3 python-minimal)) 677(define-public python-minimal-wrapper (wrap-python3 python-minimal))
676 678
679;; The Python used in pyproject-build-system.
680(define-public python-sans-pip
681 (hidden-package
682 (package/inherit python
683 (arguments
684 (substitute-keyword-arguments (package-arguments python)
685 ((#:configure-flags flags #~())
686 #~(append '("--with-ensurepip=no")
687 (delete "--with-ensurepip=install" #$flags))))))))
688
689(define-public python-sans-pip-wrapper
690 (wrap-python3 python-sans-pip))
691
692(define-public python-toolchain
693 (let ((base (package/inherit python-sans-pip-wrapper)))
694 (package
695 (inherit base)
696 (properties '())
697 (name "python-toolchain")
698 (propagated-inputs
699 (modify-inputs (package-propagated-inputs base)
700 (append python-pip
701 python-pypa-build
702 python-setuptools
703 python-wheel)))
704 (synopsis "Python toolchain")
705 (description
706 "Python toolchain including Python itself, setuptools and pip.
707Use this package if you need a minimal Python toolchain instead of just
708the interpreter."))))
709
677(define-public micropython 710(define-public micropython
678 (package 711 (package
679 (name "micropython") 712 (name "micropython")
diff --git a/guix/build-system/pyproject.scm b/guix/build-system/pyproject.scm
new file mode 100644
index 00000000000..1e365d4f218
--- /dev/null
+++ b/guix/build-system/pyproject.scm
@@ -0,0 +1,147 @@
1;;; GNU Guix --- Functional package management for GNU
2;;; Copyright © 2021 Lars-Dominik Braun <lars@6xq.net>
3;;; Copyright © 2022 Marius Bakke <marius@gnu.org>
4;;;
5;;; This file is part of GNU Guix.
6;;;
7;;; GNU Guix is free software; you can redistribute it and/or modify it
8;;; under the terms of the GNU General Public License as published by
9;;; the Free Software Foundation; either version 3 of the License, or (at
10;;; your option) any later version.
11;;;
12;;; GNU Guix is distributed in the hope that it will be useful, but
13;;; WITHOUT ANY WARRANTY; without even the implied warranty of
14;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15;;; GNU General Public License for more details.
16;;;
17;;; You should have received a copy of the GNU General Public License
18;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
19
20(define-module (guix build-system pyproject)
21 #:use-module ((gnu packages) #:select (search-auxiliary-file))
22 #:use-module (guix gexp)
23 #:use-module (guix store)
24 #:use-module (guix utils)
25 #:use-module (guix memoization)
26 #:use-module (guix gexp)
27 #:use-module (guix monads)
28 #:use-module (guix packages)
29 #:use-module (guix derivations)
30 #:use-module (guix search-paths)
31 #:use-module (guix build-system)
32 #:use-module (guix build-system gnu)
33 #:use-module (guix build-system python)
34 #:use-module (ice-9 match)
35 #:use-module (srfi srfi-1)
36 #:use-module (srfi srfi-26)
37 #:export (%pyproject-build-system-modules
38 default-python
39 pyproject-build
40 pyproject-build-system))
41
42;; Commentary:
43;;
44;; Standard build procedure for Python packages using 'pyproject.toml'.
45;; This is implemented as an extension of 'python-build-system'.
46;;
47;; Code:
48
49(define %pyproject-build-system-modules
50 ;; Build-side modules imported by default.
51 `((guix build pyproject-build-system)
52 (guix build json)
53 ,@%python-build-system-modules))
54
55(define (default-python)
56 "Return the default Python package."
57 ;; Lazily resolve the binding to avoid a circular dependency.
58 (let ((python (resolve-interface '(gnu packages python))))
59 (module-ref python 'python-toolchain)))
60
61(define sanity-check.py
62 ;; TODO: Merge with sanity-check.py in the next rebuild cycle.
63 (search-auxiliary-file "python/sanity-check-next.py"))
64
65(define* (lower name
66 #:key source inputs native-inputs outputs system target
67 (python (default-python))
68 #:allow-other-keys
69 #:rest arguments)
70 "Return a bag for NAME."
71 (define private-keywords
72 '(#:target #:python #:inputs #:native-inputs))
73
74 (and (not target) ;XXX: no cross-compilation
75 (bag
76 (name name)
77 (system system)
78 (host-inputs `(,@(if source
79 `(("source" ,source))
80 '())
81 ,@inputs
82
83 ;; Keep the standard inputs of 'gnu-build-system'.
84 ,@(standard-packages)))
85 (build-inputs `(("python" ,python)
86 ("sanity-check.py" ,(local-file sanity-check.py))
87 ,@native-inputs))
88 (outputs (append outputs '(wheel)))
89 (build pyproject-build)
90 (arguments (strip-keyword-arguments private-keywords arguments)))))
91
92(define* (pyproject-build name inputs
93 #:key source
94 (tests? #t)
95 (configure-flags ''())
96 (build-backend #f)
97 (test-backend #f)
98 (test-flags #f)
99 (phases '%standard-phases)
100 (outputs '("out" "wheel"))
101 (search-paths '())
102 (system (%current-system))
103 (guile #f)
104 (imported-modules %pyproject-build-system-modules)
105 (modules '((guix build pyproject-build-system)
106 (guix build utils))))
107 "Build SOURCE using PYTHON, and with INPUTS."
108 (define build
109 (with-imported-modules imported-modules
110 #~(begin
111 (use-modules #$@(sexp->gexp modules))
112
113 #$(with-build-variables inputs outputs
114 #~(pyproject-build
115 #:name #$name
116 #:source #+source
117 #:configure-flags #$configure-flags
118 #:system #$system
119 #:build-backend #$build-backend
120 #:test-backend #$test-backend
121 #:test-flags #$test-flags
122 #:tests? #$tests?
123 #:phases #$(if (pair? phases)
124 (sexp->gexp phases)
125 phases)
126 #:outputs %outputs
127 #:search-paths '#$(sexp->gexp
128 (map search-path-specification->sexp
129 search-paths))
130 #:inputs %build-inputs)))))
131
132
133 (mlet %store-monad ((guile (package->derivation (or guile (default-guile))
134 system #:graft? #f)))
135 (gexp->derivation name build
136 #:system system
137 #:graft? #f ;consistent with 'gnu-build'
138 #:target #f
139 #:guile-for-build guile)))
140
141(define pyproject-build-system
142 (build-system
143 (name 'pyproject)
144 (description "The PEP517-compliant Python build system")
145 (lower lower)))
146
147;;; pyproject.scm ends here
diff --git a/guix/build/pyproject-build-system.scm b/guix/build/pyproject-build-system.scm
new file mode 100644
index 00000000000..d247fd77a81
--- /dev/null
+++ b/guix/build/pyproject-build-system.scm
@@ -0,0 +1,393 @@
1;;; GNU Guix --- Functional package management for GNU
2;;; Copyright © 2021 Lars-Dominik Braun <lars@6xq.net>
3;;; Copyright © 2022 Marius Bakke <marius@gnu.org>
4;;;
5;;; This file is part of GNU Guix.
6;;;
7;;; GNU Guix is free software; you can redistribute it and/or modify it
8;;; under the terms of the GNU General Public License as published by
9;;; the Free Software Foundation; either version 3 of the License, or (at
10;;; your option) any later version.
11;;;
12;;; GNU Guix is distributed in the hope that it will be useful, but
13;;; WITHOUT ANY WARRANTY; without even the implied warranty of
14;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15;;; GNU General Public License for more details.
16;;;
17;;; You should have received a copy of the GNU General Public License
18;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
19
20(define-module (guix build pyproject-build-system)
21 #:use-module ((guix build python-build-system) #:prefix python:)
22 #:use-module (guix build utils)
23 #:use-module (guix build json)
24 #:use-module (ice-9 match)
25 #:use-module (ice-9 ftw)
26 #:use-module (ice-9 format)
27 #:use-module (ice-9 rdelim)
28 #:use-module (ice-9 regex)
29 #:use-module (srfi srfi-1)
30 #:use-module (srfi srfi-26)
31 #:use-module (srfi srfi-34)
32 #:use-module (srfi srfi-35)
33 #:export (%standard-phases
34 add-installed-pythonpath
35 site-packages
36 python-version
37 pyproject-build))
38
39;;; Commentary:
40;;;
41;;; PEP 517-compatible build system for Python packages.
42;;;
43;;; PEP 517 mandates the use of a TOML file called pyproject.toml at the
44;;; project root, describing build and runtime dependencies, as well as the
45;;; build system, which can be different from setuptools. This module uses
46;;; that file to extract the build system used and call its wheel-building
47;;; entry point build_wheel (see 'build). setuptools’ wheel builder is
48;;; used as a fallback if either no pyproject.toml exists or it does not
49;;; declare a build-system. It supports config_settings through the
50;;; standard #:configure-flags argument.
51;;;
52;;; This wheel, which is just a ZIP file with a file structure defined
53;;; by PEP 427 (https://www.python.org/dev/peps/pep-0427/), is then unpacked
54;;; and its contents are moved to the appropriate locations in 'install.
55;;;
56;;; Then entry points, as defined by the PyPa Entry Point Specification
57;;; (https://packaging.python.org/specifications/entry-points/) are read
58;;; from a file called entry_points.txt in the package’s site-packages
59;;; subdirectory and scripts are written to bin/. These are not part of a
60;;; wheel and expected to be created by the installing utility.
61;;; TODO: Add support for PEP-621 entry points.
62;;;
63;;; Caveats:
64;;; - There is no support for in-tree build backends.
65;;;
66;;; Code:
67;;;
68
69;; Re-export these variables from python-build-system as many packages
70;; rely on these.
71(define python-version python:python-version)
72(define site-packages python:site-packages)
73(define add-installed-pythonpath python:add-installed-pythonpath)
74
75;; Base error type.
76(define-condition-type &python-build-error &error python-build-error?)
77
78;; Raised when 'check cannot find a valid test system in the inputs.
79(define-condition-type &test-system-not-found &python-build-error
80 test-system-not-found?)
81
82;; Raised when multiple wheels are created by 'build.
83(define-condition-type &cannot-extract-multiple-wheels &python-build-error
84 cannot-extract-multiple-wheels?)
85
86;; Raised, when no wheel has been built by the build system.
87(define-condition-type &no-wheels-built &python-build-error no-wheels-built?)
88
89(define* (build #:key outputs build-backend configure-flags #:allow-other-keys)
90 "Build a given Python package."
91
92 (define (pyproject.toml->build-backend file)
93 "Look up the build backend in a pyproject.toml file."
94 (call-with-input-file file
95 (lambda (in)
96 (let loop
97 ((line (read-line in
98 'concat)))
99 (if (eof-object? line) #f
100 (let ((m (string-match "build-backend = [\"'](.+)[\"']" line)))
101 (if m
102 (match:substring m 1)
103 (loop (read-line in
104 'concat)))))))))
105
106 (let* ((wheel-output (assoc-ref outputs "wheel"))
107 (wheel-dir (if wheel-output wheel-output "dist"))
108 ;; There is no easy way to get data from Guile into Python via
109 ;; s-expressions, but we have JSON serialization already, which Python
110 ;; also supports out-of-the-box.
111 (config-settings (call-with-output-string (cut write-json
112 configure-flags <>)))
113 ;; python-setuptools’ default backend supports setup.py *and*
114 ;; pyproject.toml. Allow overriding this automatic detection via
115 ;; build-backend.
116 (auto-build-backend (if (file-exists? "pyproject.toml")
117 (pyproject.toml->build-backend
118 "pyproject.toml") #f))
119 ;; Use build system detection here and not in importer, because a) we
120 ;; have alot of legacy packages and b) the importer cannot update arbitrary
121 ;; fields in case a package switches its build system.
122 (use-build-backend (or build-backend auto-build-backend
123 "setuptools.build_meta")))
124 (format #t
125 "Using '~a' to build wheels, auto-detected '~a', override '~a'.~%"
126 use-build-backend auto-build-backend build-backend)
127 (mkdir-p wheel-dir)
128 ;; Call the PEP 517 build function, which drops a .whl into wheel-dir.
129 (invoke "python"
130 "-c"
131 "import sys, importlib, json\nconfig_settings = json.loads (sys.argv[3])\nbuilder = importlib.import_module(sys.argv[1])\nbuilder.build_wheel(sys.argv[2], config_settings=config_settings)"
132 use-build-backend
133 wheel-dir
134 config-settings)))
135
136(define* (check #:key inputs
137 outputs
138 tests?
139 test-backend
140 test-flags
141 #:allow-other-keys)
142 "Run the test suite of a given Python package."
143 (if tests?
144 ;; Unfortunately with PEP 517 there is no common method to specify test
145 ;; systems. Guess test system based on inputs instead.
146 (let* ((pytest (which "pytest"))
147 (nosetests (which "nosetests"))
148 (nose2 (which "nose2"))
149 (have-setup-py (file-exists? "setup.py"))
150 (use-test-backend
151 (or test-backend
152 ;; Prefer pytest
153 (if pytest 'pytest #f)
154 (if nosetests 'nose #f)
155 (if nose2 'nose2 #f)
156 ;; But fall back to setup.py, which should work for most
157 ;; packages. XXX: would be nice not to depend on setup.py here?
158 ;; fails more often than not to find any tests at all. Maybe
159 ;; we can run `python -m unittest`?
160 (if have-setup-py 'setup.py #f))))
161 (format #t "Using ~a~%" use-test-backend)
162 (match use-test-backend
163 ('pytest
164 (apply invoke (cons pytest (or test-flags '("-vv")))))
165 ('nose
166 (apply invoke (cons nosetests (or test-flags '("-v")))))
167 ('nose2
168 (apply invoke (cons nose2 (or test-flags '("-v" "--pretty-assert")))))
169 ('setup.py
170 (apply invoke (append '("python" "setup.py")
171 (or test-flags '("test" "-v")))))
172 ;; The developer should explicitly disable tests in this case.
173 (else (raise (condition (&test-system-not-found))))))
174 (format #t "test suite not run~%")))
175
176(define* (install #:key inputs outputs #:allow-other-keys)
177 "Install a wheel file according to PEP 427"
178 ;; See https://www.python.org/dev/peps/pep-0427/#installing-a-wheel-distribution-1-0-py32-none-any-whl
179 (let ((site-dir (site-packages inputs outputs))
180 (python (assoc-ref inputs "python"))
181 (out (assoc-ref outputs "out")))
182 (define (extract file)
183 "Extract wheel (ZIP file) into site-packages directory"
184 ;; Use Python’s zipfile to avoid extra dependency
185 (invoke "python"
186 "-m"
187 "zipfile"
188 "-e"
189 file
190 site-dir))
191
192 (define python-hashbang
193 (string-append "#!" python "/bin/python"))
194
195 (define* (merge-directories source destination
196 #:optional (post-move #f))
197 "Move all files in SOURCE into DESTINATION, merging the two directories."
198 (format #t "Merging directory ~a into ~a~%" source destination)
199 (for-each (lambda (file)
200 (format #t
201 "~a/~a -> ~a/~a~%"
202 source
203 file
204 destination
205 file)
206 (mkdir-p destination)
207 (rename-file (string-append source "/" file)
208 (string-append destination "/" file))
209 (when post-move
210 (post-move file)))
211 (scandir source
212 (negate (cut member <>
213 '("." "..")))))
214 (rmdir source))
215
216 (define (expand-data-directory directory)
217 "Move files from all .data subdirectories to their respective\ndestinations."
218 ;; Python’s distutils.command.install defines this mapping from source to
219 ;; destination mapping.
220 (let ((source (string-append directory "/scripts"))
221 (destination (string-append out "/bin")))
222 (when (file-exists? source)
223 (merge-directories source destination
224 (lambda (f)
225 (let ((dest-path (string-append destination "/"
226 f)))
227 (chmod dest-path #o755)
228 (substitute* dest-path
229 (("#!python")
230 python-hashbang)))))))
231 ;; Data can be contained in arbitrary directory structures. Most
232 ;; commonly it is used for share/.
233 (let ((source (string-append directory "/data"))
234 (destination out))
235 (when (file-exists? source)
236 (merge-directories source destination)))
237 (let* ((distribution (car (string-split (basename directory) #\-)))
238 (source (string-append directory "/headers"))
239 (destination (string-append out "/include/python"
240 (python-version python) "/"
241 distribution)))
242 (when (file-exists? source)
243 (merge-directories source destination))))
244
245 (define (list-directories base predicate)
246 ;; Cannot use find-files here, because it’s recursive.
247 (scandir base
248 (lambda (name)
249 (let ((stat (lstat (string-append base "/" name))))
250 (and (not (member name
251 '("." "..")))
252 (eq? (stat:type stat)
253 'directory)
254 (predicate name stat))))))
255
256 (let* ((wheel-output (assoc-ref outputs "wheel"))
257 (wheel-dir (if wheel-output wheel-output "dist"))
258 (wheels (map (cut string-append wheel-dir "/" <>)
259 (scandir wheel-dir
260 (cut string-suffix? ".whl" <>)))))
261 (cond
262 ((> (length wheels) 1)
263 ;This code does not support multiple wheels
264 ;; yet, because their outputs would have to be
265 ;; merged properly.
266 (raise (condition (&cannot-extract-multiple-wheels))))
267 ((= (length wheels) 0)
268 (raise (condition (&no-wheels-built)))))
269 (for-each extract wheels))
270 (let ((datadirs (map (cut string-append site-dir "/" <>)
271 (list-directories site-dir
272 (file-name-predicate "\\.data$")))))
273 (for-each (lambda (directory)
274 (expand-data-directory directory)
275 (rmdir directory)) datadirs))))
276
277(define* (compile-bytecode #:key inputs outputs #:allow-other-keys)
278 "Compile installed byte-code in site-packages."
279 (let* ((site-dir (site-packages inputs outputs))
280 (python (assoc-ref inputs "python"))
281 (major-minor (map string->number
282 (take (string-split (python-version python) #\.) 2)))
283 (<3.7? (match major-minor
284 ((major minor)
285 (or (< major 3)
286 (and (= major 3)
287 (< minor 7)))))))
288 (if <3.7?
289 ;; These versions don’t have the hash invalidation modes and do
290 ;; not produce reproducible bytecode files.
291 (format #t "Skipping bytecode compilation for Python version ~a < 3.7~%"
292 (python-version python))
293 (invoke "python" "-m" "compileall"
294 "--invalidation-mode=unchecked-hash" site-dir))))
295
296(define* (create-entrypoints #:key inputs outputs (configure-flags '()) #:allow-other-keys)
297 "Implement Entry Points Specification
298(https://packaging.python.org/specifications/entry-points/) by PyPa,
299which creates runnable scripts in bin/ from entry point specification
300file entry_points.txt. This is necessary, because wheels do not contain
301these binaries and installers are expected to create them."
302
303 (define (entry-points.txt->entry-points file)
304 "Specialized parser for Python configfile-like files, in particular
305entry_points.txt. Returns a list of console_script and gui_scripts
306entry points."
307 (call-with-input-file file
308 (lambda (in)
309 (let loop ((line (read-line in))
310 (inside #f)
311 (result '()))
312 (if (eof-object? line)
313 result
314 (let* ((group-match (string-match "^\\[(.+)\\]$" line))
315 (group-name (if group-match
316 (match:substring group-match 1)
317 #f))
318 (next-inside (if (not group-name)
319 inside
320 (or (string=? group-name
321 "console_scripts")
322 (string=? group-name "gui_scripts"))))
323 (item-match (string-match
324 "^([^ =]+)\\s*=\\s*([^:]+):(.+)$" line)))
325 (if (and inside item-match)
326 (loop (read-line in)
327 next-inside
328 (cons (list (match:substring item-match 1)
329 (match:substring item-match 2)
330 (match:substring item-match 3))
331 result))
332 (loop (read-line in) next-inside result))))))))
333
334 (define (create-script path name module function)
335 "Create a Python script from an entry point’s NAME, MODULE and
336 FUNCTION and return write it to PATH/NAME."
337 (let ((interpreter (which "python"))
338 (file-path (string-append path "/" name)))
339 (format #t "Creating entry point for '~a.~a' at '~a'.~%"
340 module function file-path)
341 (call-with-output-file file-path
342 (lambda (port)
343 ;; Technically the script could also include search-paths,
344 ;; but having a generic 'wrap phases also handles manually
345 ;; written entry point scripts.
346 (format port "#!~a
347# Auto-generated entry point script.
348import sys
349import ~a as mod
350sys.exit (mod.~a ())~%" interpreter module function)))
351 (chmod file-path #o755)))
352
353 (let* ((site-dir (site-packages inputs outputs))
354 (out (assoc-ref outputs "out"))
355 (bin-dir (string-append out "/bin"))
356 (entry-point-files (find-files site-dir "^entry_points.txt$")))
357 (mkdir-p bin-dir)
358 (for-each (lambda (f)
359 (for-each (lambda (ep)
360 (apply create-script
361 (cons bin-dir ep)))
362 (entry-points.txt->entry-points f)))
363 entry-point-files)))
364
365(define* (set-SOURCE-DATE-EPOCH* #:rest _)
366 "Set the 'SOURCE_DATE_EPOCH' environment variable. This is used by tools
367that incorporate timestamps as a way to tell them to use a fixed timestamp.
368See https://reproducible-builds.org/specs/source-date-epoch/."
369 ;; Use a post-1980 timestamp because the Zip format used in wheels do
370 ;; not support timestamps before 1980.
371 (setenv "SOURCE_DATE_EPOCH" "315619200"))
372
373(define %standard-phases
374 ;; The build phase only builds C extensions and copies the Python sources,
375 ;; while the install phase copies then byte-compiles the sources to the
376 ;; prefix directory. The check phase is moved after the installation phase
377 ;; to ease testing the built package.
378 (modify-phases python:%standard-phases
379 (replace 'set-SOURCE-DATE-EPOCH set-SOURCE-DATE-EPOCH*)
380 (replace 'build build)
381 (replace 'install install)
382 (delete 'check)
383 ;; Must be before tests, so they can use installed packages’ entry points.
384 (add-before 'wrap 'create-entrypoints create-entrypoints)
385 (add-after 'wrap 'check check)
386 (add-before 'check 'compile-bytecode compile-bytecode)))
387
388(define* (pyproject-build #:key inputs (phases %standard-phases)
389 #:allow-other-keys #:rest args)
390 "Build the given Python package, applying all of PHASES in order."
391 (apply python:python-build #:inputs inputs #:phases phases args))
392
393;;; pyproject-build-system.scm ends here