1. make编译简介
上一讲里介绍了用vscode的tasks.json和launch.json来配置C++程序的编译和调试。在vscode出现以前,Linux环境下是如何配置程序的自动编译呢?如果单靠手工编译链接,随着项目规模增大,会发现代码文件越来越多,依赖关系越来越复杂,手工编译链接会耗时且易出错。传统的方法是使用make工具自动编译。我们需要像配置tasks.json那样配置make的脚本Makefile。
2. Makefile文件基本结构
一个Makefile文件可分为四个区域,如下图所示。
这里先额外说明一下本人的项目组织习惯,在项目目录下会有source、resource、library、include、build五个目录。source目录下放置.c、.cpp代码源文件;resource目录下放置资源文件;library目录下放置第三方链接库文件;include目录下放置.h头文件;build目录用于编译时放置.o中间文件。Makefile文件和最终生成的可执行文件都放置在项目根目录下。
2.1 变量声明区
在这个区域,通过赋值语句,声明一些变量,比如源文件列表SRC、中间文件列表OBJS、目标文件TARGET(即编译后得到的可执行文件)、编译器CC等。
SRC_DIR = ./source
SRC = $(wildcard $(SRC_DIR)/*.cpp)
OBJS_DIR = ./build
OBJS = $(patsubst %.cpp,$(OBJS_DIR)/%.o,$(notdir $(SRC)))
TARGET = $(notdir $(shell pwd))
CC = g++
- $(wildcard source/*.cpp)是将通配符展开的函数,展开后会得到指定目录下指定类型文件的路径列表。
- $(patsubst %.cpp,%.o,路径列表)是对路径列表进行模式替换的函数,第一个参数%.cpp是原模式,第二个参数%.o是替换后的模式,第三个参数是路径列表,可以用$(变量名)的方式获取前面设定的路径列表。
- $(notdir 路径列表)是获取路径中最后一个/之后内容的函数,如果路径列表是文件列表,则获得文件名列表,如果路径是目录,则获得目录名。
- $(addprefix 前缀, 文件名列表)是对文件名添加前缀的函数,前缀是文件所要存放的路径。
- $(shell pwd)是执行shell命令pwd,该命令返回Makefile文件的所在路径(即项目根目录路径)。
2.2 编译参数区
在这个区域也是声明一些变量,这些变量是用于在后面进行编译时,附加在编译指令后的参数。其中INCLUDE是在编译时指定头文件搜索路径的-I参数;LIBS是在链接时指定第三方链接库所在路径和需要链接的库名的-L和-l参数。
CFLAGS = -w -O -g -std=c++0x -fpermissive -mfloat-abi=hard -mfpu=neon -mtune=cortex-a9
INCLUDE = -I./include
LIBS = -L./library -lOpenCL -lSPIRV_viv -lCLC -lVSC -lGAL
2.3 编译链接区
Makefile中通过xx:xx来指明依赖关系,冒号前是目标,冒号后是依赖。这种依赖关系可以构建一棵树形图,从树根到树叶依次递归。
2.3.1 超级根all:$(TARGET) run.sh
all:$(TARGET) run.sh
有时候为了做一些除了编译链接以外的工作,我们需要添加一个额外的超级根节点all。make指令会默认从第一条依赖关系开始执行,即从这个超级根开始执行。all:$(TARGET) run.sh,即根节点all下有两个分支,一个是$(TARGET),一个是run.sh。这两个分支的工作是并列关系,没有依赖关系,它们分别在下面进行定义。
2.3.2 分支run.sh
run.sh:
@echo "export LD_LIBRARY_PATH=./library\n./OpenCL"> run.sh
@chmod 777 run.sh
这里我们创建一个对可执行文件进行执行的脚本run.sh,因为该可执行文件的运行依赖于一个第三方动态链接库,所以在执行前需要将链接库所在目录导入系统的环境变量,并修改run.sh的可执行权限。
2.3.3 链接分支和其所依赖的编译分支
$(TARGET):$(OBJS)
@echo Linking $(TARGET)...
@$(CC) -rdynamic -o $@ $^ $(LIBS)
$(OBJS_DIR)/%.o:$(SRC_DIR)/%.cpp
@echo Compiling $<
@$(CC) $(CFLAGS) $(INCLUDE) -c $< -o $@
$(TARGET)分支依赖于$(OBJS),$(OBJS)进一步依赖于$(SRC)。
$(TARGET): $(OBJS)定义了链接指令,$(OBJS): $(SRC)定义了编译指令。
- @符号是静默执行的意思,其后跟着shell指令,若没有@,在make时会看到终端会输出命令本身,再输出命令结果。
- g++的编译指令中-o后跟着输出路径,-c是只编译不链接,后面跟着需要编译的源文件。编译时通过前面构造好的CFLAGS和INCLUDE引入编译参数和头文件搜索目录,链接时通过前面构造好的LIBS引入第三方动态库。
- $@指代本条依赖关系中的目标对象$(TARGET)。
- $^指代本条依赖关系中的所有依赖对象$(OBJS)。
- $<指代本条依赖关系中的第一个依赖对象。
- %.o和%.cpp是模式规则,类似通配符展开,避免逐条编写依赖。
2.4 其它功能区
.PHONY:clean print
clean:
@rm run.sh $(TARGET) $(OBJS)
print:
@echo $(SRC)
@echo $(OBJS)
@echo $(TARGET)
make进行编译链接时,会通过检查依赖文件和输出文件的最后修改时间来判断是否需要重新编译或链接。因此,我们有时候需要手工清理所生成的中间文件,并进行重新生成。这时候,可以定义一个伪依赖.PHONY超级根(如果不定义该超级根,也可直接定义clean等执行清理的根,但可能会出现与同名文件冲突的情况)。
在执行make时,添加clean参数,则可执行清理工作;添加print参数则可打印一些变量,用来调试Makefile本身。