1
0
forked from tribes/guix
Files
guix-fork/guix/build/pyproject-build-system.scm
Nicolas Graves c2d03a928c tests: builders: Switch to pyproject-build-system.
* guix/build/pyproject-build-system.scm (set-version): Ensure source
is not #f before using it.

* tests/builders.scm
(make-python-dummy)[build-system]: Switch to pyproject-build-system.
[native-inputs]: Add python-setuptools.
(python-dummy-no-setuptools): Drop it.
(check-build-success, check-build-failure): Refresh tests.
(check-build-failure): Drop python-dummy-no-setuptools.

Change-Id: I892b45c34b506ff27634e2ef706009dc81e831ec
Signed-off-by: Sharlatan Hellseher <sharlatanus@gmail.com>
2026-04-16 09:59:11 +01:00

644 lines
28 KiB
Scheme
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2021-2023 Lars-Dominik Braun <lars@6xq.net>
;;; Copyright © 2022 Marius Bakke <marius@gnu.org>
;;; Copyright © 2024-2025 Maxim Cournoyer <maxim@guixotic.coop>
;;; Copyright © 2024-2026 Nicolas Graves <ngraves@ngraves.fr>
;;; Copyright © 2026 Nguyễn Gia Phong <cnx@loang.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/>.
(define-module (guix build pyproject-build-system)
#:autoload (json builder) (scm->json scm->json-string)
#:use-module ((guix build gnu-build-system) #:prefix gnu:)
#:use-module (guix build utils)
#:use-module (guix build toml)
#:use-module (ice-9 match)
#:use-module (ice-9 ftw)
#:use-module (ice-9 format)
#:use-module (ice-9 rdelim)
#:use-module (ice-9 regex)
#:use-module (srfi srfi-1)
#:use-module (srfi srfi-2)
#:use-module (srfi srfi-26)
#:use-module (srfi srfi-34)
#:use-module (srfi srfi-35)
#:use-module (srfi srfi-71)
#:export (%standard-phases
add-installed-pythonpath
ensure-no-mtimes-pre-1980
site-packages
python-version
pyproject-build))
;;; Commentary:
;;;
;;; PEP 517-compatible build system for Python packages.
;;;
;;; PEP 517 mandates the use of a TOML file called pyproject.toml at the
;;; project root, describing build and runtime dependencies, as well as the
;;; build system, which can be different from setuptools. This module uses
;;; that file to extract the build system used and call its wheel-building
;;; entry point build_wheel (see 'build). setuptools wheel builder is
;;; used as a fallback if either no pyproject.toml exists or it does not
;;; declare a build-system. It supports config_settings through the
;;; standard #:configure-flags argument.
;;;
;;; This wheel, which is just a ZIP file with a file structure defined
;;; by PEP 427 (https://www.python.org/dev/peps/pep-0427/), is then unpacked
;;; and its contents are moved to the appropriate locations in 'install.
;;;
;;; Then entry points, as defined by the PyPa Entry Point Specification
;;; (https://packaging.python.org/specifications/entry-points/) are read
;;; from a file called entry_points.txt in the packages site-packages
;;; subdirectory and scripts are written to bin/. These are not part of a
;;; wheel and expected to be created by the installing utility.
;;; TODO: Add support for PEP-621 entry points.
;;;
;;; This module also supports in-tree build backends, which can be
;;; overridden by #:backend-path.
;;;
;;; Code:
;;;
;; Copy these procedures from python-build-system as many packages
;; rely on these.
(define (python-version python)
(let* ((version (last (string-split python #\-)))
(components (string-split version #\.))
(major+minor (take components 2)))
(string-join major+minor ".")))
(define (python-output outputs)
"Return the path of the python output, if there is one, or fall-back to out."
(or (assoc-ref outputs "python")
(assoc-ref outputs "out")))
(define (site-packages inputs outputs)
"Return the path of the current output's Python site-package."
(let ((out (python-output outputs))
(python (assoc-ref inputs "python")))
(string-append out "/lib/python" (python-version python) "/site-packages")))
(define (add-installed-pythonpath inputs outputs)
"Prepend the site-package of OUTPUT to GUIX_PYTHONPATH. This is useful when
running checks after installing the package."
(setenv "GUIX_PYTHONPATH" (string-append (site-packages inputs outputs) ":"
(getenv "GUIX_PYTHONPATH"))))
(define* (add-install-to-pythonpath #:key inputs outputs #:allow-other-keys)
"A phase that just wraps the 'add-installed-pythonpath' procedure."
(add-installed-pythonpath inputs outputs))
(define* (add-install-to-path #:key outputs #:allow-other-keys)
"Adding Python scripts to PATH is also often useful in tests."
(setenv "PATH" (string-append (assoc-ref outputs "out")
"/bin:"
(getenv "PATH"))))
;; Base error type.
(define-condition-type &python-build-error &error python-build-error?)
;; Raised when 'check cannot find a valid test system in the inputs.
(define-condition-type &test-system-not-found &python-build-error
test-system-not-found?)
;; Raised when multiple wheels are created by 'build.
(define-condition-type &cannot-extract-multiple-wheels &python-build-error
cannot-extract-multiple-wheels?)
;; Raised, when no wheel has been built by the build system.
(define-condition-type &no-wheels-built &python-build-error no-wheels-built?)
;; Raised, when no installation candidate wheel has been found.
(define-condition-type &no-wheels-found &python-build-error no-wheels-found?)
(define* (ensure-no-mtimes-pre-1980 #:rest _)
"Ensure that there are no mtimes before 1980-01-02 in the source tree."
;; Rationale: patch-and-repack creates tarballs with timestamps at the POSIX
;; epoch, 1970-01-01 UTC. This causes problems with Python packages,
;; because Python eggs are ZIP files, and the ZIP format does not support
;; timestamps before 1980.
(let ((early-1980 315619200)) ; 1980-01-02 UTC
(ftw "." (lambda (file stat flag)
(unless (or (<= early-1980 (stat:mtime stat))
(eq? (stat:type stat) 'symlink))
(utime file early-1980 early-1980))
#t))))
(define* (enable-bytecode-determinism #:rest _)
"Improve determinism of pyc files."
;; Use deterministic hashes for strings, bytes, and datetime objects.
(setenv "PYTHONHASHSEED" "0")
;; Prevent Python from creating .pyc files when loading modules (such as
;; when running a test suite).
(setenv "PYTHONDONTWRITEBYTECODE" "1"))
(define* (ensure-no-cythonized-files #:rest _)
"Check the source code for @code{.c} files which may have been pre-generated
by Cython."
(for-each
(lambda (file)
(let ((generated-file
(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$")))
(define* (set-version #:key name source inputs #:allow-other-keys)
"Provide the package version to Python build backend
that expects it to be derived from the version control information
that is not present in the source."
;; The version scheme for public version identifiers of Python packages
;; is defined by PEP 440 and maintained by the Python Packaging Authority at
;; https://packaging.python.org/en/latest/specifications/version-specifiers
(define (public-version-identifier name)
"Extract the public version identifier from a name-version string."
(let ((_ version (package-name->name+version name)))
;; git-version et al suffix version with revision and commit.
(first (string-split version #\-))))
(let ((version-backends (map (compose string->symbol car)
(filter-map
(cute assoc <> inputs)
'("python-hatch-vcs"
"python-hatch-vcs-bootstrap"
"python-pbr"
"python-poetry-dynamic-versioning"
"python-setuptools-scm"
"python-setuptools-scm-bootstrap"
"python-versioneer"))))
;; Both git and hg use -checkout suffixes.
(version (and source
(string-suffix? "-checkout" source)
(public-version-identifier name))))
(cond ((null? version-backends)
(format #t "Detected no Python build backend that expects")
(format #t " version control information, nothing to do.~%"))
(version
(for-each
(lambda (backend)
(case backend
((python-hatch-vcs python-hatch-vcs-bootstrap
python-setuptools-scm python-setuptools-scm-bootstrap)
(setenv "SETUPTOOLS_SCM_PRETEND_VERSION" version))
((python-pbr)
(setenv "PBR_VERSION" version))
((python-poetry-dynamic-versioning)
(setenv "POETRY_DYNAMIC_VERSIONING_BYPASS" version))
((python-versioneer)
(let ((bundled "versioneer.py"))
(when (file-exists? bundled)
(delete-file bundled)))
(setenv "GUIX_VERSIONEER_VERSION" version))
(else #f))) ;should never happen.
version-backends))
(else
(format #t "The source seems to be a Python package sdist")
(for-each
(lambda (backend)
(format #t (case backend
((python-hatch-vcs
python-hatch-vcs-bootstrap
python-poetry-dynamic-versioning
python-setuptools-scm
python-setuptools-scm-bootstrap)
", ~a is likely unnecessary as a native-input")
((python-pbr python-versioneer)
", nothing to do for ~a")
(else #f)) ;should never happen.
backend))
version-backends)
(format #t ".~%")))))
(define* (build #:key outputs build-backend backend-path configure-flags #:allow-other-keys)
"Build a given Python package."
(let* ((wheel-output (assoc-ref outputs "wheel"))
(wheel-dir (if wheel-output wheel-output "dist"))
(pyproject.toml (if (file-exists? "pyproject.toml")
(parse-toml-file "pyproject.toml")
'()))
;; backend-path is prepended to sys.path, so in-tree backends can be
;; found. We assume toml is json-compatible and do not encode the resulting
;; JSON list expression.
(auto-backend-path (recursive-assoc-ref
pyproject.toml
'("build-system" "backend-path")))
(use-backend-path (scm->json-string
(list->vector
(or backend-path auto-backend-path '()))))
;; There is no easy way to get data from Guile into Python via
;; s-expressions, but we have JSON serialization already, which Python
;; also supports out-of-the-box.
(config-settings (scm->json-string configure-flags))
;; python-setuptools default backend supports setup.py *and*
;; pyproject.toml. Allow overriding this automatic detection via
;; build-backend.
(auto-build-backend (recursive-assoc-ref
pyproject.toml
'("build-system" "build-backend")))
;; Use build system detection here and not in importer, because a) we
;; have alot of legacy packages and b) the importer cannot update arbitrary
;; fields in case a package switches its build system.
(use-build-backend (or build-backend
auto-build-backend
"setuptools.build_meta")))
(format #t
(string-append
"Using '~a' to build wheels, auto-detected '~a', override '~a'.~%"
"Prepending '~a' to sys.path, auto-detected '~a', override '~a'.~%")
use-build-backend auto-build-backend build-backend
use-backend-path auto-backend-path backend-path)
(mkdir-p wheel-dir)
;; Call the PEP 517 build function, which drops a .whl into wheel-dir.
(invoke "python" "-c"
"import sys, importlib, json
backend_path = json.loads(sys.argv[1]) or []
backend_path.extend(sys.path)
sys.path = backend_path
config_settings = json.loads(sys.argv[4])
builder = importlib.import_module(sys.argv[2])
builder.build_wheel(sys.argv[3], config_settings=config_settings)"
use-backend-path
use-build-backend
wheel-dir
config-settings)))
(define* (wrap #:key inputs outputs #:allow-other-keys)
(define (list-of-files dir)
(find-files dir (lambda (file stat)
(and (eq? 'regular (stat:type stat))
(not (wrapped-program? file))))))
(define bindirs
(append-map (match-lambda
((_ . dir)
(list (string-append dir "/bin")
(string-append dir "/sbin"))))
outputs))
;; Do not require "bash" to be present in the package inputs
;; even when there is nothing to wrap.
;; Also, calculate (sh) only once to prevent some I/O.
(define %sh (delay (search-input-file inputs "bin/bash")))
(define (sh) (force %sh))
(let* ((var `("GUIX_PYTHONPATH" prefix
,(search-path-as-string->list
(or (getenv "GUIX_PYTHONPATH") "")))))
(for-each (lambda (dir)
(let ((files (list-of-files dir)))
(for-each (cut wrap-program <> #:sh (sh) var)
files)))
bindirs)))
(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 (keywords->alist lst)
(let loop ((lst lst)
(result '()))
(match lst
(()
(reverse result))
((kw value rest ...)
(cons (symbol->string (keyword->symbol kw)) value)))))
(define (guile->python-keywords args)
(map (match-lambda
((head . tail)
(cons head
(map (match-lambda
((arg . ())
(cons arg '()))
((arg . keyword-args)
(cons arg (list (keywords->alist keyword-args)))))
tail))))
args))
(define* (check #:key inputs tests? test-backend test-flags pytest-guix-options
#:allow-other-keys)
"Run the test suite of a given Python package."
(if tests?
;; Unfortunately with PEP 517 there is no common method to specify test
;; systems. Guess test system based on inputs instead.
(let* ((pytest (which "pytest"))
(nosetests (which "nosetests"))
(nose2 (which "nose2"))
(stestr (which "stestr"))
(have-setup-py (file-exists? "setup.py"))
;; unittest default pattern
;; See https://docs.python.org/3/library/unittest.html\
;; #cmdoption-unittest-discover-p
(tests-found (find-files "." "test.*\\.py$"))
(use-test-backend
(or test-backend
;; By order of preference.
(and (assoc-ref inputs "python-pytest-guix")
'pytest-with-guix-plugin)
(and pytest 'pytest)
(and stestr 'stestr)
(and nosetests 'nose)
(and nose2 'nose2)
;; Fall back to setup.py. The command is deprecated, but is
;; a superset of unittest, so should work for most packages.
;; Keep it until setuptools removes `setup.py test'.
;; See https://setuptools.pypa.io/en/latest/deprecated/\
;; commands.html#test-build-package-and-run-a-unittest-suite
(and have-setup-py 'setup.py)
(and tests-found 'unittest))))
(format #t "Using ~a~%" use-test-backend)
(match use-test-backend
('pytest-with-guix-plugin
(call-with-output-file ".pytest_guix_options.json"
(lambda (port)
(scm->json
(guile->python-keywords pytest-guix-options)
port)))
(apply invoke pytest "-vv" "-p" "pytest_guix" test-flags))
('pytest
(apply invoke pytest "-vv" test-flags))
('nose
(apply invoke nosetests "-v" test-flags))
('nose2
(apply invoke nose2 "-v" "--pretty-assert" test-flags))
('setup.py
(apply invoke "python" "setup.py"
(if (null? test-flags)
'("test" "-v")
test-flags)))
('stestr
(apply invoke stestr "run" test-flags))
('unittest
(apply invoke "python" "-m" "unittest" test-flags))
('custom
(apply invoke "python" test-flags))
;; The developer should explicitly disable tests in this case.
(else (raise (condition (&test-system-not-found))))))
(format #t "test suite not run~%")))
(define* (install #:key inputs outputs #:allow-other-keys)
"Install a wheel file according to PEP 427."
;; See <https://packaging.python.org/en/latest/specifications/\
;; binary-distribution-format/#binary-distribution-format>.
(let* ((site-dir (site-packages inputs outputs))
(python (assoc-ref inputs "python"))
(version (python-version python))
(out (assoc-ref outputs "out")))
(define (extract file)
"Extract wheel (ZIP file) into site-packages directory"
;; Use Pythons zipfile to avoid extra dependency
(invoke "python" "-m" "zipfile" "-e" file site-dir))
(define (install-path key distribution)
"Return the path where KEY is supposed to be installed."
(match key
((or "purelib" "platlib")
site-dir)
("scripts"
(string-append out "/bin"))
((or "headers" "include")
(string-append out "/include/python" version "/" distribution))
("data"
out)))
(define (fix-scripts directory)
"Make scripts executable and rewrite #!python shebangs."
(for-each
(lambda (file)
(chmod file #o755)
;; PEP 427 recommends that installers rewrite
;; this odd shebang, but avoid the binary case.
(unless (elf-file? file)
(substitute* file
(("#!python")
(string-append "#!" python "/bin/python")))))
(find-files directory)))
(define (list-directories base predicate)
;; Cannot use find-files here, because its recursive.
(scandir base
(lambda (name)
(let ((stat (lstat (string-append base "/" name))))
(and (not (member name '("." "..")))
(eq? (stat:type stat) 'directory)
(predicate name stat))))))
(define (distribution-name data-directory)
"Extract the distribution name from a .data directory name.
Per PEP 427, the directory is named {distribution}-{version}.data."
(car (string-split (basename data-directory) #\-)))
(define (expand-data-directory directory)
"Move files from all subdirectories of a .data directory to their
respective destinations according to PEP 427 and sysconfig scheme keys."
(let ((distribution (distribution-name directory)))
(for-each
(lambda (key)
(let ((source (string-append directory "/" key))
(destination (install-path key distribution)))
(when (file-exists? source)
(copy-recursively source destination)
(delete-file-recursively source)
;; Scripts need executable permissions and shebang rewriting.
(when (string=? key "scripts")
(fix-scripts destination)))))
(list-directories directory (const #t)))))
(let* ((wheel-output (assoc-ref outputs "wheel"))
(wheel-dir (if wheel-output wheel-output "dist"))
(wheels-found (or (scandir wheel-dir
(cut string-suffix? ".whl" <>))
'()))
(wheels (map (cut string-append wheel-dir "/" <>) wheels-found)))
(cond
;; This can happen if the 'build phase has been changed or when using
;; the install phase using an alternative build-system.
((null? wheels-found)
(raise (condition (&no-wheels-found))))
((> (length wheels) 1)
;; This code does not support multiple wheels yet, because their
;; outputs would have to be merged properly.
(raise (condition (&cannot-extract-multiple-wheels))))
((= (length wheels) 0)
(raise (condition (&no-wheels-built)))))
(for-each extract wheels))
(let ((datadirs (map (cut string-append site-dir "/" <>)
(list-directories site-dir
(file-name-predicate "\\.data$")))))
(for-each (lambda (directory)
(expand-data-directory directory)
(rmdir directory)) datadirs))))
(define* (compile-bytecode #:key inputs outputs #:allow-other-keys)
"Compile installed byte-code in site-packages."
(let* ((site-dir (site-packages inputs outputs))
(python (assoc-ref inputs "python"))
(major-minor (map string->number
(take (string-split (python-version python) #\.) 2)))
(<3.7? (match major-minor
((major minor)
(or (< major 3)
(and (= major 3)
(< minor 7)))))))
(if <3.7?
;; These versions dont have the hash invalidation modes and do
;; not produce reproducible bytecode files.
(format #t "Skipping bytecode compilation for Python version ~a < 3.7~%"
(python-version python))
(invoke "python" "-m" "compileall"
"--invalidation-mode=unchecked-hash" site-dir))))
(define* (create-entrypoints #:key inputs outputs #:allow-other-keys)
"Implement Entry Points Specification
(https://packaging.python.org/specifications/entry-points/) by PyPa,
which creates runnable scripts in bin/ from entry point specification
file entry_points.txt. This is necessary, because wheels do not contain
these binaries and installers are expected to create them."
(define (parse-entry-point item-match)
"Parse an entry point. Return a list of script, module and function."
(list (match:substring item-match 1)
(match:substring item-match 2)
(match:substring item-match 3)))
(define (parse-line inside line)
(cond
((string-match "^\\[(console|gui)_scripts\\]$" line)
#t)
((and inside (string-match "^([^ =]+)\\s*=\\s*([^:]+):(.+)$" line))
=> parse-entry-point)
(else
#f)))
(define (entry-points.txt->entry-points file)
"Specialized parser for Python configfile-like files, in particular
entry_points.txt. Returns a list of console_script and gui_scripts
entry points."
(call-with-input-file file
(lambda (in)
(let loop ((line (read-line in))
(inside #f)
(result '()))
(if (eof-object? line)
result
(match (parse-line inside line)
((? list? entry)
(loop (read-line in) #t (cons entry result)))
(next-inside
(loop (read-line in) next-inside result))))))))
(define (create-script path name module function)
"Create a Python script from an entry points NAME, MODULE and FUNCTION
and return write it to PATH/NAME."
(let ((interpreter (which "python"))
(file-path (string-append path "/" name)))
(format #t "Creating entry point for '~a.~a' at '~a'.~%"
module function file-path)
(call-with-output-file file-path
(lambda (port)
;; Technically the script could also include search-paths,
;; but having a generic 'wrap phases also handles manually
;; written entry point scripts.
(format port "#!~a -sP
# Auto-generated entry point script.
import sys
import importlib
obj = importlib.import_module('~a')
for attr in '~a'.split('.'):
obj = getattr(obj, attr)
if __name__ == '__main__':
sys.exit(obj())~%" interpreter module function)))
(chmod file-path #o755)))
(let* ((site-dir (site-packages inputs outputs))
(out (assoc-ref outputs "out"))
(bin-dir (string-append out "/bin"))
(entry-point-files (find-files site-dir "^entry_points.txt$")))
(mkdir-p bin-dir)
(for-each (lambda (f)
(for-each (lambda (ep)
(apply create-script
(cons bin-dir ep)))
(entry-points.txt->entry-points f)))
entry-point-files)))
(define* (set-SOURCE-DATE-EPOCH* #:rest _)
"Set the 'SOURCE_DATE_EPOCH' environment variable. This is used by tools
that incorporate timestamps as a way to tell them to use a fixed timestamp.
See https://reproducible-builds.org/specs/source-date-epoch/."
;; Use a post-1980 timestamp because the Zip format used in wheels do
;; not support timestamps before 1980.
(setenv "SOURCE_DATE_EPOCH" "315619200"))
(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* ((site-packages (site-packages inputs outputs))
(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))))
(define %standard-phases
;; The build phase only builds C extensions and copies the Python sources,
;; while the install phase copies then byte-compiles the sources to the
;; prefix directory. The check phase is moved after the installation phase
;; to ease testing the built package.
(modify-phases gnu:%standard-phases
(add-after 'unpack 'ensure-no-mtimes-pre-1980 ensure-no-mtimes-pre-1980)
(add-after 'ensure-no-mtimes-pre-1980 'enable-bytecode-determinism
enable-bytecode-determinism)
(add-after 'enable-bytecode-determinism 'ensure-no-cythonized-files
ensure-no-cythonized-files)
(add-after 'ensure-no-cythonized-files 'set-version set-version)
(delete 'bootstrap)
(replace 'set-SOURCE-DATE-EPOCH set-SOURCE-DATE-EPOCH*)
(delete 'configure) ;not needed
(replace 'build build)
(delete 'check) ;moved after the install phase
(replace 'install install)
(add-after 'install 'add-install-to-pythonpath add-install-to-pythonpath)
(add-after 'add-install-to-pythonpath 'add-install-to-path
add-install-to-path)
(add-after 'add-install-to-path 'wrap wrap)
;; Must be before tests, so they can use installed packages entry points.
(add-before 'wrap 'create-entrypoints create-entrypoints)
(add-after 'wrap 'sanity-check sanity-check)
(add-after 'sanity-check 'check check)
(add-before 'check 'compile-bytecode compile-bytecode)
(add-before 'strip 'rename-pth-file rename-pth-file)))
(define* (pyproject-build #:key inputs (phases %standard-phases)
#:allow-other-keys #:rest args)
"Build the given Python package, applying all of PHASES in order."
(apply gnu:gnu-build #:inputs inputs #:phases phases args))
;;; pyproject-build-system.scm ends here