User:Foxboron/Python packaging guidelines
32-bit – CLR – CMake – Cross – DKMS – Eclipse – Electron – Font – Free Pascal – GNOME – Go – Haskell – Java – KDE – Kernel modules – Lisp – Meson – MinGW – Node.js – Nonfree – OCaml – Perl – PHP – Python – R – Ruby – Rust - Security – Shell – VCS – Web – Wine
This document covers standards and guidelines on writing PKGBUILDs for Python.
Package naming
For Python 3 library modules, use python-modulename
. Also use the prefix if the package provides a program that is strongly coupled to the Python ecosystem (e.g. pip or tox). For other applications, use only the program name.
The same applies to Python 2 except that the prefix (if needed) is python2-
.
Arch
See PKGBUILD#arch.
A Python package that contains C extensions using the setup.py ext_modules
keyword, is architecture-dependent. Otherwise it is most likely architecture-independent.
Source
Download URLs linked from the PyPI website include an unpredictable hash that needs to be fetched from the PyPI website each time a package must be updated. This makes them unsuitable for use in a PKGBUILD. PyPI provides an alternative stable scheme: PKGBUILD#source source=()
array should use the following URL templates:
- Source package
https://files.pythonhosted.org/packages/source/${_name::1}/${_name}/${_name}-${pkgver}.tar.gz
- Bilingual wheel package (Python 2 and Python 3 compatible)
https://files.pythonhosted.org/packages/py2.py3/${_name::1}/$_name/$_name-$pkgver-py2.py3-none-any.whl
- Arch specific wheel package
- in this example for
source_x86_64=('...')
. Also_py=py36
can be used to not repeat the python version: https://files.pythonhosted.org/packages/$_py/${_name::1}/$_name/$_name-$pkgver-$_py-${_py}m-manylinux1_x86_64.whl
Note that a custom _name
variable is used instead of pkgname
since python packages are generally prefixed with python-
. This variable can generically be defined as follows:
_name=${pkgname#python-}
Build and Package
Python packages are generally installed using language-specific tools, such as pip or easy_install, which are comparable to dedicated package managers in that they are designed to fetch the source files from an online repository (usually PyPI, the Python Package Index) and track the relevant files (for a detailed comparison between the two, see pip vs easy_install).
However, for managing Python packages from within PKGBUILDs, the standard-provided distutils proves to be the most convenient solution since it uses the downloaded source package's setup.py
and easily installs files under $pkgdir/usr/lib/python<python version>/site-packages/$pkgname
directory.
distutils
A distutils PKGBUILD is usually quite simple:
build() { python setup.py build } package() { python setup.py install --root="$pkgdir/" --optimize=1 --skip-build }
where:
- python is replaced with the proper binary,
python
orpython2
--root="$pkgdir/"
prevents trying to directly install in the host system instead of inside the package file, which would result in a permission error--optimize=1
compiles optimized bytecode files (.pyo
for Python 2,opt-1.pyc
for Python 3) so they can be tracked by pacman.--skip-build
optimizes away the unnecessary attempt to re-run the build steps already run in thebuild()
function.
setuptools
The Python packaging scene has largely migrated from distutils to setuptools, which is actively developed and functions as a drop-in replacement import in setup.py
. The main difference for packagers is that setuptools is packaged separately from Python itself, and must be specified as a makedepends
.
If the resulting package includes executables which import the pkg_resources
module, then setuptools must be additionally specified as a depends
in the split package_*()
functions; alternatively, if the PKGBUILD only installs the Python package for a single version of Python, setuptools should be moved from makedepends
to depends
.
pip
If you need to use pip (provided by python-pip and python2-pip), e.g. for installing wheel packages, remember to pass the following flags:
PIP_CONFIG_FILE=/dev/null pip install --isolated --root="$pkgdir" --ignore-installed --no-deps *.whl
PIP_CONFIG_FILE=/dev/null
ignores{/etc,~/.config}/pip.conf
that may be appending arbitrary flags to pip.--isolated
ignores environment variables (and again{/etc,~/.config}/pip/pip.conf
) that may otherwise also be appending arbitrary flags to pip.--ignore-installed
is necessary until https://github.com/pypa/pip/issues/3063 is resolved (otherwise pip skips the install in the presence of an earlier--user
install).--no-deps
ensures, that dependencies do not get packaged together with the main package.
pip doesn't know how to generate .pyo
files (see https://github.com/pypa/pip/issues/2209). In order to generate them manually after pip has installed the module, run:
python -O -m compileall "${pkgdir}/path/to/module"
Build-time 2to3 translation
Most Python projects target either Python 2 or Python 3, or target both using compatibility layers like six. However, some use the deprecated 2to3 keyword in setuptools to heuristically convert the source code from Python 2 to Python 3 at build time. As a result, the same source directories cannot be used to build both Python 2 and Python 3 split packages.
For packages that do this, we need a prepare() function that copies the source before it is built. Then the Python 2 and Python 3 packages can be converted and built independently without overriding each other.
makedepends=("python" "python-setuptools" "python2" "python2-setuptools") prepare() { cp -a foo-$pkgver{,-py2} } build() { cd "$srcdir/foo-$pkgver" python setup.py build cd "$srcdir/foo-$pkgver-py2" python2 setup.py build } package_python-foo() { depends=("python2") cd "$srcdir/foo-$pkgver" python setup.py install --root="$pkgdir/" --optimize=1 --skip-build } package_python2-foo() { depends=("python2") cd "$srcdir/foo-$pkgver-py2" python2 setup.py install --root="$pkgdir/" --optimize=1 --skip-build }
Check
Most python projects providing a testsuite use nosetests or pytest to run tests with test
in the name of the file or directory containing the testsuite. In general, simply running nosetests
or pytest
is enough to run the testsuite.
check(){ cd "$srcdir/foo-$pkgver" # For nosetests nosetests # For pytest pytest }
If there is a compiled C extension, the tests need to be run using a $PYTHONPATH
hack in order to find and load it.
check(){ cd "$srcdir/foo-$pkgver" # For nosetests PYTHONPATH="$PWD/build/lib.linux-$CARCH-3.7" nosetests # For pytest PYTHONPATH="$PWD/build/lib.linux-$CARCH-3.7" pytest }
Some projects provide setup.py
entry points for running the test. This works for both pytest
and nosetests
.
check(){ cd "$srcdir/foo-$pkgver" # For nosetests python setup.py nosetests # For pytest - needs python-pytest-runner python setup.py pytest }
If this is not a solution, the next best thing is to temporarily install the given package with a $PYTHONPATH
hack to get the tests running.
check(){ cd "$srcdir/foo-$pkgver" python setup.py install --root="$PWD/tmp_install" --optimize=1 PYTHONPATH="$PWD/tmp_install/usr/lib/python3.7/site-packages:$PYTHONPATH" py.test foo }
If this does not work, one can use a virtual environment to setup the tests.
check(){ ( cd "$srcdir/foo-$pkgver" virtualenv "$srcdir/pyvenv" --system-site-packages . "$srcdir/pyvenv/bin/activate" python setup.py install python setup.py pytest ) || warning "Python tests failed" }
Examples
Single python version
pkgname=python-foo pkgver=0.0.1 pkgrel=1 pkgdesc="Example Python PKGBUILD" url="https://foo.bar" arch=('any') license=('MIT') depends=('python') makedepends=('python-setuptools') checkdepends=('python-pytest' 'python-pytest-runner') source=("https://github.com/archlinux/foo/archive/${pkgname}-${pkgver}.tar.gz") sha256sums=('60e1674f26a5ca8261404f054e46bc5755dc7237300d2b18525eacd1849c704b') build(){ cd "foo-${pkgver}" python setup.py build } check(){ cd "foo-${pkgver}" python setup.py pytest } package() { cd "foo-${pkgver}" python setup.py install --root="${pkgdir}/" --optimize=1 --skip-build install -Dm 644 LICENSE "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" }
Two pythons
pkgbase=python-foo pkgname=(python-foo python2-foo) pkgver=0.0.1 pkgrel=1 pkgdesc="Example Python PKGBUILD for python and python2" url="https://foo.bar" arch=('any') license=('MIT') makedepends=('python-setuptools' 'python2-setuptools') checkdepends=('python-pytest' 'python-pytest-runner' 'python2-pytest' 'python2-pytest-runner') source=("https://github.com/archlinux/foo/archive/${pkgname}-${pkgver}.tar.gz") sha256sums=('60e1674f26a5ca8261404f054e46bc5755dc7237300d2b18525eacd1849c704b') build(){ cd "foo-${pkgver}" python setup.py build python2 setup.py build } check(){ cd "foo-${pkgver}" python setup.py pytest python2 setup.py pytest } package_python-foo() { depends=('python') cd "foo-${pkgver}" python setup.py install --root="${pkgdir}/" --optimize=1 --skip-build install -Dm 644 LICENSE "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" } package_python2-foo() { depends=('python2') cd "foo-${pkgver}" python2 setup.py install --root="${pkgdir}/" --optimize=1 --skip-build install -Dm 644 LICENSE "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" mv "${pkgdir}/usr/bin/foo" "${pkgdir}/usr/bin/foo2" }