uv の更新後に WeasyPrint が Pango を読み込めなくなった — macOS の uv run は DYLD_* 変数を削除する

uv の更新後、macOS の uv run が DYLD_FALLBACK_LIBRARY_PATH を Python に渡さなくなりました。ライブラリのパスを別の環境変数で渡し、スクリプトに自分自身を再実行させると、WeasyPrint がふたたび Pango を読み込めます。

白いカードに、緑色の山括弧と緑色の角丸長方形に挟まれた濃紺の小文字のweasyprintというWeasyPrintのロゴ
目次

はじめに

macOS の WeasyPrint が Homebrew の Pango を見つけられるのは、DYLD_FALLBACK_LIBRARY_PATH が /opt/homebrew/lib を指しているときだけです。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 が DYLD_* 変数をすべて削除した状態で Python を起動していました。

修正では、ライブラリのディレクトリを別の環境変数で渡します。dyld(macOS の動的リンカ)はこの変数には手を付けません。スクリプトはライブラリのディレクトリを DYLD_FALLBACK_LIBRARY_PATH にコピーし直し、WeasyPrint を import する前に自分自身を再実行します。Makefile の uv run は 1 行のままで、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 を直接起動してコマンドを実行すると、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 で署名されていることです。Hardened Runtime は、バイナリが com.apple.security.cs.allow-dyld-environment-variables エンタイトルメントを持っていない限り、プロセスから DYLD_* 変数を削除します。uv 0.13.5 を codesign で見ると runtime フラグが立っていて、エンタイトルメントは 1 つも付いていません。

ターミナルウィンドウ
入力
codesign -dv ~/.local/bin/uv 2>&1 | grep flags
出力
CodeDirectory v=20500 size=54552 flags=0x10000(runtime) hashes=1699+2 location=embedded
DYLD_FALLBACK_LIBRARY_PATH が消える場所プロセスが 1 列に並ぶ。make が /bin/sh を、/bin/sh が uv を、uv が Python を起動する。DYLD 変数は /bin/sh が uv を起動するときに設定するが、Hardened Runtime の uv が起動した時点で削除され、Python には届かないため libgobject の読み込みに失敗する。レシピで DYLD_FALLBACK_LIBRARY_PATH を設定makeレシピを実行/bin/shDYLD_* を設定 ✓uvHardened RuntimeDYLD_* 削除 ✗pythonDYLD_* なし ✗dlopen 失敗:libgobject-2.0-0が見つからない
/bin/sh は uv を起動するときに変数を設定しますが、Hardened Runtime の uv バイナリが動き出した時点で変数は消えています。Python は uv の環境を引き継ぐので、この変数を受け取れません。

Hardened Runtime が uv の起動時に DYLD_FALLBACK_LIBRARY_PATH を削除するので、uv はそもそもこの変数を受け取っておらず、Python に渡しようがありません。uv のどのリリースからこの署名方式になったのかは分かりません。分かっているのは、更新前は Makefile が動いていたことと、0.13.5 では DYLD_* 変数が削除されることだけです。

uv の issue で、同じ問題 (astral-sh/uv#6971)がすでに報告されています。報告で使われているのは uvx(uv tool run の短縮形)ですが、uv のメンテナーはこの issue を対応予定なし(not planned)としてクローズしました。そのため、Makefile とスクリプトの側でこの制限を回避するしかありません。

Python の中で変数を設定して再実行する

Python 自体は Hardened Runtime で署名されていないので、名前が DYLD_ で始まらない環境変数でディレクトリを Python まで届ければ十分です。Makefile はディレクトリを DOCS_PDF_LIB_DIR として渡し、スクリプトは WeasyPrint を import する前にその値を DYLD_FALLBACK_LIBRARY_PATH に戻し、dyld が Pango を見つけられるようにします。

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 を読むのはプロセスの起動時の 1 回だけなので、実行中のインタープリタに書き込んだ値は、そのインタープリタがあとから起動するプロセスにしか届きません。そこでスクリプトは、同じインタープリタを起動し直して自分自身と置き換えます。起動し直したインタープリタには、最初から変数が設定されています。

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 を読み込めます。

再実行した Python が DYLD_FALLBACK_LIBRARY_PATH を受け取る仕組みmake が /bin/sh を、/bin/sh が uv を起動する。/bin/sh が設定した DOCS_PDF_LIB_DIR は uv を通過する。uv は Python のプロセスを 1 つ起動し、そのプロセス ID の終了を待つ。そのプロセスの中で、Python はまず DYLD 変数を設定して os.execv を呼び、変数が設定された新しい Python に置き換わって Pango を読み込む。uv はまだ待っているので、uv run --with が作った一時環境はディスクに残り、新しい Python はそこから WeasyPrint を読み込む。レシピで DOCS_PDF_LIB_DIR を設定し、Python が再実行makeレシピを実行/bin/shDOCS_PDF_LIB_DIRを設定 ✓uvHardened RuntimeDOCS_PDF_LIB_DIR残る ✓PID 4242 の終了を待つPID 4242(1 つのプロセス)pythonDYLD_* を設定し os.execv を呼ぶos.execv で置き換えpythonDYLD_* 設定済みで起動 ✓、Pango を読み込むuv run --with の一時環境uv が終了していないので残る ✓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 が、シェルに DYLD_* 変数を設定しない状態で、uv run 経由の再実行を試しました。起動し直したインタープリタは、/opt/homebrew/lib から libgobject-2.0.0.dylib を読み込みました。そのあと、私のマシンでも make docs-pdf で PDF をビルドできるようになりました。

まとめ

uv の更新後、macOS で uv run の前に設定した DYLD_FALLBACK_LIBRARY_PATH が Python に届かなくなり、WeasyPrint はふたたび Pango を読み込めなくなりました。uv 0.13.5 は Hardened Runtime で署名されていて、Hardened Runtime は DYLD_* 変数を削除します。

現在はライブラリのディレクトリを DOCS_PDF_LIB_DIR で渡し、スクリプトが DYLD_FALLBACK_LIBRARY_PATH を設定してから、WeasyPrint を import する前に自分自身を再実行します。Makefile の uv run は 1 行のままで、同じビルドが Linux でも Homebrew のない Mac でも、手を加えずに動きます。

依存関係がプロジェクト環境にあるなら、変数を設定して .venv/bin/python を直接起動するほうが簡単です。

参考リンク

この記事をシェア