競技プログラマーのためのOMake入門
はじめに
本記事はCompetitive Programming (その2) Advent Calendar 2016の19日目の記事です。
この記事では、プログラムのコンパイルを自動化するツール、OMakeの使用方法を、競技プログラミングの実例に沿って簡単に解説する。
本記事の対象読者
OMakeとは
OMakeとは、プログラムを簡単にビルドするためのツールの一種である。同様のツールとしてはMakeが有名だが、OMakeは基本的にこれを更に発展させたものとなっている。 読み方はおそらく「オマケ」ではなく「オーメイク」だと思われる。
何ができるか
競技プログラミングへの応用例としては以下のような使い方が考えられる。
- 5行~20行程度の設定を書いておけば、
omake
を実行するだけで必要なプログラムが全てコンパイルされる。 omake -P
をバックグラウンドで実行しておくと、ソースコードが変更された時に自動で再コンパイルされる(継続ビルド)。- 新規にディレクトリを作成した時に、設定ファイルのコピーなどが必要ない。
OMakeのインストール
UNIX系OSでは、継続ビルドをする際にfamまたはgaminが必要になる。famは古いのでgaminの方が良いとのこと。 継続ビルドを使わない場合はインストールの必要は無い。
Ubuntuの場合
$ sudo apt install omake gamin
Bash on Ubuntu on Windows (Windows Subsystem for Linux) においても同様の方法でインストールできるが、Build 14393ではomake -P
が動作しなかった。
Insider PreviewでBuild 14986を入れたら動作した。おそらくBuild 14926以降が必要となる。
OS Xの場合(動作未確認)
筆者はOS Xの環境を持っていないが、OPAMを利用するのが良いようである。
How to install omake on OS X · GitHub
Windowsの場合(動作未確認)
ソースコードをプロジェクトページからダウンロードし、INSTALL
に書いてある内容に従ってビルドする。
OCamlをインストールしておく必要がある。
もしくはWSL(上記)、CygwinやMinGWを利用する。
OMakeの基礎知識
これらを踏まえておくと以下の記事が理解しやすいと思われる。
実行の流れ
omake
コマンドが呼ばれた場合、原則として以下の順序で依存関係の解析・ビルドが行われる。
OMakeroot
が存在するディレクトリ(ルートディレクトリと呼ぶ)まで親ディレクトリを辿る。- (通常
OMakeroot
には.SUBDIRS: .
が書かれているので)ルートディレクトリのOMakefile
を読み、変数やルールを定義する。 .SUBDIRS:
の後に記述されているディレクトリに移動し、OMakefile
を読む。もしくは、ディレクトリ名の後に記述されているルールを直接読む。- これを再帰的に行う。
参考までにMakeにおける実行の流れを以下に示す。
make
が実行された(もしくはオプションで指定された)ディレクトリのMakefile
を読む。- ルールに従ってコマンドを実行する。
すなわち、Makeの場合は他のディレクトリのMakefileを利用する場合は明示的にmake
コマンドを実行するルールを記述する必要がある。
主な構文
基本的にはMakeのものと類似している。シンタックスハイライトはMakeと同じものを使えば十分だと思われる。
- インデントが重要な言語である。インデントのレベルによってコードブロックが判別される。
- インデントにはスペースではなく水平タブを使う必要がある。
- 本記事ではタブがスペースとして表示されてしまっているので注意。
#
から行末までがコメントとみなされる。- 変数定義は
変数名 = 値
、値の更新は変数名 += 値
で行う。 - 変数の値は
$(変数名)
で参照する。 - 関数呼び出しは
$(関数名 引数1, 引数2)
で行う。
コマンドラインオプション
コマンドラインオプションはomake --help
で表示することができる。
もしくはマニュアルを読む。
実例
動作確認はUbuntu 16.04 LTS、OMake 0.9.8.5にて行っている。
以下のような構造のディレクトリにおいてOMakeを利用することを考える。言語はC++を例にしているが、コマンドによってコンパイルを行う言語なら利用可能である。
コンパイラはg++
を使用する。
また、コンパイルオプションはそれぞれのジャッジシステムに合わせて、aoj
とatcoder
では-std=gnu++1y
を、poj
では-std=c++98
を使用することにする。
contest/ ├─ aoj/ │ ├─ 0001.cpp │ ├─ 0002.cpp │ └─ ... ├─ poj/ │ ├─ 0001.cpp │ ├─ 0002.cpp │ └─ ... └─ atcoder/ ├─ abc/ │ ├─ 001/ │ │ ├─ a.cpp │ │ ├─ b.cpp │ │ └─ ... │ ├─ 002/ │ │ ├─ a.cpp │ │ └─ ... │ └─ ... └─ arc/ ├─ 001/ │ ├─ c.cpp │ └─ ... ├─ 002/ │ ├─ c.cpp │ └─ ... └─ ...
atcoder/
以下においては、新たなコンテストが開催されるなどのタイミングでabc/xxx/
のようなディレクトリが作られることを想定している。
この中の任意のディレクトリにおいて、omake
を実行すると現在のディレクトリ以下のプログラムが全てコンパイルされることを目標とする。
テンプレートの導入
プロジェクトにおいて最初にOMakeを導入する際には、プロジェクトのルート(この場合はcontest/
)において
$ omake --install
を実行する。これによって、 contest/
直下にOMakeroot
とOMakefile
の2つのファイルのテンプレートが作成される。
OMakeroot
はほとんどの場合編集する必要がなく、今回も編集しない。
OMakefile
にはC言語及びOCamlのプログラムをビルドするための設定がコメントアウトされた状態で書かれているが、今回はほぼ使用しないので中身は全て消して構わない。
aoj/
poj/
の設定
まずはこれら2つのディレクトリでビルドを行えるように設定を行う。
初めに、これらのディレクトリがプロジェクトの一部であるということをcontest/OMakefile
に記述する。
また、omake all
、omake clean
を実行できるようにするためphonyターゲットを指定する。
更に、引数無しでomake
が実行された際にはall
をターゲットとして実行するようにする。
# contest/OMakefile .PHONY: all clean .DEFAULT: all .SUBDIRS: aoj poj
次にaoj/OMakefile
及びpoj/OMakefile
に実際にビルドするためのルールを記述する。内容は主に
- どのファイルを作るか
- そのファイルをどのようなコマンドで作るか
の2点である。この場合、作成するファイルは「ディレクトリ内の全てのソースコードをそれぞれコンパイルしたもの」である。 (競技プログラミング用でない)通常のプロジェクトにおいてはターゲットと依存関係はあらかじめある程度決まっているが、 今回の例では新たなターゲットが動的に作られるという点でこれと異なる。 そのため、ワイルドカードを用いてターゲットのリストを生成する。
# contest/aoj/OMakefile # コンパイラとコンパイルオプションを変数に設定 CXX = g++ CXXFLAGS = -O2 -Wall -std=gnu++1y # srcs変数に全てのソースコード名を代入 srcs = $(glob *.cpp) # srcsの内容それぞれの拡張子を取り除き、生成するファイル名をtargs変数に代入 targs = $(rootname $(srcs)) # コンパイルのルールを記述(後述) $(targs): %: %.cpp $(CXX) $(CXXFLAGS) -o $@ $< # omake all でtargsに含まれるファイルを生成する all: $(targs) # omake clean で生成されるファイルを削除する clean: rm -f $(targs) .DEFAULT: all
glob
はワイルドカードでマッチするファイル名を取得する関数。
rootname
は引数から拡張子を取り除いた文字列を返す関数。
ターゲットを別の拡張子のついたファイルにしたい場合はreplacesuffixes
を使うと良い。
このファイルの中で最も重要なのは
$(targs): %: %.cpp $(CXX) $(CXXFLAGS) -o $@ $<
の部分である。
このうち、1行目では%
はワイルドカードを表しており、
「targs
に含まれるファイルのうち、%
にマッチするファイルは%.cpp
というファイルから以下のコマンドを使って生成する」という意味になる。
このルールの詳細はこちら。
2行目では具体的なコマンドを記述している。$(CXX)
と$(CXXFLAGS)
は定義された変数の値がそのまま展開される。
$@
と$<
はルール内で自動的に設定される特殊変数であり、それぞれ「ターゲットの名前」と「最初の依存ファイル」を表す。
C++以外の言語を使用する場合は、srcs
とtargs
の定義とこの2行を変更すれば良い。
poj/OMakefile
もコンパイルオプション以外は同一である。
# contest/poj/OMakefile CXX = g++ CXXFLAGS = -O2 -Wall -std=c++98 srcs = $(glob *.cpp) targs = $(rootname $(srcs)) $(targs): %: %.cpp $(CXX) $(CXXFLAGS) -o $@ $< all: $(targs) .DEFAULT: all clean: rm -f $(targs)
設定の集約
これでaoj/
とpoj/
についてはコンパイルができるようになったはずである。ここまでのことはMakeでもほぼ同じ記述でできる。
しかし、ほとんど同じ設定を2か所に記述するのはプログラマーとしては避けたいものである。
幸いにもOMakeではこの問題を簡単に解決できる。
設定内容を以下のようにcontest/OMakefile
に移す。
# contest/OMakefile .PHONY: all clean .DEFAULT: all CXXFLAGS = -O2 -Wall .SUBDIRS: aoj poj # BuildInfo.omの内容を取り込む include BuildInfo targs = $(rootname $(glob *.cpp)) $(targs): %: %.cpp $(CXX) $(CXXFLAGS) -o $@ $< all: $(targs) clean: rm -f $(targs) .DEFAULT: all
.SUBDIRS:
の次の行以降にインデントしてルールを記述すると、サブディレクトリのOMakefile
にルールを記述するのと同じ効果が得られる
(.SUBDIRSの内容を並列化させる)。
従って、ここで複数のディレクトリを指定しておけば複数のサブディレクトリの設定を1か所にまとめることができる。
同時に、コンパイルオプションの設定も共通する部分は外に出している。
aoj/OMakefile
とpoj/OMakefile
は使用しないので削除して良い。
代わりに、それぞれのディレクトリにBuildInfo.om
というファイルを作成して固有の設定を記述し、contest/OMakefile
から取り込む。
BuildInfo
の部分は別の名前でも良いが、その場合はinclude
の引数もそれに合わせる。
# contest/aoj/BuildInfo.om CXXFLAGS += -std=gnu++1y
# contest/poj/BuildInfo.om CXXFLAGS += -std=c++98
こうすることで設定がまとまり、設定の変更などもしやすくなった。
atcoder/
の設定
atcoder/
以下の設定に移る。こちらはサブディレクトリが新規に作られるという点で前の2つと異なる。
そのため、Makeを使用する場合は通常ディレクトリを作る度にMakefileをコピーする必要があるが、
OMakeではそのようなことをせずに解決できる。
まず、atcoder/
をプロジェクトに入れる。
# contest/OMakefile .PHONY: all clean .DEFAULT: all CXXFLAGS = -O2 -Wall .SUBDIRS: aoj poj include BuildInfo targs = $(rootname $(glob *.cpp)) $(targs): %: %.cpp $(CXX) $(CXXFLAGS) -o $@ $< all: $(targs) clean: rm -f $(targs) .DEFAULT: all # 追加 .SUBDIRS: atcoder
続いて、以下の内容でatcoder/OMakefile
を作成する。要点は、先ほどaoj/
とpoj/
の設定を共通する親ディレクトリにまとめたのと同様に、
abc/001/
、abc/002/
などのディレクトリをまとめ、更にabc/
とarc/
の設定を一つにまとめて記述することである。
また、ワイルドカードを用いることで、これらのディレクトリを明示的に列挙せずに指定することができる。
# contest/atcoder/OMakefile CXXFLAGS += -std=gnu++1y .SUBDIRS: $(glob D, *) .SUBDIRS: $(glob D, *) targs = $(rootname $(glob *.cpp)) $(targs): %: %.cpp $(CXX) $(CXXFLAGS) -o $@ $< all: $(targs) clean: rm -f $(targs) .DEFAULT: all .DEFAULT: all
このコードでは、atcoder/*/*/
にマッチするようなディレクトリ全てをサブディレクトリとして、そのディレクトリにおけるルールを定義している。
glob
にD
オプションを指定するとディレクトリのみにマッチすることを利用する。
各ディレクトリ内での設定は先ほどと同様である。
この設定によって、abc/
やarc/
以下へのディレクトリの追加に加え、例えばatcoder/agc/
などのディレクトリの追加にも変更無しで対応することができる。
なお、
.SUBDIRS: $(glob D, *) .SUBDIRS: $(glob D, *)
の代わりに
.SUBDIRS: $(glob D, */*)
と書いても良さそうに思えるが、そうするとabc/
やarc/
がプロジェクト外となり、これらのディレクトリでomake
を実行できなくなってしまう。
まとめ
OMakeではMakeと比べてディレクトリ間の依存関係の記述に優れており、これを利用することで似た構造の複数のディレクトリのビルド設定を一括して行うことができる。
また、-P
オプションを使用することでコマンドを実行する手間を省くこともでき、一刻を争うプログラミングコンテストにおいて有利に働く…かもしれない。
おまけ
OMakeにはここで紹介した以外にも
- oshという対話的実行環境がついている
- クラスがあり、オブジェクト指向プログラミングができる
などの変態強力な機能がある。興味があったら活用してみてほしい。
(筆者はそこまでは触れていない)
それでは良いOMake lifeを。