kaito_tateyama's blog

競プロ用ライブラリuta8alibの環境構築

uta8alibという競技プログラミング用ライブラリの環境構築について書きます。
今回環境を作るに当たって、ngtkanaさんngtlibを参考にした部分が大きいです。ありがとうございました。

やりたいこと

  • C++とRustで競プロをする
  • example/main.rsのように、ファイル名をmain.[rs|cpp|hpp]で統一したい
  • testを行い、CIでcheckする
  • ライブラリのコピペを行いたいので、コードとテストは別ファイルにしたい(特にRust)
  • あとでdocument(html)を生成することを考えて、コメントを各ファイルとテストに書けるようにしておく
  • git submoduleを使ってみる(C++テストフレームワークCatch2の使用)

今後の展望

  • status badgeをGitHubにつける
  • How to run CI on your library for competitive programmingを参考に、今後Verifyを導入してCIを回す
  • Git-Flowを参考に、featureブランチでライブラリ単体を開発してC++とRustが揃ったらdevelopブランチに統合、masterにまとめてマージするときにCIを走らせる
  • snippetの導入。コードからコメントを抜いたものをsnippetファイルとして自動生成したい。(VSCode, neovim(dein.vim, UltiSnip))
  • ライブラリの種類を増やす(たくさん問題を解きましょう)

開発の流れ

ディレクトリ構成を決める

ディレクトリ構成は以下のようになっています。
srcの下にcppとrustがあり、その下にディレクトリが存在していてexampleなどがライブラリの名前になります。コードはそのディレクトリの中のmain.[rs|hpp]に書きます。
testはテストコードです。srcと同様の構成になっています。

❯ tree -I 'Catch2|chan|cpp-test|rust-test'  
.  
├── CMakeLists.txt  
├── Dockerfile  
├── libtest.sh  
├── README.md  
├── src  
│   ├── cpp  
│   │   ├── example  
│   │   │   └── main.hpp  
│   │   └── include  
│   │       └── lib.hpp  
│   └── rust  
│       └── example  
│           └── main.rs  
├── test  
│   ├── cpp  
│   │   ├── CMakeLists.txt  
│   │   ├── example  
│   │   │   └── main.cpp  
│   │   └── main.cpp  
│   └── rust  
│       └── example  
│           └── main.rs  
└── todo.md  

重視したことは、少しくらい深さのある構成になっても自分が好きな方を選ぶということです。lib-name.hppというような名前のファイルをフラットにsrc直下に置く方も多いですが、僕はlib-name/main.hppの方が好きなのでこうしました。好みの問題ですね。
深さができてGitHubで見るには面倒、という弱点のために、documentを自動生成してブラウザで見る、というようにしたいです。コードブロックの右上にコピペボタンを用意すれば捗りそうです。

自分用の競プロCLIツール、chanを使う

競プロをするときは焦りがちなので、使うコマンドは最小限にしたいです。あと、打ちやすいほうがいい。僕はchanというコマンドでコンパイルして実行ファイル生成を行っていましたが、このコマンドにライブラリのテンプレート作成機能をもたせることにしました。

chan main.rs # rustc main.rsが走る  
chan main.cpp # g++ が走る  
/path/to/uta8alib$ chan generate cpp example # exampleという名前のフォルダで、C++のsrcとtestのテンプレートファイル作成。C++の場合、include/lib.hppにパスを追加する。  

引数の数で判別しています。単純な機能ですが、Rustで4時間くらいかけて書いて、Python3とかの方がこういうのは向いてるなぁと感じました。外部crateは使いませんでしたが、clap-rsを使うと高機能にできそうです。ソースコードの本体はこちらです。
重視したことは、色付きで出力させることです。はじめOptionなしでは色がなく、読みづらく感じていました。調べたところ、強制的にcoloringをONにするOptionをつければよいと分かりました。Rustは rustc --color=always を、C++は g++ -fdiagnostics-color=always を指定しています。

git submoduleを使う

ngtlibを見てgit submoduleに興味が湧いていました。また、競プロに関わることは普段はなるべくひとつにまとめておきたいものです。というわけでchanをsubmoduleとしてuta8alibに取り込みます。

やること

  • chanをGitHubに上げます。
  • uta8alib$ git submodule add git@github.com:uta8a/chan.git chanでsubmoduleを追加
  • ここで、add repo directoryのrepoとdirectoryに気をつけましょう。
  • .gitmodulesが自動生成されるので確認
[submodule "chan"]  
	path = chan  
	url = git@github.com:uta8a/chan.git  
  • こんな感じならOK
  • あとはchanに入ってcargo build --releaseでバイナリを作り、aliasでchanを使えるようにしておく。

読むべき

  • git公式 submoduleの後に--helpを入れて打っておよい。

困った時のコマンド

  • git cloneしてきたらsubmoduleがない!
    • git submodule update --init --recursive
  • submoduleが更新された。新しくしたい。
    • git submodule foreach git pull origin master

