[Wheel-builders] Packaging a library that uses PyGObject?

Geoffrey Thomas geofft at ldpreload.com
Sun Feb 23 20:26:54 EST 2020


On Sun, 23 Feb 2020, Daniel Alley wrote:

> I would like to package this library as a pre-built Python wheel: https://github.com/fedora-modularity/libmodulemd
> 
> This library uses PyGObject, so importing it looks like this:
> 
> import gi
> gi.require_version("Modulemd", "2.0")
> from gi.repository import Modulemd
> 
>  I can't find any examples of this, nor any documentation, nor discussion about it.  It seems like it would be, at minimum, a bit more complicated than libraries based on normal bindings.  Is this
> possible, and are there any special requirements that are needed to do so? 

It's been a bit since I've used gobject-introspection, but I _think_ the 
way this works for a normal (OS-installed) GObject package is

a) the package ordinarily provides no actual Python library
b) the package provides a girepository-1.0/Modulemd-2.0.typelib file in 
the system lib directory
c) when you import it, PyGObject loads the C libmodulemd library and 
generates Python bindings based on the typelib file
d) as a special case, a package _can_ provide a Python "override" library, 
but that amends the autogenerated bindings, it's not a complete set of 
bindings on its own (and it's Python code, not native code). Modulemd 
appears to do this.

So your users' code needs to be able to
- import PyGObject (and libgobject) itself, either from the system or from 
their virtualenv
- import the C libmodulemd library from your wheel, which you can compile 
for manylinux1
- find the typelib file, which you can put in your wheel
- point libgobject at the typelib file and the C library
- point PyGObject at the override file (which should hopefully be 
automatic if it's on sys.path)

The GObject docs https://developer.gnome.org/gi/stable/GIRepository.html 
say:

> GIRepository will typically look for a girepository-1.0 directory under 
> the library directory used when compiling gobject-introspection.
>
> It is possible to control the search paths programmatically, using 
> g_irepository_prepend_search_path(). It is also possible to modify the 
> search paths by using the GI_TYPELIB_PATH environment variable. The 
> environment variable takes precedence over the default search path and 
> the g_irepository_prepend_search_path() calls.

You will also need to make sure the C library is importable. For "normal" 
Python wheels, users import a compiled Python shared object (so that the 
usual Python path is used as the search path), and that shared object has 
a normal shared object dependency on the underlying C library which is 
also shipped in the wheel.auditwheel sets an $ORIGIN-relative rpath in 
that .so file (using patchelf) so that the Python module, having been 
found inside the virtualenv, can find its C library in a relative path to 
its own location. Since there is no compiled Python module in your case, 
because PyGObject is dynamically generating the bindings at runtime, I 
don't think there is a straightforward way of informing PyGObject of where 
to find the C library.

Personally, I'd approach this by first aiming for an 80% solution where I 
expect users to set GI_TYPELIB_PATH and LD_LIBRARY_PATH so that the 
typelib file and the C library can both be found, i.e., they use it by 
running something to the effect of

os.setenv("GI_TYPELIB_PATH", "myvenv/lib/girepository-1.0")
os.setenv("LD_LIBRARY_PATH", "myvenv/lib")
gi.require_version("Modulemd", "2.0")
from gi.repository import Modulemd

That would let me confirm that I've actually gotten all the libraries 
compiling properly inside the wheel and the code actually works. Then 
there's a question of how to do this automatically. A 90% solution would 
be to just decide that your wheel has a top-level Python module to do 
this, e.g., you tell your users that if they're using the wheel they just 
do "import Modulemd" and you create a Modulemd.py that does

os.setenv("GI_TYPELIB_PATH", some relative path from __file__)
etc.

(For bonus points, call g_irepository_prepend_search_path() / see if 
PyGObject has some binding to it, instead of setting $GI_TYPELIB_PATH, and 
use ctypes to load the actual C library using RTLD_GLOBAL so that it's 
already loaded when PyGObject goes looking for it, instead of setting 
$LD_LIBRARY_PATH.)

In my (naive) opinion, a 100% solution here would be teaching PyGObject 
how to find both typelib files and C libraries in paths based on sys.path, 
and then your users could use the standard upstream import instructions 
unmodified. (Actually, it's possible PyGObject does this already, but I 
don't immediately see anything about it in the docs, and my assumption is 
if you can't find examples of others doing this, the use case hasn't come 
up.)

One other question is whether your users are importing libgobject from the 
system or from a wheel. For the average desktop Linux user, it's probably 
fine to get libgobject from the system (and probably _preferable_ - you 
likely want the same version as the Gtk/GNOME/etc. libraries they might 
import, and if they're importing any of those, they almost certainly want 
the system version of Gtk etc.) It appears that PyGObject is on PyPI as 
sdists only, so if you don't want to assume your users have libgobject 
installed, you may have to first fight the battle of packaging up GObject, 
GLib, etc. into wheels.

(Relatedly, I'm guessing the reason nobody has done this yet is that most 
software that supports GObject introspection is GNOME-related in some 
fashion and therefore most people want it from their OS package manager 
and not from a wheel.)

Again, it's been a while since I've worked with GObject introspection, so 
if I got something wrong, anyone should feel free to correct me :)

-- 
Geoffrey Thomas
https://ldpreload.com
geofft at ldpreload.com


More information about the Wheel-builders mailing list