20-22 - 打造专业的编译环境
---- 整理自狄泰软件唐佐林老师课程
文章目录
- 1. 大型项目的编译(无第三方库)
- 1.1 大型项目的目录结构(无第三方库)
- 1.2 项目结构设计分析
- 1.3 需要打造的编译环境
- 1.4 解决方案设计
- 2. 第 1 阶段任务
- 2.1 关键的实现要点
- 2.2 模块 makefile 中的构成
- 2.3 实验
- 3. 第 2 阶段任务
- 3.1 关键的实现要点
- 3.2 开发中的经验假设
- 3.3 解决方案设计
- 3.4 makefile 中嵌入 shell 的 for 循环
- 3.5 工程 makefile 中的关键构成
- 3.6 链接时的注意事项
- 3.7 实验
- 4. 优化
- 4.1 问题 1
- 4.2 问题 2
- 4.3 关键问题
- 4.4 工程 makefile 的重构
1. 大型项目的编译(无第三方库)
1.1 大型项目的目录结构(无第三方库)
1.2 项目结构设计分析
- 项目被划分为不同模块
- 每个模块的代码用一个文件夹进行管理
文件夹由 inc、src、makefile 构成 - 每个模块的对外函数声明统一放置于 common/inc 中
如:common.h、xxxfunc.h
- 每个模块的代码用一个文件夹进行管理
1.3 需要打造的编译环境
- 源码文件夹在编译时不能被改动(只读文件夹)
- 在编译时自动创建文件夹(build)用于存放编译结果
- 编译过程中能够自动生成依赖关系,自动搜索需要的文件
- 每个模块可以拥有自己独立的编译方式
- 支持调试版本的编译选项
1.4 解决方案设计
- 第 1 阶段:将每个模块中的代码编译成静态库文件
- 第 2 阶段:将每个模块的静态库文件链接成最终可执行程序
2. 第 1 阶段任务
- 完成可用于各个模块编译的 makefile 文件
- 每个模块的编译结果为静态库文件(.a 文件)
2.1 关键的实现要点
- 自动生成依赖关系(
gcc -MM
) - 自动搜索需要的文件(
vpath
) - 将目标文件打包为静态库文件(
ar crs
)
2.2 模块 makefile 中的构成
2.3 实验
.PHONY : all
# 声明all为伪目标(PHONY),避免与同名文件冲突。
DIR_BUILD := /home/wx/uuxiang/makefile/20_22/00/build
DIR_COMMON_INC := /home/wx/uuxiang/makefile/20_22/00/common/inc
# 定义编译输出目录和公共头文件目录的路径。
DIR_SRC := src
DIR_INC := inc
# 定义源文件目录和头文件目录的路径。
TYPE_INC := .h
TYPE_SRC := .c
TYPE_OBJ := .o
TYPE_DEP := .dep
# 定义不同文件类型的扩展名,包括头文件(.h)、源文件(.c)、目标文件(.o)、依赖文件(.dep)。
AR := ar
ARFLAGS := crs
# 定义生成静态库的工具和其使用的参数。
CC := gcc
CFLAGS := -I$(DIR_INC) -I$(DIR_COMMON_INC)
# 定义编译器为gcc,CFLAGS包含了头文件搜索路径。
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
# 如果变量DEBUG为true,则添加-g选项用于生成调试信息。
MODULE := $(realpath .)
MODULE := $(notdir $(MODULE))
DIR_OUTPUT := $(addprefix $(DIR_BUILD)/, $(MODULE))
# 获取当前目录的绝对路径,将模块名设置为当前目录的名称,并定义模块的输出目录。
OUTPUT := $(MODULE).a
OUTPUT := $(addprefix $(DIR_BUILD)/, $(OUTPUT))
# 定义最终生成的静态库文件名,并设置输出路径。
SRCS := $(wildcard $(DIR_SRC)/*$(TYPE_SRC))
OBJS := $(SRCS:$(TYPE_SRC)=$(TYPE_OBJ))
OBJS := $(patsubst $(DIR_SRC)/%, $(DIR_OUTPUT)/%, $(OBJS))
DEPS := $(SRCS:$(TYPE_SRC)=$(TYPE_DEP))
DEPS := $(patsubst $(DIR_SRC)/%, $(DIR_OUTPUT)/%, $(DEPS))
# 查找源文件,生成目标文件和依赖文件列表,并将它们从源目录映射到输出目录。
vpath %$(TYPE_INC) $(DIR_INC)
vpath %$(TYPE_INC) $(DIR_COMMON_INC)
vpath %$(TYPE_SRC) $(DIR_SRC)
# 设置文件搜索路径,vpath用于在指定目录中查找特定类型的文件。
-include $(DEPS)
# 包含所有生成的依赖文件,如果依赖文件不存在则跳过(-表示忽略错误)。
all : $(OUTPUT)
@echo "Success! Target ==> $(OUTPUT)"
# 定义默认目标all,生成最终的静态库,并打印成功信息。
$(OUTPUT) : $(OBJS)
$(AR) $(ARFLAGS) $@ $^
# 规则:生成静态库,将目标文件归档为静态库。
$(DIR_OUTPUT)/%$(TYPE_OBJ) : %$(TYPE_SRC)
$(CC) $(CFLAGS) -o $@ -c $(filter %$(TYPE_SRC), $^)
# 规则:编译源文件生成目标文件。
$(DIR_OUTPUT)/%$(TYPE_DEP) : %$(TYPE_SRC)
@echo "Creating $@ ..."
@set -e; \
$(CC) $(CFLAGS) -MM -E $(filter %$(TYPE_SRC), $^) | sed 's,\(.*\)\.o[ :]*,$(DIR_OUTPUT)/\1$(TYPE_OBJ) $@ : ,g' > $@
# 规则:生成依赖文件。通过gcc的-MM选项生成依赖信息,并使用sed命令调整格式,将生成的依赖信息重定向到依赖文件中。
3. 第 2 阶段任务
- 完成编译整个工程的 makefile 文件
- 调用模块 makefile 编译生成静态库文件
- 链接所有模块的静态库文件,最终得到可执行程序
3.1 关键的实现要点
- 如何自动创建 build 文件夹以及子文件夹?
- 如何进入每一个模块文件夹进行编译?
- 编译成功后如何链接所有模块静态库?
3.2 开发中的经验假设
项目中的各个模块在设计阶段就已经基本确定,因此,在之后的开发过程中不会频繁随意的增加或减少
3.3 解决方案设计
- 定义变量保存模块名列表(模块名变量)
- 利用 shell 中的 for 循环遍历模块名变量
- 在 for 循环中进入模块文件夹进行编译
- 循环结束后链接所有的模块静态库文件
3.4 makefile 中嵌入 shell 的 for 循环
- 注意事项:
makefile 中嵌入 shell 代码时,如果需要使用 shell 变量的值,必须在变量名前加上$$
(例如:$$dir
)
3.5 工程 makefile 中的关键构成
3.6 链接时的注意事项
- gcc 在进行静态库链接时必须遵循严格的依赖关系
gcc -o app.out x.a y.a z.a
其中的依赖关系必须为:x.a–>y.a,y.a–>z.a
默认情况下遵循自左向右的依赖关系
- 如果不清楚库间的依赖,可以使用 -Xlinker 自动确定依赖关系
gcc -o app.out -Xlinker "-("z.a y.a x.a -Xlinker "-)"
3.7 实验
- common/makefile:
.PHONY : all
# 声明all为伪目标(PHONY),避免与同名文件冲突。
DIR_BUILD := /home/wx/uuxiang/makefile/20_22/02/build
DIR_COMMON_INC := /home/wx/uuxiang/makefile/20_22/02/common/inc
# 定义编译输出目录和公共头文件目录的路径。
DIR_SRC := src
DIR_INC := inc
# 定义源文件目录和头文件目录的路径。
TYPE_INC := .h
TYPE_SRC := .c
TYPE_OBJ := .o
TYPE_DEP := .dep
# 定义不同文件类型的扩展名,包括头文件(.h)、源文件(.c)、目标文件(.o)、依赖文件(.dep)。
AR := ar
ARFLAGS := crs
# 定义生成静态库的工具和其使用的参数。
CC := gcc
CFLAGS := -I$(DIR_INC) -I$(DIR_COMMON_INC)
# 定义编译器为gcc,CFLAGS包含了头文件搜索路径。
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
# 如果变量DEBUG为true,则添加-g选项用于生成调试信息。
MODULE := $(realpath .)
MODULE := $(notdir $(MODULE))
DIR_OUTPUT := $(addprefix $(DIR_BUILD)/, $(MODULE))
# 获取当前目录的绝对路径,将模块名设置为当前目录的名称,并定义模块的输出目录。
OUTPUT := $(MODULE).a
OUTPUT := $(addprefix $(DIR_BUILD)/, $(OUTPUT))
# 定义最终生成的静态库文件名,并设置输出路径。
SRCS := $(wildcard $(DIR_SRC)/*$(TYPE_SRC))
OBJS := $(SRCS:$(TYPE_SRC)=$(TYPE_OBJ))
OBJS := $(patsubst $(DIR_SRC)/%, $(DIR_OUTPUT)/%, $(OBJS))
DEPS := $(SRCS:$(TYPE_SRC)=$(TYPE_DEP))
DEPS := $(patsubst $(DIR_SRC)/%, $(DIR_OUTPUT)/%, $(DEPS))
# 查找源文件,生成目标文件和依赖文件列表,并将它们从源目录映射到输出目录。
vpath %$(TYPE_INC) $(DIR_INC)
vpath %$(TYPE_INC) $(DIR_COMMON_INC)
vpath %$(TYPE_SRC) $(DIR_SRC)
# 设置文件搜索路径,vpath用于在指定目录中查找特定类型的文件。
-include $(DEPS)
# 包含所有生成的依赖文件,如果依赖文件不存在则跳过(-表示忽略错误)。
all : $(OUTPUT)
@echo "Success! Target ==> $(OUTPUT)"
# 定义默认目标all,生成最终的静态库,并打印成功信息。
$(OUTPUT) : $(OBJS)
$(AR) $(ARFLAGS) $@ $^
# 规则:生成静态库,将目标文件归档为静态库。
$(DIR_OUTPUT)/%$(TYPE_OBJ) : %$(TYPE_SRC)
$(CC) $(CFLAGS) -o $@ -c $(filter %$(TYPE_SRC), $^)
# 规则:编译源文件生成目标文件。
$(DIR_OUTPUT)/%$(TYPE_DEP) : %$(TYPE_SRC)
@echo "Creating $@ ..."
@set -e; \
$(CC) $(CFLAGS) -MM -E $(filter %$(TYPE_SRC), $^) | sed 's,\(.*\)\.o[ :]*,$(DIR_OUTPUT)/\1$(TYPE_OBJ) $@ : ,g' > $@
# 规则:生成依赖文件。通过gcc的-MM选项生成依赖信息,并使用sed命令调整格式,将生成的依赖信息重定向到依赖文件中。
- main/makefile:
.PHONY : all
# 声明all为伪目标(PHONY),避免与同名文件冲突。
DIR_BUILD := /home/wx/uuxiang/makefile/20_22/02/build
DIR_MAIN_INC := /home/wx/uuxiang/makefile/20_22/02/main/inc
# 定义编译输出目录和公共头文件目录的路径。
DIR_SRC := src
DIR_INC := inc
# 定义源文件目录和头文件目录的路径。
TYPE_INC := .h
TYPE_SRC := .c
TYPE_OBJ := .o
TYPE_DEP := .dep
# 定义不同文件类型的扩展名,包括头文件(.h)、源文件(.c)、目标文件(.o)、依赖文件(.dep)。
AR := ar
ARFLAGS := crs
# 定义生成静态库的工具和其使用的参数。
CC := gcc
CFLAGS := -I$(DIR_INC) -I$(DIR_MAIN_INC)
# 定义编译器为gcc,CFLAGS包含了头文件搜索路径。
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
# 如果变量DEBUG为true,则添加-g选项用于生成调试信息。
MODULE := $(realpath .)
MODULE := $(notdir $(MODULE))
DIR_OUTPUT := $(addprefix $(DIR_BUILD)/, $(MODULE))
# 获取当前目录的绝对路径,将模块名设置为当前目录的名称,并定义模块的输出目录。
OUTPUT := $(MODULE).a
OUTPUT := $(addprefix $(DIR_BUILD)/, $(OUTPUT))
# 定义最终生成的静态库文件名,并设置输出路径。
SRCS := $(wildcard $(DIR_SRC)/*$(TYPE_SRC))
OBJS := $(SRCS:$(TYPE_SRC)=$(TYPE_OBJ))
OBJS := $(patsubst $(DIR_SRC)/%, $(DIR_OUTPUT)/%, $(OBJS))
DEPS := $(SRCS:$(TYPE_SRC)=$(TYPE_DEP))
DEPS := $(patsubst $(DIR_SRC)/%, $(DIR_OUTPUT)/%, $(DEPS))
# 查找源文件,生成目标文件和依赖文件列表,并将它们从源目录映射到输出目录。
vpath %$(TYPE_INC) $(DIR_INC)
vpath %$(TYPE_INC) $(DIR_MAIN_INC)
vpath %$(TYPE_SRC) $(DIR_SRC)
# 设置文件搜索路径,vpath用于在指定目录中查找特定类型的文件。
-include $(DEPS)
# 包含所有生成的依赖文件,如果依赖文件不存在则跳过(-表示忽略错误)。
all : $(OUTPUT)
@echo "Success! Target ==> $(OUTPUT)"
# 定义默认目标all,生成最终的静态库,并打印成功信息。
$(OUTPUT) : $(OBJS)
$(AR) $(ARFLAGS) $@ $^
# 规则:生成静态库,将目标文件归档为静态库。
$(DIR_OUTPUT)/%$(TYPE_OBJ) : %$(TYPE_SRC)
$(CC) $(CFLAGS) -o $@ -c $(filter %$(TYPE_SRC), $^)
# 规则:编译源文件生成目标文件。
$(DIR_OUTPUT)/%$(TYPE_DEP) : %$(TYPE_SRC)
@echo "Creating $@ ..."
@set -e; \
$(CC) $(CFLAGS) -MM -E $(filter %$(TYPE_SRC), $^) | sed 's,\(.*\)\.o[ :]*,$(DIR_OUTPUT)/\1$(TYPE_OBJ) $@ : ,g' > $@
# 规则:生成依赖文件。通过gcc的-MM选项生成依赖信息,并使用sed命令调整格式,将生成的依赖信息重定向到依赖文件中。
- module/makefile:
.PHONY : all
# 声明all为伪目标(PHONY),避免与同名文件冲突。
DIR_BUILD := /home/wx/uuxiang/makefile/20_22/02/build
DIR_MODULE_INC := /home/wx/uuxiang/makefile/20_22/02/module/inc
DIR_COMMON_INC := /home/wx/uuxiang/makefile/20_22/02/common/inc
# 定义编译输出目录和公共头文件目录的路径。
DIR_SRC := src
DIR_INC := inc
# 定义源文件目录和头文件目录的路径。
TYPE_INC := .h
TYPE_SRC := .c
TYPE_OBJ := .o
TYPE_DEP := .dep
# 定义不同文件类型的扩展名,包括头文件(.h)、源文件(.c)、目标文件(.o)、依赖文件(.dep)。
AR := ar
ARFLAGS := crs
# 定义生成静态库的工具和其使用的参数。
CC := gcc
CFLAGS := -I$(DIR_INC) -I$(DIR_MODULE_INC) -I$(DIR_COMMON_INC)
# 定义编译器为gcc,CFLAGS包含了头文件搜索路径。
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
# 如果变量DEBUG为true,则添加-g选项用于生成调试信息。
MODULE := $(realpath .)
MODULE := $(notdir $(MODULE))
DIR_OUTPUT := $(addprefix $(DIR_BUILD)/, $(MODULE))
# 获取当前目录的绝对路径,将模块名设置为当前目录的名称,并定义模块的输出目录。
OUTPUT := $(MODULE).a
OUTPUT := $(addprefix $(DIR_BUILD)/, $(OUTPUT))
# 定义最终生成的静态库文件名,并设置输出路径。
SRCS := $(wildcard $(DIR_SRC)/*$(TYPE_SRC))
OBJS := $(SRCS:$(TYPE_SRC)=$(TYPE_OBJ))
OBJS := $(patsubst $(DIR_SRC)/%, $(DIR_OUTPUT)/%, $(OBJS))
DEPS := $(SRCS:$(TYPE_SRC)=$(TYPE_DEP))
DEPS := $(patsubst $(DIR_SRC)/%, $(DIR_OUTPUT)/%, $(DEPS))
# 查找源文件,生成目标文件和依赖文件列表,并将它们从源目录映射到输出目录。
vpath %$(TYPE_INC) $(DIR_INC)
vpath %$(TYPE_INC) $(DIR_MODULE_INC)
vpath %$(TYPE_INC) $(DIR_COMMON_INC)
vpath %$(TYPE_SRC) $(DIR_SRC)
# 设置文件搜索路径,vpath用于在指定目录中查找特定类型的文件。
-include $(DEPS)
# 包含所有生成的依赖文件,如果依赖文件不存在则跳过(-表示忽略错误)。
all : $(OUTPUT)
@echo "Success! Target ==> $(OUTPUT)"
# 定义默认目标all,生成最终的静态库,并打印成功信息。
$(OUTPUT) : $(OBJS)
$(AR) $(ARFLAGS) $@ $^
# 规则:生成静态库,将目标文件归档为静态库。
$(DIR_OUTPUT)/%$(TYPE_OBJ) : %$(TYPE_SRC)
$(CC) $(CFLAGS) -o $@ -c $(filter %$(TYPE_SRC), $^)
# 规则:编译源文件生成目标文件。
$(DIR_OUTPUT)/%$(TYPE_DEP) : %$(TYPE_SRC)
@echo "Creating $@ ..."
@set -e; \
$(CC) $(CFLAGS) -MM -E $(filter %$(TYPE_SRC), $^) | sed 's,\(.*\)\.o[ :]*,$(DIR_OUTPUT)/\1$(TYPE_OBJ) $@ : ,g' > $@
# 规则:生成依赖文件。通过gcc的-MM选项生成依赖信息,并使用sed命令调整格式,将生成的依赖信息重定向到依赖文件中。
- makefile:
.PHONY : all compile link clean rebuild
# 声明伪目标,避免与同名文件冲突。这些伪目标包括all、compile、link、clean和rebuild。
MODULES := common \
module \
main
# 定义模块列表,这些模块会被单独编译。
MKDIR := mkdir
RM := rm -fr
# 定义用于创建目录和删除文件/目录的命令。
CC := gcc
LFLAGS :=
# 定义C编译器为gcc,LFLAGS用于链接时的额外参数(当前为空)。
DIR_PROJECT := $(realpath .)
DIR_BUILD := build
DIR_BUILD_SUB := $(addprefix $(DIR_BUILD)/, $(MODULES))
MODULE_LIB := $(addsuffix .a, $(MODULES))
MODULE_LIB := $(addprefix $(DIR_BUILD)/, $(MODULE_LIB))
# 定义项目的根目录,构建目录,以及各模块的构建子目录。
# MODULE_LIB用于存储各模块生成的静态库文件名,并加上构建目录前缀。
APP := app.out
APP := $(addprefix $(DIR_BUILD)/, $(APP))
# 定义最终生成的应用程序文件名,并加上构建目录前缀。
all : compile $(APP)
@echo "Success! Target ==> $(APP)"
# 默认目标all,先编译模块,再生成最终的应用程序,并输出成功信息。
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "Begin to compile ..."
@set -e; \
for dir in $(MODULES); \
do \
cd $$dir && $(MAKE) all DEBUG:=$(DEBUG) && cd .. ; \
done
@echo "Compile Success!"
# 编译目标compile,首先创建必要的构建目录,然后遍历每个模块目录,执行`make all`命令来编译每个模块,并传递DEBUG变量。最后输出编译成功信息。
link $(APP) : $(MODULE_LIB)
@echo "Begin to link ..."
$(CC) -o $(APP) -Xlinker "-(" $^ -Xlinker "-)" $(LFLAGS)
@echo "Link Success!"
# 链接目标link,依赖于所有模块的静态库。使用gcc进行链接,生成最终的应用程序,并输出链接成功信息。
# `-Xlinker "-(" $^ -Xlinker "-)"` 是为了确保静态库按顺序链接,防止符号丢失。
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
# 目标:创建构建目录和各模块的子目录。
clean :
@echo "Begin to clean ..."
$(RM) $(DIR_BUILD)
@echo "Clean Success!"
# 清理目标clean,删除整个构建目录,输出清理成功信息。
rebuild : clean all
# 重建目标rebuild,先执行clean再执行all,即先清理再重新编译和链接。
4. 优化
4.1 问题 1
- 所有模块 makefile 中使用的编译路径均为写死的绝对路径,一旦项目文件夹移动,编译必将失败
- 解决方案:
- 在工程 makefile 中获取项目的源码路径
- 根据项目源码路径:
拼接得到编译文件夹的路径(DIR_BUILD)
拼接得到全局包含路径(DIR_COMMON_INC) - 通过定义命令行变量将路径传递给模块 makefile
- 这样使得工程文件夹随意移动
4.2 问题 2
- 所有模块 makefile 的内容完全相同(复制粘贴)
- 当模块 makefile 需要移动时,将涉及多处相同的改动
- 解决方案:
- 将模块 makefile 拆分为两个模板文件
mkd-cfg.mk:定义可能改变的变量
mod-rule.mk:定义相对稳定的变量和规则 - 默认情况下,模块 makefile 复用模板文件实现功能(include)
模块makefile怎么知道模板文件的具体位置?
- 将模块 makefile 拆分为两个模板文件
4.3 关键问题
- 模块 makefile 如何知道模板文件的具体位置?
- 解决方案:通过命令行变量进行模板文件位置的传递
4.4 工程 makefile 的重构
- 拆分命令变量,项目变量,以及其它变量和规则到不同文件
- cmd-cfg.mk:定义命令相关的变量
- pro-cfg.mk:定义项目变量以及编译路径变量等
- pro-rule.mk:定义其它变量和规则
- 最后的工程 makefile 通过包含拆分的文件构成(include)
20-22 - 打造专业的编译环境/20_22/03