🐷

Makefileの書き方

2020/09/26に公開
2

c/c++のコンパイルの定義を記述するMakefileの書き方を説明します。
Qiitaで執筆したMakefileの書き方 (初心者向け)を一部改変した内容になります。

初心者向けにわかりやすく書いたつもりです。Makefileの書き方がわからない人はぜひ参考にしてください。

そもそもMakefileって?

c/c++のコンパイルに必要なコマンド、ソースコード、オプション、依存関係などを書き込んだファイルです。
このファイルを使って、makeコマンドを実行することで、コンパイルを実施します。

Makefileにはこんな利点があります。

  • 一度書いてしまえばコンパイルの手間が圧倒的に減りミスも少なくなる (特に、大きなプログラムであれば、この恩恵は大きい)
  • 共通のMakefileを用意することで、開発者間でコンパイルのルールが統一できる

Makfileの書き方

ソースコードSample.cppをコンパイルするためのMakeifleのサンプルを書いてみます。

# (1)コンパイラ
CC  = g++
# (2)コンパイルオプション
CFLAGS    =
# (3)実行ファイル名
TARGET  = Sample
# (4)コンパイル対象のソースコード
SRCS    = Sample.cpp
# (5)オブジェクトファイル名
OBJS    = $(SRCS:.cpp=.o)
 
# (6)インクルードファイルのあるディレクトリパス
INCDIR  = -I../inc
 
# (7)ライブラリファイルのあるディレクトリパス
LIBDIR  = 
 
# (8)追加するライブラリファイル
LIBS    = 

# (9)ターゲットファイル生成
$(TARGET): $(OBJS)
	$(CC) -o $@ $^ $(LIBDIR) $(LIBS)
	
# (10)オブジェクトファイル生成
$(OBJS): $(SRCS)
	$(CC) $(CFLAGS) $(INCDIR) -c $(SRCS)

# (11)"make all"で make cleanとmakeを同時に実施。
all: clean $(OBJS) $(TARGET)
# (12).oファイル、実行ファイル、.dファイルを削除
clean:
	-rm -f $(OBJS) $(TARGET) *.d

上記のサンプルについて、3つのブロックに分けて説明していきます。

  1. Makeに必要な情報
  2. ターゲットの生成
  3. Make実行オプション

1. Makeに必要な情報

Makeに必要な情報を記載してきます。コンパイルの下準備的な感じです。サンプルの1-19行目に当たります。

(1)コンパイラ

使用するコンパイラを記載します。今回は、gccコンパイラを使っています。
ただし、C++をコンパイルするため表記はg++です。

(2)コンパイルオプション

コンパイルに使用するオプションです。
追加することで「使用できる機能」や「コンパイル時に出力される警告」などを制御することができます。
例えば、以下のようなオプションがあります。

  • -Wall : コンパイル時の警告をすべて表示する。
  • -O2 : コードの最適化レベル

(3)実行ファイル名

実行ファイルの名前をつけます。好きな名前をつけてください。

(4)コンパイル対象のソースコード

コンパイル対象のソースコードを指定します。
今回は、1ファイルしか指定していませんが、ソースファイルが複数ある場合は複数指定してください。複数指定する場合の例。


SRCS    = hoge1.cpp
SRCS    += hoge2.cpp
SRCS    += hoge3.cpp

(5)オブジェクトファイル名

オブジェクトファイルの名称を定義します。
ソースファイル名と同一のオブジェクトファイルを作ることが多いです。

(6)インクルードファイルのあるディレクトリパス

参照するインクルードファイルが存在するパスを指定します。
インクルードファイル名は記載不要です。

(7)ライブラリファイルのあるディレクトリパス

OSSなどライブラリを指定する必要がある場合は、そのライブラリが存在するパスを指定します。

# 記述例
-L/usr/local/lib -L/lib/x86_64-linux-gnu/

サンプルプログラムでは、ライブラリは使わないため省略しています。

(8)追加するライブラリファイル

追加するライブラリファイルを指定します。

# 記述例
LIBS = -lpthread -lprocps

なお、上記のライブラリは、実際には以下のファイル名で保存されています。
libpthread.so.0, libprocps.so.7 (※ バージョンによって、末尾の数字は異なる。)
ライブラリのファイル名を直接書くわけではない点に注意してください。

サンプルプログラムでは、ライブラリは使わないため省略しています。

2. ターゲットの生成

ここで言うターゲットとは「TARGET」と「OBJS」のことを指します。
「1.」で示した情報を形成して、オブジェクトファイルと実行ファイルの生成ルールを記述する箇所になります。
サンプルの21-27行目に当たります。

