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

本頁目錄
引言
macOS 上的 WeasyPrint 只有在 DYLD_FALLBACK_LIBRARY_PATH 指向 /opt/homebrew/lib 時,才找得到 Homebrew 的 Pango。我在用 Markdown 產生 PDF 的那篇文章裡,把這個變數設定在 Makefile 的 uv run 前面。但是當我把 uv 更新到 0.13.5 之後,make docs-pdf 又出現了找不到函式庫的錯誤:
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。
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 並執行了指令,這時是看得到這個變數的:
輸入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 啟動並執行同一個指令時,變數卻不見了:
輸入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:
輸入codesign -dv ~/.local/bin/uv 2>&1 | grep flags輸出CodeDirectory v=20500 size=54552 flags=0x10000(runtime) hashes=1699+2 location=embedded/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 裡已經有人回報過同樣的問題 (astral-sh/uv#6971)。回報時用的是 uvx(也就是 uv tool run),但 uv 的維護者以「不打算處理」(not planned)將它關閉了。因此只能想辦法在 Makefile 和指令碼裡繞過這個限制。
在 Python 裡設定變數並重新執行
Python 本身沒有啟用 hardened runtime,所以只要改用名稱不是 DYLD_ 開頭的環境變數,就能把函式庫目錄送進 Python。Makefile 用 DOCS_PDF_LIB_DIR 傳遞目錄,指令碼再在匯入 WeasyPrint 之前把這個值轉回 DYLD_FALLBACK_LIBRARY_PATH,讓 dyld 找得到 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.py光是設定 os.environ 還不夠。dyld 只會在行程啟動時讀取一次 DYLD_FALLBACK_LIBRARY_PATH,所以在執行中的直譯器裡寫入這個值,只會影響它之後啟動的行程。因此指令碼會重新啟動同一個直譯器來取代自己,這樣新的直譯器在啟動時就會帶有這個變數:
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 會保留行程 ID。對 uv 來說,它啟動的行程還沒有結束,所以不會刪除臨時環境。重新啟動的直譯器因此還能從 uv run --with 的臨時環境載入 WeasyPrint。
/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的問題 (astral-sh/uv#6971) - WeasyPrint 找不到函式庫時的疑難排解,包含 macOS 上的
DYLD_FALLBACK_LIBRARY_PATH - WeasyPrint 維護者說明 Python 行程必須在設定好變數的狀態下啟動 (Kozea/WeasyPrint#2427)
- Python.org 的 macOS 建置因為 hardened runtime 而忽略
DYLD_LIBRARY_PATH的問題 - Apple DTS 說明啟用 hardened runtime 的輔助工具需要 Allow DYLD Environment Variables entitlement
- Apple 的 Allow DYLD Environment Variables entitlement 說明,包含
DYLD_*變數能被用來注入程式碼




