Makefileのお勉強

ようやく新しいmac miniが発表されました。今使っているのが5年前に買ったmac mini (メモリ4G、Core 2 Duo、HDD320GBぐらい)で、そろそろ速度的にも容量的にも限界が来ていたので、今回の新作発表会で何が発表されても買い替えるつもりでした。なので、mac miniが発表されていなければ、iMacを買うことになるところでした。危ない危ない。5k iMacはそりゃ欲しいですが、今の自分の財政状態であんなモノを買おうものなら、ガチで生活を切り詰めねばならぬところでした。

さて、今回は自分のためのMakefileのお勉強メモです。しかし本当に、ソフトウェアの世界は勉強せにゃならん範囲が途方もない気がする。。。もっと絞ってやれるソフトの仕事もあるのかもしれないけれど、少なくとも自分はWeb技術ばっかやっているわけにはいかなくて、下回りもしっかり見えるようにしないといけないのです。うー。

今回はこれをベースにしたメモ書きです。

1章 簡単なメイクファイルを書いてみよう

  • makeは、プログラムの構築に必要な「編集-コンパイル-デバッグ」の繰り返しに費やす時間を節約するためのもの。
  • makeのコマンドライン引数にターゲットが指定されていれば、makefile中のそのターゲットが構築される。指定されていなければ、makefile中の最初のターゲット(デフォルトターゲット)が構築される。
  • makefileには、プログラムを構築するためのルールが書かれている。ルールはターゲット(target)必須項目(prereq)実行コマンド(commands)の3つの部分からなる。
    • ターゲット … ファイル、もしくは何か作り出されるもの
    • 必須項目 … ターゲットが作成される前に存在しなければならないもの
    • 実行コマンド … 必須項目からターゲットを作るためのもの
  • ルールの書き方は、Target: Prereq1 Prereq2 …で、次の行で先頭にタブを入れてからCommandsを書く(このタブによって、makeは続く文字列をコマンドとして解釈する)。Target, Prereq, Commandsは何個書いてもよい(TargetとPrereqはスペースで列挙、Commandsは改行で列挙)
  • makeがルールを解釈する段階に入ると、まずターゲットと必須項目に列挙されているファイルを探す。必須項目の中で関連するルールを持つものがある場合には、まずそれらを更新することから始める。その後でターゲットであるファイルについて検討する。必須項目の中でターゲットより新しいものがあるなら、ターゲットはコマンドを実行することで再構築する
  • コマンド行はそれぞれシェルに渡され、それぞれ別のシェル(サブシェル)で実行される
  • gccの-lオプションは、アプリケーションプログラムにリンクしなければならないシステムライブラリを指示する。必須項目に-l<NAME>という形式の指定が存在している場合には、libNAME.soというファイル(動的ライブラリ)を探す。なければ次にlibNAME.a(静的ライブラリ)を探す
  • makeは、ルールのターゲットと必須項目を比較して、ターゲットの方が必須項目より新しければ、そのルールについては何もしない。必須項目の中にターゲットより新しいものがあれば、実行コマンドを使ってターゲットを再構成する。
  • makefileは普通トップダウン構造を持ち、先頭にあるallといった最も一般的なターゲットの構築が規定の動作となる。その他の詳細なターゲットに続いてプログラムを保守管理するための、例えば不要なファイルの一時ファイルを削除するcleanのようなターゲットが最後に位置する。
  • makeに対するコメント文字は”#”。ここから行末までを無視する。
  • 長い行はバックスラッシュ”\”で継続可能。

