From 78796ad91e7adac98e5e92f6df5baedba0369169 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Krzy=C5=9Bk=C3=B3w?= Date: Thu, 29 Feb 2024 00:07:11 +0100 Subject: [PATCH] feat(docs): Add troubleshooting guide for CairoSVG crash --- docs/plugins/requirements/image-processing.md | 130 ++++++++++++++++++ includes/debug/cairo-lookup-linux.py | 100 ++++++++++++++ includes/debug/cairo-lookup-macos.py | 51 +++++++ includes/debug/cairo-lookup-windows.py | 33 +++++ 4 files changed, 314 insertions(+) create mode 100644 includes/debug/cairo-lookup-linux.py create mode 100644 includes/debug/cairo-lookup-macos.py create mode 100644 includes/debug/cairo-lookup-windows.py diff --git a/docs/plugins/requirements/image-processing.md b/docs/plugins/requirements/image-processing.md index c53cbf6adb2..7ac2dcd6fd6 100644 --- a/docs/plugins/requirements/image-processing.md +++ b/docs/plugins/requirements/image-processing.md @@ -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 `\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 diff --git a/includes/debug/cairo-lookup-linux.py b/includes/debug/cairo-lookup-linux.py new file mode 100644 index 00000000000..eb81b94d0e0 --- /dev/null +++ b/includes/debug/cairo-lookup-linux.py @@ -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...") diff --git a/includes/debug/cairo-lookup-macos.py b/includes/debug/cairo-lookup-macos.py new file mode 100644 index 00000000000..d50c721e740 --- /dev/null +++ b/includes/debug/cairo-lookup-macos.py @@ -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...") diff --git a/includes/debug/cairo-lookup-windows.py b/includes/debug/cairo-lookup-windows.py new file mode 100644 index 00000000000..0c91af131f0 --- /dev/null +++ b/includes/debug/cairo-lookup-windows.py @@ -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...")