ソフトウェアまわりの備忘録

研究室で培ったソフトウェアの知識を貯めておく備忘録。

多段makeの利用(第三回)

ソースファイルが別階層にある場合

前回の記事ではMMDオプションにより依存関係を自動解決する方法を示した。しかし、「ソースファイルは同じ階層(ディレクトリ)に存在する」ことを仮定していた。Makefileで違う階層にあるファイル達をコンパイルするにはどうすればいいのか。以下のツリー構造を考えてみる。
.
├── Makefile
├── dirA
│   └── sample1.cpp
├── dirB
│   └── sample2.cpp
├── dirC
│   ├── dirD
│   │   └── sample4.cpp
│   └── sample3.cpp
├── include
│   ├── Test.hpp
│   └── header.h
└── main.cpp

ちなみにこの階層図はtreeというコマンドで作成した(参照:[Linux] ディレクトリ構成図作るのに便利だよ tree コマンド | バシャログ。)。各ファイルの中身は"sample○ is executed."と画面表示する関数が書いてある。main.cpp内でsample1()~sample4()まで順に呼び出している。各ソースファイルと実行結果は一番下に示す。

このときのMakefileを手動で書いてあげると、

GCC    = g++
CFLAGS = -O2 -MMD -Wall -Wextra
INCLUDE= ./include
SRCS   = main.cpp dirA/sample1.cpp dirB/sample2.cpp dirC/sample3.cpp dirC/dirD/sample4.cpp
TARGET = main
OBJS   = $(SRCS:.cpp=.o)
DEPS   = $(SRCS:.cpp=.d)
TILDE  = $(SRCS:.cpp=.cpp~)

.cpp.o:
	$(GCC) $(CFLAGS) -c $< -o $@ -I$(INCLUDE)

$(TARGET): $(OBJS)
	$(GCC) $(CFLAGS) -o $@ $+

default: $(OBJS) $(TARGET)

clean:
	$(RM) $(OBJS) $(DEPS) $(TILDE) $(TARGET)

-include $(DEPS)

となる。前回のMakefilのSRCSという変数にディレクトリ名を含めて直接追加すればよい。しかしファイルの移動、名前変更、ファイルの増加でここに全て書くのは限界がある。そこで、

各階層に階層内にあるファイルリストが書かれたMakefileを置く

これが多段makeと呼ばれるものである。各階層にMakefileを配置してカレントにあるMakefileが最終的にそれらを取り込む。ファイルのリストが分散することで管理が容易になる。

カレントディレクトリのMakefileの変更点

以下の項目を変更する。

  • SRCSをグローバル関数へ(「SRCS=」を「SRC:=」に)
  • Makefileが配置してあるディレクトリの指定
  • 配置してあるMakefileの取り込み(include)

である。これらを踏まえてSRCSの部分を以下のように変更
カレントディレクトリのMakefileのSRCS部分(他は変更なし)

SRCS   :=
REL    := dirA/
include $(REL)Makefile
REL    := dirB/
include $(REL)Makefile
REL    := dirC/
include $(REL)Makefile
SRCS+=main.cpp

変数:=にしないとうまくいかない(純拡張変数と呼ぶらしい:GNU make 日本語訳(Coop編) - makeの実行方法)。グローバル変数になるのか?どうしてできなくなるかまだ分かってない(判明したら追加修正)。変数RELにより配置先を指定し、includeする。

配置先のMakefileの作成

配置先のMakefileは変数SRCSにファイルの一覧を+=で追加していけばいい(override変数と呼ぶらしい:GNU make 日本語訳(Coop編) - makeの実行方法)。以下のようになる。DirAのMakefileはこれだけ書いてあるだけ(DirB、DirDも同様)
DirAのMakefile

SRCS+=\
	$(REL)sample1.cpp\

「SRCS+=$(REL)ファイル名」とすればいい。今回は一個しかないが複数個存在する場合は下に足していけばいい。また、DirCはDirD内のMakefileを取り込むようにもう一段階includeを書く
DirCのMakefile

SRCS+=\
$(REL)sample3.cpp\

REL:=$(REL)dirD/
include $(REL)Makefile

注意するのは1行目の「$(REL)ディレクトリ名」であること。カレントディレクトリからのパスにならないといけない。

多段makeについて補足「-C」オプション

今回の多段make、includeによりサブディレクトリのMakefileを取り込んで行ったが

make -C dirA

というように「-C」オプションを使って実行する方法もある。includeと-Cオプションの違いは大きく二つある。

このオプションを使うと、別階層のMakefileを動かすことになり相対パスが変わるということ。includeした場合、「includeしているMakfileの階層から見たパス」になるのに対して、make -Cは「-Cで移動した先にあるMakefileの階層から見たパス」になる。私はmake -Cを使うと相対パスが変わってしまうのが面倒なのでincludeを使った。また、-Cで指定されるMakefileは完全に独立していなければいけない。つまりmakeのルールや変数、ターゲットをまた別に書かなければいけない。階層ごとに独立したプログラムであればこの方式を取ったほうがいい。私は各階層に置くMakefileはファイルリストだけを並べた最小限のものにしたかったためincludeを使った手法を紹介した。

残りの問題点

このMakefileはまだ改善の余地がある。それは

  • 中間ファイルが散らかる(*.dとか*.oとかファイル数が多くなるとコンパイル後すごくみにくい)
  • 複数のMakefileを書き換える必要がある(できるだけ変更箇所は少なくしたい)

このうち次回は関数を使って、自動的にファイルの追加を検知するMakefileを作る。目標はいかにMakefileを変更せずに自動化できるか、コンパイル後の中間ファイルの整理である。

ソースファイルと実行結果

実行結果

sample1 is executed.
sample2 is executed.
sample3 is executed.
sample4 is executed.

main.cpp

#include "header.h"
#include "Test.hpp"

int main(void){

  Test obj;

  obj.sample1();
  obj.sample2();
  obj.sample3();
  obj.sample4();

  return 0;
}

Makefile

GCC    = g++
CFLAGS = -O2 -MMD -Wall -Wextra
INCLUDE= ./include

SRCS   :=
REL    := dirA/
include $(REL)Makefile
REL    := dirB/
include $(REL)Makefile
REL    := dirC/
include $(REL)Makefile
SRCS+=main.cpp


TARGET = main
OBJS   = $(SRCS:.cpp=.o)
DEPS   = $(SRCS:.cpp=.d)
TILDE  = $(SRCS:.cpp=.cpp~)

.cpp.o:
	$(GCC) $(CFLAGS) -c $< -o $@ -I$(INCLUDE)

$(TARGET): $(OBJS)
	$(GCC) $(CFLAGS) -o $@ $+

default: $(OBJS) $(TARGET)

clean:
	$(RM) $(OBJS) $(DEPS) $(TILDE) $(TARGET)

-include $(DEPS)

header.h

#include <iostream>

Test.hpp

#include "header.h"

class Test{
public:
  void sample1();
  void sample2();
  void sample3();
  void sample4();
};

dirA/sample1.cpp

#include "Test.hpp"

void Test::sample1(void){
  std::cout<<"sample1 is executed."<<std::endl;
}

(sample2~sample4も表示が違うだけで中身は同様)