2章 ルール

  • あるルールのターゲットは別のルールの必須項目であっても構わないため、ターゲットと必須項目の集まりは依存関係の連鎖構造またはグラフ構造を形成する。
  • ルールにはいくつか種類がある。
    • 明示的なルール … 最もよく見られるタイプのルール
    • パターンルール … ファイル名の代わりにワイルドカードを用いる
    • サフィックスルール … makeに元から備わっている一般的なルールを書く為の手段
    • 暗黙ルール … makeに組み込まれたルールデータベースに存在するパターンルールもしくはサフィックスルール
    • 静的パターンルール … 指摘されたターゲットファイルのリストに対してのみ適用されるパターンルール
  • ルールが複数のターゲットを持つときは、すべてのターゲットが同じ必須項目を持っていることを意味する。
  • 一つのターゲットに対して、複数のルールを定義してもよい。makeはターゲットを見るたびに、そのターゲットと必須項目を依存関係グラフに登録していく。すでにそのターゲットがグラフに登録されていた場合、そこに追加の必須項目を加える(これがうまく機能するのは、makeの実行が2段階に分かれていて、依存関係グラフの作成は1段階目で行われているから?)。なお、コマンド行を書くのは、どれか一つのにする必要がある。他のルールは、依存関係を表現するために使う。一つのルール内での必須項目の更新順は左から右が保証されるが、複数のルール間での必須項目の更新順は、他に依存関係がない場合は、不定になる(abc defか、def abcになるかはわかからない)。
  • makefileではワイルドカード(~, *, ?, […], [^…]など)が使える。
  • ワイルドカードのパターンがターゲットや必須項目に現れた際には、make自身がすぐにワイルドカードを展開する。一方、パターンがコマンドの中に現れた際には、サブシェルがコマンドを実行する時点でワイルドカードを展開する
  • 実際のファイルを示していない、疑似ターゲット(all, cleanなど)を使うこともできる。疑似ターゲットは、ルールに関連づけられたコマンドがターゲット名のファイルを作らないため、常に最新ではないとmakeに判断されるころになり、必ずコマンドが行される
  • 疑似ターゲットと同名のファイルが存在してしまうと、疑似ターゲットを使うルールでは大概必須項目が空なので、常にターゲットのファイルが最新扱いになり、疑似ターゲットを使うルールで実行するつもりだったコマンドが実行されなくなる。これを回避するために、疑似ターゲットを指定するための特殊なターゲット.PHONYを利用できる。例えば .PHONY: clean を書いてからcleanをターゲットとしたルールを書くと、cleanというファイルが存在していたとしても、cleanターゲットに関連づけられたコマンドは必ず実行されるようになる。
  • 疑似ターゲットは、makefileに埋め込まれたシェルスクリプトと考えることもできる。疑似ターゲットを他のターゲットの必須項目にすることで、そのターゲットを構築する前に疑似ターゲットのスクリプトを実行することができる
  • 変数名はmakeが認識できるように$( )か${ }で囲むが、変数名が一文字の場合は囲む必要はない。
  • 自動変数とは、実行するルールが定まった後にmakeにより自動的に設定されるもの。そのため、使用できるのはコマンド行のみ。主なものは以下の7つ。
    • $@ … ターゲットのファイル名を表す
    • $% … ライブラリの構成指定中の要素を表す
    • $< … 最初の必須項目のファイル名を表す
    • $? … ターゲットよりも後で更新された必須項目のすべてを、スペースで区切ったリストで表す
    • $^ … すべての必須項目をスペースで区切ったリストで表す(重複したファイルはリストから取り除かれる)
    • $+ … すべての必須項目をスペースで区切ったリストで表す(重複を含む)
    • $* … ターゲットファイル名の一部を表す一部とはサフィックスを除いたファイル名を示すのが普通(設定可能)
  • VPATH変数、もしくはvpath命令を使えば、makeにファイルを探すディレクトリを教えることができる。ディレクトリを複数教えるときは、VPATH = src include のように空白で並べる。
  • VPATH変数は、ターゲットか必須項目のファイルを探すときに有効で、コマンドに書かれているファイルには無効。よって、コマンドにincludeファイルなどの読み込み先を教えるには、変数CFLAGSを CFLAGS = -I include のように宣言して、コマンド内で gcc $(CFLAGS)のように使う
  • makeに組み込まれている暗黙ルールや、変数の定義を知りたい時は、オプションで –print-data-base (-p) を指定してmakeを実行する。
  • 組み込みルールは、COPILE.c = $(CC) $(CFLAGS) $(CPPFLAGS) $(TARGET__ARCH) -c のように、変数から作られていたりする。これらの変数をmakeファイルで設定するときには十分な注意が必要。例えばCFLAGS = -I project/includeのような記述をmakefileの中にすると、コマンドラインmake時に $ make CFLAGS=-DDEBUG (DEBUGを有効にするためにDEBUGを定義するオプション追加)としたときに、CFLAGSの値は上書きされて無効になってしまう
  • ccコマンドが-lオプションを見つけると、システムの標準ライブラリディレクトリから、ライブラリlibNAME.so(libNAME.a)を探す。この仕組みにより、makefileのポータビリティが高くなっている。また、-Lオプションを使うと、ライブラリの検索場所を指定できる。また、-l, -Lを使わずに、パスとファイル名を指定するやり方で必須項目に組み込むことも可能。
  • リンカはコマンドラインで指定された順番でライブラリを探そうとする。そのため、もしライブラリAが未解決のシンボルを含んでいて、それがライブラリBで定義されていた場合、リンカのコマンドラインではBの前にAを置く必要がある。コマンドライン上のライブラリの順番は基本的に重要

