プログラミング/make

出典: フリー教科書『ウィキブックス(Wikibooks)』
Wikipedia
Wikipedia
ウィキペディアmake (UNIX)の記事があります。

makeはソースコードから実行形式のプログラムやライブラリを自動的にビルドするビルド自動化ツールです。 統合開発環境や言語固有のコンパイラー機能でもビルドプロセスを管理できますが、make はプログラムのビルド以外にも、あるファイルが変更されると自動的に他のファイルから更新されなければならないようなプロジェクトの管理にも使用することができます。 特にUnixやUnix系OSではmakeが広く使われています。

はじめに[編集]

makeは何度も書き直され、同じファイルフォーマットと基本的なアルゴリズム原理を使用し、独自の非標準的な拡張を行ったスクラッチから書き起こされた亜種も多数存在します。

  • BSD-make
  • GNU-make

の2つが多く使われているので、本書では BSD-make(しばしば bmake)とGNU-make(しばしば gmake)に共通する構文・機能ついて説明します(余裕があれば違いについても説明します)。

分割コンパイル[編集]

make の説明に入る前に、分割コンパイルについてザックリ説明します。

プログラミング言語には、コンパイラーによってソースコードから実行形式のプログラムをコンパイルするコンパイル型言語と、ソースコードをインタープリターが逐次実行するインタープリタ-型の2つに分類されます。

分割コンパイルは、コンパイラー型のプログラミング言語で複数のソースコードから1つの実行形式のプログラムを作る方式のことで、1つ1つのソースコードを小さくしたり関連した機能だけをまとめる目的で行なわれます(分割統治法の1つだと考えられます)。

C言語の分割コンパイルの例[編集]

実際に3つのファイルに分割したC言語で書かれたプログラムの例を見てみましょう。

Main.c
// Main.c:
#include </stdio.h>
#include "imath.h"

int main(void) {
    for (int i = 0; i < 3; i++)
        for (int j = 0; j < 3; j++)
            print("ipow(%d, %d) = %d\n", i, j, ipow(i, j));
}
imath.h
// imath.h:
extern int ipow(int x, unsigned y);
imath.c
// imath.c:
#include "imath.h"

int ipow(int x, unsigned y) {
    if (y == 0)
        return 1;
    if (y == 1)
        return x;
    return x * ipow(x, y - 1);
}
コマンドラインでのビルド作業の様子
% mkdir imath
% cd imath/
% cat > Main.c
// Main.c:
#include <stdio.h>
#include "imath.h"

int main(void) {
    for (int i = 0; i < 3; i++)
        for (int j = 0; j < 3; j++)
            print("ipow(%d, %d) = %d\n", i, j, ipow(i, j));
}
% cat > imath.h
// imath.h:
extern int ipow(int x, unsigned y);
% cat > imath.c
// imath.c:
#include "imath.h"

int ipow(int x, unsigned y) {
    if (y == 0)
        return 1;
    if (y == 1)
        return x;
    return x * ipow(x, y - 1);
}
% cc -c imath.c
% cc -c Main.c
% cc -o imath Main.o imath.o
% ./imath 
ipow(0, 0) = 1
ipow(0, 1) = 0
ipow(0, 2) = 0
ipow(1, 0) = 1
ipow(1, 1) = 1
ipow(1, 2) = 1
ipow(2, 0) = 1
ipow(2, 1) = 2
ipow(2, 2) = 4
  1. imath.c をコンパイルし imath.o を得ています。
  2. Main.c をコンパイルし Main.o を得ています。
  3. Main.o と imath.o (とランタイムライブラリー)をリンクして実行形式 imath を得ています。

このプログラムは、0の0乗を扱っています。 0**0を1としていますが宗派によっては 0**0を0とする場合もあります。

imath.c をその仕様に変更してみましょう。

imath.c
// imath.c:
#include "imath.h"

int ipow(int x, unsigned y) {
    if (x == 0)
        return x;
    if (y == 0)
        return 1;
    if (y == 1)
        return x;
    return x * ipow(x, y - 1);
}

実行してみましょう。

% ./imath 
ipow(0, 0) = 1
ipow(0, 1) = 0
ipow(0, 2) = 0
ipow(1, 0) = 1
ipow(1, 1) = 1
ipow(1, 2) = 1
ipow(2, 0) = 1
ipow(2, 1) = 2
ipow(2, 2) = 4

