Cargoのサブプロジェクトとreplace

κeenです。最近Cargoのreplace機能を使おうとしてハマったのでメモを残しておきます。

最近何箇所かで喋ったのでご存知の方もいらっしゃるかと思いますが、Cargoにはワークスペース機能があります。 fat repositoryスタイルに便利な機能で、1つのディレクトリ下に複数のサブプロジェクトを配置しつつtarget/は共有するのでマイクロプロジェクトのモジュール性とモノリシックプロジェクトのコンパイルの速さを実現することが出来ます。

さて、問題になるのはクレートを公開するときの依存関係の記述です。 サブプロジェクト同士で依存し合っているのでCargo.tomlにはmy-project-lib = {path = "../my-project-lib"} のような依存の記述がある筈です。 しかしながらローカルのクレートに依存しているとcrates.ioに公開は出来ません。 そこで今回はそれをどうにかするお話。

replace

Cargoにはreplaceという機能があります。 特定のライブラリの特定のバージョンを、crates.ioにあるものではなくてユーザが指定した場所にあるものを使うように指示します。 Specifying Dependenciesのドキュメントによると、以下のように使えます。

例えばuuidクレートを使っているときにそれが依存しているrandクレートにバグを見付けて修正し、修正した結果を試したいとします。 その時にreplaceは以下のように使えます。

# ワークスペースの親プロジェクト
...

[replace]
"rand:0.3.14" = { path = "./rand" }
# ワークスペースの子プロジェクト

[package]
name = "my-awesome-crate"
version = "0.2.0"
authors = ["The Rust Project Developers"]

[dependencies]
uuid = "0.2"

間接的に依存してるrand:0.3.14を、crates.ioにあるものではなく指定したパスにあるものを使うように指示しています。

この仕組みを使って先のローカルパス問題を解決出来ないでしょうか。すなわち、

# 子プロジェクト
[dependencies]
my-project-lib = "0.1.0"

のように、見た目上はcrates.ioにあるものに依存しておきつつ実際は

# 親プロジェクト
[replace]
"my-project-lib:0.1.0" = { path = "./my-project-lib" }

のように実際の依存先をローカルのパスに向ければ問題を解決できないでしょうか。

この答えは、半分no、半分yesになります。

replaceはあくまでcrates.ioにあるものを置き換えるのでまだ公開していないプロジェクトに対しては使えません。 一方、一度crates.ioに公開してしまえば上記のテクニックは可能になります。

なので一番最初にcrates.ioに公開する時の手順はやや煩雑になります。

  1. (ローカルサブプロジェクトへの依存の記述はpath = "..."のまま)
  2. どのプロジェクトにも依存しないローカルサブプロジェクトを公開する
  3. 既に公開されたローカルサブプロジェクトへの依存は順次replaceに書き換える
  4. ローカルパスへの依存のなくなったローカルプロジェクトから順に公開していく
  5. 全てのサブプロジェクトのローカルサブプロジェクトへの依存がreplaceに置き換わったら以後はそのまま使える。

このようなテクニックは例えばdieselなどで使われています。 この辺、改善あるといいですね。

Written by κeen