自分のコードは1行も変えていないのに、nix flake update を叩いた翌日から環境構築コマンドが通らなくなった。(いや、何もしてないんですけど?)原因を追ってみると、犯人は自分の設定ファイルではなく、依存しているコマンドラインツールのビルドがどこかの時点で壊れていたことだった。しかも直そうとして分かったのは、「今回のパッケージを1個直す」だけでは同じことがまた起きる、という構造的な問題だった。じゃあ今回はどこまで直せば「直した」ことになるのか——そこまで踏み込まないと、また同じ日が来る。
この記事で学べること
- nixpkgsのローリングチャンネル(unstable)が「全パッケージのビルド成功」を保証しない、という前提の受け止め方
- 依存パッケージのリンカクラッシュを、overlayで一時的に迂回する方法
flake update後にビルド検証し、失敗したらflake.lockを自動で元に戻す仕組みの作り方- 「個別バグを直す」と「更新パイプラインを自己防衛させる」の使い分け
前提条件
- Nix Flakes + home-manager(もしくはnix-darwin)でmacOS/Linuxの開発環境を管理している
- 環境更新を
nix flake updateのような1コマンドに集約している - 依存パッケージの一部がビルド元(ソースコンパイル)で提供されている
何が起きたか
ある日、プロンプト表示ツール(Rust製)のバージョンが上がった直後から、macOS(Apple Silicon)向けのビルドだけが失敗するようになった。エラーはコンパイル自体ではなく、最終段階のリンク処理でクラッシュしている(ビルドの最後の最後で盛大にコケるタイプの不具合、精神的ダメージが地味に大きい)。手元のコードには何も手を入れていないのに、更新コマンド一発で環境全体が組み上がらなくなった。
原因を追うと、これは自分の設定の問題ではなく、macOS標準のリンカ(ld64)とそのRust製ツールが要求するlibc++のhardeningオプションの組み合わせに起因する、上流側の既知の不具合だった。しかもビルドファーム側でも同じ失敗が再現しており、あらかじめビルド済みのバイナリキャッシュも存在しない。つまりキャッシュ待ちで解決する類のものではなかった。
上流にはすでに修正パッチが用意されていて、開発ブランチにはマージ済み。ただし、実際に使っているローリングチャンネル(unstable)にその修正が反映されるまでには数日のタイムラグがある(発送済み、到着日未定の荷物を待っている感覚に近い)。「直った」と思いきや、自分の手元にはまだ届いていない——という状態だ。
即時対応:overlayでリンカだけ差し替える
チャンネル反映を待たずに直したかったので、上流と同じ回避策をoverlay(既存パッケージの定義を上書きする仕組み)で先取りすることにした。原因が「デフォルトのリンカ(ld64)が特定条件でクラッシュする」ことだったので、対象パッケージのビルド時だけ別のリンカ(LLVMのlld)に切り替える。
# 対象パッケージ 1.26.0 は darwin で ld64 の libc++ hardening 問題により
# リンカがクラッシュしビルド失敗する(上流Issueで報告済み)。
# 修正PRは merge 済みだが、チャンネルにまだ反映されていないため先取りする。
# チャンネル反映後に削除可。
(_final: prev: {
target-pkg =
if prev.stdenv.hostPlatform.isDarwin then
prev.target-pkg.overrideAttrs (old: {
nativeBuildInputs = (old.nativeBuildInputs or [ ]) ++ [ prev.llvmPackages.lld ];
env = (old.env or { }) // {
NIX_CFLAGS_LINK = "-fuse-ld=lld";
};
})
else
prev.target-pkg;
})
ポイントは条件分岐をisDarwinだけに絞っていること。Linux環境ではそもそもこの不具合が起きないので、余計な差分を持ち込まない。あわせて「チャンネル反映後に削除可」とコメントに残しておく。overlayによる回避策は放置すると「なぜこれが必要だったか誰も分からないコード」になりがちなので、消せるタイミングを未来の自分(か誰か)に伝える一言が地味に効く。
これで目の前の更新は通るようになった(正直、とりあえず今日動けばいい)。だが、これは今回のパッケージ1個の話であって、次に別のパッケージが同じようにチャンネルの先頭で一時的に壊れたら、また同じ調査から同じ手順を、1からやり直すことになる。
根本対応:更新コマンド自体を自己防衛させる
そこで見直したのは「なぜこの手の問題が定期的に起きるのか」という前提の方だった。ローリングチャンネルは、そのチャンネル内の全パッケージが常にビルド可能であることまでは保証しない。特にmacOS(darwin)向けはビルドファーム側のジョブが手薄になりがちで、たまたま壊れたリビジョンの先頭を踏むことがある。つまり、次に別のパッケージで似た事態が起きるのは「もしも」ではなく「いつか」の話だ。
だとすれば直すべきは、個別パッケージのビルド不具合そのものではなく、「壊れたリビジョンを踏んだときに更新コマンドが困らないようにする」仕組みの方になる。更新スクリプトに、更新前後を比較して壊れていたら戻す、という自己防衛ロジックを足した。
実装は次のような形。nix flake updateの前にflake.lockを一時退避しておき、更新後の入力で実際にビルドが通るかnix build --no-linkで検証、失敗したら退避しておいたファイルを書き戻す。
LOCK_BACKUP=$(mktemp)
cp flake.lock "$LOCK_BACKUP"
nix flake update
# 更新後の入力でビルド検証し、失敗したら flake.lock をロールバックする。
# ローリングチャンネルは対象環境の全パッケージのビルド成功を保証しないため、
# 壊れたリビジョンを踏んでも更新コマンドが止まらないようにする。
verify_or_rollback() {
if nix build --no-link "$@"; then
rm -f "$LOCK_BACKUP"
else
echo "WARNING: updated inputs failed to build. Rolling back flake.lock..."
mv "$LOCK_BACKUP" flake.lock
fi
}
verify_or_rollback \
".#homeConfigurations.example-darwin.activationPackage" \
".#darwinConfigurations.example-host.system"
検証に通れば新しい入力のままswitchまで進み、通らなければ退避しておいたflake.lockに戻して、既知の正常な入力のまま更新作業を続行する。「更新できないなら諦める」ではなく「更新に失敗した分だけ元に戻して、動く状態は維持する」という設計だ。
注意点・Tips
verify_or_rollbackは実行しているマシンの構成しか検証しない。 macOS上で更新したときにLinux向けの構成までは検証されないため、複数OS/複数ユーザー分の構成を1つのflakeで管理している場合、他環境向けの破壊は見逃したままflake.lockが進む可能性が残る。これは「実行コストと検証範囲のトレードオフ」として割り切った部分で、全構成を毎回ビルド検証するなら実行時間と引き換えになる(全部検証すると今度は「更新が遅い」で怒られる未来が見える)。- overlayでの回避策は「暫定パッチ」であることを明示しておく。 コメントに「上流の修正がチャンネルに反映されたら削除可」と書いておかないと、半年後に見た人(自分を含む)が「なぜこんな迂回コードがあるのか」から調査を始めることになる。
- ロールバックは「直前の状態に戻す」だけで、原因究明を代替しない。 自動ロールバックは更新作業を止めないための緩衝材であって、壊れたパッケージ自体の修正が要らなくなるわけではない。今回のケースでも、overlayでの回避とロールバック機構は別々に効いている。
まとめ
依存パッケージのビルド失敗は、直そうと思えば「そのパッケージだけ直して終わり」にできる。だが、それが起きる構造(ローリングチャンネルは全パッケージのビルド成功を保証しない)まで踏み込むと、直すべき対象は個別バグから更新パイプライン自体に変わる。今回はoverlayでの即時回避と、更新コマンドへの自動ロールバック追加という2段構えにしたことで、「次に別のパッケージが同じように壊れても、更新コマンドは止まらない」状態を先に作れた。
宣言的なインフラ管理は「壊れないこと」を前提にしがちだが、実際には「壊れたときにどう振る舞うか」を仕組みに組み込んでおく方が、長期的には手間が少ない(壊れない前提で作ったものほど、壊れた日に一番慌てる)。
AIエージェントを使った開発フローの構築・改善でも、「失敗を前提にした自己防衛設計」という発想は同じように活きる。関心があればAI開発支援サービス、気になる方はお問い合わせからどうぞ。



