目次

RustRover+Cargo での競プロ用ファイル構成

RustRoverは、JetBrains社が提供するRust用IDE。

Rustによる競プロ環境を作る際、IDEとしてRustRoverを使った場合、ファイル構成をどうするか。

JetBrains社のIDEにおけるRust開発は、かつてはCLionにRustプラグインを入れていたのが、 最近、RustRoverという独立したエディタとなった。
ベースは同じなので、多くの項目は CLion+Rustプラグイン でも同様のことが言えると思われる。

あくまで初心者が調べた結果なので、「○○はできない」とか書いていても調査不足で実はできるかもしれない。

環境

Rust自体のインストールは本記事では扱わない。

やりたいこと

  1. 全てのコンテストのコードを1つのIDEプロジェクトで管理したい
    • 過去に解いた問題のコードも全て残しておきたい
  2. 挑戦中のコンテストの.rsファイルについて、
    1. どれから解きはじめてもいいよう、問題数分の.rsファイルを予め用意しておきたい
    2. コード補完が効くようにしたい
    3. ショートカット Shift+Alt+F10 で、編集中のファイルを起点として簡単にビルド+実行したい
    4. コード以外(Cargo.tomlなど)は、コンテスト中には編集せずに済むようにしたい
  3. 過去に解いた問題の.rsファイルについて
    1. ファイルをまたいで過去のコードを検索できるようにしたい
      • なるべくIDEの検索機能を使いたい(ショートカットですぐ起動でき、便利なので)
    2. 常に過去の全ファイルがビルド構成に入っていなくてもよい
      • コンテストが終われば適宜除くことにしてよい
      • むしろ全て入ってるとコード解析が重すぎてえらいことになりそうなので除ける方がよい
  4. ビルドされたクレートや実行ファイルはあまり肥大化しない方が嬉しい
  5. .rsファイル名は、abc001_a.rs のように、コンテスト+問題番号 の情報がある方が嬉しい

補足

何故上記のようなことが「やりたいこと」になるに至ったか。

IDEプロジェクト

「IDEプロジェクト」とは、IDEで一般的に開くプロジェクトの単位である。
RustRover(などJetBrains社のIDE)では以下の手順で作られる。

ビルドツールにCargoを指定してプロジェクトを作成すると、以下のような構成が自動で作られる。

【デフォルトのIDEプロジェクトの構成】

project_root/
 |
 |- src/
 |   |- main.rs
 |
 |- Cargo.toml
 |
 |- (他にもあるが省略)

上例での project_root 以下に、競技プログラミングで書いたコードが全て収まるようにしたい。
(または「AtCoder」など、コンテストサイト単位でもいい。この辺の適切な粒度は場合により変わるだろうが)

上記が主な理由。

複数のプロジェクトを同時に開いたり、 設定すればproject_root 以外のフォルダもIDEプロジェクトに含めることも、 できるといえばできるのだが、あまり余計な設定をせずに済ませたい。

Cargoプロジェクト

IDEプロジェクトとは別に、「Cargoプロジェクト」という概念もある。

コマンドから cargo new project_name などで作成される、Cargo.toml をビルド設定ファイルとした1つのファイル構成を指す。

Cargoプロジェクト 最小構成

project_name/
 |
 |- src/
 |   |- main.rs
 |
 |- Cargo.toml

この project_name/ に移動して cargo build などとすると、main.rs がコンパイルされ、exe ができる。

ということは、1つの問題ごとに1つのCargoプロジェクトが必要になる?
そのように使ってもよいが、Cargo.tomlの設定次第で複数の.rsファイルを管理することもできる(後述)。
とはいえ、100個も200個も同時に管理するには向かない。せいぜい1つのコンテストの問題数(6~10問)程度が適量。

また、1つのIDEプロジェクトに複数のCargoプロジェクトを作ってもよい。

IDEプロジェクト→Cargoプロジェクト、Cargoプロジェクト→rsファイル、それぞれが1対多の関係になる。

1つのCargoプロジェクトで1つのコンテストを管理する、というのがよいだろう。

IDEが認識するファイル

IDEプロジェクト下に.rsファイルを置いても、全てが即座にコード補完されたり、ビルド→実行できるわけではない。

以上の2つをして、はじめてコード補完や、IDEからのビルドができる。

Cargoプロジェクトのアタッチ

RustRoverではIDEプロジェクト下に(ルート以外の)Cargo.toml があっても、 自動ではそれを管理対象としては認識しない。
明示的に「アタッチ」する必要がある。

Cargoの流儀に沿ったrsファイルの場所

アタッチされたCargo.tomlのあるフォルダ下に .rs ファイルがあっても、 特定のファイル以外は、それがCargoプロジェクトに属する.rsファイルだとは認識しない。

IDEに認識されない.rsファイルは、コード補完が制限される。
(どの Cargo.toml で定義された依存関係を使って補完すればいいかなどの情報が無い)

また特に、そのファイルを起点として実行させようと思った場合、 その.rsファイルが「クレートルート」だと認識してもらう必要がある。

