您的位置:首页 > 运维架构 > Linux

Makefile学习笔记

2016-01-15 10:40 766 查看
概述
1 前言

2 准备

3 简单的Makefile

4 Visualize

5 g选项

变量与函数
1 变量

2 wildcard notdir patsubst

3 隐含规则

4 自动化变量

自动依赖
1 问题

2 多条规则匹配

3 解决方案

通用Makefile
1 自己写的Makefile

2 Github上的通用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,可以看到效果:



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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  makefile linux