Create debian package out of a Python installed via UV

Camilo Matajira Avatar

Objective

Package a Python interpreter, that I will install via UV, in a debian package.
Specifically, I chose Python 3.9 (yes it’s old).

Alternative Review

To the best of my knowledge, it has not been done.

Solution

The Makefile below illustrates how to achieve it. You can also clone the project here: https://gitlab.com/matajira/debian-only-uv-python#

How to run it?

PREFIX = /opt/venvs/debianuvpython/
SHELL = /bin/bash

default: help

install:
    curl -LsSf https://astral.sh/uv/0.7.5/install.sh | sh
    source $$HOME/.local/bin/env && uv python install 3.9 && uv python list
    mkdir -p $(DESTDIR)$(PREFIX)

    cp -R /root/.local/share/uv/python/cpython-3.9.22-linux-x86_64-gnu/ $(DESTDIR)$(PREFIX)
    patchelf --replace-needed "\$$ORIGIN/../lib/libpython3.9.so.1.0" \
          "$(PREFIX)/cpython-3.9.22-linux-x86_64-gnu/lib/libpython3.9.so.1.0" $(DESTDIR)$(PREFIX)/cpython-3.9.22-linux-x86_64-gnu/bin/python3
    patchelf --replace-needed "\$$ORIGIN/../lib/libpython3.9.so.1.0" \
         "$(PREFIX)/cpython-3.9.22-linux-x86_64-gnu/lib/libpython3.9.so.1.0" $(DESTDIR)$(PREFIX)/cpython-3.9.22-linux-x86_64-gnu/lib/libpython3.so

clean:
    rm -rf ./.venv

DOCKER_RUN := docker run  -v$$PWD:/home/debian-only-uv-python-1.0.0 debian:bullseye
build:
    $(DOCKER_RUN) bash -c "apt-get update && apt-get install -y devscripts debmake build-essential patchelf && \
     cd /home/debian-only-uv-python-1.0.0 && debmake -y --native; 
     cat debian/rules | awk '/override_dh_shlibdeps:/ {exit 0} END {exit 1}' || 
     echo -e 'override_dh_shlibdeps:\n\tdh_shlibdeps -Xcpython-3.9.22-linux-x86_64-gnu' >> debian/rules; debuild && cp ../*deb ./"

help:
    echo "Debian loves uv"
make build

How it works?

Basically, when creating a debian package you can take several approaches.
In my case, I took advantage of dh_autoinstall.
dh_autoinstall will check if you have a Makefile, and if you have it, it will execute make install.
If you put the correct logic there (despite that it is a low-level approach) you can have the debian package.

There are only two keys to make this work.

  1. You need to patch the python interpreter.
    Remember, cpython as the name suggest is a C project. C projects are compiled, and sometimes during the compilation the binary is dynamically linked. This is the case of the Python that comes with debian, but it’s also de case of the python that you install via uv.

This is how ldd looks like for python3 that is part of my ubuntu machine:

ldd /usr/bin/python3
linux-vdso.so.1 (0x00007ffef3f7e000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007e79ce44e000)
libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x00007e79ce432000)
libexpat.so.1 => /lib/x86_64-linux-gnu/libexpat.so.1 (0x00007e79ce406000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007e79ce000000)
/lib64/ld-linux-x86-64.so.2 (0x00007e79ce551000)

This is how ldd looks like for python3.9 that I installed via uv:

ldd /home/camilo/.local/share/uv/python/cpython-3.9.20-linux-x86_64-gnu/bin/python3.9
linux-vdso.so.1 (0x00007fff2e5c9000)
<strong><em>/home/camilo/.local/share/uv/python/cpython-3.9.20-linux-x86_64-gnu/bin/../lib/libpython3.9.so.1.0 (0x00007f19a7200000)
</em></strong>libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f19a71e3000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f19a71de000)
libutil.so.1 => /lib/x86_64-linux-gnu/libutil.so.1 (0x00007f19a71d9000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f19a70f0000)
librt.so.1 => /lib/x86_64-linux-gnu/librt.so.1 (0x00007f19a70eb000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f19a6e00000)
/lib64/ld-linux-x86-64.so.2 (0x00007f19a872b000)

As you can see, there are differences on the dynamic libraries used. But specially, the most interesting (and problematic) is that in the python interpreter there is a hardcoded path to libpython3.9.so.1.0

The hardcoding looks like this:

patchelf --print-needed python3.0<br>$ORIGIN/../lib/libpython3.9.so.1.0

And this is something that cannot be fixed by modifying LD_LIBRARY_PATH.

So, to fix we will need to patch the executable and change the path libpython3.9.so.1.0 to a correct place. This is why you see this lines in the Makefile:

patchelf --replace-needed "\$$ORIGIN/../lib/libpython3.9.so.1.0" "$(PREFIX)/cpython-3.9.22-linux-x86_64-gnu/lib/libpython3.9.so.1.0" $(DESTDIR)$(PREFIX)/cpython-3.9.22-linux-x86_64-gnu/bin/python3
patchelf --replace-needed "\$$ORIGIN/../lib/libpython3.9.so.1.0" "$(PREFIX)/cpython-3.9.22-linux-x86_64-gnu/lib/libpython3.9.so.1.0" $(DESTDIR)$(PREFIX)/cpython-3.9.22-linux-x86_64-gnu/lib/libpython3.so
  1. dh_shlibdeps is going to complain. Why? Because it will check that all the executables can find they’re corresponding dynamic libraries.
    Despite that we ‘fix’ the python executable, in the debian environment the fix does not have much sense because (I believe) of the change-root or fake-root during the build.
    You will get an error similar to this:
    dpkg-shlibdeps: error: cannot find library $ORIGIN/../lib/libpython3.9.so.1.0 needed by debian/debianuvpython/opt/venvs/debianuvpython/bin/python (ELF format: 'elf64-x86-64' abi: 'ELF:64:l:amd64:0'; RPATH: '')

So the fix is to tell dh_shlibdeps to ignore this type of errors, once we are confident that our solution works, we can do it. We simply tell dh_shlibdeps to ignore problems for executables with this name cpython-3.9.22-linux-x86_64-gnu.

dh_shlibdeps -Xcpython-3.9.22-linux-x86_64-gnu

Final result:

docker run -it -v$PWD:/home --rm debian:bookworm /bin/bash
root@0c87b5d597a2:/# cd home
root@0c87b5d597a2:/home# dpkg -i debian-only-uv-python_1.0.0_amd64.deb
Selecting previously unselected package debian-only-uv-python.
(Reading database  7437 files and directories currently installed.)
Preparing to unpack debian-only-uv-python_1.0.0_amd64.deb 
Unpacking debian-only-uv-python (1.0.0) …
Setting up debian-only-uv-python (1.0.0) …
Processing triggers for libc-bin (2.36-9+deb12u10) …
root@0c87b5d597a2:/home# /opt/venvs/debianuvpython/cpython-3.9.22-linux-x86_64-gnu/bin/python --version
Python 3.9.22

Tagged in :

Camilo Matajira Avatar

One response to “Create debian package out of a Python installed via UV”

  1. […] to insert a Python interpreter (installed via uv) inside a Debian package.You can read that project here: You can clonse the first project […]