# 我更新 uv 之後 WeasyPrint 就載入不到 Pango 了 — macOS 上的 uv run 會移除 DYLD_* 變數

> 更新了 uv 之後，macOS 上的 uv run 便不再把 DYLD_FALLBACK_LIBRARY_PATH 傳給 Python。只要改用另一個環境變數傳遞函式庫路徑，再讓指令碼重新執行自己，WeasyPrint 就能再次載入 Pango。

- Source: https://oharu121.com/zh-tw/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

---
## 引言

macOS 上的 WeasyPrint 只有在 `DYLD_FALLBACK_LIBRARY_PATH` 指向 `/opt/homebrew/lib` 時，才找得到 Homebrew 的 Pango。我在[用 Markdown 產生 PDF 的那篇文章](/zh-tw/blog/markdown-source-of-truth-git-weasyprint-pdf-pipeline/)裡，把這個變數設定在 Makefile 的 `uv run` 前面。但是當我把 uv 更新到 0.13.5 之後，`make docs-pdf` 又出現了找不到函式庫的錯誤：

```text
OSError: cannot load library 'libgobject-2.0-0': dlopen(libgobject-2.0-0, 0x0002): tried: 'libgobject-2.0-0' (no such file)
```

Pango 還裝著，Makefile 也沒有改過。**問題出在 `uv run`，它在啟動 Python 時把所有 `DYLD_*` 變數都移除了**。

解決方法是改用另一個環境變數來傳遞函式庫目錄，dyld（macOS 的動態連結器）不會處理這個變數。指令碼再把函式庫目錄複製回 `DYLD_FALLBACK_LIBRARY_PATH`，並在匯入 WeasyPrint 之前重新執行自己。Makefile 仍然只有一行 `uv run`，不論有沒有 Homebrew，建置都一樣能夠運作。

## 為什麼透過 `uv run` 啟動的 Python 讀不到 `DYLD_FALLBACK_LIBRARY_PATH`？

**變數確實有設定，只是在 Python 啟動之前就被 uv 移除了**。在完整的錯誤訊息裡，dyld 列出了它搜尋過的每一個目錄，但其中並沒有 `/opt/homebrew/lib`。

```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).
```

我在設定了變數的情況下，直接啟動 Python 並執行了指令，這時是看得到這個變數的：

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

同樣在設定了變數的情況下，透過 `uv run` 啟動並執行同一個指令時，變數卻不見了：

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

差別在於 `uv` 執行檔是用 hardened runtime 簽署的。**除非執行檔帶有 `com.apple.security.cs.allow-dyld-environment-variables` 這個 entitlement，否則 hardened runtime 會把行程中的 `DYLD_*` 變數移除**。用 `codesign` 查看 uv 0.13.5，可以看到 `runtime` flag，而且這個執行檔沒有任何 entitlement：

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

> **話說**
>
> **dyld、hardened runtime 與 entitlement**
>
> hardened runtime 是 macOS 程式碼簽署的一種模式，也是公證（notarization）的必要條件。dyld 會在行程啟動時載入它需要的函式庫，`DYLD_*` 變數則是告訴 dyld 要去哪裡找。`DYLD_*` 變數也能被用來把外來的程式碼注入行程，所以在啟用 hardened runtime 的執行檔上，dyld 會忽略並移除這些變數。entitlement 是簽署時寫進執行檔的權限宣告，用來開放 hardened runtime 預設禁止的行為。

*Figure — LaunchChainBefore: `/bin/sh` 在啟動 `uv` 時設定了變數，但啟用 hardened runtime 的 `uv` 執行檔一開始執行，變數就被移除了。Python 繼承了 `uv` 的環境，所以也拿不到這個變數。*

hardened runtime 在 uv 啟動時就移除了 `DYLD_FALLBACK_LIBRARY_PATH`，所以 uv 根本沒有拿到這個變數，也就無從傳給 Python。我不知道 uv 從哪個版本開始改用 hardened runtime 簽署，只知道更新之前 Makefile 是正常的，而 0.13.5 會移除 `DYLD_*` 的相關變數。

