ビルドシステムの記述自由度と保守性のトレードオフ

突然だが、Makefile というものがある。
主に C や C++ で使われるツールで、この Makefile を作っておけば、 gcc / g++ のコマンド(往往にして gcc –Wall -o hello hello.c みたいに長くなりがちである)をいちいち入力することなく、make と打つだけで肩代わりしてくれる優れものである。
記述量を減らせるだけではない。例えば、 hoge.c を オブジェクトファイル hoge.o に変え、 fuga.c を オブジェクトファイル fuga.o に変えて、さらにヘッダファイル guee.h と一緒にリンクして最終的に実行ファイルを作る ... というような複数の工程を make 一つで済ませることができるというメリットがある。


この Makefile についてちゃんと知る機会がこの前あった。OSに関する授業である。
UNIX系のOSはC言語で書かれていて、Cを使えばOSがプロセスを扱う仕組みをシステムコールのレベルで理解できるので、OSの学習のためにはCを書くのである(もちろん一般的に使われているほとんどの言語でOSを操作することはできる。ただ大体の場合、その操作のための関数/メソッドは人間が扱いやすいように API でラップされているので、「中が本当はどうなっているのか」を勉強するのにはCのような低級言語が学習・研究用によい)。
で、Makefile について「ちゃんと知る」というのはどういうことだったかというと、Makefile の存在自体は前から知っていたが、Makefile が実はとても自由に記述できるファイルだということを知らなかったので、勉強になったのである。
たとえば、Makefileシェルスクリプトのように変数が使える。

CC = gcc
CFLAGS = -Wall

test.o: test.c
    $(CC) $(CFLAGS) -c test.c

他にもググるとと Makefile には無数の文法事項が存在していることがわかる。


もしかしたらCやC++ばかり書く人にとってはこれは当たり前に受け入れている話なのかもしれないが、僕はそうではない人なので、これは面白いことだと思った。何が面白いかというと、世の中のソフトウェア開発で(Cに比べると)一般的に使われているような高級言語では、ビルドシステムの記述自由度は Makefile よりも断然低い。

高級言語を扱う際は Makefile はおそらくあまり使わない。そもそも GNU Make 自体が UNIX系に強く依存しているので、プラットフォームに関わらず普遍的に使用できることを目指す Web の世界とは方向性が違う。
その代わりに、それぞれの言語に応じたパッケージ管理システムがあり、その中でビルドシステムに相当するものも用意されている。
例えば、JavaScript、特にサーバーサイドでも動く Node.js に関して言えば、npm というパッケージ管理システムがある。この npm において、ビルドシステムに相当するのが package.json である。

{
  "name": "prompts",
  "version": "2.0.4",
  "description": "Lightweight, beautiful and user-friendly prompts",
  "license": "MIT",
  "repository": "terkelg/prompts",
  "main": "index.js",
  "author": {
    "name": "Terkel Gjervig",
    "email": "terkel@terkel.com",
    "url": "https://terkel.com"
  },
  "files": [
    "lib",
    "dist",
    "index.js"
  ],
  "scripts": {
    "start": "node lib/index.js",
    "build": "babel lib -d dist",
    "prepublishOnly": "npm run build",
    "test": "tape test/*.js | tap-spec"
  },
(略)
  "dependencies": {
    "kleur": "^3.0.2",
    "sisteransi": "^1.0.0"
  },
(略)

これはとあるOSSの中の package.json である。ビルドと関係ない項目も色々あるが、dependencies の中に書かれている外部ファイルを読み込み、scripts に書かれているコマンドでプログラムを実行したりファイルを書き出したりする、という意味では Makefile に近い役割をしていると言える(インタプリタである node よりも、ECMAScript を素の JavaScript に変換する babel が元の gcc の意味に近い。細かい話だけど...)
この JSON というファイルは、記述自由度がとても低くて、もちろん中で変数なんか定義できないし、何ならコメントすら書けない。JSON で値として使えるのは null、真偽値、数値、文字列 と、それらから成る配列、オブジェクトだけだと決まっている。Makefile と比べるととても制約が強い。
JavaScript 以外でも、このような設定ファイルは、YAML だとか、TOML だとか、制約が強いマークアップ言語が使われるのが普通である。CIを回すことをビルドの一種と考えれば、.travis.yml とかもそうである。


さて、このように書くと、自由に書ける Makefile がすごいという話になるが、必ずしもそういうわけでもないと思う。
あまりに自由に書けるビルドファイルは、こだわり過ぎると俺流のファイルを作ることになってしまい、他の人が読んでも理解しづらいものになってしまうだろう。正直、ググると出てくる Makefile の例も、行数が多いものは何をやっているのだか一目では分からない。
Makefile 以外の例でこの現象に近いものを一つあげると、webpack.config.js というのがある。これは Webpack というフロントエンドでは必須に近いツールの設定ファイルだが、JSONではなく普通の JavaScript で書かれるため、見た目がグチャグチャになりがちで、何をやっているのか解読するのが慣れていないと難しい。
一方で、記述自由度の低いビルドシステムは、その分単純に書くことしかできないので、他の人が読みやすい。すなわち保守性が高い。世の中のソフトウェア開発は一人でやるものではないし、世界中で協力して作っていくソフトウェアも星の数ほどあるので、保守性が高いことはとても大切なことである。
このようにビルドシステムの記述自由度と保守性の間にはトレードオフの関係があるのではないかと考えている。


最後に、このトレードオフのバランスを上手くとっていると自分が思う例を一つ挙げて終える。それは Dockerfile である。
Docker というのは比較的新しい(もう新しいとは言えない位に時代は経過したかもしれない・・・)仮想化技術である。コンテナ型と呼ばれていて、非常にざっくり言うと、今までは元のOSの上に新しい仮想OSを乗っけていたのを、元のOSの一部分を切り出してそれを新しいOSにしてみたよ、だから効率が良くなったよ、という感じの技術である。
Dockerfile は Docker が提供する仮想OSの元となる"イメージ"をビルドするためのビルドシステムだが、面白いことに、ただOSを用意するだけではなくて、そのOSの環境変数を変えたり、apt-get で外部のソフトウェアを入れたりする部分も Dockerfile に書くことができる。だから、例えば Nginx + Laravel + Vue の環境が整った仮想OSを用意できる Dockerfile、みたいなものが存在する(このようにインフラを一本のコードでまとめていくことで保守管理しやすくしていこう、というような思想が Infrastructure as Code と呼ばれる)。
このように書くと Dockerfile は Makefile のように記述自由度の高い、ゆえに読みづらいファイルになっていると思われるかもしれない。しかし、Dockerfile には制約がある。必ず [FROM, RUN, CMD など命令の種類を判別するラベル] + [命令] というフォーマットで書かねばならず、このラベルの種類は限られている。ゆえに読みやすさ・保守性の高さもある程度確保できているのではないかと思う。実際、Dockerfile は Docker Hub という場所でOSSとして共有されていく仕組みが整備されている。Web 開発に限らず、データサイエンスの分野でも Docker はかなり普及している。
このように考えていくと、あるシステムがソフトウェア開発において覇権を握る上で、記述自由度と保守性の双方をうまく担保することがキーポイントになっているのではないだろうか。