Makefile学习笔记

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,可以看到效果:

最简单的Makefile

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

使用-MM时输入test.cpp,输出编译目标test.o的依赖文件,没有新文件生成。

使用-MMD

使用-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. 参考资料

  1. 跟我一起写Makefile:wiki.ubuntu.org.cn/跟我一起写Makefile
  2. GenericMakefile:https://github.com/mbcrawfo/GenericMakefile

推荐阅读更多精彩内容