uv 的 issue 裡已經有人回報過[同樣的問題](https://github.com/astral-sh/uv/issues/6971)。回報時用的是 `uvx`（也就是 `uv tool run`），但 uv 的維護者以「不打算處理」（not planned）將它關閉了。**因此只能想辦法在 Makefile 和指令碼裡繞過這個限制**。

> **小心喔**
>
> **直接啟動 `uv run --with` 的直譯器行不通**
>
> 讓 `uv run --with` 印出 `sys.executable`，再用設定好變數的 shell 啟動它，會得到 `No such file or directory`。`uv run --with` 建立的臨時環境會在 `uv` 結束時被刪除。如果相依套件放在專案環境裡（例如 `uv run --group docs`），設定好變數後直接啟動 `.venv/bin/python` 就行得通。

## 在 Python 裡設定變數並重新執行

Python 本身沒有啟用 hardened runtime，所以只要改用名稱不是 `DYLD_` 開頭的環境變數，就能把函式庫目錄送進 Python。**Makefile 用 `DOCS_PDF_LIB_DIR` 傳遞目錄，指令碼再在匯入 WeasyPrint 之前把這個值轉回 `DYLD_FALLBACK_LIBRARY_PATH`，讓 dyld 找得到 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
```

光是設定 `os.environ` 還不夠。**dyld 只會在行程啟動時讀取一次 `DYLD_FALLBACK_LIBRARY_PATH`**，所以在執行中的直譯器裡寫入這個值，只會影響它之後啟動的行程。因此指令碼會重新啟動同一個直譯器來取代自己，這樣新的直譯器在啟動時就會帶有這個變數：

```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` 會保留行程 ID**。對 `uv` 來說，它啟動的行程還沒有結束，所以不會刪除臨時環境。重新啟動的直譯器因此還能從 `uv run --with` 的臨時環境載入 WeasyPrint。

*Figure — LaunchChainAfter: `/bin/sh` 設定的 `DOCS_PDF_LIB_DIR` 能夠通過 `uv`。Python 在同一個行程裡用 `os.execv` 取代自己，重新啟動時就帶有 `DYLD_FALLBACK_LIBRARY_PATH`。`uv` 還在等待這個行程，所以臨時環境仍然保留，新的 Python 能從臨時環境載入 WeasyPrint。*

當 Python 在同一個行程裡用 `os.execv` 取代自己並重新執行之後，指令碼會從頭執行，再呼叫一次 `_reexec_with_lib_dir()`。如果沒有任何標記，指令碼就會再次設定變數並重新執行，無限循環下去。**所以函式在呼叫 `os.execv` 之前，會先設定 `_DOCS_PDF_REEXECED=1`**，重新啟動的指令碼看到這個變數就會直接返回。

只有 macOS 需要這個處理。在 Linux 上，`sys.platform` 不是 `darwin`，函式會直接返回。在沒有 Homebrew 的 Mac 上，Makefile 裡的 `brew --prefix` 什麼都拿不到，`DOCS_PDF_LIB_DIR` 是空的，所以函式同樣會在做任何修改之前就返回。這樣一來，同一個 Makefile 在不同 OS 的機器上都能使用。

Claude Code 在 shell 裡完全不設定 `DYLD_*` 變數的情況下，透過 `uv run` 測試了重新執行。重新啟動的直譯器從 `/opt/homebrew/lib` 載入了 `libgobject-2.0.0.dylib`。之後，`make docs-pdf` 在我的機器上也重新建置出了 PDF。

## 總結

我在 macOS 上把 `DYLD_FALLBACK_LIBRARY_PATH` 設定在 `uv run` 前面，但更新 uv 之後，這個變數就不再傳到 Python，導致 WeasyPrint 載入不到 Pango 了。這是因為 uv 0.13.5 使用 hardened runtime 簽署，而 hardened runtime 會移除 `DYLD_*` 變數造成 Pango 無法被載入。

**現在函式庫目錄改用 `DOCS_PDF_LIB_DIR` 傳遞，指令碼會設定 `DYLD_FALLBACK_LIBRARY_PATH`，並在匯入 WeasyPrint 之前重新執行自己**。Makefile 仍然只有一行 `uv run`，同一套建置在 Linux 和沒有 Homebrew 的 Mac 上，不用任何修改也能執行。

如果相依套件放在專案環境裡，設定好變數後直接啟動 `.venv/bin/python` 是更簡單的解決方法。

## 參考連結

- [macOS 上用 `uvx` 啟動的指令碼拿不到 `DYLD_FALLBACK_LIBRARY_PATH` 的問題](https://github.com/astral-sh/uv/issues/6971)
- [WeasyPrint 找不到函式庫時的疑難排解，包含 macOS 上的 `DYLD_FALLBACK_LIBRARY_PATH`](https://doc.courtbouillon.org/weasyprint/stable/first_steps.html)
- [WeasyPrint 維護者說明 Python 行程必須在設定好變數的狀態下啟動](https://github.com/Kozea/WeasyPrint/issues/2427)
- [Python.org 的 macOS 建置因為 hardened runtime 而忽略 `DYLD_LIBRARY_PATH` 的問題](https://bugs.python.org/issue40198)
- [Apple DTS 說明啟用 hardened runtime 的輔助工具需要 Allow DYLD Environment Variables entitlement](https://developer.apple.com/forums/thread/134064)
- [Apple 的 Allow DYLD Environment Variables entitlement 說明，包含 `DYLD_*` 變數能被用來注入程式碼](https://developer.apple.com/documentation/bundleresources/entitlements/com.apple.security.cs.allow-dyld-environment-variables)
