1. 概述
1.1 前言
之前在Linux下写C/C++都是直接输命令行,虽然有使用make的经历,但没有自己动手写过Makefile。最近看一些开源项目代码,突然对Makefile很感兴趣,于是花了几天时间学习和实验,将心得整理在此,便于以后深入。
学习过程中主要是参考了《跟我一起写Makefile》和GenericMakefile。
1.2 准备
使用Ubuntu 14.04,make版本为3.81,g++版本为4.8.2。在test目录下新建circle.h, square.h两个头文件,circle.cpp, square.cpp, test.cpp三个源文件,每个文件内容如下:
Circle.h
#ifndef __CIRCLE__H__
#define __CIRCLE__h__
#define PI 3.14
class Circle {
public:
Circle(void);
};
#endif
Circle.cpp
#include <iostream>
#include <cstdlib>
#include "circle.h"
using namespace std;
Circle::Circle(void) {
cout << "Circle" << endl;
}
Square.h
#ifndef __SQUARE__H__
#define __SQUARE__H__
class Square {
public:
Square(void);
};
#endif
Square.cpp
#include <iostream>
#include <cstdlib>
#include "square.h"
using namespace std;
Square::Square(void) {
cout << "Square" << endl;
}
test.cpp
#include <iostream>
#include <cstdlib>
#include "circle.h"
#include "square.h"
using namespace std;
int main() {
Circle c;
Square s;
cout << PI << endl;
return 0;
}
定义了circle和square两个简单的类,以及一个宏PI,在test中简单测试。
1.3 简单的Makefile
直接看一个简单粗暴易理解的Makefile:
test: circle.o square.o test.o
@echo "Linking .o files"
g++ -o test circle.o square.o test.o
circle.o: circle.cpp circle.h
@echo "compiling circle.o"
g++ -c circle.cpp
square.o: square.cpp square.h
@echo "compiling square.o"
g++ -c square.cpp
test.o: test.cpp circle.h square.h
@echo "compiling test.o"
g++ -c test.cpp
.PHONY: clean
clean:
-rm *.o
-rm test
概括地讲,Makefile里定义了一系列规则,每条规则由目标、依赖和命令三部分组成,比如在关于circle.o的规则里,circle.o是目标,circle.h和circle.cpp是依赖,@echo "compiling circle.o"和g++ -c circle.cpp是命令。
make的核心是通过比较目标文件和依赖文件的时间戳,决定是否执行命令,可以说展开来就是一个if-else结构。当目标文件不是比所有依赖文件都要“新”的时候,才需要执行命令。还是以circle.o那条规则举例,第一次运行时,circle.o不存在,于是执行g++ -c circle.cpp创建circle.o;之后运行时,若circle.h和circle.cpp都没被修改,那它们都比circle.o要“旧”,没必要重新生成circle.o。
此外关于Makefile的一些零碎知识点:
- 每条规则前面都要用tab缩进
- 第一条规则的目标是“终极目标”,也就是直接执行make时默认使用的规则,比如此处就是test
- 关于@:用echo xxx会输出“echo xxx”,用@echo xxx才会会出“xxx”
- 关于-:删除不存在的文件会出错导致make终止,前面加上-表示忽略可能的错误
- clean并不是目标文件,而是希望make执行清除操作;通过.PHONY把clean标记成伪目标,避免了当前目录下真的有文件clean时,由于没有更“新”的依赖文件,导致清除操作不执行
输入make和make clean,可以看到效果:
1.4 Visualize
用图形来思考的话,Makefile里定义了一棵表示文件依赖关系的树,目标文件相当于parent node,依赖文件相当于许多child node。要求parent node的最后修改时间晚于所有child node的最后修改时间,不满足这个条件时就需要执行命令,重新修正这棵树。
1.5 g++选项
g++编译选项非常多,这里只记录目前用到的:
- -c 只激活预处理、编译和汇编,生成.o结尾的obj文件
- -o 输出文件
- -I 后面加头文件搜索目录
- -MM 生成文件关联信息
- -MMD 类似于-MM,但将输出导入到同名的.d文件里
-c、-o、-I都很熟悉,-MM、-MMD有些陌生,动手试一试就知道了。
使用-MM时输入test.cpp,输出编译目标test.o的依赖文件,没有新文件生成。
使用-MMD输入test.cpp,依赖文件信息会输入到自动创建的文件test.d中,这里用了-c是因为单用-MMD时g++编译后还会尝试链接,所以用-c告诉g++只进行编译。不过这里即使不用-c,虽然会报错,但test.d文件还是会正常创建的。
注意-MM和-MMD输出的内容和Makefile里的“目标: 依赖”部分格式是完全相同的,之后会用到这个性质。
2. 变量与函数
2.1 变量
Makefile里可以定义变量,使用时用$(变量)获得变量的值,比如定义变量:
TARGET = test
OBJS = test.o circle.o square.o
CXX = g++
那么使用变量的规则:
$(TARGET): $(OBJS)
$(CXX) -o $(TARGET) $(OBJS)
就相当于:
test: test.o circle.o square.o
g++ -o test test.o circle.o square.o
2.2 wildcard notdir patsubst
Makefile支持通配符, *.h和 *cpp分别表示所有的头文件和源文件,但是规则里不能这么写,需要展开成具体形式。对此Makefile提供了wildcard函数,wildcard返回已经存在的、使用空格分开的、匹配此模式的所有文件列表,比如:
SRCS = $(wildcard *.cpp)
则SRCS的值就是“circle.cpp square.cpp test.cpp"。
类似的可以得到所有.h文件,但是make第一次执行时还没有.o文件,要怎么给OBJS赋值呢?此时可以用patsubst函数,patsubst起到替换的作用,比如:
SRCS = $(wildcard *.cpp)
OBJS = $(patsubst %.cpp, %.o, $(SRCS))
则OBJS的值就是“circle.o square.o test.o”。
最后,notdir的作用就是去掉目录信息,使得文件列表里只有文件名。
2.3 隐含规则
其实到这里为止,需要时再查点资料,对于日常的自娱自乐已经足够hack出够用的Makefile了。想把事情做得更加优雅,可以使用隐含规则。
SRCS = $(wildcard *.cpp)
OBJS = $(patsubst %.cpp, %.o, $(SRCS))
TARGET = test
CXX = g++
$(TARGET): $(OBJS)
$(CXX) -o $(TARGET) $(OBJS)
circle.o: circle.cpp circle.h
square.o: square.cpp square.h
test.o: test.cpp
.PHONY: clean
clean:
-rm *.o
-rm $(TARGET)
这里circle.o、square.o和test.o三条规则都只定义了目标和依赖,而没有写命令,但是这个时候Makefile可以正常工作。因为make能自动推导出一些简单的规则,比如用.cpp文件生成.o文件。
另外需要注意,不写命令和空命令是不同的,具体来说:
SRCS = $(wildcard *.cpp)
OBJS = $(patsubst %.cpp, %.o, $(SRCS))
TARGET = test
CXX = g++
$(TARGET): $(OBJS)
$(CXX) -o $(TARGET) $(OBJS)
circle.o: circle.cpp circle.h ;
square.o: square.cpp square.h ;
test.o: test.cpp ;
.PHONY: clean
clean:
-rm *.o
-rm $(TARGET)
执行make会报错,因为空命令相当于明确地告诉make,不希望使用隐含规则。
2.4 自动化变量
实际项目里文件之间的依赖关系非常复杂,手工维护每条规则的话实在无法愉快地玩耍,这时候可以把部分工作交给程序,Makefile里最主要的自动化变量是:
- $@ 规则的目标文件名
- $^ 规则的依赖文件列表
- $< 规则的第一个依赖文件
直接来看使用自动化变量的Makefile例子:
test: circle.o square.o test.o
$(CXX) -o $@ $^
%.o: %.cpp
$(CXX) -c $<
%是Makefile规则里使用的通配符,相当于 *,所以这里一条“%.o: %.cpp”的规则相当于“circle.o: circle.cpp”、“square.o: square.cpp”、“test.o: test.cpp”三条规则,非常省事。
具体地说,在上面的两条规则里,$@是"test", $^是"circle.o square.o test.o",$<是具体规则对应的.cpp文件,比如circle.o,$<就是circle.cpp。
3. 自动依赖
3.1 问题
其实上面那个Makefile是有问题的,单有“%.o: %.cpp”的模式规则是不够的:
%.o: %.cpp
$(CXX) -c $<
显然的,当.h文件更新而.cpp文件未更新时,.o文件不会更新。
比较naive的解决方案是直接在依赖里添加头文件:
HDR = $(wildcard *.h)
%.o: %.cpp $(HDR)
$(CXX) -c $<
但这种方法的问题是修改一个.h,所有的.o文件都会被波及。比如只修改circle.h,运行make时与circle.h无关的square.o也会重新生成。
3.2 多条规则匹配
在给出解决方案之前,我们首先岔开一下,研究一下多条规则同时匹配时,make是如何处理的,修改刚才的Makefile为:
HDRS = $(wildcard *.h)
SRCS = $(wildcard *.cpp)
OBJS = $(patsubst %.cpp, %.o, $(SRCS))
TARGET = test
CXX = g++
CXXFLAGS =
$(TARGET): $(OBJS)
$(CXX) -o $(TARGET) $(OBJS)
circle.o: circle.cpp circle.h
@echo "using specific rule"
$(CXX) -c circle.cpp
%.o: %.cpp
@echo "using generic rule"
$(CXX) -c $< $
.PHONY: clean
clean:
-rm *.o
-rm $(TARGET)
要生成circle.o,既可以使用具体规则,也可以使用模式规则,此时make会如何选择呢?
可以看到,make选择了具体规则。事实上,3.81以下版本的make会使用第一条匹配的规则,以上的make会优先匹配具体规则,所以现在这种写法能保证circle.o总是用具体规则生成。
这个问题在Stack Overflow上也有讨论:
http://stackoverflow.com/questions/11455182/when-multiple-pattern-rules-match-a-target
3.3 解决方案
这样就能得到比较满意的方案了:
HDRS = $(wildcard *.h)
SRCS = $(wildcard *.cpp)
OBJS = $(patsubst %.cpp, %.o, $(SRCS))
DEPS = $(patsubst %.cpp, %.d, $(SRCS))
TARGET = test
CXX = g++
$(TARGET): $(OBJS)
$(CXX) -o $(TARGET) $(OBJS)
-include $(DEPS)
%.o: %.cpp
$(CXX) -MMD -c $<
.PHONY: clean
clean:
-rm *.o
-rm *.d
-rm *.gch
-rm $(TARGET)
include的作用是包含文件,这里就是那些.d文件的内容。
运行make,结果正常:
修改circle.h里定义的PI为3.1415,再次运行make:
比较两张图可以看到,与circle.h无关的square.o没有被重新创建。
另外,.gch文件是为了编译器为了提高速度而设计的文件,clean的时候需要一并删除,否则可能干扰正常编译。
4. 通用Makefile
4.1 自己写的Makefile
边学边写,自己做了一个通用的,多目录情况下自动生成依赖的Makefile,还是挺有成就感的。
假设头文件放在HDR_DIR下,源文件放在SRC_DIR下,在BIN_DIR下生成可执行文件,并创建链接文件TARGET。
TARGET = main
BIN_NAME = main
HDR_DIR = ./include
SRC_DIR = ./src
OBJ_DIR = ./obj
BIN_DIR = ./bin
CXX = g++
CXXFLAGS = -g -Wall
HDRS = $(wildcard $(HDR_DIR)/*.h)
SRCS = $(wildcard $(SRC_DIR)/*.cpp)
OBJS = $(patsubst %.cpp, $(OBJ_DIR)/%.o, $(notdir $(SRCS)))
DEPS = $(patsubst %.o, %.d, $(OBJS))
.PHONY: all
all: dir $(TARGET)
$(TARGET): $(BIN_DIR)/$(BIN_NAME)
-ln -s $(BIN_DIR)/$(BIN_NAME) $(TARGET)
$(BIN_DIR)/$(BIN_NAME): $(OBJS)
$(CXX) $(CXXFLAGS) $^ -o $@
-include $(DEPS)
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.cpp
$(CXX) $(CXXFLAGS) -I $(HDR_DIR) -c -MMD $< -o $@
.PHONY: dir
dir:
-mkdir $(OBJ_DIR)
-mkdir $(BIN_DIR)
.PHONY: print
print:
@echo HDRS = $(HDRS)
@echo SRCS = $(SRCS)
@echo OBJS = $(OBJS)
@echo DEPS = $(DEPS)
.PHONY: clean
clean:
-rm -r $(OBJ_DIR)
-rm -r $(BIN_DIR)
-rm $(TARGET)
4.2 Github上的通用Makefile
GenericMakefile提供了功能强大的C/C++项目Makefile,只需要修改很少一部分信息就可以用在各种项目里,非常值得阅读。
GenericMakefile:https://github.com/mbcrawfo/GenericMakefile
5. 学习总结
这里顺便记录一下自己的学习过程:
- 感性认识Makefile
- Makefile解决了什么问题?——编译自动化
- Makefile里有什么?——规则和变量
- 粗略了解make工作原理,写最简单的Makefile
- 怎样定义最简单的规则?——目标、依赖、命令
- 怎样使用变量?——直接定义,使用时加$取值
- 何时执行命令?——比较目标和依赖的时间戳
- 借助make完成特定操作?——定义伪目标
- 使用高级特性
- 获取文件列表?——wildcard与patsubst
- 处理目录信息?——notdir
- 怎样少写些规则?——隐含规则与模式规则
- 使用自动化变量?——$@、$^、$<
- 实现自动依赖
- 为什么需要自动依赖?——避免手工维护代码依赖关系
- 怎样生成自动依赖?——g++生成依赖文件,include引入Makefile
- 看各种项目的Makefile,重点阅读GenericMakefile
- 头文件与源文件在不同目录下?——用g++的-I参数增加头文件搜索目录
- 处理多平台等复杂情形?——使用ifeq-else-endif结构
最后,初次使用markdown编辑器,感觉相当好用。
6. 参考资料
- 跟我一起写Makefile:wiki.ubuntu.org.cn/跟我一起写Makefile
- GenericMakefile:https://github.com/mbcrawfo/GenericMakefile