Skip to content

Commit a2cb35d

Browse files
Gutskamilkrzyskowsquidfunk
authored
Improved error handling on social plugin (#6818)
* fix(social): CairoSVG OSError handling in social plugin Related issue: #6817 Co-authored-by: Guts <[email protected]> * feat(docs): Add troubleshooting guide for CairoSVG crash --------- Co-authored-by: Kamil Krzyśków <[email protected]> Co-authored-by: Guts <[email protected]> Co-authored-by: Martin Donath <[email protected]>
1 parent abfac1a commit a2cb35d

File tree

6 files changed

+367
-14
lines changed

6 files changed

+367
-14
lines changed

Diff for: docs/plugins/requirements/image-processing.md

+124
Original file line numberDiff line numberDiff line change
@@ -134,3 +134,127 @@ The following environments come with a preinstalled version of [pngquant]:
134134
[pngquant]: https://pngquant.org/
135135
[built-in optimize plugin]: ../../plugins/optimize.md
136136
[pngquant-winbuild]: https://github.com/jibsen/pngquant-winbuild
137+
138+
## Troubleshooting
139+
140+
### Cairo library was not found
141+
142+
After following the installation guide above it may happen that you still get
143+
the following error:
144+
145+
```bash
146+
no library called "cairo-2" was found
147+
no library called "cairo" was found
148+
no library called "libcairo-2" was found
149+
cannot load library 'libcairo.so.2': error 0x7e. Additionally, ctypes.util.find_library() did not manage to locate a library called 'libcairo.so.2'
150+
cannot load library 'libcairo.2.dylib': error 0x7e. Additionally, ctypes.util.find_library() did not manage to locate a library called 'libcairo.2.dylib'
151+
cannot load library 'libcairo-2.dll': error 0x7e. Additionally, ctypes.util.find_library() did not manage to locate a library called 'libcairo-2.dll'
152+
```
153+
154+
This means that the [`cairosvg`][PyPi CairoSVG] package was installed, but the
155+
underlying [`cairocffi`][PyPi CairoCFFI] dependency couldn't [find][cffi-dopen]
156+
the installed library. Depending on the operating system the library lookup
157+
process is different:
158+
159+
!!! tip
160+
Before proceeding remember to fully restart any open Terminal windows, and
161+
their parent hosts like IDEs to reload any environmental variables, which
162+
were altered during the installation process. This might be the quick fix.
163+
164+
=== ":material-apple: macOS"
165+
166+
On macOS the library lookup checks inside paths defined in [dyld][osx-dyld].
167+
Additionally each library `name` is checked in [three variants][find-library-macOS]
168+
with the `libname.dylib`, `name.dylib` and `name.framework/name` format.
169+
170+
[Homebrew] should set every needed variable to point at the installed
171+
library directory, but if that didn't happen, you can use the debug script
172+
below to see what paths are looked up.
173+
174+
A [known workaround][cffi-issue] is to add the Homebrew lib path directly
175+
before running MkDocs:
176+
177+
```bash
178+
export DYLD_FALLBACK_LIBRARY_PATH=/opt/homebrew/lib
179+
```
180+
181+
View source code of [cairo-lookup-macos.py]
182+
183+
```bash title="Python Debug macOS Script"
184+
curl "https://raw.githubusercontent.com/squidfunk/mkdocs-material/master/includes/debug/cairo-lookup-macos.py" | python -
185+
```
186+
187+
=== ":fontawesome-brands-windows: Windows"
188+
189+
On Windows the library lookup checks inside the paths defined in the
190+
environmental `PATH` variable. Additionally each library `name` is checked
191+
in [two variants][find-library-Windows] with the `name` and `name.dll` format.
192+
193+
The default installation path of [GTK runtime] is:
194+
195+
```powershell
196+
C:\Program Files\GTK3-Runtime Win64
197+
```
198+
199+
and the libraries are in the `<INSTALL-DIR>\lib` directory. Use the debug
200+
script below to check if the path is included. If it isn't then:
201+
202+
1. Press ++windows+r++.
203+
2. Run the `SystemPropertiesAdvanced` applet.
204+
3. Select "Environmental Variables" at the bottom.
205+
4. Add the whole path to the `lib` directory to your `Path` variable.
206+
5. Click OK on all open windows to apply changes.
207+
6. Fully restart any open Terminal windows and their parent hosts like IDEs.
208+
209+
```powershell title="You can also list paths using PowerShell"
210+
$env:Path -split ';'
211+
```
212+
213+
View source code of [cairo-lookup-windows.py]
214+
215+
```powershell title="PowerShell - Python Debug Windows Script"
216+
(Invoke-WebRequest "https://raw.githubusercontent.com/squidfunk/mkdocs-material/master/includes/debug/cairo-lookup-windows.py").Content | python -
217+
```
218+
219+
=== ":material-linux: Linux"
220+
221+
On Linux the library lookup can [differ greatly][find-library-Linux] and is
222+
dependant from the installed distribution. For tested Ubuntu and Manjaro
223+
systems Python runs shell commands to check which libraries are available in
224+
[`ldconfig`][ubuntu-ldconfig], in the [`gcc`][ubuntu-gcc]/`cc` compiler, and
225+
in [`ld`][ubuntu-ld].
226+
227+
You can extend the `LD_LIBRARY_PATH` environmental variable with an absolute
228+
path to a library directory containing `libcairo.so` etc. Run this directly
229+
before MkDocs:
230+
231+
```bash
232+
export LD_LIBRARY_PATH=/absolute/path/to/lib:$LD_LIBRARY_PATH
233+
```
234+
235+
You can also modify the `/etc/ld.so.conf` file.
236+
237+
The Python script below shows, which function is being run to find installed
238+
libraries. You can check the source to find out what specific commands are
239+
executed on your system during library lookup.
240+
241+
View source code of [cairo-lookup-linux.py]
242+
243+
```bash title="Python Debug Linux Script"
244+
curl "https://raw.githubusercontent.com/squidfunk/mkdocs-material/master/includes/debug/cairo-lookup-linux.py" | python -
245+
```
246+
247+
[PyPi CairoSVG]: https://pypi.org/project/CairoSVG
248+
[PyPi CairoCFFI]: https://pypi.org/project/CairoCFFI
249+
[osx-dyld]: https://www.unix.com/man-page/osx/1/dyld/
250+
[ubuntu-ldconfig]: https://manpages.ubuntu.com/manpages/focal/en/man8/ldconfig.8.html
251+
[ubuntu-ld]: https://manpages.ubuntu.com/manpages/xenial/man1/ld.1.html
252+
[ubuntu-gcc]: https://manpages.ubuntu.com/manpages/trusty/man1/gcc.1.html
253+
[cffi-issue]: https://github.com/squidfunk/mkdocs-material/issues/5121
254+
[cffi-dopen]: https://github.com/Kozea/cairocffi/blob/f1984d644bbc462ef0ec33b97782cf05733d7b53/cairocffi/__init__.py#L24-L49
255+
[find-library-macOS]: https://github.com/python/cpython/blob/4d58a1d8fb27048c11bcbda3da1bebf78f979335/Lib/ctypes/util.py#L70-L81
256+
[find-library-Windows]: https://github.com/python/cpython/blob/4d58a1d8fb27048c11bcbda3da1bebf78f979335/Lib/ctypes/util.py#L59-L67
257+
[find-library-Linux]: https://github.com/python/cpython/blob/4d58a1d8fb27048c11bcbda3da1bebf78f979335/Lib/ctypes/util.py#L92
258+
[cairo-lookup-macos.py]: https://raw.githubusercontent.com/squidfunk/mkdocs-material/master/includes/debug/cairo-lookup-macos.py
259+
[cairo-lookup-windows.py]: https://raw.githubusercontent.com/squidfunk/mkdocs-material/master/includes/debug/cairo-lookup-windows.py
260+
[cairo-lookup-linux.py]: https://raw.githubusercontent.com/squidfunk/mkdocs-material/master/includes/debug/cairo-lookup-linux.py

Diff for: includes/debug/cairo-lookup-linux.py

+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import inspect
2+
import os
3+
import shutil
4+
import subprocess
5+
from ctypes import util
6+
7+
8+
class CustomPopen(subprocess.Popen):
9+
10+
def __init__(self, *args, **kwargs):
11+
print(f"Subprocess command:\n {' '.join(args[0])}")
12+
super().__init__(*args, **kwargs)
13+
14+
def communicate(self, *args, **kwargs):
15+
out, _ = super().communicate(*args, **kwargs)
16+
out = out.rstrip()
17+
print("Subprocess output:")
18+
if out:
19+
print(f" {os.fsdecode(out)}")
20+
else:
21+
print(f" Output is empty")
22+
return out, _
23+
24+
def __getattribute__(self, name_):
25+
att = super().__getattribute__(name_)
26+
if name_ == "stdout" and att is not None:
27+
att.read = self.read_wrapper(att.read)
28+
return att
29+
30+
@staticmethod
31+
def read_wrapper(func):
32+
33+
if func.__name__ == "wrapper":
34+
return func
35+
36+
def wrapper(*args, **kwargs):
37+
output = func(*args, **kwargs)
38+
print("Subprocess output:")
39+
for line_ in os.fsdecode(output).split("\n"):
40+
print(line_)
41+
return output
42+
43+
return wrapper
44+
45+
46+
subprocess.Popen = CustomPopen
47+
48+
print("ctypes.util script with the find_library:")
49+
print(inspect.getsourcefile(util.find_library), end="\n\n")
50+
51+
print("find_library function:")
52+
func_lines = list(map(str.rstrip, inspect.getsourcelines(util.find_library)[0]))
53+
indent = len(func_lines[0]) - len(func_lines[0].lstrip())
54+
for line in func_lines:
55+
print(line.replace(" " * indent, "", 1))
56+
57+
library_names = ("cairo-2", "cairo", "libcairo-2")
58+
filenames = ("libcairo.so.2", "libcairo.2.dylib", "libcairo-2.dll")
59+
c_compiler = shutil.which("gcc") or shutil.which("cc")
60+
ld_env = os.environ.get("LD_LIBRARY_PATH")
61+
first_found = ""
62+
63+
print("\nLD_LIBRARY_PATH =", ld_env, end="\n\n")
64+
65+
for name in library_names:
66+
if hasattr(util, "_findSoname_ldconfig"):
67+
result = util._findSoname_ldconfig(name)
68+
print(f"_findSoname_ldconfig({name}) ->", result)
69+
if result:
70+
print(f"Found {result}")
71+
if not first_found:
72+
first_found = result
73+
print("---")
74+
if c_compiler and hasattr(util, "_findLib_gcc"):
75+
result = util._findLib_gcc(name)
76+
print(f"_findLib_gcc({name}) ->", result)
77+
if result and hasattr(util, "_get_soname"):
78+
result = util._get_soname(result)
79+
if result:
80+
print(f"Found {result}")
81+
if not first_found:
82+
first_found = result
83+
print("---")
84+
if hasattr(util, "_findLib_ld"):
85+
result = util._findLib_ld(name)
86+
print(f"_findLib_ld({name}) ->", result)
87+
if result and hasattr(util, "_get_soname"):
88+
result = util._get_soname(result)
89+
if result:
90+
print(f"Found {result}")
91+
if not first_found:
92+
first_found = result
93+
print("---")
94+
if hasattr(util, "_findLib_crle"):
95+
result = util._findLib_crle(name, False)
96+
print(f"_findLib_crle({name}) ->", result)
97+
if result and hasattr(util, "_get_soname"):
98+
result = util._get_soname(result)
99+
if result:
100+
print(f"Found {result}")
101+
if not first_found:
102+
first_found = result
103+
print("---")
104+
105+
if first_found:
106+
filenames = (first_found,) + filenames
107+
108+
print(f"The path is {first_found or 'not found'}")
109+
print("List of files that FFI will try to load:")
110+
for filename in filenames:
111+
print("-", filename)

Diff for: includes/debug/cairo-lookup-macos.py

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import os
2+
from ctypes.macholib import dyld
3+
from itertools import chain
4+
5+
library_names = ("cairo-2", "cairo", "libcairo-2")
6+
filenames = ("libcairo.so.2", "libcairo.2.dylib", "libcairo-2.dll")
7+
first_found = ""
8+
names = []
9+
10+
for name in library_names:
11+
names += [
12+
"lib%s.dylib" % name,
13+
"%s.dylib" % name,
14+
"%s.framework/%s" % (name, name),
15+
]
16+
17+
for name in names:
18+
for path in dyld.dyld_image_suffix_search(
19+
chain(
20+
dyld.dyld_override_search(name),
21+
dyld.dyld_executable_path_search(name),
22+
dyld.dyld_default_search(name),
23+
)
24+
):
25+
if os.path.isfile(path):
26+
print(f"Found: {path}")
27+
if not first_found:
28+
first_found = path
29+
continue
30+
31+
try:
32+
if dyld._dyld_shared_cache_contains_path(path):
33+
print(f"Found: {path}")
34+
if not first_found:
35+
first_found = path
36+
continue
37+
except NotImplementedError:
38+
pass
39+
40+
print(f"Doesn't exist: {path}")
41+
print("---")
42+
43+
if first_found:
44+
filenames = (first_found,) + filenames
45+
46+
print(f"The path is {first_found or 'not found'}")
47+
print("List of files that FFI will try to load:")
48+
for filename in filenames:
49+
print("-", filename)

Diff for: includes/debug/cairo-lookup-windows.py

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import os
2+
3+
library_names = ("cairo-2", "cairo", "libcairo-2")
4+
filenames = ("libcairo.so.2", "libcairo.2.dylib", "libcairo-2.dll")
5+
first_found = ""
6+
names = []
7+
8+
for name in library_names:
9+
if name.lower().endswith(".dll"):
10+
names += [name]
11+
else:
12+
names += [name, name + ".dll"]
13+
14+
for name in names:
15+
for path in os.environ["PATH"].split(os.pathsep):
16+
resolved_path = os.path.join(path, name)
17+
if os.path.exists(resolved_path):
18+
print(f"Found: {resolved_path}")
19+
if not first_found:
20+
first_found = resolved_path
21+
continue
22+
print(f"Doesn't exist: {resolved_path}")
23+
print("---")
24+
25+
if first_found:
26+
filenames = (first_found,) + filenames
27+
28+
print(f"The path is {first_found or 'not found'}")
29+
print("List of files that FFI will try to load:")
30+
for filename in filenames:
31+
print("-", filename)

Diff for: material/plugins/social/plugin.py

+26-7
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,24 @@
4949
from mkdocs.utils import copy_file
5050
from shutil import copyfile
5151
from tempfile import NamedTemporaryFile
52+
53+
from .config import SocialConfig
54+
5255
try:
53-
from cairosvg import svg2png
5456
from PIL import Image, ImageDraw, ImageFont
55-
except ImportError:
56-
pass
57+
except ImportError as e:
58+
import_errors = {repr(e)}
59+
else:
60+
import_errors = set()
5761

58-
from .config import SocialConfig
62+
cairosvg_error: str = ""
63+
64+
try:
65+
from cairosvg import svg2png
66+
except ImportError as e:
67+
import_errors.add(repr(e))
68+
except OSError as e:
69+
cairosvg_error = str(e)
5970

6071

6172
# -----------------------------------------------------------------------------
@@ -76,10 +87,18 @@ def on_config(self, config):
7687
return
7788

7889
# Check dependencies
79-
if "Image" not in globals():
90+
if import_errors:
91+
raise PluginError(
92+
"Required dependencies of \"social\" plugin not found:\n"
93+
+ str("\n".join(map(lambda x: "- " + x, import_errors)))
94+
+ "\n\n--> Install with: pip install \"mkdocs-material[imaging]\""
95+
)
96+
97+
if cairosvg_error:
8098
raise PluginError(
81-
"Required dependencies of \"social\" plugin not found. "
82-
"Install with: pip install \"mkdocs-material[imaging]\""
99+
"\"cairosvg\" Python module is installed, but it crashed with:\n"
100+
+ cairosvg_error
101+
+ "\n\n--> Check out the troubleshooting guide: https://t.ly/MfX6u"
83102
)
84103

85104
# Move color options

0 commit comments

Comments
 (0)