WeasyPrint lost Pango after a uv update — uv run strips DYLD_* variables on macOS
After a uv update, uv run stopped passing DYLD_FALLBACK_LIBRARY_PATH to Python on macOS. Re-executing with it in another variable lets WeasyPrint load Pango.

On this page
Introduction
WeasyPrint on macOS finds Homebrew’s Pango only when DYLD_FALLBACK_LIBRARY_PATH points at /opt/homebrew/lib, and my article on building PDFs from Markdown set it in the Makefile in front of uv run. After I updated uv to 0.13.5, make docs-pdf failed with the error that line was written to fix:
OSError: cannot load library 'libgobject-2.0-0': dlopen(libgobject-2.0-0, 0x0002): tried: 'libgobject-2.0-0' (no such file)Pango was still installed, and the Makefile had not changed. uv run was starting Python with every DYLD_* variable removed.
The fix passes the library directory in another environment variable, one that dyld (the macOS dynamic linker) does not touch, and the script copies it back into DYLD_FALLBACK_LIBRARY_PATH and re-executes itself before importing WeasyPrint. The Makefile keeps its single uv run line, and the build works the same with or without Homebrew.
Why Python started by uv run never sees DYLD_FALLBACK_LIBRARY_PATH
The variable was set, and uv dropped it before Python started. In the full error, dyld lists every directory it searched, but /opt/homebrew/lib is not among them:
OSError: cannot load library 'libgobject-2.0-0': dlopen(libgobject-2.0-0, 0x0002): tried: 'libgobject-2.0-0' (no such file), '/System/Volumes/Preboot/Cryptexes/OSlibgobject-2.0-0' (no such file), '/Users/<user>/.local/share/uv/python/cpython-3.14.9-macos-aarch64-none/lib/libgobject-2.0-0' (no such file), '/usr/lib/libgobject-2.0-0' (no such file, not in dyld cache), 'libgobject-2.0-0' (no such file).With the variable set, I started Python directly and ran a command, and Python sees the variable:
INDYLD_FALLBACK_LIBRARY_PATH=/opt/homebrew/lib: ~/.local/share/uv/python/cpython-3.14-macos-aarch64-none/bin/python3 -c "import os; print(repr(os.environ.get('DYLD_FALLBACK_LIBRARY_PATH')))"OUT'/opt/homebrew/lib:'With the variable still set, running the same command through uv run loses it:
INDYLD_FALLBACK_LIBRARY_PATH=/opt/homebrew/lib: uv run --no-project python -c "import os; print(repr(os.environ.get('DYLD_FALLBACK_LIBRARY_PATH')))"OUTNoneThe difference is that the uv binary is signed with the hardened runtime, and the hardened runtime scrubs DYLD_* variables from the process unless the binary carries the com.apple.security.cs.allow-dyld-environment-variables entitlement. codesign shows the flag on uv 0.13.5, and the binary carries no entitlements at all:
INcodesign -dv ~/.local/bin/uv 2>&1 | grep flagsOUTCodeDirectory v=20500 size=54552 flags=0x10000(runtime) hashes=1699+2 location=embedded/bin/sh sets the variable while starting uv, and it is gone once the hardened uv binary runs. Python inherits uv’s environment, so it never gets the variable either.The hardened runtime removes DYLD_FALLBACK_LIBRARY_PATH as uv starts, so uv never has the variable to pass on to Python. I do not know which uv release started signing this way. The Makefile worked before I updated, and 0.13.5 strips DYLD_* variables.
The same problem is already reported in uvx invocation does not propagate DYLD_FALLBACK_LIBRARY_PATH (astral-sh/uv#6971). The report uses uvx, which is shorthand for uv tool run, and uv’s maintainers closed it as not planned. So the fix has to be a workaround in the Makefile and the script.
Set the variable inside Python and re-execute
Python itself is not hardened, so the directory only has to reach it in a variable whose name does not start with DYLD_. The Makefile passes the directory as DOCS_PDF_LIB_DIR, and the script turns it back into DYLD_FALLBACK_LIBRARY_PATH before WeasyPrint is imported, so dyld can find Pango.
BREW_PREFIX := $(shell brew --prefix 2>/dev/null)
docs-pdf: DOCS_PDF_LIB_DIR="$(if $(BREW_PREFIX),$(BREW_PREFIX)/lib,)" \ uv run --with markdown --with weasyprint python scripts/docs/build_docs_pdf.pySetting os.environ is not enough on its own. dyld reads DYLD_FALLBACK_LIBRARY_PATH once, when a process starts, so a value written into the running interpreter only reaches processes it launches later. The script therefore replaces itself with a fresh copy of the same interpreter, which starts with the variable already in place:
import osimport sys
def _reexec_with_lib_dir() -> None: lib_dir = os.environ.get("DOCS_PDF_LIB_DIR") if sys.platform != "darwin" or not lib_dir or os.environ.get("_DOCS_PDF_REEXECED"): return current = os.environ.get("DYLD_FALLBACK_LIBRARY_PATH", "") os.environ["DYLD_FALLBACK_LIBRARY_PATH"] = f"{lib_dir}:{current}" if current else lib_dir os.environ["_DOCS_PDF_REEXECED"] = "1" os.execv(sys.executable, [sys.executable, *sys.argv])
_reexec_with_lib_dir()
import markdown # noqa: E402from weasyprint import CSS, HTML # noqa: E402os.execv keeps the process ID. To uv, the process it started has not exited, so it does not delete the temporary environment, and the restarted interpreter can still load WeasyPrint from it.
DOCS_PDF_LIB_DIR, set by /bin/sh, passes through uv. Python replaces itself with os.execv inside the same process and restarts with DYLD_FALLBACK_LIBRARY_PATH set. uv is still waiting on that process, so the temporary environment stays, and the new Python loads WeasyPrint from it.After Python replaces itself with os.execv inside the same process, the script runs from the top and calls _reexec_with_lib_dir() again. Without a marker, it would set the variable and re-execute again, forever. The function therefore sets _DOCS_PDF_REEXECED=1 before calling os.execv, and the restarted script sees it and returns immediately.
Only macOS needs this. On Linux, sys.platform is not darwin, so the function returns at once. On a Mac without Homebrew, brew --prefix in the Makefile finds nothing, so DOCS_PDF_LIB_DIR is empty and the function returns before touching anything. The same Makefile therefore works on machines running different operating systems.
Claude Code tested the re-exec through uv run with no DYLD_* variable set in the shell, and the restarted interpreter loaded libgobject-2.0.0.dylib from /opt/homebrew/lib. After that, make docs-pdf built the PDFs again on my machine.
Summary
After a uv update, DYLD_FALLBACK_LIBRARY_PATH set in front of uv run stopped reaching Python on macOS, and WeasyPrint failed to load Pango again. uv 0.13.5 is signed with the hardened runtime, which scrubs DYLD_* variables.
The library directory now travels as DOCS_PDF_LIB_DIR, and the script sets DYLD_FALLBACK_LIBRARY_PATH and re-executes itself before importing WeasyPrint. The Makefile keeps one uv run line, and the same build runs on Linux and on a Mac without Homebrew untouched.
If your dependencies live in a project environment, starting .venv/bin/python directly with the variable set is the simpler fix.
References
uvxinvocation does not propagateDYLD_FALLBACK_LIBRARY_PATHto the invoked script on macOS (astral-sh/uv#6971)- WeasyPrint troubleshooting for missing libraries, including
DYLD_FALLBACK_LIBRARY_PATHon macOS - WeasyPrint maintainer on launching the Python process with the variable set (Kozea/WeasyPrint#2427)
- Python.org macOS builds ignoring
DYLD_LIBRARY_PATHbecause of the hardened runtime - Apple DTS on hardened helper tools needing the Allow DYLD Environment Variables entitlement
- Apple’s Allow DYLD Environment Variables entitlement reference, including that
DYLD_*variables can inject code




