Skip to content

Commit

Permalink
feat(docs): Add troubleshooting guide for CairoSVG crash
Browse files Browse the repository at this point in the history
  • Loading branch information
kamilkrzyskow committed Mar 5, 2024
1 parent 51f66fd commit 78796ad
Show file tree
Hide file tree
Showing 4 changed files with 314 additions and 0 deletions.
130 changes: 130 additions & 0 deletions docs/plugins/requirements/image-processing.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,133 @@ The following environments come with a preinstalled version of [pngquant]:
[pngquant]: https://pngquant.org/
[built-in optimize plugin]: ../../plugins/optimize.md
[pngquant-winbuild]: https://github.com/jibsen/pngquant-winbuild

## Troubleshooting

### Cairo library was not found

After following the installation guide above it may happen that you still
get the following error:

```bash
no library called "cairo-2" was found
no library called "cairo" was found
no library called "libcairo-2" was found
cannot load library 'libcairo.so.2': error 0x7e. Additionally, ctypes.util.find_library() did not manage to locate a library called 'libcairo.so.2'
cannot load library 'libcairo.2.dylib': error 0x7e. Additionally, ctypes.util.find_library() did not manage to locate a library called 'libcairo.2.dylib'
cannot load library 'libcairo-2.dll': error 0x7e. Additionally, ctypes.util.find_library() did not manage to locate a library called 'libcairo-2.dll'
```

This means that the [`cairosvg`][PyPi CairoSVG] package was installed,
but the underlying [`cairocffi`][PyPi CairoCFFI] dependency couldn't
[find][cffi-dopen] the installed library. Depending on the operating system
the library lookup process is different:

!!! tip
Before proceeding remember to fully restart any open Terminal windows, and
their parent hosts like IDEs to reload any environmental variables, which
were altered during the installation process. This might be the quick fix.

=== ":material-apple: macOS"

On macOS the library lookup checks inside paths defined in [dyld][osx-dyld].
Additionally each library `name` is checked in [three variants][find-library-macOS]
with the `libname.dylib`, `name.dylib` and `name.framework/name` format.

[Homebrew] should set every needed variable to point at the installed
library directory, but if that didn't happen, you can use the debug script
below to see what paths are looked up.

A [known workaround][cffi-issue] is to add the Homebrew lib path directly
before running MkDocs:

```bash
export DYLD_FALLBACK_LIBRARY_PATH=/opt/homebrew/lib
```

??? example "Python Debug macOS Script"

You can run the code below as a `debug.py` script or directly in
the interpreter.

```py
--8<-- "includes/debug/cairo-lookup-macos.py"
```

=== ":fontawesome-brands-windows: Windows"

On Windows the library lookup checks inside the paths defined in the
environmental `PATH` variable. Additionally each library `name` is checked
in [two variants][find-library-Windows] with the `name` and `name.dll` format.

The default installation path of [GTK runtime] is:

```powershell
C:\Program Files\GTK3-Runtime Win64
```

and the libraries are in the `<INSTALL-DIR>\lib` directory. Use the debug
script below to check if the path is included. If it isn't then:

1. Press ++windows+r++
2. Run the `SystemPropertiesAdvanced` applet
3. Select "Environmental Variables" at the bottom
4. Add the whole path to the `lib` directory to your `Path` variable.
5. Fully restart any open Terminal windows and parent hosts like IDEs to
reload the `Path` inside them.

```powershell title="You can also list paths using PowerShell"
$env:Path -split ';'
```

??? example "Python Debug Windows Script"

You can run the code below as a `debug.py` script or directly in
the interpreter.

```py
--8<-- "includes/debug/cairo-lookup-windows.py"
```

=== ":material-linux: Linux"

On Linux the library lookup can [differ greatly][find-library-Linux] and is
dependant from the installed distribution. For tested Ubuntu and Manjaro
systems Python runs shell commands to check which libraries are available in
[`ldconfig`][ubuntu-ldconfig], in the [`gcc`][ubuntu-gcc]/`cc` compiler, and
in [`ld`][ubuntu-ld].

You can extend the `LD_LIBRARY_PATH` environmental variable with an absolute
path to a library directory containing `libcairo.so` etc. Run this directly
before MkDocs:

```bash
export LD_LIBRARY_PATH=/absolute/path/to/lib:$LD_LIBRARY_PATH
```

You can also modify the `/etc/ld.so.conf` file.

The Python script below shows, which function is being run to find installed
libraries. You can check the source to find out what specific commands are
executed on your system during library lookup.

??? example "Python Debug Linux Script"

You can run the code below as a `debug.py` script or directly in
the interpreter.

```py
--8<-- "includes/debug/cairo-lookup-linux.py"
```

[PyPi CairoSVG]: https://pypi.org/project/CairoSVG
[PyPi CairoCFFI]: https://pypi.org/project/CairoCFFI
[osx-dyld]: https://www.unix.com/man-page/osx/1/dyld/
[ubuntu-ldconfig]: https://manpages.ubuntu.com/manpages/focal/en/man8/ldconfig.8.html
[ubuntu-ld]: https://manpages.ubuntu.com/manpages/xenial/man1/ld.1.html
[ubuntu-gcc]: https://manpages.ubuntu.com/manpages/trusty/man1/gcc.1.html
[cffi-issue]: https://github.com/squidfunk/mkdocs-material/issues/5121
[cffi-dopen]: https://github.com/Kozea/cairocffi/blob/f1984d644bbc462ef0ec33b97782cf05733d7b53/cairocffi/__init__.py#L24-L49
[find-library-macOS]: https://github.com/python/cpython/blob/4d58a1d8fb27048c11bcbda3da1bebf78f979335/Lib/ctypes/util.py#L70-L81
[find-library-Windows]: https://github.com/python/cpython/blob/4d58a1d8fb27048c11bcbda3da1bebf78f979335/Lib/ctypes/util.py#L59-L67
[find-library-Linux]: https://github.com/python/cpython/blob/4d58a1d8fb27048c11bcbda3da1bebf78f979335/Lib/ctypes/util.py#L92
100 changes: 100 additions & 0 deletions includes/debug/cairo-lookup-linux.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import inspect
import os
import shutil
import subprocess
from ctypes import util


