このブログ記事では、LinuxにおけるMakeの基礎を紹介します。

Cプログラマへの道 #14 - モジュール化では、 オブジェクトファイルとヘッダーファイルを使用してコードをモジュール化する方法について説明しました。 これには複数の手順でファイルをコンパイルする必要があります。コンパイルやビルドを簡単で効率的に行うために、 これらの手順をビルドシステムで自動化することができます。
バッシュスクリプト
コンパイルを管理する1つの方法は、バッシュスクリプトを使用することです。以下のように、 コードファイルをコンパイルするためのすべてのコマンドを含むシェルスクリプトを作成できます。
#! /bin/bash
gcc -c -o LinkedList.c LinkedList.o
gcc -c -o main.c main.o
gcc -o bin LinkedList.o main.o
./build.sh
を実行することで、すべてのコマンドを一度に実行して実行ファイルbin
をビルドできます。
ただし、すべてのコマンドがハードコードされており、コードの変更が困難であるため、このビルドシステムは理想的ではありません。
たとえば、コンパイラを変更する場合、各コマンドを手動で編集する必要があります。この問題を改善するために、次のように変数とロジックを使用できます。
#! /bin/bash
CC=gcc
BINARY=bin
declare -a CFILES=(
[0]=LinkedList.c
[1]=main.c
)
declare -a OBJECTS=(
[0]=LinkedList.o
[1]=main.o
)
for i in ${!CFILES[@]}
do
$CC -c -o ${CFILES[$i]} ${OBJECTS[$i]}
done
$CC -o $BINARY ${OBJECTS[@]}
変数とforループを使用することで、コードを簡単に修正できます。しかし、この方法でも依然として理想的なビルドシステムではありません。 なぜなら、変更が加えられたファイルやそれに依存するファイルだけを再コンパイルすることができないからです。このようなロジックを実装するにはさらに複雑なコードが必要となり、 コードベースが大きくなるにつれて実現が困難になります。
Make
Makeは、ビルドプロセスを自動化するためのビルドシステムツールで、依存関係を簡単に管理できるようにします。 Linuxを含むUnix系オペレーティングシステムで広く使用されており、開発者が定義したMakefileを使用して動作します。 Makefileには、依存関係に関するルールが記述されています。以下は、同じプロジェクトの例となるMakefileです。
CC=gcc
CFILES=main.c LinkedList.c
OBJECTS=main.o LinkedList.o
BINARY=bin
all:$(BINARY)
$(BINARY):$(OBJECTS)
$(CC) -o $@ $^
%.o:%.c
$(CC) -c -o $@ $^
clean:
rm -rf $(BINARY) *.o
Makefileでは変数を定義し、$()
を使って参照できます。make
を実行すると、デフォルトでall
ルールを探し、
右側にある依存関係とその下に記載されたコマンドを使用します。コマンドが指定されていない場合、Makeは次に進み、
依存関係BINARY
を作成するルールを探します。このルールでは、すべてのOBJECTS
を作成し、それをコマンドに渡します。
$@
は左側の変数、$^
は右側の変数を表します。そのため、コマンドはgcc -o bin main.o LinkedList.o
に変換されます。
しかし、これらのOBJECTS
はまだ作成されていないため、Makeは次にオブジェクトの作成に関するルールを確認します。
このルールでは、任意のオブジェクトファイル%.o
を作成するには、%.c
を使用してgcc -c -o %.o %.c
を実行する必要があると指定されています。
ディレクトリ内に既に%.c
ファイルが存在する場合、Makeはこのコマンドを実行して必要なオブジェクトをすべて作成し、
それらを上記のルールに渡して実行ファイルを作成します。最後のclean
ルールは、バイナリとすべてのオブジェクトファイルを削除します。
このルールはmake clean
を実行することで利用できます。Makeを使用すると、依存関係に関するルールを簡単に記述できるだけでなく、
ルールに基づいて変更されたファイルとそれに依存するファイルのみを再コンパイルすることも可能です。またclean
のようにビルドに関連する操作に関するルールを設定することもできます。
ヘッダーファイル
Makeは、依存関係ルールにリストされた%.c
ファイルの変更を監視し、再コンパイルが必要なファイルを判断できます。
しかし、ヘッダーファイル%.h
の変更を検出することはできません。ヘッダーファイルの変更を反映させるためには、
.d
拡張子を持つ依存ファイルを作成する必要があります。以下のMakefileは、GCCを使用した依存ファイルの作成と、
それらを使用したMakeによる依存関係のチェック、より管理しやすいファイル構造を取り入れています。
BINARY=bin
CODEDIRS=. lib # `.c`ファイルのディレクトリ
INCDIRS=. ./include/ # `.h`ファイルのディレクトリ
CC=gcc
DEPFLAGS=-MP -MD # `.d`ファイル作成のためのフラグ
CFLAGS=$(foreach D, $(INCDIRS), -I$(D)) $(DEPFLAGS)
# `-I<directory_name>` :`.d`ファイル作成に用いられるヘッダーファイルの場所を指定するためのフラグ
CFILES=$(foreach D, $(CODEDIRS), $(wildcard $(D)/*.c)) # 正規表現で全ての`.c`ファイルのパスを検索
OBJECTS=$(patsubst %.c, %.o, $(CFILES))
DEPFILES=$(patsubst %.c, %.d, $(CFILES))
all:$(BINARY)
$(BINARY):$(OBJECTS)
$(CC) -o $@ $^
%.o:%.c
$(CC) -c -o $@ $^
clean:
rm -rf $(BINARY) *.o
-include $(DEPFILES) # 依存関係を`.d`ファイルを使用し含める
$(foreach D, DIRS, OPERATION(D))
は、DIRS
を反復処理し、D
にOPERATION
を適用します。
$(wildcard REGEX)
は、正規表現REGEX
に一致するすべてのパスを検索します。
$(patsubst %.c, %.o, FILES)
は、FILES
内の左側のパターンに一致するファイルを右側の形式に置き換えます。
これらの操作に加え、GCCのフラグ-I
、-MP -MD
、-include
を使用することで、
ヘッダーファイルに基づいて.d
ファイルを作成しすべての依存情報を反映できます。
メインソースコードをルートディレクトリに、すべてのヘッダーファイルをinclude
サブディレクトリに、
モジュールをlib
サブディレクトリに配置し、上記のMakefileを使用して、
プロジェクトに加えられた如何なる変更を反映する自動化されたビルドを利用できます。
結論
この記事では、CのコンテキストでMakeとMakefileの基本を取り上げましたが、
任意のプログラミング言語におけるプロジェクトのビルドに、GitやDockerなどの他のDevOpsツールと組み合わせて使用することも可能です。
また、他の開発者がルールを理解できるようにするためのhelp
ルールを追加することもできます。
これについては、以下に引用したSteenssone, S. (2023)の記事を読むことで学べます。
リソース
- Parmer, G. 2020. Makefiles: 95% of what you need to know. YouTube.
- Steenssone, S. 2023. Make your Makefile user-friendly: Create a custom ‘help’ target. Medium.