Chapter 21

 §2. Rustプロジェクトのビルド

通常、Rustのプロジェクト(クレート)はCargoで管理します。今回は、Nixpkgsが提供しているRust用ビルドユーティリティを使ってクレートをNixパッケージ化します。

devShellの用意

Rustにはrustupという優れたツールがあり、通常はこれを使ってRustのツールチェーンを管理しますが、今回はNixのdevShellを用いて宣言的にツールチェーン管理を行います。

Rustツールチェーンの管理

では、Nixpkgsからrustccargoをインストールして……といきたいところですが、Nixpkgsから直接Rustツールチェーンをインストールするのは実用的ではありません。というのもNixpkgsがメンテナンスしているRustのバージョンは最新の安定板のみであり、過去のバージョンを取得しようとするとNixpkgsのコミットを遡らなければならず、nightlyは提供されていません。また、rustupを経由しないため、コンパイルターゲットを追加(例: WASMをターゲットに追加)することができません。

そこでrust-overlayを利用します。rust-overlayはRustツールチェーンのoverlayを提供しているFlakeで、ツールチェーンのバージョン指定やコンパイルターゲットの追加が可能です。

https://github.com/oxalica/rust-overlay

rust-overlay以外にもRustツールチェーンを提供するFlakeはいくつか存在し、fenixはrust-overlayと並んでよく利用されています。fenixはrust-analyzerも提供しています。

https://github.com/nix-community/fenix

nixpkgs-mozilla

実は、Mozilla公式もnixpkgs-mozillaというFlakeからRustのoverlayを提供しているのですが、上記のFlakeはnixpkgs-mozillaの代替を目的として提供されているため、nixpkgs-mozillaをRustのために利用するのは推奨されていないようです。

devShellの設定

devShellを設定します。

ディレクトリの作成
mkdir rust-with-nix
cd rust-with-nix
touch flake.nix
flake.nix
{
  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
    flake-utils.url = "github:numtide/flake-utils";
    rust-overlay = {
      url = "github:oxalica/rust-overlay";
      inputs.nixpkgs.follows = "nixpkgs";
    };
  };

  outputs =
    {
      nixpkgs,
      flake-utils,
      rust-overlay,
      ...
    }:
    flake-utils.lib.eachDefaultSystem (
      system:
      let
        pkgs = import nixpkgs {
          inherit system;
          overlays = [ rust-overlay.overlays.default ];
        };
      in
      {
        devShells.default = pkgs.mkShell {
          packages = [
            pkgs.rust-bin.stable.latest.default
          ];
        };
      }
    );
}

rust-overlayをinputsに追加する際、inputs.nixpkgs.followというオプションを指定しています。rust-overlayはNixpkgsに依存しているため、素朴に導入すると「私たちのFlakeのinputsのNixpkgs」と「rust-overlayのinputsのNixpkgs」で2つのバージョンのNixpkgsが存在することになります。依存関係の整合性という観点では分離されている方が望ましいですが、その分ストレージを多く消費します。今回はrust-overlayが依存しているNixpkgsを私たちが導入したNixpkgsに置き換えています。

Overlayを入れるとpkgsrust-binというattributeが追加されます。nix developでdevShellを起動し、cargorustcがインストールされていることが確認できたらOKです。

1. 基本的なビルド

クレートの作成

クレートを初期化します。

クレートの初期化
$ cargo init

# 実行できることを確認
$ cargo run
Hello, world!

Nixパッケージ化

まずはこのままの状態でNixパッケージ化してみます。

Nixpkgsから提供されているpkgs.rustPlatform.buildRustPackageを使います。

nix/rust-with-nix.nix
{ rustPlatform }:
rustPlatform.buildRustPackage {
  pname = "rust-with-nix";
  version = "0.1.0";

  src = ../.;
  cargoLock.lockFile = ../Cargo.lock;
}
flake.nix
 {
   inputs = # ...

   outputs =
     {
       nixpkgs,
       flake-utils,
       rust-overlay,
       ...
     }:
     flake-utils.lib.eachdefaultsystem (
       system:
       let
         pkgs = # ...
       in
       {
         devshells.default = # ...
+        packages.default = pkgs.callPackage ./nix/rust-with-nix.nix { };
       }
     );
 }

Nixでビルドして実行できることを確認します。

$ nix run
Hello, world!

rust-overlayを利用する

pkgs.rustPlatformはNixpkgsのCargoやrustcを内部で使用するため、今回私たちがdevShellで利用しているrust-overlayのRustツールチェーンを使うように変更します。

pkgs.makeRustPlatformでcargoとrustcをrust-overlayのものに置き換えたrustPlatformを作成します。

nix/rust-with-nix.nix
-{ rustPlatform }:
+{ makeRustPlatform, rust-bin }:
+let
+  toolchain = rust-bin.stable.latest.default;
+  rustPlatform = makeRustPlatform {
+    cargo = toolchain;
+    rustc = toolchain;
+  };
+in
 rustPlatform.buildRustPackage {
   pname = "rust-with-nix";
   version = "0.1.0";

   src = ../.;
   cargoLock.lockFile = ../Cargo.lock;
 }

問題なく実行できるはずです。

$ nix rust
Hello, world!

2. OpenSSLに依存するクレートのビルド

