Linux基礎 #9 - Makefiles

Last Edited: 1/17/2025

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

DevOps

Cプログラマへの道 #14 - モジュール化では、 オブジェクトファイルとヘッダーファイルを使用してコードをモジュール化する方法について説明しました。 これには複数の手順でファイルをコンパイルする必要があります。コンパイルやビルドを簡単で効率的に行うために、 これらの手順をビルドシステムで自動化することができます。

バッシュスクリプト

コンパイルを管理する1つの方法は、バッシュスクリプトを使用することです。以下のように、 コードファイルをコンパイルするためのすべてのコマンドを含むシェルスクリプトを作成できます。

build.sh
#! /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をビルドできます。 ただし、すべてのコマンドがハードコードされており、コードの変更が困難であるため、このビルドシステムは理想的ではありません。 たとえば、コンパイラを変更する場合、各コマンドを手動で編集する必要があります。この問題を改善するために、次のように変数とロジックを使用できます。

build.sh
#! /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です。

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による依存関係のチェック、より管理しやすいファイル構造を取り入れています。

Makefile
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を反復処理し、DOPERATIONを適用します。 $(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)の記事を読むことで学べます。

リソース