Trouble getting a PIP Package to work as a snap

I have a Python Project hosted on GitHub which builds a working PIP Package.
I have setup Snapcraft to autobuild from my GitHub repository.
Snapcraft succeeds in building a snap file which can be installed.

I get:
alexander@alexander-xps-13:~/Programming/git/ocrmypdfgui$ ocrmypdfgui
Traceback (most recent call last):
File “/snap/ocrmypdfgui/9/bin/ocrmypdfgui”, line 5, in
from ocrmypdfgui.main import main
ModuleNotFoundError: No module named ‘ocrmypdfgui’

My complete project including snapcraft.yaml can be found here:

I would very much appreciate help understanding why I am getting a Module Not Found Error when my PIP Package works. Google searching unfortunately has not helped me with this issue so far. What am I missing?

Thanks in advance!

Hi, i have seen your yaml file, looks like you have defined python package as stage packages.

Thanks for your time!

I mainly do not understand why the snap does not find my own modules.

This:

File “/snap/ocrmypdfgui/10/bin/ocrmypdfgui”, line 5, in
from ocrmypdfgui.main import main
ModuleNotFoundError: No module named ‘ocrmypdfgui’

it is launching the correct python file in bin which in turn imports the main module from my ocrmypdfgui program.

the module is installed in /snap/ocrmypdfgui/10/lib/python3.8/site-packages/ocrmypdfgui:

Tree Output:

alexander@alexander-xps-13:/snap/ocrmypdfgui/10/lib/python3.8/site-packages/ocrmypdfgui$ tree
.
├── gui.py
├── init.py
├── main.py
├── ocr.py
├── plugin_progressbar.py
└── pycache
├── gui.cpython-38.pyc
├── init.cpython-38.pyc
├── main.cpython-38.pyc
├── ocr.cpython-38.pyc
└── plugin_progressbar.cpython-38.pyc

This is the snaps /bin

alexander@alexander-xps-13:/snap/ocrmypdfgui/10/bin$ tree
.
├── activate
├── activate.csh
├── activate.fish
├── Activate.ps1
├── chardetect
├── coloredlogs
├── dumppdf.py
├── humanfriendly
├── img2pdf
├── img2pdf-gui
├── ocrmypdf
├── ocrmypdfgui
├── pdf2txt.py
├── pip
├── pip3
├── pip3.8
├── pycache
│ ├── dumppdf.cpython-38.pyc
│ └── pdf2txt.cpython-38.pyc
├── python -> python3
├── python3 -> /usr/bin/python3.8
└── tqdm

As far as I understand everything is in place for the program to at least start.
I do not understand why

/snap/ocrmypdfgui/10/bin/ocrmypdfgui.py

does not find my module which is in

/snap/ocrmypdfgui/10/lib/python3.8/site-packages/

Python has a concept of a path specifically for modules, you have two choices.

  1. In the Python Code itself, import sys and then sys.path.append("/snap/ocrmypdfgui/current/lib/python3.8/site-packages"), perhaps probe for the existence of the $SNAP variable if you want to avoid doing this for any other packages.

  2. Set the PYTHONPATH environment variable

apps:
  ocrmypdfgui:
    command: bin/ocrmypdfgui
    environment:
      PYTHONPATH: $SNAP/lib/python3.8/site-packages
1 Like

That helped a lot! Thank you!
I missed that part when I read the tutorial…

A quick follow up question.

During creation of the Snap I am inculding the leptonica library liblept.so.5.
When I mount the snap and search it manually I can find that file inside the snap here:

/snap/ocrmypdfgui/current/usr/lib/x86_64-linux-gnu/

The Python project I am running uses pythons ctypes.util import find_library() to look for that file. I get a

OSError: cannot load library ‘liblept.so.5’: liblept.so.5: cannot open shared object file: No such file or directory

The same Program runs fine on a “normal” ubuntu installation. I am dealing only with my python code and .deb and pip packages. Snap build runs without errors on snapcraft.io

Is this due to python not looking outside of PYTHONPATH? What is the correct way to get it to look in the location stated above?

Thanks again in advance.