なので、先述のやりたいことの2-Ⅱ,2-Ⅲは、IDEに.rsファイルをクレートルートだと認識させられればよい。

RustRoverは、アタッチされたCargo.tomlから、以下の.rsファイルを自動的にそのプロジェクト所属だと認識する。

なので、補完を効かせショートカットから即実行したいコードは、上3つのどれかに書く必要がある。

【フォルダ構成例】

project_root/
 |
 |- src/
 |   |- main.rs           ←○:認識される
 |   |
 |   |- bin/
 |   |   |- abc001_a.rs   ←○:認識される
 |   |   |- abc001_b.rs   ←○:認識される
 |   |   |
 |   |   |- abc001/
 |   |   |   |- c.rs      ←×:サブディレクトリ内は認識されない
 |
 |- other_dir/
 |   |- abc001/
 |   |   |- a.rs          ←○:下のCargo.tomlで指定しているので認識される
 |
 |- Cargo.toml
Cargo.toml
[package]
name = "hoge"
version = "piyo"

[[bin]]
name = "a"
path = "other_dir/abc001/a.rs"

容量の肥大化

Cargoでは、使用するクレートを事前コンパイルしたり、.rsファイルをコンパイル+実行すると、 Cargo.toml と同階層の target/debug 以下にキャッシュ(exeやデバッグ時に必要な情報など)が作成される。

特にライブラリとして使う外部クレートは、AtCoderで使用できる全てをdependenciesに記述すると、それなりの容量になる。

また、自前コードのコンパイル結果はバイナリ名(○○.exe)ごとに作成される。
キャッシュは簡単なコードでも1つ1~2MBあるので、数が増えると容量が大きくなる。
気になる場合、定期的に削除する必要がある。

基本的には、「競プロ用のtargetフォルダ」を1つ作って、 全てのキャッシュはそこに置くように設定して運用する。
これにより、外部クレートの再コンパイルが毎回走ったり、同じキャッシュが複数箇所に保存される事態が防げる。

また、自前コードのバイナリ名を実用上問題ない程度(問題番号基準で “a.exe”, “b.exe” など)に重複させれば、 同名のファイルは上書きされるので一定以上には容量を増やさない運用にできる。

ただし重複させるとデメリットもある。
Cargoは賢いので、.exeとソースコードの最終更新時刻を比較して、.exeの方が新しければ再ビルドはされない。
なので、以下のような状況が生じうる。

cargo clean -p <project-name> とするとCargoプロジェクトに属するキャッシュが消える(らしい。未調査)

定期的に削除する手間と、うっかり異なるバイナリが実行される危険と、どちらを取るかはその人次第となる。

強制的に再ビルドする設定も探せばあると思うけど、外部クレートは時間かかるので再ビルドしてほしくない。
自前クレートだけは常に再ビルド、みたいな設定ってできるのかな? 未調査。

ファイル名について

上記のバイナリ名にも少し関係する話題。

IDEエディタのタブには基本的に、開いているファイル名のみが表示される。
複数の問題を開いているとき、“a.rs” だけだとどの問題のA問題かわからない。

RustRoverでは、なかなかに賢い実装がされていて、 「abc001/src/bin/a.rs」と「abc002/src/bin/a.rs」が同時に開かれている場合、 タブ表示は「abc001/…/a.rs」「abc002/…/a.rs」と、冗長にならず、かつ異なっている部分が分かりやすいような表記となる。

ただ、「今はabc002を解いてるんだけど、a.rs だけは、abc001のa.rsの方のみが開かれたままになっていた」場合は、 やっぱり「a.rs」とだけしか表示されず、abc002の a.rs と勘違いしてしまいかねない。

なので、なるべくなら「abc001_a.rs」などのように、ファイル名にコンテスト名も含めた名前にしたい。

だが、その場合は「容量の肥大化」で述べたように、 ファイル名がそのままバイナリ名になるような構成にしていた場合はどんどん新しいexeが溜まっていく。

binセクションを使えば、ファイル名とバイナリ名を別々に変えられる。

Cargo.toml
# ...略...

# ファイル名は abc001_a.rs のまま、バイナリ名は a となる
[[bin]]
name = "a"
path = "src/abc001_a.rs"

[[bin]]
name = "b"
path = "src/abc001_b.rs"

cargo-atcoder, cargo-compete などの利用

これらを使うと、AtCoderへのログイン、サンプルコードの自動テスト・送信など、 Cargoに競技プログラミングに適した機能を導入できる。

すごく便利そう、ではあるが、、、

などで、ひとまずは見送り。

カスタマイズしつつ使用する場合は、以下の情報が参考になるかも。

フォルダ構成の方針の候補

以上を踏まえた上で、フォルダ構成をどうするかについて、3つ程度の案を示す。

main.rs を書き換え続ける

ツールの設定変更などが必要なく、この中では最も単純な方法。

IDEプロジェクトでデフォルトで作成されるCargoプロジェクトにおいて、src/main.rs を書き換えて使い続ける。

