競技プログラマーのためのOMake入門

はじめに

本記事はCompetitive Programming (その2) Advent Calendar 2016の19日目の記事です。

www.adventar.org

この記事では、プログラムのコンパイルを自動化するツール、OMakeの使用方法を、競技プログラミングの実例に沿って簡単に解説する。

本記事の対象読者

OMakeとは

OMakeとは、プログラムを簡単にビルドするためのツールの一種である。同様のツールとしてはMakeが有名だが、OMakeは基本的にこれを更に発展させたものとなっている。 読み方はおそらく「オマケ」ではなく「オーメイク」だと思われる。

OMake プロジェクトページ

1. ガイド — 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(上記)、CygwinMinGWを利用する。

OMakeの基礎知識

これらを踏まえておくと以下の記事が理解しやすいと思われる。

実行の流れ

omakeコマンドが呼ばれた場合、原則として以下の順序で依存関係の解析・ビルドが行われる。

  1. OMakerootが存在するディレクトリ(ルートディレクトリと呼ぶ)まで親ディレクトリを辿る。
  2. (通常OMakerootには.SUBDIRS: .が書かれているので)ルートディレクトリのOMakefileを読み、変数やルールを定義する。
  3. .SUBDIRS:の後に記述されているディレクトリに移動し、OMakefileを読む。もしくは、ディレクトリ名の後に記述されているルールを直接読む。
  4. これを再帰的に行う。

参考までにMakeにおける実行の流れを以下に示す。

  1. makeが実行された(もしくはオプションで指定された)ディレクトリのMakefileを読む。
  2. ルールに従ってコマンドを実行する。

すなわち、Makeの場合は他のディレクトリのMakefileを利用する場合は明示的にmakeコマンドを実行するルールを記述する必要がある。

主な構文

基本的にはMakeのものと類似している。シンタックスハイライトはMakeと同じものを使えば十分だと思われる。

  • インデントが重要な言語である。インデントのレベルによってコードブロックが判別される。
    • インデントにはスペースではなく水平タブを使う必要がある。
    • 本記事ではタブがスペースとして表示されてしまっているので注意。
  • #から行末までがコメントとみなされる。
  • 変数定義変数名 = 値、値の更新は変数名 += 値で行う。
  • 変数の値は$(変数名)で参照する。
  • 関数呼び出し$(関数名 引数1, 引数2)で行う。

コマンドラインオプション

コマンドラインオプションはomake --helpで表示することができる。 もしくはマニュアルを読む。

実例

動作確認はUbuntu 16.04 LTS、OMake 0.9.8.5にて行っている。

以下のような構造のディレクトリにおいてOMakeを利用することを考える。言語はC++を例にしているが、コマンドによってコンパイルを行う言語なら利用可能である。 コンパイラg++を使用する。 また、コンパイルオプションはそれぞれのジャッジシステムに合わせて、aojatcoderでは-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/直下にOMakerootOMakefileの2つのファイルのテンプレートが作成される。 OMakerootはほとんどの場合編集する必要がなく、今回も編集しない。 OMakefileにはC言語及びOCamlのプログラムをビルドするための設定がコメントアウトされた状態で書かれているが、今回はほぼ使用しないので中身は全て消して構わない。

aoj/ poj/の設定

まずはこれら2つのディレクトリでビルドを行えるように設定を行う。

初めに、これらのディレクトリがプロジェクトの一部であるということをcontest/OMakefileに記述する。 また、omake allomake 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++以外の言語を使用する場合は、srcstargsの定義とこの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/OMakefilepoj/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/*/*/にマッチするようなディレクトリ全てをサブディレクトリとして、そのディレクトリにおけるルールを定義している。 globDオプションを指定するとディレクトリのみにマッチすることを利用する。 各ディレクトリ内での設定は先ほどと同様である。

この設定によって、abc/arc/以下へのディレクトリの追加に加え、例えばatcoder/agc/などのディレクトリの追加にも変更無しで対応することができる。

なお、

.SUBDIRS: $(glob D, *)
    .SUBDIRS: $(glob D, *)

の代わりに

.SUBDIRS: $(glob D, */*)

と書いても良さそうに思えるが、そうするとabc/arc/がプロジェクト外となり、これらのディレクトリでomakeを実行できなくなってしまう。

まとめ

OMakeではMakeと比べてディレクトリ間の依存関係の記述に優れており、これを利用することで似た構造の複数のディレクトリのビルド設定を一括して行うことができる。 また、-Pオプションを使用することでコマンドを実行する手間を省くこともでき、一刻を争うプログラミングコンテストにおいて有利に働く…かもしれない。

おまけ

OMakeにはここで紹介した以外にも

  • oshという対話的実行環境がついている
  • クラスがあり、オブジェクト指向プログラミングができる

などの変態強力な機能がある。興味があったら活用してみてほしい。 (筆者はそこまでは触れていない)

それでは良いOMake lifeを。