find where in your snap that file lives and add the dir to LD_LIBRARY_PATH …

Thank you for your reply.

Unfortunately that does not seem to fix the issue.

The snap ocrmypdfgui works when leptonica is installed on the system manually. I cannot get the snap to link this correctly. I added:

environment:
...
LD_LIBRARY_PATH: $SNAP/usr/lib/x86_64-linux-gnu
...

without any success. Do you have another idea? The path is the correct location of the lib as far as I can tell.

A quick follow-up question:

Why does the snap work when I have the library locally installed outside of the snap? Should it look outside of the sandbox?

Which package is allowing the snap to work?

The previous $LD_LIBRARY_PATH value looks correct, I’ll see if I can dig in on my end.

The problem comes down to Python’s ctypes.util find_library() having a dependency on /sbin/ldconfig, when the host has liblept installed, the snap works, if the host doesn’t, the snap fails. Even if the host has a copy of the library, the snap would still be using it’s own version bundled internally.

The easiest fix would be to change the one line that even bothers looking for the library, because this is in a snap, we know the library will always where we put it. Keep the $LD_LIBRARY_PATH change, and under ocrmypdfgui part, add the following.

override-build: |
      snapcraftctl build
      sed -i 's/find_library(libname)/"liblept.so.5"/g' $SNAPCRAFT_PART_INSTALL/lib/python3.6/site-packages/ocrmypdf/leptonica.py

It’s also worth mentioning you should probably change,

desktop: /build/ocrmypdfgui/parts/get-source/src/gui/ocrmypdfgui.desktop

to

desktop: $SNAPCRAFT_PROJECT_DIR/gui/ocrmypdfgui.desktop

So that it builds correctly regardless of absolute paths which aren’t guaranteed to be consistent. You can then drop the following part entirely, since I presume it’s only there to have gotten the .desktop file somewhere you expected it.

parts:
  get-source:
    plugin: dump
    source: https://github.com/alexanderlanganke/ocrmypdfgui.git

Here’s a link to the line in the Python VM source that’s the root of the issue, for the curious.

1 Like

This can be fixed by adding binutils to the base snap or as a stage package in the app snap. I think we should petition to include this deb package in all the core* snaps as a common requirement for many snaps, especially python snaps that require to use cpython library loading via pip packages.

I haven’t tested this, but I surmised the above due to the following code in the linked file:

        def find_library(name):
            # See issue #9998
            return _findSoname_ldconfig(name) or \
                   _get_soname(_findLib_gcc(name)) or _get_soname(_findLib_ld(name))

Both _findSoname_ldconfig and _findLib_gcc fail and fall through to _findLib_ld. The configured LD_LIBRARY_PATH is only inspected in this latter function (_findLib_ld), which requires ld to be accessible on the PATH. The ld executable is included in the binutils package. Therefore we can get python to respect the LD_LIBRARY_PATH when loading shared objects within cpython by making ld available (i.e. installing binutils either in the base snaps - core, core18, core20, core22, et al - or by installing binutils into every snap that uses python with cpython libraries)

Edit to add link to the _findLib_ld function: https://github.com/python/cpython/blob/fa6304a5225787054067bb56089632146d288b20/Lib/ctypes/util.py#L300

2 Likes

I had the same problem as OP, this did nothing

@James-Carroll Is there a better solution in 2024?

If it’s the same problem as OP, I doubt the answer has changed.

I know there were talks about pre-loading the library caches for snaps, but I don’t think they’ve gotten anywhere.

I’d still have to imagine the way that the Python VM was doing it back then is still the same way it’s doing it now. I.E, in this thread it was essentially looking at the hosts cache and identifying the libraries the host had rather than the snap namespace has.

Aside from changing Python / the app itself, I’d have to guess the only other solution could be to try replacing /sbin/ldconfig via a layout with a wrapper that simply always gives a positive response when asking for a specific library, but this could break other things subtly if they were to call to the wrapper and not get the expected response.

Fundamentally, the problem is that Python ends up treating the cache as a source of truth of what’s installed, and not just literally a cache, meaning stuff that’s still valid and would work gets pre-emptively prevented from trying to do so.

1 Like