3章 変数とマクロ

  • 変数として使えない文字は”:”, “#”, “=”の3つだけ。空白があっても構わないが、使うべきではない。
  • makeの変数は2種類。
    • 単純展開変数 … :=代入演算子を使って定義する。代入行がmakefileから読み込まれると演算子の右辺が即時評価され、その結果の値が変数に代入される。右辺に値が設定されていない変数が入っているときは、その部分は空文字列として展開される
    • 再帰展開変数 … =代入演算子を使って定義する。代入行がmakefileから読み込まれた時点では右辺は展開されず、変数が使われるときに初めて右辺が展開される。つまり、必要な変数がmakefileの後ろの方で定義されていてもよい、ということ。また、使われるたびに右辺は再評価される
  • ?=演算子で代入を行うと、変数が値を持っていないときにかぎり、指定された代入を実行する
  • +=演算子は、変数にテキストを追加する
  • define命令を使って、マクロを作ることができる。define マクロ名〜endefの間に、改行しながらコマンドを記述する。その際、先頭に@のあるコマンド行は、コマンドがmakeにより実行される際にコマンド自体は表示されなくなる(出力だけが表示される)。
  • makeは実行された際に、2段階に分かれた作業を行う。最初の段階ではmakefileとそこからインクルードされたmakefileを読み込む。このとき、変数とルールがmakeの内部データベースに格納され、依存関係グラフが作成される。次の段階では依存関係グラフを解析し更新すべきターゲットを特定した後、更新に必要なコマンドを実行する。
  • makefileの要素が展開されるルールは以下のとおり。
    • 変数の代入における代入の左辺は、第一段階でmakeが代入行を読み込んだときに展開される
    • =か?=の右辺は、第二段階にて使われるまで展開されない
    • :=の右辺は、直ちに展開される
    • +=の左辺が単純変数として定義されたものである場合には、右辺も直ちに展開される。そうでなければ、右辺の展開は後で行われる
    • (define命令を使った)マクロ定義では、マクロ名は直ちに展開されるが、本体は使われるまで展開されない
    • ルールでは常にコマンドが後で展開されるが、ターゲットと必須項目は直ちに展開される
  • makefile実行中の変数は、通常1つの値を持ち続ける。例えばCFLAGSに格納されているオプションは、通常全てのCファイルのコンパイルに適用される。しかし、特定のファイルだけに追加のコマンドラインオプションを適用したいような場合には、ターゲット固有の変数を使う。これはターゲットに付随した変数定義で、ターゲットとその必須項目を処理している間だけ有効になる。例えば、gui.o: CPPFLAGS += -DUSE_NEW_MALLOC=1 と書いておけば、gui.oターゲットが処理されている間のみ、既定のCPPFLAGSに一時的に値を追加した状態でコンパイルを実行できる。
  • 変数の与え方は色々ある。
    • makefileやインクルードされるファイルの中で定義
    • コマンドライン上で”make CFLAGS=-g”のようにする。コマンドライン上の代入は、それぞれシェルに対して1つの引数でなければならない。コマンドライン上で行われた代入は、makefile内で行われる代入と環境変数を元とする値を上書きする
    • 環境変数はすべてmakeの開始時にmake変数として定義される。この変数の優先度は最も低いので、makefile内やコマンドラインで行われる代入により、環境変数から来ている値は上書きされてしまう。環境変数は?=の条件付き代入と組み合わせて利用しやすい。デフォルト値の設定に使うことができる。
    • ifdef, ifndef, ifeq, ifneq, else, endifを使った条件分岐も可能。
    • CFLAGSやCXXFLAGSについて、自分で確認できたことは以下。
      • 暗黙のルール内のルールは、$(CFLAGS)などを使って定義されている。言い換えると、適当に定義した変数が暗黙のルールに適用されることはない。
      • 暗黙のルールに使われているからといって、何か値が入っているというわけでもない。CFLAGSやCXXFLAGSなどのデフォルト値は空である。
      • ということで、CFLAGSなどに自分で値を好きに入れてよいが、そのときに=演算子を使っている例が多いが、全部が全部これで良いのかが疑問。+=演算子を使うべきでは、と思うこともある。
      • 以下、参考URL

