1 简介
GNU Make 是一款核心构建工具,它控制着如何从程序的源代码中生成可执行文件以及其他非源码文件(如库文件、文档等)。
1.1 Make 的知识来源:Makefile
Make 本身并不知道如何构建你的程序,它的“知识”来源于一个名为 Makefile 的文件。
-
核心逻辑:Makefile 列出了每一个需要生成的非源码文件(目标),并详细记录了如何通过其他文件计算/生成它的过程。
-
开发规范:当你编写程序时,必须同步编写 Makefile。这样,其他用户才能通过 Make 轻松地编译和安装你的程序,而无需手动执行复杂的命令。
1.2 Make 的四大核心能力
- 抽象化构建细节:允许最终用户在完全不了解构建细节的情况下,完成程序的编译与安装。所有的编译器参数、链接选项、路径依赖都被“硬编码”在你提供的 Makefile 中,用户只需输入 make 即可。
- 智能依赖推导与增量更新:根据源代码的修改情况,自动识别哪些文件需要更新。
- 时间戳对比:它是基于“源文件”与“目标文件”的修改时间来做判断的。
- 依赖拓扑:如果文件 A 依赖文件 B,而文件 B 又依赖源码 C,当你修改了 C,Make 会自动推导出正确的更新顺序:先更新 B,再更新 A。
- 效率至上:如果你只改动了极少数源文件,Make 绝不会重新编译整个项目,它只更新那些直接或间接依赖已修改源码的文件。
- 语言无关性:不局限于任何特定的编程语言。
- 自定义命令:对于程序中的每个非源码目标,Makefile 允许你指定任意的 Shell 命令 来生成它。
- 多用途支持:你可以调用编译器生成目标文件(.o),调用链接器生成可执行程序,调用 ar 更新静态库,甚至调用 TeX 或 Makeinfo 来格式化排版文档。
- 超越构建的任务自动化:用途远不止于“编译打包”。
- 全生命周期管理:你可以用它来控制程序的安装(Install)、卸载(Uninstall)、生成标签表(Tags tables)。
- 万能工具:任何你认为需要频繁重复、值得记录下来的操作步骤,都可以写进 Makefile,交给 Make 自动执行。
1.3 独有优势
GNU Make 拥有许多超越其他版本(如 Unix 标准 Make)的强大功能:
- 中间文件管理:它能自动生成、使用并随后删除那些不需要永久保存的中间临时文件。
- 灵活的命令行参数
- 自由软件:这是 GNU Make 与大多数商业版本最本质的区别——它是自由且开源的。
2 准备与运行
三个步骤:
- 安装 make/build-essential
- 编写Makefile文件
- 执行 make
2.1 安装
安装make或者build-essential;以ubuntu系统为例
sudo apt-get update
sudo apt-get install build-essential;
2.1 Makefile文件
基本语法规则
- 大小写敏感:通常变量名字用大写,其它是小写
- 命名字符:可以包含字母、数字和下划线。甚至可以包含冒号、句号等,但不建议用
- 变量类型:只有一种“显式类型”——字符串(String);但从“求值行为”看,它有四种“逻辑类型”。
- 字符串类型:所有的变量存储的都是文本字符串
- 行为类型:变量定义方式不同意义也不太同,见章节4
- “伪”布尔类型:没有 bool,但在 ifeq 和 $(if) 指令中,空为假
- 列表类型:通过空格分隔的字符串,会被视为“列表”。
- \:续行符、转义符
- 制表符:命令行依赖:开头必须是一个制表符;其它不可用
- 分号 ;:有两种完全不同的用法
- 规则行:把目标、依赖和命令写在同一行,可以用分号分隔。
- 命令行:用于分隔 Shell 命令。
- 空格:变量中左侧忽略,右侧保留;非变量表示缩进
- ,:分隔符号
- #:注释符,行首开始,至行末结束(受 \ 影响可续行)。
- $:引用变量或
- $$: 表示$, 转义它自己, 主要用在shell脚本中表示$
- $(数字):占位符,从1开始
Makefile 文件是由多个规则组成,规则有标准结构,如下
target: dependencies ...
commands
...
Makefile 的三大支柱:目标、前置条件与命令
- 目标 (Target):能得到的产物
- 文件:它是程序生成的产物,比如可执行文件或目标文件(.o)。
- 动作:它不产生文件,而是一个要执行的操作名。最典型的例子就是 clean(用于清理垃圾文件)。这种目标被称为伪目标(Phony Targets)。
- 依赖/前置条件 (Prerequisite):必须的输入文件,可以是任意个;
- 命令 (commands):执行的具体动作;可以包含任意条Shell 命令。
2.3 执行
make
执行此指令需要理解下面几个概念细节
- 指定目标:运行 Make 时,你可以指定更新某个特定的目标;如果不指定,Make 会默认更新 Makefile 中列出的第一个目标(通常习惯命名为 all)。
- 递归更新:为了生成你指定的目标,Make 会自动检查并先更新所有作为输入的前置目标。
- 状态检查:Make 会通过 Makefile 判断哪些文件“应该”保持最新,然后剔除那些“已经是”最新的文件。
- 判定标准:如果目标文件的修改时间比它所有的依赖文件都新,那么它就是最新的,无需重新生成。不存在时,则需要先生成
- 更新顺序:严格遵守拓扑顺序,确保每个目标在其被作为其他目标的输入之前,已经完成了更新。
命令行传参:变量赋值: make [目标] 变量名=值;变量名字直接可以在Makefile文件中使用
常用功能选项
- -j [N]:多线程编译。通常设为 CPU 核心数的 1 到 2 倍(如 -j16)。
- -k:遇到错误不立刻停止,尽可能编译其他不相关的模块。
- -n:只打印命令,不实际执行。
- -B:强制重新编译。认为所有目标都已过期,全部重来
3 规则
这是 Makefile 的灵魂。它定义了目标、依赖和执行命令。规则分为三类
- 显式规则:精确的写出目标和依赖。例如:main.o: main.c。
- 模式规则:用通配符使用通配符 %,实现自动化构建的基石。使用在目标和依赖中
- 隐式规则:内置的一些规则
依赖也有2种
- 常规依赖:如果依赖文件变了,目标必须重新生成。
- 唯存在依赖:语法是在依赖前加一个竖线 |,目录存在就行,时间戳变化无影响
伪目标 语法: .PHONY: target_name,可以保证没有依赖且无文件生成时,可以执行命令
特殊符号控制命令:
- @:静默执行。命令本身不会打印在屏幕上,只显示结果(常用于 echo)。
- -:忽略错误。即便这条命令执行失败(返回非零值),Make 也会继续往下走。
- +:始终执行。即使开启了make -n(只显示不执行)模式,带 + 的命令依然会运行。
4 变量
在Makefile中,变量不是键值对,更像是动态宏替换。
定义
- := (Simply Expanded):直接展开;逻辑:定义时立即扫描右侧内容。如果右侧引用了别的变量,取当前值。
- = (Recursively Expanded):递归展开;只有在使用到这个变量时,才去全篇扫描它的值。
- ?= (Conditional):条件赋值,变量没定义过,就赋值;定义过了,就啥也不干。常用于设置默认值。
- += : 可以对变量进行追加
变量使用: 用$(VAR) 或 ${VAR}
自动变量:在命令中有效
- $@:目标文件名
- $<:第一个依赖文件名
- $^:所有依赖文件名
- $+:所有依赖文件名(包含重复),类似 $^ 但不去重
- $?:所有比目标更新的依赖;问号代表:到底是谁变了导致我要重编?
- $*:模式规则中 % 匹配到的字符串
内置变量 预定义了一堆变量,即使你不写,它们也存在。
- 程序名:
- CC:C 编译器(默认 cc)
- CXX:C++ 编译器(默认 g++)
- RM:删除命令(默认 rm -f)
- 标志位 (Flags):
- CFLAGS:传给 C 编译器的参数
- CXXFLAGS:传给 C++ 编译器的参数
- CPPFLAGS:C 预处理器参数
- LDFLAGS:链接器参数
模板变量:本质上就是一个“多行变量”,但它是一种特殊的、专门为 call 和 eval 设计的变量。
# 这就是一个模板,本质上是名为 PROGRAM_TEMPLATE 的变量
define PROGRAM_TEMPLATE
$(1): $(2)
$(CC) $(CFLAGS) $$^ -o $$@
endef
- 变量属性:你可以像引用普通变量一样,通过 $(PROGRAM_TEMPLATE) 打印出它的内容。
- 多行支持:define ... endef 允许你保留换行符和 Tab 键,这对于生成 Makefile 规则(Rule)至关重要。
- 模板是动态的,它预留了占位符:$(1), $(2), $(3)...在定义时没有意义,使用 $(call PROGRAM_TEMPLATE, arg1, arg2) 时,这些占位符才会被替换。所以使用时,才会出现$$这种去了两次值
高级用法:
- 变量引用与替换:$(var:suffix=replacement)
- 变量的“变量”: $(xxx$(var))
- 目标特定变量: target : variable-assignment,仅仅在目标时进行的变量处理
- shell 函数变量:$(shell xxx),直接获取系统命令的输出。
5 函数
在 Makefile 里,函数调用的格式统一为 $(function arguments)。
主要分为:字符串处理、文件名操作、控制流以及高级 API 四类。
- 字符串操作:用来“搓”字符串,是实现自动化重命名、过滤文件的核心
- $(patsubst pattern,replacement,text):按照模式替换字符串
- $(filter pattern...,text):保留符合模式的,删掉不符合的
- $(subst from,to,text):简单的文本替换(不带模式)。
- 文件名操作函数:处理路径、寻找文件的利器。
- $(wildcard pattern):最常用。扫描磁盘,返回匹配的文件列表。
- $(notdir names...):去掉路径,只留文件名。
- $(dir names...):取目录部分。
- 控制流与逻辑函数:像编程语言一样具备逻辑
- $(foreach var,list,text):循环遍历。
- $(if condition,then-part,else-part):判断是否为空。
- $(and condition,con2,...): or,逻辑运算;返回空字符串或者打破逻辑的第一个字符串
- $(shell command):运行 Shell 命令并拿回输出结果。
- 高级“黑魔法”
- $(call variable,param1,param2...):自定义函数。你可以把一段复杂的逻辑存进变量,然后像调函数一样调它。
- $(eval text):终极 API。它会把生成的文本重新当成 Makefile 语法解析一遍。通常用于根据列表动态生成成百上千条规则。
- 输出函数: 无等级控制打印,需要自己使用条件变量控制
- $(info ...):仅打印。不影响解析,调试变量值,确认逻辑分支
- $(warning ...):打印 + 警告。会带上文件名和行号,但继续解析。
- $(error ...):打印 + 终止。立刻停止解析,报错退出。
6 指令
指令(Directives) 是 Makefile 的“元语法”。它们不属于规则、变量或函数,而是直接告诉 Make 解释器:“请改变你处理这个文件的行为”。
-
条件指令:
- ifeq (arg1, arg2):判断两个值是否相等。
- ifneq (arg1, arg2):判断两个值是否不相等。
- ifdef variable:判断变量是否已定义(只要有值,哪怕是空字符串后的空格,也算定义)。
- ifndef variable:判断变量是否未定义。
- else:可选的分支。
- endif:结束标志。
-
控制与流程指令
- include filenames...:暂停读取当前 Makefile,转而读取并执行指定的文件,结束后返回。
- -include filenames...:即使文件不存在也不报错(常用于加载 .d 自动依赖文件)。
- vpath pattern directories:设置搜索路径。告诉 Make 如果在当前目录找不到文件,去这些目录里找。
- override variable-assignment:确保在 Makefile 中对变量的赋值能覆盖命令行传入的参数(make CC=gcc 这种命令行参数默认优先级最高,除非加了 override)。
-
变量与环境指令:控制变量的作用域和可见性。
- export variable:将变量传递给子 Make 进程(即递归调用 make -C 时)。
- unexport variable:防止变量传递给子进程。
- undefine variable:彻底删除一个变量,使其变为未定义状态
- private variable-assignment:在目标特定变量(Target-specific variables)中使用,防止该变量被继承到依赖项中。
7 C/C++ 相关
核心编译变量:
- CC:C 语言编译器
- CXX:C++ 语言编译器
- CFLAGS:传给 C 编译器的参数(如 -O2, -Wall)
- CXXFLAGS:传给 C++ 编译器的参数
- CPPFLAGS: 预处理器参数(如 -I 包含路径, -D 宏定义)
- LDFLAGS:链接器参数(如 -L 库路径)
- LDLIBS: 要链接的库(如 -lpthread, -lm)
关键步骤及指令:
- 编译:将 .c 或 .cpp 转换成 .o(目标文件)。
$(CXX) $(CXXFLAGS) $(CPPFLAGS) -c $< -o $@
- -c:只编译,不链接。
- -o $@:指定输出文件名(目标)。
- $<:自动化变量,代表第一个依赖文件(源码)。
- 链接:将一堆 .o 文件和系统库打包成可执行文件。
$(CXX) $(LDFLAGS) $^ $(LDLIBS) -o $@
- $^:所有依赖文件(所有的 .o)。
- $(LDLIBS):必须放在最后,否则在某些 Linux 发行版下会找不到符号。
- 静态库:本质上就是一个“.o 文件的压缩包”。使用ar,简单地把一堆目标文件捆绑在一起。
$(LIB_NAME): $(OBJS)
ar rcs $@ $^
- r:插入文件。
- c:创建库。
- s:索引库(加速链接)。
- 动态库:一个特殊的、可重定位的可执行文件。必须经过链接器,且源码在编译成 .o 时,必须加上相关参数
- 编译期:必须加 -fPIC
- 链接期:必须加 -shared
# 1. 编译:必须加 -fPIC
%.o: %.cpp
$(CXX) -fPIC -c $< -o $@
# 2. 链接:使用 -shared 生成动态库
$(LIB_NAME): $(OBJS)
$(CXX) -shared -o $@ $^
如果在此文章中您有所收获,请给作者一个鼓励,点个赞,谢谢支持
技术变化都很快,但基础技术、理论知识永远都是那些;作者希望在余后的生活中,对常用技术点进行基础知识分享;如果你觉得文章写的不错,请给予关注和点赞;如果文章存在错误,也请多多指教!