注意

  • .gitmodulesを書いてgit submodule pullみたいなことをやりたいが、それはできない。addで追加して、.gitmodulesは生成物と捉えたほうがよい。

Rustのテスト方法を考える

今回、cargoで外部crateを入れるつもりはなかったので、cargoを使わないディレクトリ構成をしていました。しかしテストをするにはcargoがよいです。そこで、テストのときにcargo newしてcargo testするという方法に決めました。
libtest.sh# rust test の箇所を見ると分かるのですが、catでsrcとtestのファイルを結合したものをcargo newで生成したディレクトリの中に入れて、lib.rsに追記しています。
cargoを完全にテストツールとして使っていて強引な気がしますがうまく行っています。一つのファイルにまとめているのでprivate method testができ、わざわざ pub をつけずにライブラリを書くことが出来ます。

C++のテスト方法を考える

git submoduleを使うと同じ手順でCatch2を導入しました。

やること

  • google testやcatch2など、C++テストフレームワークを検討する
  • catch2の使い方を調べる
  • cmakeを理解する
  • testを書く
  • CMakeLists.txtを書く
  • makeを通す

細かく分けるとこんな感じでした。テストフレームワークの選定についてはwikipediaのユニットテストフレームワークの記事のC++の欄を参考にしました。

ヘッダ・ファイルのみからなり、外部に依存しない。(wikipediaの記事より引用)

たくさんあるのでgoogle testとCatch2しか見れませんでしたが、違いがいまいち分からなかったのでngtlibでも使われているCatch2を選択しました。
CMakeLists.tstについてはngtlibを参考にしたいのであまり理解できていません。以下の記事が参考になりました。

参考

makeが通った時は嬉しかったです。

Gitの開発手順を見直す

ここまでずっとmasterにpushしてきましたが、そろそろCIを導入するとなると、masterにプルリク送ったときのみCIが回るようにしたいです。現在はdevelopとmasterの2つですが、ライブラリを作る時はfeature-xxxというブランチを作り、RustとC++が揃った段階でdevelopに取り込み、masterへマージしてCI回すという形にしたいです。

参考

CIでテストを回す

以前GitHub Actionsを使ったことがあったので、今回はCircle CIを使ってみました。結論のconfig.ymlこちらです。

version: 2.0  
  
jobs:  
  build_and_test:  
    docker:  
      - image: uta8a/circleci-rust-cmake:0.0.1  
    environment:  
      USER: uta8a  
    working_directory: ~/work  
    steps:  
      - checkout  
      - run: git submodule sync  
      - run: git submodule update --init  
      - run: ./libtest.sh  
  
workflows:  
  version: 2  
  build:  
    jobs:  
      - build_and_test:  
          filters:  
            branches:  
              only: master  

注意

  • 現在version 2.1まで指定できますが、localのcircleci CLIツールが2.0までしか対応していないので2.0を指定します。
  • dockerで2つimageを指定しようとしたら最初の一つしか反映されませんでした。無料枠であることが関係していそうですが原因は分かりませんでした。
    • そのため、DockerHubに自分で作ったイメージを上げました。DockerHubへのリンク
    • これは、circleci/rustをベースにしてrikorose/gcc-makeを参考に書き足しています。gccは8で、cmakeは3.15でした。
  • cargoをdockerで使おうとすると$USERが足りないというエラーが出ます。そのため、environmentでUSERを指定します。
  • git submoduleを使っている場合は、checkout後にsubmoduleも引っ張ってくるようコマンドを指定します。
  • branchはmasterのみに設定します
  • workflowsのversionは、全体のversionが2.1のときは記述がいらないのですが、2.0では必須です。
  • circleciのデバッグ作業はローカルのCLIツールを用いましょう。circleci config validateでyamlが正しいかどうか判定でき、circleci local execute --job build_and_testで特定のjobを実際に回せます(dockerを使います)

status badgeをGitHubにつける

公式のdocumentを読むと簡単に導入できます。Different Styleの方が好みだったのでそうしました。

LICENSEを加える

加えました。MIT。

今後の展望

Logoを作ってやる気を高める

Verifyしてみる

snippetの導入

競プロで使うエディタの選定

どこでも競プロできるようにシェルスクリプトを書く

ライブラリの種類をどうやって増やすか(アルゴリズムを学ぶ)

この記事を書くに当たって

[article](#toc) で見出しへのanchor linkが張れるのですが、これは一癖あって、hugoで生成されたもののidをみながらそれと一致するようにやっていくとよいです。例えば、 ##C++でのテスト方法を考える#c--でのテスト方法を考える に変化します。

終わりに

今回環境構築がメインでもう力尽きそうですがここからがスタートラインです。今年は競プロのレートに興味を持たずに、アルゴリズムに興味を持つことを目標にしていきたいです。