4章 関数

  • とりあえず省略。必要に応じて見直す。

5章 コマンド

  • コマンドは基本的に1行のシェルスクリプト。makeは1行ごとに取り出し、サブシェルに渡して実行する。
  • makeの既定のシェルは/bin/sh。このシェルはmakeの変数SHELLにより変更できるが、この変数に限っては、makefile内で明示的に設定しなければならない(環境変数として設定していても有効にならない。ユーザの選択したシェルがmakefileの動作に影響を及ぼさないようにするため)。
  • ターゲットに続く行で最初の文字がタブである行は、(バックスラッシュを使った前の行からの継続を除いて)コマンドであると認識される。
  • 各コマンドはそれぞれ別のシェルで実行されるので、一緒に実行しなければならないコマンド列は特殊な扱いが必要になる。
    • 基本的には、行末のバックスラッシュをつけて行を継続させるように書く。
    • ディレクトリ変更(cd)してから何かする、といった場合には、cd directoryの後に、“;”, “&”, “&&”のどれかを付けてからバックスラッシュをつけて改行する。”;”は、前のコマンドの実行が終わったら次のコマンドを実行する。”&”は、前のコマンドの完了を待たずに次のコマンドを実行する。”&&”は、前のコマンドが成功したときのみ、次のコマンドを実行する。
  • コマンドの頭に”@”をつけると、makeはそのコマンドを実行したことを出力しない(実行結果は出力される)。

6章 大きなプロジェクトの管理

  • 個別のプログラムやライブラリといった主要なコンポーネントに分割することで、大きなソフトウェアプロジェクトは単純かされる。そういったコンポーネントは、それぞれ個別のディレクトリに格納され、専用のmakefileによって管理される。システム全体を構築するには、個々のコンポーネント用makefileを適切な順番で呼び出すトップレベルのmakefileを使うという方法がある。この手法はトップレベルのmakefileがそれぞれのコンポーネントに対してmakeを再帰的に実行するため、再帰的makeと呼ばれる。
  • MAKE変数は、makefileからサブディレクトリ内のmakeを実行する際に使われる。そのMAKE変数はmakeにより認識され、再帰的実行で同じmakeが使われるようにパスも含めた値が設定される。
  • makeがサブディレクトリに移動してmakefileを読むようにするには、–directoryオプション(-Cオプション)を使う。

7章 ポータブルなmakefile

  • makefileが対処しなければならない共通の問題として、以下がある。
    • プログラム名 … 例えば、CやC++コンパイラの名前が違う
    • パス名 … プログラムやファイルの置き場所はプラットフォームごとに異なる
    • オプション … コマンドラインオプションがプラットフォームごとに異なる
    • シェル機能 … 既定ではmakeは/bin/shを使ってコマンドスクリプトを実行するが、シェル機能は実装により大きく異なる
    • プログラムの挙動
    • OS
  • プログラムを管理する一般的な方法は、変更される可能性のあるプログラム名やパス名を変数に入れること。変数は、 MV ?= mv -f のように、単純に定義する。こうすることで、変数はコマンドラインで設定したりmakefilew書き換えたり、環境変数を設定することで変更することができる。

 

とりあえずここまで。。。あとは、実際にmakefileを見ながら、都度理解していくことにします。