快速编写一个可复用的Makefile
前言
本文将从以下几个方面展开阐述:第一,分析项目的依赖关系;第二,写项目的Makefile编译项目;第三,优化Makefile文件。读懂这篇文章前提是得了解C/C++编译过程:C/C++编译过程
一、项目分析
1. 目录结构和源码
- source
- main.cpp
- Time.cpp
- Time.h
- Makefile
main.cpp 代码如下(示例):
// main.cpp
#include "Time.h"
int main(int argc, char *argv[])
{
Time time;
time.printTime();
return 0;
}`
Time.h 代码如下(示例):
// Time.h
#ifndef TIME_H
#define TIME_H
class Time
{
private:
int m_nHour;
int m_nMinute;
int m_nSecond;
public:
Time();
Time(int hour, int minute, int second);
~Time();
void printTime();
};
#endif
Time.cpp 代码如下(示例):
// Time.h
#include <iostream>
#include "Time.h"
Time::Time()
{
}
Time::Time(int hour, int minute, int second) : m_nHour(hour), m_nMinute(minute), m_nSecond(second)
{
}
Time::~Time()
{
}
void Time::printTime()
{
std::cout << m_nHour << ":" << m_nMinute << ":" << m_nSecond << std::endl;
}
由于源码过于简单,这里不再赘述。
2. 分析依赖关系
使用G++ -MM 可以查看依赖关系,所以目标main.o 依赖于 main.cpp 和 Time.h;Time.o依赖于Time.cpp Time.h。
psy@ubuntu:~/Desktop/test/source$ g++ -MM main.cpp
main.o: main.cpp Time.h
psy@ubuntu:~/Desktop/test/source$ g++ -MM Time.cpp
Time.o: Time.cpp Time.h
二、编译项目
1. 使用显示规则
分析完项目的依赖关系之后就可以写Makefile了,我们先用显示规则写一个最简单的Makefile。后边逐步优化:
代码如下(示例):
# 第一版Makefile
main.exe: main.o Time.o
g++ -o main.exe main.o Time.o
main.o: main.cpp Time.h
g++ -o main.o -c main.cpp
Time.o: Time.cpp Time.h
g++ -o Time.o -c Time.cp
执行make main.exe 效果如下:
psy@ubuntu:~/Desktop/test/source$ make main.exe
g++ -o main.o -c main.cpp
g++ -o Time.o -c Time.cpp
g++ -o main.exe main.o Time.o
最终生成了可执行文件main.exe,此时完成了简单项目的编译。
2. 伪目标
编译完成后,当前目录下多了main.exe,main.o,Time.o三个文件:
psy@ubuntu:~/Desktop/test/source$ ls
main.cpp main.exe main.o Makefile Time.cpp Time.h Time.o
那么如何实现大型项目make clean的清理工作呢,接下来我们稍微改一下Makefile:
# 第一版Makefile
main.exe: main.o Time.o
g++ -o main.exe main.o Time.o
main.o: main.cpp Time.h
g++ -o main.o -c main.cpp
Time.o: Time.cpp Time.h
g++ -o Time.o -c Time.cpp
clean:
rm *.exe *.o:w
执行 make clean
psy@ubuntu:~/Desktop/test/source$ make clean
rm *.exe *.o
psy@ubuntu:~/Desktop/test/source$ ls
main.cpp Makefile Time.cpp Time.h
psy@ubuntu:~/Desktop/test/source$
可以看到此时多出的.o文件和.exe已被删除,我们接下来在当前目录下新创建一个名字为clean的文件,再执行make clean:
psy@ubuntu:~/Desktop/test/source$ touch clean
psy@ubuntu:~/Desktop/test/source$ make clean
make: 'clean' is up to date.
显然,我们执行make clean的目的是做目标文件的清理工作,但是make却不这么认为。把clean当做了一个目标文件处理。那么我们如何指定让make做清理工作呢,指定clean为伪目标:
# 第一版Makefile
.PHONY:clean #伪目标
main.exe: main.o Time.o
g++ -o main.exe main.o Time.o
main.o: main.cpp Time.h
g++ -o main.o -c main.cpp
Time.o: Time.cpp Time.h
g++ -o Time.o -c Time.cpp
clean:
rm *.exe *.o
此时再执行make clean,就和我们期望的结果一致
psy@ubuntu:~/Desktop/test/source$ make clean
rm *.exe *.o
2. 普通变量
假设现在这个项目的源文件是C语言的,那么Makefile是不是需要把g++改为gcc呢,答案是肯定的。Makefile引入变量可以更方便我们维护和管理项目,接下来在Makefile中使用多个变量:
# 第二版Makefile 引入变量
.PHONY:clean #伪目标
CC = gcc
CXX = g++
RM = rm
RM_FLAGS = -rf
main.exe: main.o Time.o
$(CXX) -o main.exe main.o Time.o
main.o: main.cpp Time.h
$(CXX) -o main.o -c main.cpp
Time.o: Time.cpp Time.h
$(CXX) -o Time.o -c Time.cpp
clean:
$(RM) $(RM_FLAGS) *.exe *.o
效果和前面的一致,只是更加方便维护。注意变量的赋值有三种方式:第一种 = 是递归扩展;第二种 := 覆盖赋值;第三种,?= 如果已经定义那么不再赋值。尤其要注意第一种:
foo = $(bar)
bar = $(ugh)
ugh = Huh
echo $(foo)
$(foo)变量的值是Huh,所以变量在赋值时一定要注意细节。
2. 自动变量
仔细观察Makefile的规则,有一个共同点。都是目标:依赖,新行TAB键跟语句:
Time.o: Time.cpp Time.h
$(CXX) -o Time.o -c Time.cpp
那么语句: $(CXX) -o Time.o -c Time.cpp 的-o后面跟的目标文件和-c后面跟的依赖文件是不变的,所以可以使用自动变量来完成目标和依赖的处理。
# 第三版Makefile 引入自动变量
.PHONY:clean #伪目标
CC = gcc
CXX = g++
RM = rm
RM_FLAGS = -rf
main.exe: main.o Time.o
$(CXX) -o $@ $^
main.o: main.cpp Time.h
$(CXX) -o $@ -c $<
Time.o: Time.cpp Time.h
$(CXX) -o $@ -c $<
clean:
$(RM) $(RM_FLAGS) *.exe *.o
这里只介绍三个常用的自动变量,需要用到的时候再去查资料。$@:目标,$^:所有依赖,$<第一个依赖文件。
2. 常用函数
假设一个项目有成百上千个源文件。如果目标和依赖都手动去写。那工作量将不堪设想,有没有一个函数能获取当前目录下的所有源文以及根据源文件得到目标文件?我们再把Makefile文件修改一下:
# 第三版Makefile 引入自动变量
.PHONY:clean #伪目标
CC = gcc
CXX = g++
RM = rm
RM_FLAGS = -rf
SOURCES = $(wildcard *.cpp) # 获取当前目录下的所有.cpp文件名
OBJS = $(patsubst %.cpp,%.o, $(SOURCES)) # 根据.cpp文件得到对用的.o文件
main.exe: $(OBJS)
$(CXX) -o $@ $^
%.o: %.cpp
$(CXX) -o $@ -c $^
clean:
$(RM) $(RM_FLAGS) *.exe *.o
引入 wildcard 函数和 patsubst 函数之后就不必人为的去写明目标和依赖,这里介绍几个常用的函数:
$(wildcard *.cpp) # 获取当前目录下的所有.cpp文件名
$(patsubst %.cpp,%.o, $(SOURCES)) # 根据.cpp文件得到对用的.o文件
$(filter %.cpp, $^) # 过滤$^得到以.cpp后缀的文件
$(filter-ou %.cpp, $^) # 过滤$^中所有以.cpp后缀的文件
$(strip $^) # 去除$^中多余的空格
三、优化Makefile
1. 模块化编译
大型项目一般都是分模块编译,这样更方便维护项目。将每个模块编译为独立的一个库(静态库/动态库)。最后链接为可执行文件。我们把项目分为两个模块00_Main和01_Time,目录结构如下所示:
-
source
- 00_Main
- main.cpp
- Makefile
- 01_Time
- Time.cpp
- Time.h
- Makefile
源文件的内容和依赖关系没发生变化,只是放到了不同的路径下。那么接下来我们分析一下这个项目如何编译。首先把00_Main编译为lib00_Main.so,再把01_Time编译为lib01_Time.so。最后链接为可执行文件。接下来我们修改一下两个模块中的Makefile:
- 00_Main
# 第四版Makefile 共享库
DEBUG = n
MKDIR = mkdir
MKDIR_FLAGS = -p
RM = rm
RM_FLAGS = -rf
CXX = g++
CXX_FLAGS_R = -fPIC -shared -O3 -DNDEBUG -fexceptions -fnon-call-exceptions
CXX_FLAGS_D = -fPIC -shared -O0 -g3 -fexceptions -fnon-call-exceptions
SO_NAME = 00_Main # 动态库名称
ROOT_PATH = ../..
INTERMEDIATE_DIR_R = $(ROOT_PATH)/build/Release/$(SO_NAME)
INTERMEDIATE_DIR_D = $(ROOT_PATH)/build/Debug/$(SO_NAME)
INCLUDE_DIR = -I$(ROOT_PATH)/source/01_Time -I$(ROOT_PATH)/source/00_Main
SOURCES = $(wildcard *.cpp) # 获取当前目录下的所有.cpp文件名
OBJS = $(patsubst %.cpp,%.o, $(SOURCES)) # 根据.cpp文件得到对用的.o文件
lib$(SO_NAME).so: $(OBJS)
ifeq (y, $(DEBUG))
cd $(INTERMEDIATE_DIR_D) && $(CXX) $(CXX_FLAGS_D) -o $@ $^ -Wl,-E
else
cd $(INTERMEDIATE_DIR_R) && $(CXX) $(CXX_FLAGS_R) -o $@ $^ -Wl,-E
endif
%.o: %.cpp
ifeq (y, $(DEBUG))
$(MKDIR) $(MKDIR_FLAGS) $(INTERMEDIATE_DIR_D) && $(CXX) $(CXX_FLAGS_D) -c -fmessage-length=0 $(INCLUDE_DIR) \
-MMD -MP -MF"$(patsubst %.o,$(INTERMEDIATE_DIR_D)/%.d, $@)" -MT"$(patsubst %.o,$(INTERMEDIATE_DIR_D)/%.d, $@)" \
-o $(INTERMEDIATE_DIR_D)/$@ $<
else
$(MKDIR) $(MKDIR_FLAGS) $(INTERMEDIATE_DIR_R) && $(CXX) $(CXX_FLAGS_R) -c -fmessage-length=0 $(INCLUDE_DIR) \
-MMD -MP -MF"$(patsubst %.o,$(INTERMEDIATE_DIR_R)/%.d, $@)" -MT"$(patsubst %.o,$(INTERMEDIATE_DIR_R)/%.d, $@)" \
-o $(INTERMEDIATE_DIR_R)/$@ $<
endif
上面展示的Makefile是00_Main下的,同理01_Time下的Makefile也替换成这个。但贡献库的名称改为 SO_NAME = 01_Time # 动态库名称。这个Makefile可以复用,项目增加模块只需要拷贝到相同的目录下,改一下SO_NAME即可。
psy@ubuntu:~/Desktop/test/build/Release/00_Main$ ls
lib00_Main.so main.d main.o
psy@ubuntu:~/Desktop/test/build/Release/01_Time$ ls
lib01_Time.so Time.d Time.o
进入00_Main目录和01_Time目录分别运行make之后,可以看到已经创建了lib00_Main.so以及lib01_Time.so。
2. Debug和Realse版本
为了节省时间,上面的Makefile也实现了这个功能,把DEBUG = n改为DEBUG = y可以生成调试版本的动态库。调试版本和发布版本,实际上就是编译参数不同。
CXX_FLAGS_R = -fPIC -shared -O3 -DNDEBUG
CXX_FLAGS_D = -fPIC -shared -O0 -g3
3. 链接生成可执行文件
四、总结
提示:这里对文章进行总结:
本文仅仅简单介绍了Makefile的一些简单使用方法。