過去のコードを残せないので、必要ならコピーなどして残しておく。

コンテスト毎にCargoプロジェクトを作り、binでパス指定

project_root/
 |
 |- abc/
 |   |- abc001/
 |   |   |
 |   |   |- .cargo/
 |   |   |   |- config.toml
 |   |   |
 |   |   |- src/
 |   |   |   |- abc001_a.rs
 |   |   |   |- abc001_b.rs
 |   |   |   |- abc001_c.rs
 |   |   |
 |   |   |- Cargo.toml  (sub)
 |   |   |
 |
 |- src/
 |   |- main.rs
 |
 |- Cargo.toml  (root)
 |
 |- target/
config.toml
[build]
target-dir = "../../target"
Cargo.toml(sub)
[package]
name = "abc001"
version = "0.1.0"
edition = "2021"

[dependencies]
proconio = "0.4.5"
# ... など利用するクレート

[[bin]]
name = "a"
path = "src/abc001_a.rs"

[[bin]]
name = "b"
path = "src/abc001_b.rs"

# ...

コンテスト毎に上記 abc001/ のような構成でファイルを作成し、Cargo.toml(sub) をIDEにアタッチする。

これらの一連の操作を自動化しておくとよい。

ただ、Cargo.toml(sub) をIDEにアタッチする作業は、手動で行う必要がある(たぶん)。

ファイル生成後に Config.toml(sub) を開くとIDEエディタの上部に 「The file does not belong to a known Cargo project」という警告バーが出る。
バーの右側に「Select Cargo.toml」とあるので、そこをクリックすることで、Cargo.toml(sub) がIDEにアタッチされる。

または、View → Tool Windows → Cargo を開き、Cargo.toml(sub)をエディタで開きながら、 Cargoツールウィンドウで「+」ボタン(Attach Cargo Project)を選択することでも、アタッチされる。

コンテストが終わってデタッチする場合も、ツールウィンドウから「-」ボタンでできる。

留意点

Cargo.toml(root) に bin や workspace など他のファイルやプロジェクトを使う旨の記述がある場合、 Cargo.toml(sub)アタッチ時に「あなた、rootのプロジェクトの一員じゃないよね?」的なエラーが出ることがある。

具体的な条件は追っていないが、Cargo.toml(root) にはそういう記述を除いて試すと上手くいくかも?

コンテスト毎にCargoプロジェクトを作り、Cargo.tomlのworkspaceを利用

もう一つ、targetを共有する方法として、 「abc001/Cargo.toml のプロジェクトは、ルートのCargoプロジェクトのサブプロジェクトですよ」ということを、 ルートの Cargo.toml に教える方法がある。

Cargo.toml の [workspace] セクションで指定する。

project_root/
 |
 |- abc/
 |   |- abc001/
 |   |   |
 |   |   |- src/
 |   |   |   |- abc001_a.rs
 |   |   |   |- abc001_b.rs
 |   |   |   |- abc001_c.rs
 |   |   |
 |   |   |- Cargo.toml  (sub)
 |   |   |
 |
 |- src/
 |   |- main.rs
 |
 |- Cargo.toml  (root)
 |
 |- target/
Cargo.toml(root)
[package]
name = "atcoder"
version = "0.1.0"
edition = "2021"

[dependencies]
proconio = "0.4.5"
# ... など利用するクレート

[workspace]
members = [
    "abc/abc001",
    "abc/abc002",
]
Cargo.toml(sub)
[package]
name = "abc001"
version = "0.1.0"
edition = "2021"

[dependencies]
proconio = "0.4.5"
# ... など利用するクレート

[[bin]]
name = "a"
path = "src/abc001_a.rs"

[[bin]]
name = "b"
path = "src/abc001_b.rs"

# ...

「コンテスト毎にCargoプロジェクトを作り、binでパス指定」と比較して、

個人的にはbinセクションで指定した方が楽な気がする。
まぁ、一応この方法でもできるということで。

その他の参考情報

AtCoderコンテストにRustで参加するためのガイドブック

(RustRoverは関係なく)Rust+Cargoでの競プロコンテスト用プロジェクト作成の流れの一例が以下に紹介されている。

これは問題A,B,C,…の1問毎にCargoプロジェクトを作成する方法である。
ただ、そのままだとCargoプロジェクトは依存クレートを事前コンパイルした結果などで容量がかさばるので、 プロジェクト間で target ディレクトリをシンボリックリンクで共有する方法がとられている。

Twitter上のやりとり

Rustの環境構築について、意見が交わされている。

Xユーザーのきりさん: 「Rust の環境構築をしようとしてるけど、このままだと 1 問ごとにプロジェクト(?)が生成されてフォルダ・ファイル数がすごいことになりそうなんだけどみんなどうやってるんだろう」 / Twitter

C/C++では

以下の情報がある

また、使ったことないので使用感は不明だが、以下のプラグインが、1ファイルだけ実行するのに使えそう