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

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

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

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

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

環境

  • Windows10/11
  • RustRover 2023.3 EAP

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」など、コンテストサイト単位でもいい。この辺の適切な粒度は場合により変わるだろうが)

  • プロジェクト下のファイルは簡単にIDE内のツリーでたどり、開いて中を確認できるが、外のファイルはたどりにくい
  • 「プロジェクト内のコードを単語検索」などのIDE機能により、コードの検索をしやすい

上記が主な理由。

複数のプロジェクトを同時に開いたり、 設定すれば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ファイルを置いても、全てが即座にコード補完されたり、ビルド→実行できるわけではない。

  • CargoプロジェクトをIDEにアタッチする
  • Cargoの流儀に沿った場所に.rsファイルを置く

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

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

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

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

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

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

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

  • クレートルート: コンパイラの開始点。ビルドするとそのファイルを起点とする.exeが作られる。exeを実行すると、まずそのファイルのmain()が呼ばれるような.rsファイル

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

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

  • Cargo.toml があるフォルダを xxx として、
    • ★ xxx/src/main.rs
    • ★ xxx/src/bin 直下にある.rsファイル
    • ★ Cargo.toml内で指定された.rsファイル
    • xxx/src/lib.rs (※クレートルートとしては認識されない)
    • 使用される.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あるので、数が増えると容量が大きくなる。
気になる場合、定期的に削除する必要がある。

  • バイナリ名
    • main.rs の場合、Cargo.toml内のpackageセクションで name = “xxx” で指定した名称
    • src/bin 内に作成した場合、ファイル名
    • Cargo.tomlのbinセクションでパス指定した場合、name=“xxx” で指定した名称

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

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

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

  • ABC001 の A 問題を解く
    • → a.exe が生成される
  • ABC002 の A 問題を解く
    • → a.exe は、ABC002で生成されたものに上書きされた
  • ふと、ABC001の A 問題の挙動を確認したくなった
    • → ABC001 A のソースコードは a.exe はより古いのでコンパイルされず、ABC002 の A 問題用の処理が実行される
    • 何でもいいのでわずかに書き換えてからビルドすると大丈夫

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に競技プログラミングに適した機能を導入できる。

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

  • Cargo自体の使い方と、このツールの使い方を同時に覚えるのが大変
  • 開発者が特にこだわっていない部分を、自分用にどうカスタマイズするか(できるか)調べるのが大変
  • 今後、AtCoderの言語アップデートやcargoの変化に追従してくれるか、どうしても個人開発者に依存してしまい、怪しい部分がある

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

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

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

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

  • main.rs を書き換え続ける
  • コンテスト毎にCargoプロジェクトを作り、binでパス指定
  • コンテスト毎にCargoプロジェクトを作り、Cargo.tomlのworkspaceを利用

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にアタッチする。

  • ターミナルから abc/ に移動して cargo new abc001
    • 上記の abc001/ のうち、.cargo と src/abc001_xx.rs 以外の構成が自動でできあがる
  • コンテストの問題数だけ.rsファイルを作成
  • Cargo.toml の dependencies セクションに、使用する外部クレートを列挙
  • Cargo.toml の bin セクションに、各.rsを指定
  • .cargo/config.toml に、target ディレクトリを設定

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

ただ、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でパス指定」と比較して、

    • config.toml がなくても、target はルートの target となる
    • Cargo.toml(sub) をIDEにアタッチしなくてもよい
      • △ただし、Cargo.toml(root)のリロードは必要となるので、手間的にそんな変わらない
  • ×
    • Cargo.toml(root) を書き換える必要がある
      • 自動化する場合、既存ファイルを書き換えるのは、構文解析が必要になり、面倒

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

その他の参考情報

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

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

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

Twitter上のやりとり

C/C++では

以下の情報がある

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

software/intellij/rustrover/procon.txt · 最終更新: 2023/11/14 by ikatakos
CC Attribution 4.0 International
Driven by DokuWiki Recent changes RSS feed Valid CSS Valid XHTML 1.0