読者です 読者をやめる 読者になる 読者になる

 

Makefileの書き方

目的

C++やCなどコンパイラ型言語を使ってプログラミングをするときは、何度も何度もコンパイルを繰り返します。 ここで、今RaspberryPi(ARM Linuxボード)を使ってプログラムを作成しているとします。 プログラムをホストマシンで組み、クロスコンパイルして実行ファイルをRaspberryPiへと転送します。 このとき、作業では次のコマンドを打ち込みます。

                    gcc-linaro-arm-linux-gnueabihf-g++ -Wall -Wextra -O2 -g -MMD -MP -L/usr/local/lib -I../include -I/usr/local/include -o cmd
                    scp cmd user@192.168.7.2:/usr/bin/
                

これだけを見ると「たったの2行?」と感じるかもしれませんが、コンパイルして実行するたびにこのコマンドを何度も何度も打ち込むのはとても大変で非効率的です。 1回で160文字、10回繰り返すと1600文字、30回繰り返すと4800文字、これだけあれば、ソースコード100行分は優に超える文字数になります。 この無駄な時間を減らすためにあるのが、今回のテーマmakeです。

間違いを恐れずにmakeを大雑把にいうと、シェルスクリプトのコンパイル特化版です。 また、大規模で分割されたプログラムなどで、必要なものだけコンパイルするため高速化を図れます。

では、makeを使うとどのくらい短縮できるのでしょうか? 答えは次のコマンドが表していますね。 makeを使った時に、先ほどと同じ操作をするために打つコマンドです。

                    make
                

「えっ?これだけ?」と思うかもしれません。 その通りです。 makeを使うとこれだけで、コンパイルから転送までが済んでしまいます。 あんな長ったらしいコマンドとはもうおさらばです。

ただし最初からmakeと打つだけでできるわけではありません。 makeを使うためには、コンパイルの設定を書き込んだMakefileが必要になります。 Makefileはその名の通りmakeのファイルで、コンパイラの情報からオプション・依存関係まで、様々なものを書き込んでおくのですが、これがなかなか慣れない書き方をしなければなりません。 今回は、このMakefileを書いて作業の効率を上げる方法を解説していきます。

方法

では、ここからはMakefileの書き方です。

準備

まずは、makeをインストールしてください。

                        # ArchLinux
                        $ sudo pacman -S make

                        # Ubuntu Debian系
                        $ sudo apt-get install make

                        # Fedora Redhat系
                        $ sudo yum -y install make
                    

今回はArchLinuxでC++を使って解説しますが、LinuxならUbuntuでもFedoraでも基本的な操作は変わりません。

次に、普段コンパイルに使用しているコマンドを用意してください。 今回は先ほど上げた2行のコマンドとします。

オプションなどについては別の回で解説します。

書き方

まずは、先に完成したMakefileです。

                            CROSS   := ~/Program/cross_gcc/gcc-linaro-arm-linux-gnueabihf-raspbian-x64/bin/arm-linux-gnueabihf

                            ifneq ($(CROSS),)
                                CROSS_PREFIX	:= $(CROSS)-
                            endif

                            CXX		 = $(CROSS_PREFIX)g++
                            CXXFLAGS = -Wall -Wextra -O2 -g -MMD -MP
                            LDFLAGS	 =
                            LIBS	 = -L/usr/local/lib
                            INCLUDE	 = -I../include -I/usr/local/include
                            BIN_DIR  = ../bin/
                            TARGET 	 = $(BIN_DIR)$(shell basename `readlink -f ..`)
                            OBJDIR	 = ./obj
                            ifeq "$(strip $(OBJDIR))" ""
                                OBJDIR = .
                            endif
                            SOURCES	:= $(wildcard *.cpp)
                            OBJECTS	 = $(addprefix $(OBJDIR)/,$(SOURCES:.cpp=.o))
                            DEPENDS	 = $(OBJECTS:.o=.d)

                            all: $(BIN_DIR) $(TARGET)

                            $(TARGET): $(OBJECTS)
                                $(CXX) -o $@ $^ $(LDFLAGS)

                            $(OBJDIR)/%.o:%.cpp
                                @[ -d $(OBJDIR) ] || mkdir -p $(OBJDIR)
                                $(CXX) $(CXXFLAGS) $(INCLUDE) -o $@ -c $<

                            $(BIN_DIR):
                                mkdir -p $(BIN_DIR)

                            clean:
                                $(RM) -r $(OBJECTS) $(DEPENDS) $(TARGET) $(BIN_DIR) `readlink -f $(OBJDIR)`

                            upload: $(BIN_DIR) $(TARGET)
                                scp $(TARGET) root@192.168.7.2:/usr/bin/


                            -include $(DEPENDS)
                        

