Nix：Rustのビルドの仕組みと改善

この記事は[Nix Advent Calendar 2024](https://adventar.org/calendars/10086)の6日目の記事です。

# 概要

NixでRustアプリケーションのパッケージ化時に遭遇したエラーとその解決方法を紹介します。そのために、NixにおけるRustのビルドのフローを確認し、エラーの発生箇所を明らかにし、修正方法を説明します。

今回の問題を解決するために、[パッチ](https://github.com/NixOS/nixpkgs/pull/282798)を作成しました。

# 導入

Rustでは、[同じパッケージ名で異なるバージョンや、同一のバージョンで異なるGitリポジトリを持つこと](https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html)が可能です。たとえば、次のように`Cargo.toml`に記述できますが、Nixのビルドではエラーが発生します。

```toml
[dependencies]
rand = "0.8"
rand_07 = { package = "rand", version = "0.7" }
rand_08 = { package = "rand", version = "0.8", git = "https://github.com/rand/xxx", rev="xx" }
```

Nixでのビルドでは、RustのCargoはインターネットに直接アクセスできません。これは、Nixのビルド環境がインターネットアクセスを制限しているためです。そのため、`cargo vendor`というコマンドを使用してローカルに関連パッケージをダウンロードし、それをもとにビルドを行います。しかし、上記のような重複したパッケージを使うプロジェクトにおいて、cargo vendorを使用してビルドすると、通常の`cargo build`では成功するプロジェクトが失敗することがあります。

そのような場合、Nixでのパッケージ化は困難を伴います。具体的には、重複したパッケージの除去や、`cargo vendor`が生成したファイルを手動で修正する必要が出てきます。

次に、なぜNixのビルドフローが通常のビルドフローと異なるのかを説明し、NixにおけるRustのビルドフローとその問題点を紹介したうえで、修正方法について解説します。

# なぜNixにおけるRustのビルドで問題が起こるのか？

通常のビルドフロー（`cargo build`）では、Cargoが必要なパッケージをその都度ダウンロードします。一方、Nixでは`cargo vendor`コマンドを使用してローカルにパッケージをダウンロードします。この際、パッケージは`パッケージ名＋バージョン`のディレクトリに配置されます。しかし、Gitのハッシュが異なるパッケージであっても、同じ`パッケージ名＋バージョン`のディレクトリが使用されるため、衝突が発生し、パッケージの配置に失敗します。

`cargo vendor`がCargoに組み込まれる前の`cargo-vendor`ツールには、Gitハッシュによる衝突を回避するオプション(--no-merge-sources)がありましたが、Cargoに組み込まれた際にその機能は削除されました。そのため、Nixでのビルドが成功したり失敗したりする状況が続いていました。

この問題は現在の`cargo vendor`に起因しており、NixのRustビルドフレームワークが変更されても、本質的な解決には至っていないと考えられます。

# NixにおけるRustのビルドフロー

[nixpkgsのpkgs/build-support/rust/build-rust-package](https://github.com/NixOS/nixpkgs/tree/master/pkgs/build-support/rust/build-rust-package)には、`buildRustPackage`が定義されており、Rustプロジェクトのビルドが管理されています。

NixにおけるRustのビルドフローは、以下のような手順で行われます。フローの簡略化のため、Cargo.lockを使う場合で説明します。

1. **cargo-vendor-dirの作成**: [`importCargoLock`](https://github.com/NixOS/nixpkgs/blob/master/pkgs/build-support/rust/import-cargo-lock.nix)を使ってcargo-vendor-dirを作成します。
2. `importCargoLock`の中でCargoのパッケージ（Crate）を[crates.io](https://github.com/NixOS/nixpkgs/blob/master/pkgs/build-support/rust/import-cargo-lock.nix#L107-L109)から取り出します。そこにないものは`outputHashes`から検索し、Gitからダウンロードしようとしますが、Gitのハッシュが見つからないパッケージがあればエラーで失敗します。
3. `importCargoLock`の中でCargoのパッケージをダウンロードしたあと、ワークスペースの設定のための親のCargo.tomlと子のCargo.tomlの融合を[`replace-workspace-values.py`](https://github.com/NixOS/nixpkgs/blob/master/pkgs/build-support/rust/replace-workspace-values.py)で行います。この融合では、ワークスペース全体で共通の依存関係やバージョンを管理し、各サブプロジェクトで整合性のあるビルドができるようにする目的があります。融合したパッケージをcargo-vendor-dirの中に配置します。
4. `importCargoLock`の中でオフラインでビルドするための`.cargo/config.toml`ファイルを生成します。
5. **ビルドの実行**: `buildRustPackage`関数の`nativeBuildInputs`に設定されている[`cargoBuildHook`](https://github.com/NixOS/nixpkgs/blob/master/pkgs/build-support/rust/hooks/cargo-build-hook.sh)を使って`cargo build`を実行し、ビルドします。
6. **成果物の生成**: ビルドが成功すると、バイナリやその他の成果物がNixストアに配置され、再利用可能な形で保存されます。

# 問題の修正方法

`cargo-vendor`のGitハッシュによる衝突を回避するオプション(--no-merge-sources)の振る舞いを再現するスクリプトを用意することで、この問題を解決しました。Gitハッシュによる衝突を回避するように、Gitハッシュを含めたディレクトリ名にパッケージ名＋バージョンを配置し、重複による衝突を避けることで、すべての依存パッケージを正しく配置できるようにしました。

`outputHashes`も従来はパッケージ名＋バージョンに対するハッシュ値しか設定できませんでしたが、パッケージ名＋バージョン＋Gitハッシュに対するハッシュ値を設定できるようにしました。

```
outputHashes = {
  "quantization-0.1.0" = "sha256-ggVqJiftu0nvyEM0dzsH0JqIc/Z1XILyUSKiJHeuuZs=";
  "tonic-0.9.2" = "sha256-ZlcDUZy/FhxcgZE7DtYhAubOq8DMSO17T+TCmXar1jE=";
  "wal-0.1.2-acaf1b2ebd5de3a871f4d2c48e13fc8788ffa43b" = "sha256-CeHQWHUVsHZvIy/7ftDWzbJ7BTARjsKvWHinEjhgL10=";
  "wal-0.1.2-fad0e7c48be58d8e7db4cc739acd9b1cf6735de0" = "sha256-nBGwpphtj+WBwL9TmWk7qXiEqlIWkgh/2V9uProqhMk=";
};
```

[sui](https://github.com/junjihashimoto/sui/tree/feature/nix)や[qdrant](https://github.com/junjihashimoto/qdrant/tree/nix/v1.8.4)といったパッケージで修正を確認しました。

# まとめ

NixにおけるRustのビルドで遭遇するパッケージの衝突問題について、その原因と解決方法を紹介しました。今回のパッチにより、より安定したビルド環境を提供することが可能になります。この知見が、NixでRustのアプリケーションをパッケージ化する際の助けになれば幸いです。