あれ、変わりません。ああ、書換えてからコンパイルをしてませんね。

% cc -c imath.c
% ./imath 
ipow(0, 0) = 1
ipow(0, 1) = 0
ipow(0, 2) = 0
ipow(1, 0) = 1
ipow(1, 1) = 1
ipow(1, 2) = 1
ipow(2, 0) = 1
ipow(2, 1) = 2
ipow(2, 2) = 4

?、まだ変わりません。リンクも必要ですね。

% cc -c imath.c
% cc -o imath Main.o imath.o
% ./imath 
ipow(0, 0) = 0
ipow(0, 1) = 0
ipow(0, 2) = 0
ipow(1, 0) = 1
ipow(1, 1) = 1
ipow(1, 2) = 1
ipow(2, 0) = 1
ipow(2, 1) = 2
ipow(2, 2) = 4
やっと変更が反映されました。
このように、ソースコードの変更によって部分的なビルドが必要になります。
Main.oは変更の影響を受けないので再コンパイルしていませんね。

この例は単純なので、全てのソースコードの再コンパイルの要否を容易に把握できますが、小さなツールのソースコードでもファイルは十数から数十に及ぶので把握が困難になりがちです。 このような時のために Makefile を用意します。

make を使ったビルド[編集]

Makefile[編集]

imath をビルドするための素朴な Makefile を書いてみます。

Makefile
# Makefile:

imath: Main.o imath.o
        cc -o imath  Main.o imath.o

Main.o: Main.c imath.h
        cc -c Main.c

imath.o: imath.c imath.h
        cc -c imath.c

使ってみます。

% make imath
`imath' is up to date.
% rm *.o
% make imake
make: don't know how to make imake. Stop

make: stopped in /usr/home/user1/imath
% make imath
cc -c Main.c
cc -c imath.c
cc -o imath  Main.o imath.o
% ./imath 
ipow(0, 0) = 0
ipow(0, 1) = 0
ipow(0, 2) = 0
ipow(1, 0) = 1
ipow(1, 1) = 1
ipow(1, 2) = 1
ipow(2, 0) = 1
ipow(2, 1) = 2
ipow(2, 2) = 4
% make
`imath' is up to date.
% _
  1. make は make ターゲット名 の形式で呼出します
  2. 依存関係のある Main.o imath.o のタイムスタンプがターゲット imath のタイムスタンプより古いので何もしません
  3. Main.o と imath.o を消してみます。
  4. 再び、make
  5. 綴を imake と間違えました
  6.  
  7.  make は停止しました
  8. 正しい名前で make を起動
    依存関係imath: Main.o imath.oから
    依存関係Main.o: Main.c imath.hが適応されアクションcc -c Main.cが実行されます
    依存関係imath.o: imath.c imath.hが適応されアクションcc -c imath.cが実行されます
    Main.o imath.oの用意ができたのでcc -o imath Main.o imath.oが実行されます

Makefileの基本構文[編集]

ターゲット
依存関係の : の左側、ファイルを書くとソースのタイムスタンプをもとにアクションに要否を make が判断します
ソース
依存関係の : の右側、複数可(空白区切り)
アクション
依存関係の次からの行、行頭に水平タブが1つ以上必要です。
imath: Main.o imath.o
        cc -o imath  Main.o imath.o
ターゲット
imath
ソース
Main.o imath.o
アクション
cc -o imath Main.o imath.o
となります。

コメント[編集]

Makefile にはコメントを書くことができます。

行の中に # が出てきたら、そこから行末までがコメントです。

組込みルール[編集]

make には既にルールが組込まれていて、例えば、.c から .o を得るルールが組込まれているので

# Makefile:

imath: Main.o imath.o
        cc -o imath  Main.o imath.o

Main.o: Main.c imath.h
#       cc -c Main.c

imath.o: imath.c imath.h
#       cc -c imath.c

としてもアクションをコメントにしても

% rm *.o
% make
cc    -c -o Main.o Main.c
cc    -c -o imath.o imath.c
cc -o imath  Main.o imath.o
と組込みルールが適用されます。
cc -c -o Main.o Main.c
と組込みルールでは生成されるファイルを明示しているところが違います。

ディフォルトターゲット[編集]

