feature image

2024年12月3日 | ブログ記事

AtCoder環境を持ち運ぶ

はじめに

この記事はアドベントカレンダー2024 3日目の記事です。


こんにちは。24Bの@zoi_dayoです。日頃はAtCoderとかWeb開発とかをちょびちょびやっています。

さて、皆さんは競技プログラミングの環境構築をしていますか? コードテストで実行する派閥の人でなければ、最低限g++くらいは入れているんじゃないかと思います。テストケースの取得や確認を手伝ってくれるonline-judge-toolsとかを入れている人もそれなりにいるはずです。

しかし、「コンテストにはデスクトップPCで出るけど、出先でちょっと解くのはノートでやりたい」という人だと、環境構築を2回もやらないといけません。面倒ですね。というわけで、Dockerを使って「競プロ環境を持ち運べる」ようにしてみようというお話です。

Dockerって何?

Dockerは競プロではあまり出てこないので軽く解説しておきます。
Dockerはコンテナ仮想化のためのツールです。コンテナをテキストファイルで定義し、実行できます。重くない仮想環境みたいな感じです。もっと簡単に言うと、PCのなかにまっさらなPCを作ったり、それを削除したり、設計図を共有できたりします。

(もっと細かく言うと、実行しているプロセス(≒アプリ)に対してファイル読み書きや通信の制限をかけ、仮想的なファイルシステムを見せるようにしたものです。)

これを使うことで、

という状況を実現できます。

環境を作る

では、AtCoder環境に入れておきたいものを列挙してみます。

一つ一つ入れていきましょう。せっかくなので、できる限り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でもいいので、言語に合わせて好きなものを入れましょう。ほぼスプレッドシートのコマンドをそのまま使っているのですが、いくつか変更点があります。

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 さんの記事が出ます。楽しみですね〜〜!

zoi_dayo icon
この記事を書いた人
zoi_dayo

24B 競プロやWebをちょびっとやったりしていた

この記事をシェア

このエントリーをはてなブックマークに追加
共有

関連する記事

2021年8月12日
CPCTFを支えたWebshell
mazrean icon mazrean
2024年8月29日
クロスコンパイルRust
H1rono_K icon H1rono_K
2023年7月13日
アルゴリズム班はやとき王選手権「競(けい)プロ」を開催しました!
abap34 icon abap34
2021年4月18日
ベズー係数とN項の拡張ユークリッドの互除法
0214sh7 icon 0214sh7
2024年12月2日
【中国民族音楽作曲技法】①五声音階を認識しましょう
Natsuki icon Natsuki
2023年4月29日
CPCTF2023 PPC作問陣 Writeup
noya2 icon noya2
記事一覧 タグ一覧 Google アナリティクスについて 特定商取引法に基づく表記