はじめに
この記事はアドベントカレンダー2024 3日目の記事です。
こんにちは。24Bの@zoi_dayoです。日頃はAtCoderとかWeb開発とかをちょびちょびやっています。
さて、皆さんは競技プログラミングの環境構築をしていますか? コードテストで実行する派閥の人でなければ、最低限g++
くらいは入れているんじゃないかと思います。テストケースの取得や確認を手伝ってくれるonline-judge-tools
とかを入れている人もそれなりにいるはずです。
しかし、「コンテストにはデスクトップPCで出るけど、出先でちょっと解くのはノートでやりたい」という人だと、環境構築を2回もやらないといけません。面倒ですね。というわけで、Dockerを使って「競プロ環境を持ち運べる」ようにしてみようというお話です。
Dockerって何?
Dockerは競プロではあまり出てこないので軽く解説しておきます。
Dockerはコンテナ仮想化のためのツールです。コンテナをテキストファイルで定義し、実行できます。重くない仮想環境みたいな感じです。もっと簡単に言うと、PCのなかにまっさらなPCを作ったり、それを削除したり、設計図を共有できたりします。
(もっと細かく言うと、実行しているプロセス(≒アプリ)に対してファイル読み書きや通信の制限をかけ、仮想的なファイルシステムを見せるようにしたものです。)
これを使うことで、
- 仮想環境のように、PC本体の環境を破壊しない
- 設定をテキストファイルとして共有でき、どのPCでも同じ環境を使える
という状況を実現できます。
環境を作る
では、AtCoder環境に入れておきたいものを列挙してみます。
clang++
org++
- lldb
online-judge-tools
- AtCoder Library
- 自分のライブラリ
- Boost Library
- NeoVim + プラグイン達
一つ一つ入れていきましょう。せっかくなので、できる限りAtCoder側の設定と合わせます。記事執筆時点では、AtCoder側のバージョンは公式ページやスプレッドシートから確認できます。
Dockerfileの準備
DockerコンテナのレシピはDockerfileというファイルに書いていきます。
まずOSを準備して、apt updateをしておきます。公式に則ってUbuntu 22.10を指定しているのですが、バージョンが古いらしく、パッケージ取得元のURLを書き換えなければいけません。
FROM ubuntu:22.10
RUN sed -i -e 's/ports.ubuntu.com\/ubuntu-ports/old-releases.ubuntu.com\/ubuntu/g' /etc/apt/sources.list \
&& apt update
コンパイラ
g++
でもclang++
でもpython3
でもいいので、言語に合わせて好きなものを入れましょう。ほぼスプレッドシートのコマンドをそのまま使っているのですが、いくつか変更点があります。
- (なぜか)ダウンロードが遅いのでBoostライブラリのURLを変更しています。
- rootユーザーとして実行するため、sudoを消しています。
- ビルドサイズを減らすため、
&&
でつなげています。(最後に一時ファイルの削除などをするともっと減らせる気がしますが、していません...)
RUN apt install unzip
# C++ 23 (Clang)
RUN cd /tmp \
&& apt install -y lsb-release wget software-properties-common gnupg \
&& wget https://apt.llvm.org/llvm.sh -O llvm.sh \
&& sed -i.bak -e 's/^add-apt-repository /&-y /' llvm.sh \
&& chmod +x llvm.sh \
&& ./llvm.sh 16 \
&& update-alternatives --install /usr/bin/clang clang /usr/bin/clang-16 10 \
&& update-alternatives --install /usr/bin/clang++ clang++ /usr/bin/clang++-16 10 \
&& mkdir /opt/ac-library \
&& wget https://github.com/atcoder/ac-library/releases/download/v1.5.1/ac-library.zip -O ac-library.zip \
&& unzip ac-library.zip -d /opt/ac-library \
&& wget https://archives.boost.io/release/1.82.0/source/boost_1_82_0.tar.gz -O boost_1_82_0.tar.gz \
&& tar xf boost_1_82_0.tar.gz \
&& cd boost_1_82_0 \
&& ./bootstrap.sh --with-toolset=clang --without-libraries=mpi,graph_parallel \
&& ./b2 -j3 toolset=clang variant=release link=static runtime-link=static cxxflags="-std=c++2b" stage \
&& ./b2 -j3 toolset=clang variant=release link=static runtime-link=static cxxflags="-std=c++2b" --prefix=/opt/boost/clang install \
&& cd /tmp \
&& apt install -y libgmp3-dev \
&& apt install -y libeigen3-dev \
&& apt install -y libz3-4 libz3-dev \
&& apt install -y gdb libbz2-dev liblzma-dev libsqlite3-dev libssl-dev lzma lzma-dev zlib1g-dev liblz4-dev liblzo2-dev
NeoVim
apt install neovim
としたいところですが、残念なことに少しバージョンが古いです。ではGitHubからバイナリを落としてくればいいかと思うのですが、この方法ではx86向けのバイナリしか手に入りません。armのmacbook(上のDocker)でも動作させたいので、困りました。
なにかいい方法がある気もしますが、考えるのが面倒なので自分でビルドしてしまいましょう。こうすれば、動いているのがx86なのかarmなのか...などと考えなくていいはずです。
RUN apt install -y git
RUN cd /tmp \
&& git clone https://github.com/neovim/neovim.git \
&& cd neovim \
&& git checkout stable \
&& apt install -y ninja-build gettext cmake unzip curl build-essential \
&& make CMAKE_BUILD_TYPE=Release \
&& make install \
&& rm -rf /tmp/neovim
GitHub Copilot用のnodejsも入れておきます。
RUN apt install -y nodejs
online-judge-tools
普通にpythonで入れればよいです。
RUN apt install -y python3 python3-pip
RUN pip3 install online-judge-tools
ビルド
さて、作ったDockerfileからコンテナを構築し、使ってみましょう。
$ docker build . -t kyopro-env
$ docker run -it --name kyopro kyopro-env /bin/bash
コマンドがちゃんと使えることを確認して、exit
で終了できます。
フォルダ追加
これで起動はできたのですが、このままでは使いにくいので、必要なフォルダをマウントしていきます。
(ビルド時ではなく起動時のマウントにすることで、内容変更時に再ビルドしなくて良いようにしています)
テンプレート
適当にテンプレートを置いておきましょう。
src
└── tmp
├── Makefile
├── compile_flags.txt
└── main.cpp
内容は好きなものをどうぞ。
dotfiles
NeoVimとかBashの設定を./dotfiles/
においています。dotfilesレポジトリを使いたい人はこのパスにgit submoduleなどすると良い気がします。
dotfiles
├── .bashrc
└── .config
└── nvim
├── init.lua
└── lua
├── config
│ └── lazy.lua
└── plugins
├── Comment.lua
├── copilot.lua
├── indent-blankline.lua
├── lualine.lua
├── nightfox.lua
├── nvim-autopairs.lua
├── nvim-cmp.lua
├── nvim-lspconfig.lua
├── nvim-tree.lua
└── nvim-treesitter.lua
NeoVimの設定は人によって変わるので置いておくとして、.bashrc
にはこんなものを作ってみました。
function new () {
if [[ $# != 1 && $# != 2 ]]; then
echo "Usage: new {contestName} or new {contestName}/{problemName} or new {contestName} {problemName}"
return 1
fi
cd /kyopro
if [[ $# -eq 1 ]]; then
if [[ $1 == *"/"* ]]; then
contestName=$(echo $1 | cut -d'/' -f1)
problemName=$(echo $1 | cut -d'/' -f2)
mkdir -p $contestName/$problemName
cd $contestName/$problemName
oj d https://atcoder.jp/contests/${contestName}/tasks/${contestName}\_${problemName}
cp /kyopro/tmp/* .
else
contestName=$1
python3 contest.py $contestName | xargs -n2 bash -c "mkdir -p $contestName/\$0; cd $contestName/\$0; oj d https://atcoder.jp\$1; cp /kyopro/tmp/* ."
cd $contestName
fi
elif [[ $# -eq 2 ]]; then
contestName=$1
problemName=$2
mkdir -p $contestName/$problemName
cd $contestName/$problemName
oj d https://atcoder.jp/contests/${contestName}/tasks/${contestName}\_${problemName}
cp /kyopro/tmp/* .
else
echo "Usage: new {problemUrl} or new {contestName}/{problemName} or new {contestName} {problemName}"
return 1
fi
}
src/contest.py
はこうなっています。あまりきれいなコードではないです...
import urllib.request
import http.cookiejar
import appdirs
import pathlib
import sys,os,os.path
from bs4 import BeautifulSoup
args = sys.argv
contestName = args[1]
cookie_path = pathlib.Path(appdirs.user_data_dir('online-judge-tools')) / 'cookie.jar'
cj = http.cookiejar.LWPCookieJar()
if os.path.exists(cookie_path):
cj.load(cookie_path)
opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj))
r = opener.open('https://atcoder.jp/contests/' + contestName + '/tasks?lang=ja')
html = r.read().decode('utf-8')
soup = BeautifulSoup(html, "lxml")
tr_list = soup.find_all("tr")
for tr in tr_list:
if tr.td == None:
continue;
td_list = tr.find_all("td")
problem_name = td_list[0].get_text().lower()
problem_url = td_list[0].a.attrs["href"]
print(problem_name, problem_url)
online-judge-toolsのログイン情報を借りてAtCoderにアクセスし、問題一覧を取得してるわけです。
これで、new abc123
とかnew abc123/a
とかnew abc123 a
とかできるはずです。バグってたらすみません...
認証情報の保存
さて、このままではコンテナを作り直すたびにGitHub Copilotやonline-judge-toolsのログインをやり直さなければいけません。とても面倒なので、認証情報を保存しておきましょう。
online-judge-toolsの認証情報は/root/.local/share/online-judge-tools
、Copilotの認証情報は/root/.config/github-copilot
に保存されているので、これらのフォルダをホストからマウントしてしあげれば良さそうです。
secrets
├── copilot
│ └── .gitkeep
└── oj-tools
└── .gitkeep
もしコンテナ内からGitHubへのpushなどをするなら、ssh鍵あたりも保存しておいてもいいかもしれません。
ただし、secrets
フォルダは絶対に.gitignore
で除外しておいてください。ログイン情報が流出してしまいます...
secrets/*/*
!secrets/*/.gitkeep
フォルダ構成
以下のようにマッピングします。ホスト→コンテナの表記です。
./src/ -> /kyopro/
./dotfiles -> /root/
./secrets/copilot -> /root/.config/github-copilot/
./secrets/oj-tools -> /root/.local/share/online-judge-tools/
これらはビルド時ではなく、イメージからコンテナを立てる時に指定するものです。(でなければ、ビルド時にコピーされるだけで、コンテナ内での変更がホストに保存されません。)
なので、コンテナ作成コマンドを以下のようにしましょう。
docker run -id --name kyopro \
-v $(dirname `pwd`)/src:/kyopro \
-v $(dirname `pwd`)/dotfiles:/root \
-v $(dirname `pwd`)/secrets/copilot:/root/.config/github-copilot \
-v $(dirname `pwd`)/secrets/oj-tools:/root/.local/share/online-judge-tools \
kyopro-env /bin/bash
オプションを-d
にしたので、これでバックグラウンドでコンテナが起動したことになります。ここから実際にシェルを開くには、
docker exec -it kyopro /bin/bash
とすればよいです。
完成!
これで完成です! このDockerfileとフォルダたちをまとめてGitHubに上げて、他のPCからクローンすれば、どこでも同じ環境を使えるというわけです。また、他の用事でコンパイラやライブラリを入れ替えたとしても競プロ環境は破壊されません。嬉しいですね〜
まあ、普通に環境構築をすればいいという話はありますが... 誰かが「めっちゃ使いやすい環境」を作り、それを使えば良い、みたいになったら競プロのハードルが下がるかも...?
明日は @Pugma さんと @d_etteiu8383 さんの記事が出ます。楽しみですね〜〜!