20行目までが、変数などの定義、22行目からがコンパイルなどの操作をしています。

変数にはファイル名などを"="や":="などの演算子で代入することができます。
変数を使用する際には$(変数名)のようにして使用します。

変数の役割は以下の通りです。

変数名役割
CROSS クロスコンパイラへのパス(最後の"-gcc" "-g++"は除いて代入)
CXX コンパイラの名前("$(CROSS)-g++"が代入される。CROSSが空の場合は"g++"のみ)
CXXFLAGS コンパイルオプション(複数の場合はスペース区切りで列挙)
LDFLAGS  
LIBS ライブラリへのパス
INCLUDE インクルードするファイルへのパス
BIN_DIR 作成する実行ファイルを保存するフォルダ名
TARGET 作成する実行ファイルの名前(今回は上の階層のディレクトリ名を取得)
OBJDIR コンパイル・リンク中に生成されるファイルの一時保存場所
SOURCES コンパイルするソースファイル名(今回は .cppのファイル)
OBJECTS コンパイル・リンクで生成されるファイル名
DEPENDS  

変数を定義したら、それらを用いてコンパイルなどの操作を行います。

操作のためには依存関係を記述します。書き方は以下のようになります。

                            ターゲット名: 依存ファイル名
                                コマンド
                        

このようなブロックのものを複数記述することができます。

では、ブロックが複数あるときには、どのブロックが実行されるのでしょうか?
makeでは、ターミナルで"make"のみを実行した時にはターゲット名が"all"のものが実行され、"make ターゲット名"のように指定すると、指定したターゲット名を持つブロックが実行されます。
今回のmakefileでは"make"を実行すると22行目"all"ブロック(コマンドなし)が、"make upload"を実行すると37行目"upload"ブロックが実行されます。

では、ターゲット名について見ていきましょう。
ターゲット名には、基本的にはそのブロックを実行すると生成されるファイル名を記述します。
31行目"$(BIN_DIR)"はこのブロックを実行すると、"$(BIN_DIR)"を生成するということがわかります。
"all" "upload" "clean"などのように、生成するファイル名でないものをターゲット名にすることもできます。 ただし、このとき"all"は先述のように特別な意味を持ちます。

次に、依存ファイルについて見ていきましょう。
ターゲット名のコロンを挟んだうしろにあるのが依存ファイルです。 スペースつなぎで複数記述することができます。 依存ファイル名の部分には、そのブロックを実行するために必要なファイル(依存ファイル)名を記述します。
22行目"all"を見てみましょう。 このブロックを実行するためには$(BIN_DIR)と$(TARGET)の2つが必要だということがわかります。 makeはこの情報を元に、現在$(BIN_DIR)と$(TARGET)の2つがあるかどうかをチェックして、2つともある場合にはそのブロックを実行します。
では、どちらか一方、もしくは両方がなかった場合はどのようになるのでしょうか?
makeは依存ファイルが欠けているときは、そのファイルを生成してから現在のブロックを実行します。 つまり、今回$(BIN_DIR)がなかったとすると、まずは$(BIN_DIR)を生成しようとします。 この時に呼び出されるのが、不足ファイルと同じターゲット名を持つブロックです。 今回は$(BIN_DIR)ブロックが該当します。
これで、無事$(BIN_DIR)が生成されると、元のブロックに戻ってそのブロックを実行します。

このようにして、makeは不足ファイルがあるとそのファイルを生成しながらブロックを実行していきます。 これのようにしてmakeは動作していきます。

流れ

では、基本的な書き方を学んだところで、今回のMakefileの大まかな流れを見てみましょう。

まず、"make"を実行すると"all"ブロックに入ります。 依存関係にある2つのファイルは生成されていないため、"$(BIN_DIR)"ブロックと"$(TARGET)"ブロックが呼び出されます。
"$(TARGET)"ブロックでは$(OBJECTS)を依存ファイルとしているため、"$(OBJECTS)/%.o"ブロックが呼び出されます。 "$(OBJECTS)/%.o"ブロックでは%.cppを依存ファイルとしていますが、このファイルはすでに作成しているプログラムファイルのため、依存関係が満たされてコマンドが実行されます。
これにより、$(OBJECTS)が生成されるため、"$(TARGET)"ブロックが実行されます。
これにより、$(TARGET)が生成されるため、"all"ブロックが実行されますが、コマンドがないためここで終了します。
"all"ブロックでは処理が行われませんでしたが、"$(TARGET)" "$(OBJECTS/%.o"ブロックでコンパイル・リンクがされたため、実行ファイルが生成されました。

これが、このMakefileのおおまかな流れです。

おわりに

軽いmakeについての説明をさせていただきましたが、いかがでしたでしょうか?
わからないことなどがありましたら、ご質問だくさい。

2015/6/29