(9)ターゲットファイル生成

以下の2行の構成でターゲットファイルの生成ルールを記述しています。


$(TARGET): $(OBJS)
	$(CC) -o $@ $^ $(LIBDIR) $(LIBS)

1行目は、$(TARGET): $(OBJS)と記載して、「TARGET」が「OBJS」に依存することを示しています。
2行目は、コマンド行と言います。リンク対象のライブラリを指定します。
今回は、リンクするライブラリは存在しないため、「OBJS」との依存関係のみ管理することになります。

(10)オブジェクトファイル生成

(9)で依存する「OBJS」の生成ルールです。オブジェクトファイルは、ソースと機械語の中間ファイルに当たるため、ソースコードに依存します。基本的な記載ルールは、(9)と同じです。

3. Make実行オプション

実は、「1.」、「2.」だけでコンパイルはできるのですが、makeのルールをオプションとして自由に定義することが可能です。
こうすることで、より効率よくコンパイルができるようになります。
サンプルでは、よく指定される、2つ例を示します。
サンプルの29-33行目に当たります。

(11)"make all"で make cleanとmakeを同時に実施。

タイトルのとおり、(12)のcleanとmakeコマンドを同時に実行してくれるallオプションを作りました。
make allとうつだけで、前のビルド時に生成したファイルを削除したうえで、再ビルドを実行してくれます。

(12).oファイル、実行ファイル、.dファイルを削除

ビルドで生成したファイルを削除します。
削除対象は、オブジェクトファイル、実行ファイル、そしてデフォルトで生成される依存関係ファイルを削除してくれます。

どうやって使うのか?

上で書いたサンプルのMakefileを実際に使ってコードをコンパイルしてみます。
また、冒頭で記載したMakefileの利点が伝わるように、「Makeifleを『使わない』コンパイル」と「Makeifleを『使う』コンパイル」の2つを比較してみます。

【再掲】Makefileの利点

  • 一度書いてしまえばコンパイルの手間が圧倒的に減りミスも少なくなる (特に、大きなプログラムであれば有るほどこの恩恵は大きい)
  • 共通のMakefileを用意することで、開発者間でコンパイルのルールが統一できる

準備

サンプルとして、Hello world.を出力するプログラムをコンパイルします。

#include <iostream>

using namespace std;

int main(){
  cout << "Hello world." << endl;
  return 0;
}

ディレクトリ階層は以下のようにします。

HogeProject
├── inc
│   └── ... # 任意
└── src
    ├── Makefile
    └── Sample.cpp

Sample.cppHello world.の実装をしています。

1. Makeifleを『使わない』コンパイル

まずは、Makefileを使わないコンパイルをやってみます。
コンパイル対象のソースであるSample.cppがある階層で以下の通りコマンドを実行する。

$ cd HogeProject/src
$ g++ -std=c++0x  -I../inc -c Sample.cpp; g++ -o Sample Sample.o

プログラムの規模が小さいので、それほど面倒ではありませんが、それでも手打ちでコンパイルするのは、かなりの手間です。

2. Makeifleを『使う』コンパイル

同じことをMakefileを使ってやってみます。
Sample.cppと同階層にMakefileを配置して、以下の通りコマンドを実行しています。

$ cd HogeProject/src
$ make
g++  -I../inc -c Sample.cpp
g++ -o Sample Sample.o  

以上!!

Sampleが実行ファイルになります。

$ ./Sample 
Hello world.

コンパイル結果をクリアしたい場合は、以下のようなコマンドを実行すればよいです。

$ make clean
rm -f Sample.o Sample *.d

まとめ

私は、新卒でMakefileを扱うことになったとき、全くわからずすごく苦労しました。
1つ1つの定義の意味がわからないのもありましたが、「Makefileがなぜ必要で(何が便利で)」というのがわからなず、もやもやしていました。この記事がMakefileの書き方が悩んでいる方の救いになればさいわいです。
不明点等のフィードバックがあれば、ぜひコメントよろしくお願いします。

Discussion

yagiyukiyagiyuki

話題のGPT-4でMakefileを作らせて見たところ、結構いい感じに作れてしまいました。(この記事の価値が,,,ちょっと下がりました)

この記事で共有したSample.cppのコードはちゃんと動かせます。
ただ、incの階層が間違えていたと微妙な点はありますね。
今後、改善されるとは思いますが、まだ人手でコードを改修する必要性はありそうです。