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

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

- Source: https://oharu121.com/ja/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 が Homebrew の Pango を見つけられるのは、`DYLD_FALLBACK_LIBRARY_PATH` が `/opt/homebrew/lib` を指しているときだけです。[Markdown から PDF を生成する記事](/ja/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` が `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` はありません。

```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 を直接起動してコマンドを実行すると、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 で署名されていることです。**Hardened Runtime は、バイナリが `com.apple.security.cs.allow-dyld-environment-variables` エンタイトルメントを持っていない限り、プロセスから `DYLD_*` 変数を削除します**。uv 0.13.5 を `codesign` で見ると `runtime` フラグが立っていて、エンタイトルメントは 1 つも付いていません。

```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、エンタイトルメント**
>
> Hardened Runtime は、公証（notarization）に必要な macOS のコード署名モードです。dyld はプロセスの起動時に必要なライブラリを読み込み、`DYLD_*` 変数はその探索場所を dyld に指示します。`DYLD_*` 変数は外部のコードをプロセスに注入する手段にもなるため、Hardened Runtime のバイナリでは dyld がこの変数を無視して削除します。エンタイトルメントは署名時にバイナリへ書き込まれる権限の宣言で、Hardened Runtime が既定で禁止している動作を許可します。

*Figure — LaunchChainBefore: `/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 で、[同じ問題](https://github.com/astral-sh/uv/issues/6971)がすでに報告されています。報告で使われているのは `uvx`（`uv tool run` の短縮形）ですが、uv のメンテナーはこの issue を対応予定なし（not planned）としてクローズしました。**そのため、Makefile とスクリプトの側でこの制限を回避するしかありません**。

> **気を付けて**
>
> **`uv run --with` のインタープリタを直接起動する方法は使えない**
>
> `uv run --with` に `sys.executable` を出力させ、変数を設定したシェルからそのインタープリタを起動すると、`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 を import する前にその値を `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` を読むのはプロセスの起動時の 1 回だけ**なので、実行中のインタープリタに書き込んだ値は、そのインタープリタがあとから起動するプロセスにしか届きません。そこでスクリプトは、同じインタープリタを起動し直して自分自身と置き換えます。起動し直したインタープリタには、最初から変数が設定されています。

```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 が、シェルに `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` を直接起動するほうが簡単です。

## 参考リンク

- [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)
- [Python プロセスを変数を設定した状態で起動するよう案内する WeasyPrint メンテナーのコメント](https://github.com/Kozea/WeasyPrint/issues/2427)
- [Hardened Runtime が原因で Python.org の macOS ビルドが `DYLD_LIBRARY_PATH` を無視する問題](https://bugs.python.org/issue40198)
- [Hardened Runtime のヘルパーツールには Allow DYLD Environment Variables エンタイトルメントが必要だとする Apple DTS の回答](https://developer.apple.com/forums/thread/134064)
- [Apple の Allow DYLD Environment Variables エンタイトルメントの説明（`DYLD_*` 変数でコードを注入できることを含む）](https://developer.apple.com/documentation/bundleresources/entitlements/com.apple.security.cs.allow-dyld-environment-variables)