少し発展的なビルドを行います。reqwestというHTTPクライアントライブラリを導入します。

依存クレートの追加
cargo add reqwest --features=blocking
blocking feature

通常、reqwestは非同期ランタイム上で動作します。Rust本体には非同期処理を表現する型や構文などはあるものの、実際にタスクを実行するランタイムはライブラリとして追加しなければなりません。今回はあくまでNixによるビルドの解説なので、blocking featureを有効にして、素のRustで同期的に動かせるようにしています。

main.rsを以下のように書き換えます。

src/main.rs
fn main() {
    let url = match std::env::args().nth(1) {
        Some(url) => url,
        None => panic!("No URL provided"), // 引数がない場合は異常終了
    };

    let response = reqwest::blocking::get(&url).unwrap();

    println!("Statu code: {}", response.status());
}

引数にURLを受け取って、そのURLにGETリクエストを送り、レスポンスのステータスコードを表示するプログラムです。

cargo runで実行してみます。

$ cargo run https://example.com
# エラー!
# pkg-configまたはOpenSSLが見つからないと怒られる

筆者の環境では「pkg-configが見つからないぞ」と怒られてしまいました。しかし、読者の環境によってはビルドに成功するかもしれません。

デフォルトのreqwestはOpenSSLに依存しており、ビルド時にpkg-configを利用してOpenSSLのライブラリを探します。そのため、pkg-configやOpenSSLをインストールしていない環境ではビルドエラーが発生します。これはCargo単体では解決できない依存関係であり、まさしく暗黙的な依存です。

最も簡単で推奨されている解決方法は、reqwestのrustls-tls featureを有効化して、OpenSSLの代わりにRust実装のTLSライブラリrustlsを利用するようにすることです。

ですが今回は敢えてrustlsを使わず、devShellでOpenSSLをインストールしてビルドする方法を試してみます。

devShellの拡張

devShellにOpenSSLとpkg-configを追加します。

flake.nix
  # ...
  devShells.default = pkgs.mkShell {
-   packages = [
-     pkgs.rust-bin.stable.latest.default
-   ];
+   packages = with pkgs; [
+     openssl
+     pkg-config
+     rust-bin.stable.latest.default
+   ];
  };
  # ...

再びdevShellを起動してビルドします。

devShell内で実行
$ nix develop
$ cargo run https://example.com
Statu code: 200

ビルドが成功し、example.comへ送ったリクエストに対するレスポンスのステータスコードが表示されました。

devShellは外部ライブラリを多用する場面に非常で有効的です。グローバルインストールを行わず、プロジェクト単位で依存関係を管理できるため、他のプロジェクトとの依存関係の衝突を気にせずに開発を進めることができます。

Nixパッケージ化

devShellと同様、ビルド式にも依存関係を追加する必要があります。mkDerivation関数と同様に、buildInputsに実行時依存(OpenSSL)、nativeBuildInputsにビルド時依存(pkg-config)を追加します。

-{ makeRustPlatform, rust-bin }:
+{
+  makeRustPlatform,
+  rust-bin,
+  openssl,
+  pkg-config,
+}:
 let
   toolchain = rust-bin.stable.latest.default;
   rustPlatform = makeRustPlatform {
     cargo = toolchain;
     rustc = toolchain;
   };
 in
 rustPlatform.buildRustPackage {
   pname = "rust-with-nix";
   version = "0.1.0";

+  buildInputs = [ openssl ];
+  nativeBuildInputs = [ pkg-config ];

   src = ../.;
   cargoLock.lockFile = ../Cargo.lock;
 }

Nixでもビルドできることを確認します。

$ nix build
$ ./result/bin/rust-with-nix https://example.com
Statu code: 200

成功です!

内部で何をしているのか?

pkgs.rustPlatform.buildRustPackageは、引数としてSHA-256文字列あるいはCargo.lockのPathを受け取り、依存クレートを取得するfetcherに変換します。内部ではpkgs.fetchzippkgs.fetchurlを利用してcrates.ioからクレートのtarballを取得する処理が走っているようです。

依存関係とソースコードの取得が終われば、後はcargo buildをbuildPhaseで実行してパッケージをビルドします。また、テストが存在する場合はcheckPhaseでcargo testが実行されます(doCheck = falseで無効化可能・テストがネットワークを利用する場合はfalseにするとよい)。

詳細はNixpkgsのRustのビルドに関するドキュメントを参照してください。

Nipxkgs以外のRustのビルドユーティリティ

RustはNixコミュニティでも非常に人気のある言語なので、Nixpkgs以外からも複数のビルドユーティリティが提供されています。筆者の観測範囲では、特にcraneがよく利用されているようです。

https://github.com/ipetkov/crane
https://github.com/nix-community/naersk
https://github.com/cargo2nix/cargo2nix

その他の言語のビルド

Rust以外の言語でも、同じようなビルドをサポートする関数が提供されています。Nixpkgsの/doc/languages-framworksディレクトリに各言語のビルドに関するドキュメントが格納されているので、目的の言語のドキュメントを参照してください。

https://github.com/NixOS/nixpkgs/tree/master/doc/languages-frameworks

また、Nixpkgs以外のビルドユーティリティを探すならawesome-nixも役に立つかもしれません。

https://github.com/nix-community/awesome-nix?tab=readme-ov-file#programming-languages