# 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.

- Source: https://oharu121.com/blog/weasyprint-uv-run-strips-dyld-fallback-library-path-macos/
- Published: 2026-10-26T16:03:44+09:00
- Tags: WeasyPrint, uv, macOS, Python, GNU Make

---
## 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](/blog/markdown-source-of-truth-git-weasyprint-pdf-pipeline/) 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:

```text
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:

```text
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:

```bash
DYLD_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')))"
# '/opt/homebrew/lib:'
```

With the variable still set, running the same command through `uv run` loses it:

```bash
DYLD_FALLBACK_LIBRARY_PATH=/opt/homebrew/lib: uv run --no-project python -c "import os; print(repr(os.environ.get('DYLD_FALLBACK_LIBRARY_PATH')))"
# None
```

The 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:

```bash
codesign -dv ~/.local/bin/uv 2>&1 | grep flags
# CodeDirectory v=20500 size=54552 flags=0x10000(runtime) hashes=1699+2 location=embedded
```

> **HEADS UP**
>
> **dyld, the hardened runtime, and entitlements**
>
> The hardened runtime is the code-signing mode notarization requires. dyld loads a process's libraries, and `DYLD_*` variables tell it where to look. Those variables can also inject foreign code, so dyld drops them for a hardened binary. An entitlement is a permission signed into a binary that allows something the hardened runtime blocks.

*Figure — LaunchChainBefore: `/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`](https://github.com/astral-sh/uv/issues/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.**

> **CAREFUL**
>
> **Starting the Python of `uv run --with` directly fails**
>
> Printing `sys.executable` from `uv run --with` and starting that interpreter from a shell with the variable set fails with `No such file or directory`. `uv run --with` deletes its temporary environment when `uv` exits. With the dependencies in a project environment, such as `uv run --group docs`, starting `.venv/bin/python` directly with the variable set does work.

## 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.**

```make title="Makefile"
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.py
```

Setting `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:

```python title="scripts/docs/build_docs_pdf.py"
import os
import 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: E402
from weasyprint import CSS, HTML  # noqa: E402
```

**`os.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.

*Figure — LaunchChainAfter: `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

- [`uvx` invocation does not propagate `DYLD_FALLBACK_LIBRARY_PATH` to the invoked script on macOS](https://github.com/astral-sh/uv/issues/6971)
- [WeasyPrint troubleshooting for missing libraries, including `DYLD_FALLBACK_LIBRARY_PATH` on macOS](https://doc.courtbouillon.org/weasyprint/stable/first_steps.html)
- [WeasyPrint maintainer on launching the Python process with the variable set](https://github.com/Kozea/WeasyPrint/issues/2427)
- [Python.org macOS builds ignoring `DYLD_LIBRARY_PATH` because of the hardened runtime](https://bugs.python.org/issue40198)
- [Apple DTS on hardened helper tools needing the Allow DYLD Environment Variables entitlement](https://developer.apple.com/forums/thread/134064)
- [Apple's Allow DYLD Environment Variables entitlement reference, including that `DYLD_*` variables can inject code](https://developer.apple.com/documentation/bundleresources/entitlements/com.apple.security.cs.allow-dyld-environment-variables)