Makefileで最初に出てくるターゲットがmakeを引数を渡されなかったときにビルドされるターゲットで、allを使うのが一般的です

# Makefile:

all: imath

imath: Main.o imath.o
        cc -o imath  Main.o imath.o

Main.o: Main.c imath.h
imath.o: imath.c imath.h

ファイル以外のターゲット[編集]

ターゲットは、all の様にファイルでないものでも構いません。その場合はタイムスタンプの比較はできないので必ず実行されます。 clean がよく定義されます。

# Makefile:

all: imath

clean:
        rm imath Main.o imath.o

imath: Main.o imath.o
        cc -o imath Main.o imath.o

Main.o: Main.c imath.h
imath.o: imath.c imath.h

.PHONYを使ったダミーターゲットの記述[編集]

ファイル以外をターゲットにするのは便利ですが、一つ問題があります

$ touch clean
$ make clean
make: Nothing to be done for 'clean'.
clean と言う名前のファイルが有ると、make は「既に clean が存在し、依存関係もないので特にすることはない」と判断してしまいます。
# Makefile:
.PHONY: all clean

all: imath

clean:
        rm imath Main.o imath.o

imath: Main.o imath.o
        cc -o imath Main.o imath.o

Main.o: Main.c imath.h
imath.o: imath.c imath.h
ファイルでないターゲットを .PHONY: の右に列挙します。
$ make clean
rm imath Main.o imath.o

変数[編集]

Makefile内で何度も参照される事柄は、変数に定義して複数箇所で参照できます。変数を使用することで、コードの可読性を高め、メンテナンスを容易にします。

例えば、次のようなMakefileがあります:

# Makefile:
.PHONY: all clean

EXEC=imath
SRCS=Main.c imath.c
OBJS=Main.o imath.o

all: $(EXEC)

clean:
        rm $(EXEC) $(OBJS)

$(EXEC): $(OBJS)
        cc -o $(EXEC) $(OBJS)

Main.o: Main.c imath.h
imath.o: imath.c imath.h

このMakefileでは、EXECSRCSOBJSといった変数が定義されています。これらの変数を使用することで、コンパイル対象のファイルや実行ファイル名を簡潔に指定できます。

変数の定義方法は、変数名=値の形式です。変数を参照する場合は$(変数名)とします。

例えば、$(EXEC)imath$(SRCS)Main.c imath.c$(OBJS)Main.o imath.oに展開されます。

変数を使うことで、ファイル名やコンパイラオプションなどの繰り返しの部分を一箇所で管理することができます。これにより、変更が必要な場合に変更箇所を見落とすリスクを減らすことができます。

[TODO:ディフォルトターゲット・暗黙のルール・組込みルール・変数・マクロ・条件実行…bmakeとgmakeの双方で動くMakefileを書くコツ]

CMakeからの利用とその利点[編集]

Makefileとは異なり、CMakeはクロスプラットフォームなビルドシステムのためのツールです。CMakeはプラットフォームやコンパイラの違いを吸収し、様々な環境でコードをビルドすることができます。以下に、CMakeを使用することの利点と、Makefileからの利用方法について説明します。

利点:[編集]

  1. クロスプラットフォーム: CMakeはWindows、Linux、macOSなどのさまざまなプラットフォームで動作し、プラットフォーム固有のビルドスクリプトを作成する必要がありません。
  2. コンパイラの選択: CMakeは、使用するコンパイラを柔軟に選択できます。開発者は、コンパイラを変更するだけでビルドシステムを切り替えることができます。
  3. 自動依存解決: CMakeは、ソースコード内の依存関係を自動的に解決し、必要なライブラリやパッケージを見つけます。これにより、手動で依存関係を設定する手間が省けます。
  4. 統合開発環境(IDE)のサポート: CMakeは、主要な統合開発環境(Visual Studio、Xcode、CLionなど)と統合されており、開発者はIDE内でCMakeプロジェクトを直接開くことができます。
  5. 簡潔な構文: CMakeの構文はシンプルで直感的であり、ビルドスクリプトを簡潔に保ちます。

Makefileからの利用方法:[編集]