class CustomPopen(subprocess.Popen):

def __init__(self, *args, **kwargs):
print(f"Subprocess command:\n {' '.join(args[0])}")
super().__init__(*args, **kwargs)

def communicate(self, *args, **kwargs):
out, _ = super().communicate(*args, **kwargs)
out = out.rstrip()
print("Subprocess output:")
if out:
print(f" {os.fsdecode(out)}")
else:
print(f" Output is empty")
return out, _

def __get_attribute__(self, name_):
att = super().__getattribute__(name_)
if name_ == "stdout":
print("Subprocess output:")
for line_ in att:
print(os.fsdecode(line_))
return att


subprocess.Popen = CustomPopen

print("ctypes.util script with the find_library:")
print(inspect.getsourcefile(util.find_library), end="\n\n")

print("find_library function:")
func_lines = list(map(str.rstrip, inspect.getsourcelines(util.find_library)[0]))
indent = len(func_lines[0]) - len(func_lines[0].lstrip())
for line in func_lines:
print(line.replace(" " * indent, "", 1))

library_names = ("cairo-2", "cairo", "libcairo-2")
filenames = ("libcairo.so.2", "libcairo.2.dylib", "libcairo-2.dll")
c_compiler = shutil.which("gcc") or shutil.which("cc")
ld_env = os.environ.get("LD_LIBRARY_PATH")
first_found = ""

print("\nLD_LIBRARY_PATH =", ld_env, end="\n\n")

for name in library_names:
if hasattr(util, "_findSoname_ldconfig"):
result = util._findSoname_ldconfig(name)
print(f"_findSoname_ldconfig({name}) ->", result)
if result:
print(f"Found {result}")
if not first_found:
first_found = result
print("---")
if c_compiler and hasattr(util, "_findLib_gcc"):
result = util._findLib_gcc(name)
print(f"_findLib_gcc({name}) ->", result)
if result and hasattr(util, "_get_soname"):
result = util._get_soname(result)
if result:
print(f"Found {result}")
if not first_found:
first_found = result
print("---")
if hasattr(util, "_findLib_ld"):
result = util._findLib_ld(name)
print(f"_findLib_ld({name}) ->", result)
if result and hasattr(util, "_get_soname"):
result = util._get_soname(result)
if result:
print(f"Found {result}")
if not first_found:
first_found = result
print("---")
if hasattr(util, "_findLib_crle"):
result = util._findLib_crle(name, False)
print(f"_findLib_crle({name}) ->", result)
if result and hasattr(util, "_get_soname"):
result = util._get_soname(result)
if result:
print(f"Found {result}")
if not first_found:
first_found = result
print("---")

if first_found:
filenames = (first_found,) + filenames

print(f"The path is {first_found or 'not found'}")
print("List of files that FFI will try to load:")
for filename in filenames:
print("-", filename)

input("Press ENTER to end the script...")
51 changes: 51 additions & 0 deletions includes/debug/cairo-lookup-macos.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import os
from ctypes.macholib import dyld
from itertools import chain

library_names = ("cairo-2", "cairo", "libcairo-2")
filenames = ("libcairo.so.2", "libcairo.2.dylib", "libcairo-2.dll")
first_found = ""
names = []

for name in library_names:
names += [
"lib%s.dylib" % name,
"%s.dylib" % name,
"%s.framework/%s" % (name, name),
]

for name in names:
for path in dyld.dyld_image_suffix_search(
chain(
dyld.dyld_override_search(name),
dyld.dyld_executable_path_search(name),
dyld.dyld_default_search(name),
)
):
if os.path.isfile(path):
print(f"Found: {path}")
if not first_found:
first_found = path
continue

try:
if dyld._dyld_shared_cache_contains_path(path):
print(f"Found: {path}")
if not first_found:
first_found = path
continue
except NotImplementedError:
pass

print(f"Doesn't exist: {path}")
print("---")

if first_found:
filenames = (first_found,) + filenames

print(f"The path is {first_found or 'not found'}")
print("List of files that FFI will try to load:")
for filename in filenames:
print("-", filename)

input("Press ENTER to end the script...")
33 changes: 33 additions & 0 deletions includes/debug/cairo-lookup-windows.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import os

library_names = ("cairo-2", "cairo", "libcairo-2")
filenames = ("libcairo.so.2", "libcairo.2.dylib", "libcairo-2.dll")
first_found = ""
names = []

for name in library_names:
if name.lower().endswith(".dll"):
names += [name]
else:
names += [name, name + ".dll"]

for name in names:
for path in os.environ["PATH"].split(os.pathsep):
resolved_path = os.path.join(path, name)
if os.path.exists(resolved_path):
print(f"Found: {resolved_path}")
if not first_found:
first_found = resolved_path
continue
print(f"Doesn't exist: {resolved_path}")
print("---")

if first_found:
filenames = (first_found,) + filenames

print(f"The path is {first_found or 'not found'}")
print("List of files that FFI will try to load:")
for filename in filenames:
print("-", filename)

input("Press ENTER to end the script...")

0 comments on commit 78796ad

Please sign in to comment.