pyproject-installer builds and installs PEP 517/518 Python projects inside network-isolated environments, with the deliberate constraints needed by downstream system packagers. It is the build/install backbone used to package Python projects for RPM-based distributions.
-
Supported platform: Unix. Currently, platform-specific parts:
- pipe is used for calling build backend hooks in subprocess
- script wrappers are generated only for Unix systems
-
Source tree can be either checkout of VCS or unpacked source distribution.
-
An installation result will be consumed by external tool like RPM. The main usage of
pyproject-installerlooks like:external tool => (pyproject-installer: build => install to destdir) => external tool packages destdirTherefore, there is no need to build intermediate source distribution for build wheel, only
build_wheelbackend's hook is actually called.Note: an installation into Python virtual environment is also supported, but only the manual uninstallation of such packages is possible (tools should refuse an uninstallation of distribution with missing
RECORDfile).
-
Only stdlib or vendored dependencies can be used in runtime for bootstrapping any Python project. Current vendored packages:
tomli(used for parsingpyproject.tomlconfiguration file). Note:tomliis the part of stdlib since Python 3.11.packaging(used for parsing PEP508 dependencies)
-
Installation of build dependencies is up to the caller. These dependencies of Python projects are managed externally with system package managers like
aptordnf. External source of upstream's dependencies may be used for provision of formatted list of dependencies to external tools. -
INSTALLER file is not installed by default(optional). https://peps.python.org/pep-0627/#optional-installer-file:
The INSTALLER file is also made optional, and specified to be used for informational purposes only. It is still a single-line text file containing the name of the installer.
This value should be used for informational purposes only. For example, if a tool is asked to uninstall a project but finds no RECORD file, it may suggest that the tool named in INSTALLER may be able to do the uninstallation.
-
OS environment of this project is a
network-isolatedenvironment, which implies that a local loopback interface is the only available network interface. Thus,pyproject-installerdoesn't perform any network activity (e.g. it doesn't install build dependencies specified via PEP518 configuration or PEP517'sget_requires_for_*hooks). This also makes it difficult or impossible to create an isolated Python environment for calling build backend hooks specified in PEP517, therefore, current Python environment is the only available environment. -
There is no post-installation bytecompilation. PEP427 says that wheel installers should compile any installed .py to .pyc. External tools like RPM already provide Python bytecompilation means, which compile for multiple optimization levels at a time. No point to compile modules twice.
-
RECORD file is not installed. https://peps.python.org/pep-0627/#optional-record-file:
Specifically, the RECORD file is unnecessary when projects are installed by a Linux system packaging system, which has its own ways to keep track of files, uninstall them or check their integrity. Having to keep a RECORD file in sync with the disk and the system package database would be unreasonably fragile, and no RECORD file is better than one that does not correspond to reality.
-
Built distribution can be checked within Python virtual environment with the help of
runcommand. -
Project's dependencies sources can be managed (i.e. stored, synced, verified or evaluated) with the help of
depscommand.
These options are accepted by the top-level command and must appear before the subcommand token.
-C DIRChange to
DIRbefore running the subcommand. Matches the semantics ofmake -C,git -C, andtar -C: after the flag takes effect, defaults such as{cwd}/distand{cwd}/pyproject_deps.jsonresolve against the new directory.Default: current working directory (no change). Empty
DIR(e.g.-C "") is treated the same as omitting the flag.Example:
python -m pyproject_installer -C /path/to/project build
Build project from source tree in current Python environment according to PEP 517. This doesn't trigger installation of project's build dependencies.
python -m pyproject_installer buildPositional arguments:
srcdir(positional)Source directory.
Default: current working directory
Example:
python -m pyproject_installer build .
Options:
--outdir OUTDIR, -o OUTDIROutput directory for built wheel.
Default:
{srcdir}/distExample:
python -m pyproject_installer build --outdir ~/outdir
Upon successful build pyproject_installer dumps wheel filename into
{OUTDIR}/.wheeltracker.
--sdistBuild source distribution (sdist) instead of binary one (wheel).
Note: installer supports only wheel format.
Default: build wheel
Example:
python -m pyproject_installer build --sdist
--backend-config-settings BACKEND_CONFIG_SETTINGSAd-hoc configuration for build backend as dumped JSON dictionary.
Default:
None
Examples of passing config_settings:
# setuptools >= 64.0.0
python -m pyproject_installer build --backend-config-settings='{"--build-option": ["--python-tag=sometag", "--build-number=123"]}'
# setuptools < 64.0.0
python -m pyproject_installer build --backend-config-settings='{"--global-option": ["--python-tag=sometag", "--build-number=123"]}'
# pdm backend
python -m pyproject_installer build --backend-config-settings='{"--python-tag": "sometag"}'Install project built in wheel format. This doesn't trigger installation of project's runtime dependencies.
python -m pyproject_installer installPositional arguments:
wheel(positional)Wheel file to install.
Default: constructed as directory
{cwd}/distand wheel filename read from{cwd}/dist/.wheeltrackerExample:
python -m pyproject_installer install wheel.whl
Options:
--destdir DESTDIR, -d DESTDIRWheel installation root will be prepended with destdir.
Default:
/Example:
python -m pyproject_installer install --destdir ~/destdir
--installer INSTALLERName of installer to be recorded in
dist-info/INSTALLER.Default:
None,INSTALLERwill be omittedExample:
python -m pyproject_installer install --installer custom_installer
--no-strip-dist-infoDon't strip dist-info. By default only
METADATAandentry_points.txtfiles are allowed indist-infodirectory.Note:
RECORDis unconditionally filtered out.Default:
FalseExample:
python -m pyproject_installer install --no-strip-dist-info
--rpm-filelist PATHWrite an RPM
%files-compatible filelist of every installed file, plus the computed.pycpaths for every installed.pyunderpurelib/platlib(PEP 3147 / PEP 488). The file is consumable by%files -f <PATH>without post-processing, letting a spec declare package contents as%files -f dist/foo.filesrather than enumerating them by hand. Paths in the output are system-absolute (--destdiris stripped). Parent directory ofPATHmust exist.What is emitted:
- File lines - every file the installer writes to
--destdir:.pysources, compiled extensions, entry-point scripts, dist-info files (METADATA, and with--no-strip-dist-infoevery file that survives filtering), theINSTALLERfile (when--installeris used), and the relocated contents of{dist}-{ver}.data/<scheme>/..pycexpansion - for every.pyfile whose deepest owning scheme ispureliborplatlib, three bytecode paths are computed (importlib.util.cache_from_sourceat optimisation levels 0, 1, 2) and emitted..pyfiles outsidepurelib/platlib(scripts, data, headers, dist-info) are not expanded: RPM's bytecompile step does not compile them, so predicting.pycthere would yield phantom paths that fail%filesparsing.- Man page globbing - files under
scheme["data"]/share/man/get a trailing*so the filelist still matches after RPM'sbrp-compresscompresses them post-%install(uncompressed pages gain a compression extension). Suffixes brp decompresses (.gz,.bz2,.Z) are stripped first - brp will recompress them, so the on-disk extension may differ from the wheel's. Others (.xz,.zst, ...) are kept literal - brp leaves them alone.%direntries - directories the package claims ownership of:
{dist}-{ver}.dist-infois always owned (installer creates it even when stripped down toMETADATA); its subdirectories (e.g. PEP 639licenses/under--no-strip-dist-info) are owned too.scheme["headers"](typically/usr/include/pythonX.Y/<dist>, per-distribution namespaced) is owned only when at least one installed file lives under it; its intermediate subdirectories are owned too.- Every intermediate directory between an installed file and its owning scheme root - i.e. package and sub-package dirs under
purelib/platlib- is owned. For.pyfiles in sub-packages, the sibling__pycache__directory is owned.What is deliberately not emitted:
- The site roots themselves (
scheme["purelib"],scheme["platlib"]- e.g./usr/lib/pythonX.Y/site-packages,/usr/lib64/pythonX.Y/site-packages) - owned by the Python runtime package.- The shared site-level
__pycache__(/usr/lib{,64}/pythonX.Y/site-packages/__pycache__) - every top-level single-file module would fight for ownership; no%diris emitted even though.pycfile lines for top-level modules land inside it.%dirfor FHS-standard roots (/usr/bin,/usr/share, etc.) or any intermediate directory under them. Files installed toscheme["scripts"]orscheme["data"]are emitted file-only; if your package ships app-specific data directories that no other package owns, add%dirlines for them manually in the spec.Note:
sys.pycache_prefixmust be at its default (None).importlib.util.cache_from_sourcerespects it, so any non-default value (PYTHONPYCACHEPREFIX,-X pycache_prefix=...) would redirect every computed.pycpath under that prefix and desync the filelist from the buildroot; the installer refuses to run with--rpm-filelistin that case.Default:
None, filelist is not writtenExample:
python -m pyproject_installer install --destdir /tmp/buildroot --rpm-filelist dist/foo.filesExample output -
dist/foo.filesafter installing a pure-Pythonfoo-1.0-py3-none-any.whlcontaining thefoopackage, afoo_cliconsole script, and a.data/data/share/foo/asset.datasset on Python 3.13 with purelib =/usr/lib/python3/site-packages:%dir /usr/lib/python3/site-packages/foo %dir /usr/lib/python3/site-packages/foo-1.0.dist-info %dir /usr/lib/python3/site-packages/foo/__pycache__ /usr/bin/foo_cli /usr/lib/python3/site-packages/foo-1.0.dist-info/METADATA /usr/lib/python3/site-packages/foo-1.0.dist-info/entry_points.txt /usr/lib/python3/site-packages/foo/__init__.py /usr/lib/python3/site-packages/foo/__pycache__/__init__.cpython-313.opt-1.pyc /usr/lib/python3/site-packages/foo/__pycache__/__init__.cpython-313.opt-2.pyc /usr/lib/python3/site-packages/foo/__pycache__/__init__.cpython-313.pyc /usr/share/foo/asset.datEmitted paths follow
sysconfig.get_paths()of the running interpreter. For a platform-specific wheel (e.g.foo-1.0-cp313-cp313-linux_x86_64.whl) on a splitlib/lib64layout where platlib =/usr/lib64/python3/site-packages, the same contents plus a compiled extension land under the platlib root:%dir /usr/lib64/python3/site-packages/foo %dir /usr/lib64/python3/site-packages/foo-1.0.dist-info %dir /usr/lib64/python3/site-packages/foo/__pycache__ /usr/bin/foo_cli /usr/lib64/python3/site-packages/foo-1.0.dist-info/METADATA /usr/lib64/python3/site-packages/foo-1.0.dist-info/entry_points.txt /usr/lib64/python3/site-packages/foo/__init__.py /usr/lib64/python3/site-packages/foo/__pycache__/__init__.cpython-313.opt-1.pyc /usr/lib64/python3/site-packages/foo/__pycache__/__init__.cpython-313.opt-2.pyc /usr/lib64/python3/site-packages/foo/__pycache__/__init__.cpython-313.pyc /usr/lib64/python3/site-packages/foo/_ext.cpython-313-x86_64-linux-gnu.so /usr/share/foo/asset.dat
--platlibForce the install to land in the
platlibsite-packages directory regardless of the wheel'sRoot-Is-Purelibflag. Both the unprefixed wheel root and any.data/purelib/content are redirected toplatlib. Mutually exclusive with--purelib.Default: unset, the wheel's
Root-Is-Purelibflag is honouredExample:
python -m pyproject_installer install --destdir /tmp/buildroot --platlib
--purelibForce the install to land in the
purelibsite-packages directory regardless of the wheel'sRoot-Is-Purelibflag. Both the unprefixed wheel root and any.data/platlib/content are redirected topurelib. Mutually exclusive with--platlib. The symmetric inverse of--platlib.Default: unset, the wheel's
Root-Is-Purelibflag is honouredExample:
python -m pyproject_installer install --destdir /tmp/buildroot --purelib
Run command within Python virtual environment that has access to system and user site packages, their console scripts and installed built package.
python -m pyproject_installer runPositional arguments:
command(positional, variadic)Command to run within virtual environment.
Example:
python -m pyproject_installer run pytest
Dash note:
https://docs.python.org/3/library/argparse.html#arguments-containing If you have positional arguments that must begin with
-and don't look like negative numbers, you can insert the pseudo-argument--which tellsparse_args()that everything after that is a positional argument:
python -m pyproject_installer run -- pytest -vraOptions:
--wheel WHEELWheel file to install into virtual environment.
Default: constructed as directory
{cwd}/distand wheel filename read from{cwd}/dist/.wheeltrackerExample:
python -m pyproject_installer run --wheel wheel.whl pytest
Note: venv's directory name is .run_venv.
Collect PEP 508 requirements from different sources, store and evaluate them in Python environment.
python -m pyproject_installer deps --helpCommon deps options:
--depsconfigConfiguration file to use.
Default:
{cwd}/pyproject_deps.jsonExample:
python -m pyproject_installer deps --depsconfig foo.json
Show configuration and data of dependencies' sources.
<source names>(positional)Source names to show.
Default: all
Example:
python -m pyproject_installer deps show build
See python -m pyproject_installer deps show --help for full options.
Configure source of Python dependencies. Supported sources: standardized formats like PEP 517, PEP 518, PEP 735 or core metadata are fully supported, while tool-specific formats like pip, tox, poetry, hatch, pdm or pipenv have limited support.
<source name>(positional)Source name.
<source type>(positional)Choices:
pep517,pep518,pep735,metadata,pip_reqfile,poetry,tox,hatch,pdm,pipenv
<source-specific options>(positional, variadic)Specific configuration options for source.
Default:
[]
Examples:
# PEP 518 dependencies
python -m pyproject_installer deps add build_pep518 pep518
# PEP 517 dependencies
python -m pyproject_installer deps add build_pep517 pep517
# core metadata dependencies
python -m pyproject_installer deps add runtime metadata
# PEP 735 dependency group
python -m pyproject_installer deps add check pep735 test
# pip requirements file
python -m pyproject_installer deps add check pip_reqfile requirements.txt
# tox testenv
python -m pyproject_installer deps add check tox tox.ini testenv
# poetry dev group
python -m pyproject_installer deps add check poetry dev
# hatch environment
python -m pyproject_installer deps add check hatch hatch.toml test
# pdm group
python -m pyproject_installer deps add check pdm test
# pipenv packages
python -m pyproject_installer deps add check pipenv Pipfile packagesSee python -m pyproject_installer deps add --help for full options.
Sync stored requirements to configured sources.
<source names>(positional)Source names to sync.
Default: all
Example:
python -m pyproject_installer deps sync build
--verifySync sources, but print diff and exit with code 4 if the sources were unsynced.
Default: only sync
Example:
python -m pyproject_installer deps sync --verify build
--verify-excludeRegex patterns; exclude from diff requirements whose PEP 503-normalized names match one of the patterns. Requires
--verify.Default:
[]Example:
python -m pyproject_installer deps sync --verify build --verify-exclude 'foo.*'
See python -m pyproject_installer deps sync --help for full options.
Evaluate stored requirements according to PEP 508 in current Python environment and print them to stdout in PEP 508 format (by default) or specified one.
<source names>(positional)Source names to evaluate.
Default: all
Example:
python -m pyproject_installer deps eval build
--depformatFormat of dependency to print. Supported substitutions:
$name- project's name;$nname- PEP 503 normalized project's name;$fextra- project's extras (expanded first with--depformatextra).Default: PEP 508 format
Example:
python -m pyproject_installer deps eval build --depformat='python3-$nn'
--depformatextraFormat of extras to print (one extra of dependencies per line). Result is expanded in the format specified by
--depformatas$fextra. Supported substitutions:$extra.Default:
''Example:
python -m pyproject_installer deps eval build --depformat='python3-$nn$fextra' --depformatextra='+$extra'
--extraPEP 508
extramarker to evaluate with.Default:
NoneExample:
python -m pyproject_installer deps eval build --extra tests
--excludeRegex patterns; exclude requirement having PEP 503-normalized name that matches one of these patterns.
Default:
[]Example:
python -m pyproject_installer deps eval build --exclude types- pytest-cov
See python -m pyproject_installer deps eval --help for full options.
Deconfigure source of Python dependencies.
<source name>(positional)Source name to delete.
Example:
python -m pyproject_installer deps delete build
See python -m pyproject_installer deps delete --help for full options.
pyproject-installer consists of builder and installer, both of which provide
only necessary and sufficient functionality.
Functionally, today's builder is similar to build. The key differences are:
- highly specialized defaults (see description)
- highly specialized features to drop extra runtime dependencies like
pep517. No point to vendorpep517to call onlybuild_wheelbackend hook in subprocess.
Functionally, today's installer is similar to installer. The key differences are:
- highly specialized defaults and features (see description)
Both can be replaced with pip. But again, no
point to use full-featured complex pip to highly specialized task.
There is a self-hosted build backend to avoid dependency on any other backend.
For example, bootstrap can be done as:
export PYTHONPATH=$(pwd)/src
python -m pyproject_installer build
python -m pyproject_installer install --destdir=/rootdirTests are run from an installed venv, matching CI:
- create a venv and install the project with its test dependencies
(requires
pip 25.1+):python -m venv .venv .venv/bin/python -m pip install --upgrade pip .venv/bin/python -m pip install --group test .venv/bin/python -m pip install .
- unit tests can be run as:
.venv/bin/pytest tests/unit - integration tests (require internet connection and
gittool) can be run as:.venv/bin/pytest tests/integration
See CHANGELOG.md.
Distributed under the terms of the MIT license, pyproject-installer is
free and open source software.