CMakeをMakefileと組み合わせることで、さらなる柔軟性と効率を実現できます。以下は、MakefileからCMakeを使用する方法の基本的な手順です:

  1. CMakeLists.txtの作成: プロジェクトのルートディレクトリにCMakeLists.txtという名前のファイルを作成します。このファイルには、プロジェクトの構造やビルド手順を記述します。
  2. Makefileの生成: CMakeコマンドを使用して、Makefileを生成します。一般的なコマンドは次のとおりです:
    cmake -G "Unix Makefiles" -S . -B build/
    
    このコマンドは、CMakeを使用してUnix環境でMakefileを生成するためのものです。各オプションの説明は以下の通りです:
    • -G "Unix Makefiles": このオプションは、CMakeが生成するビルドシステムを指定します。ここでは、UnixシステムでMakefileを生成するように指定しています。
    • -S .: このオプションは、ソースディレクトリを指定します。.はカレントディレクトリを表します。つまり、プロジェクトのルートディレクトリがソースディレクトリとして指定されています。
    • -B build/: このオプションは、ビルドディレクトリを指定します。ここでは、build/という名前のディレクトリが指定されています。このディレクトリは、ビルドの中間ファイルや生成された実行ファイルなどのビルドに関連するすべてのファイルを含みます。
  3. ビルド: 生成されたMakefileを使用してプロジェクトをビルドします。一般的なコマンドは次のとおりです:
    cmake --build build/
    
    ここで、--buildオプションを使用して、ビルドディレクトリ内のMakefileを使ってビルドを実行します。

CMakeをMakefileと組み合わせることで、プロジェクトのビルドプロセスをさらに効率化し、クロスプラットフォームな開発を実現できます。

小史[編集]

makeの歴史には、BSD Make(またはpmake)も重要な役割を果たしています。以下では、makeとBSD Makeの歴史について説明します。

makeの歴史:[編集]

  • 初期のmake: makeは1970年代初頭にベル研究所のスティーブ・ジョンソンによって開発されました。彼はUNIXの開発中にソフトウェアのビルドプロセスを自動化する必要性を感じ、そのためのツールとしてmakeを作成しました。
  • GNU Make: 1980年代には、GNUプロジェクトがGNU Makeをリリースし、これが標準的なmakeの実装として広く採用されました。GNU Makeは、オリジナルのmakeよりも多くの機能と柔軟性を提供し、より大規模で複雑なプロジェクトのビルドに適しています。

BSD Makeの歴史:[編集]

  • BSD Make(pmake): BSD Make(またはpmake:Portable Make)は、1980年代にAndrew Tannenbaumによって開発されました。BSD Makeは、UNIX以外のシステムでもmakeを使用してソフトウェアをビルドすることを可能にしました。多くのUNIX系オペレーティングシステムで採用され、特にBSD系のUNIX(FreeBSD、OpenBSD、NetBSDなど)で広く使用されています。

BSD MakeとGNU Makeは、それぞれ異なる機能やシンタックスを持ちながら、似た役割を果たしています。両者は異なるコミュニティやプロジェクトで広く使用されており、プロジェクトの要件や好みに応じて選択されます。GNU MakeはGNUプロジェクトの一部であり、BSD MakeはBSDプロジェクトの一部として開発されていますが、両者はそれぞれ独立して普及しています。

ベストプラクティス[編集]

Makefileを効果的に使用するためのベストプラクティスを以下に示します:

シンプルで読みやすい構造
Makefileはシンプルで読みやすい構造を持つべきです。ターゲット、依存関係、コマンドのセクションを明確に分け、コメントを追加して理解しやすくします。
変数とマクロの活用
同じ値が複数の場所で使用される場合は、変数やマクロに格納して再利用します。これにより、コードの重複を避け、変更が容易になります。
デフォルトターゲットの設定
デフォルトのターゲットを設定することで、Makefileを実行するだけで最も一般的なタスクが実行されるようにします。
条件付き実行
特定の条件下でのみ実行されるターゲットやコマンドを定義することで、プラットフォーム間の違いを処理します。
ドキュメントとコメント
Makefile内の各セクションやターゲットには、適切なコメントを追加して機能や目的を説明します。これにより、他の開発者が理解しやすくなります。
クリーンアップのルールを含める
不要なファイルやビルドアーティファクトを削除するクリーンアップルールをMakefileに含めることで、ビルド環境を整理し、不要なファイルを削除できます。

これらのベストプラクティスに従うことで、Makefileを効果的に使用し、ソフトウェア開発プロセスをスムーズにします。

脚註[編集]