...menustart
...menuend
- make
- make 会一次查找 GNUmakefile, makefile, Makefile
- 也可以使用 -f 指定一个工作文件
-d
参数用于调试 Makefile
- make 会一次查找 GNUmakefile, makefile, Makefile
# 目标 : 条件
# 命令
all: TinyEdit
TinyEdit: main.o line.o ...
...
main.o : main.c line.h buffer.h tedef.h
cc -c -o main.o main.c
- 目标可以作为参数,直接传给 make
- i.e.
make main.o
- 默认执行 make all
- i.e.
- 一个规模宏大的项目,比如 Linux 内核,源码文件数量数十万计,编译一次内核大概需要几十分钟甚至数小时
- 如果每做一点修改,都要编译这么久,那简直就是灾难
- make 解决这个问题的方法 就是哪个文件进行了修改 就只编译它
- make 会依次把所有的目标,以及它们所对应的依赖, 进行时间戳(文件最后的修改时间)的对比。
- 当发现某一个目标 比它的某些依赖的时间戳小的时候,就重新生成它。
- Makefile的基本语法是:
目标1 目标2 目标3 ... : 条件1 条件2 条件3 ...
命令 1
命令 2
命令 3
...
- 至少一个目标
- 0个或 多个条件
- 如果 没有给定条件, 只有 目标文件不存在时, 才会执行相应的命令去生成目标
- 每个命令 必须以 制表符
Tab
开头- 如果在非命令行上不慎输入了Tab的话,它之后的内容多数情况下会被当作命令来解释
- 注释
#
开始的部分为注释- 不过切记 Tab的问题, 只要在 Tab后, 就会被作为命令解释, 包括
#
后面的内容
- Makefile 支持 折行
- 在行尾 使用
\
- 在行尾 使用
- 现在需要给 生成的程序加入调试信息, 以便在发现bug的时候可以调试
- 可以在每个 命令中加入
-g
选项。 只是这是一个馊主意 - 有什么办法 是的只修改一个地方就能搞定一切呢,就是变量
CC := gcc -g
- 可以在每个 命令中加入
- make 在解析
${CC} 或 $(CC)
时,就会展开为gcc -g
- Makefile 中有一种特殊的变量, 不用定义,而且还会随着上下文的不同而发生改变,我们称之为 自动变量
- 最为常用的6个自动变量
变量名 | 作用 |
---|---|
$@ |
目标的文件名 |
$< |
第一个条件的文件名 |
$? |
时间戳在目标之后的所有条件, 并以空格 隔开这些条件 |
$^ |
所有条件的文件名,空格隔开,并且 去了重 |
$+ |
与 $^ 类似,只是 没有去重 |
$* |
目标的主文件名, 不包含扩展名 |
- 自动变量只能用于 命令中
buffer.o : buffer.c buffer.h tools.h $(CC) $(CFLAGS) -o $@ $< main.o : main.c line.h buffer.h tedef.h # cc -c -o main.o main.c $(CC) $(CFLAGS) -o $@ $<
-
我们发现,上面的 buffer.o , main.o 的命令完全一样
- 很自然的,我们就会想用同一的规则来处理它们 , 这就是 模式规则 (实践中有限制,需配合其它手段)
all: TinyEdit TinyEdit: main.o line.o buffer.o ... $(LD) $(LDFLAGS) $^ -o $@ myless : myless.o line.o buffer.o ... $(LD) $(LDFLAGS) $^ -o $@ %.o : %.c $(CC) $(CFLAGS) -o $@ $<
-
模式中的
%
类似通配符, 它可以放在 模式中的任意位置,但是它只能出现一次 -
限制:
- 模式规则中的目标与条件 为 一对一关系, 无法处理 一对多关系.
-
使用
make -p
可以查看 所有内置变量
- 虽然前面使用了 模式规则的Makefile 很精简,但是并不推荐,因为作为C语言的头文件 无法成为条件, 毕竟头文件修改了也需要重新编译.
- 后面会介绍一种 既使用模式规则, 又能照顾好依赖关系的方法...
-
条件一定会是文件,但是目标就未必。
-
我们把 不生成目标文件的规则中的目标,就称为 假目标。
- 因为没有文件生成, 所以假目标的命令就一定会执行
- 不过有的时候 虽然定义了一个不会生成目标文件的目标, 但很难保证项目中不会有 与这个目标重同名的文件存在, e.g. 一个
clean
的文件- 一旦有同名文件存在,我们的假目标就不会工作了。 需要进行一下特殊的处理
- 这个特殊的处理方式, 就是让 这个假目标 成为一个特殊目标 .PHONY 的条件。
- 这样无论项目中是否有重名的文件,这个目标对对应的命令都会被执行。
.PHONY: clean clean: rm -f foo
-
另一个例子
.PHONY: build clean tool lint help all: build build: go build -v . tool: go tool vet . |& grep -v vendor; true gofmt -w .
-
现在我将代码重新组织了一下,建立 src , include 子目录, 并将 所有 .c 文件移动到 src 中, 所有 .h 移动到 include中
- 这样就需要 修改 Makefile 规则中的条件,添加子目录。 但是这样很麻烦,还很容易出错
- 于是 Makefile 引入了 VPATH 和 vpath
-
VPATH 的值 是需要 make 搜索的 子目录, 以空格分割
LDFLAGS := -lcurses VPATH := src include ...
-
虽然VPATH 可以解决路径的问题,但是有一个限制:
- 如果在多个路径中找到同名文件, 会采用第一个被找到了。 这种问题在 .h 文件中经常出现, 会很麻烦
- 为此 就需要 vpath
-
vpath 使用 模式来指定 在某个具体的路径中,搜索哪些类型的文件
vpath %.c src vpath %.h include
- Makefile 使用
include
指令来 包含其他文件,功能有二- 类似与C语言那样引入一个头文件,实现代码服用
- 用来自动产生依赖关系的信息
include FILE1 FILE2 FILE3
- include 可以引入任意多个文件,且支持 shell的通配符和变量
- 当make 遇到include 指令,
- 就会读取 它指定的文件
- 一旦遇到一个不存在的,不要紧, 顶多抱怨一句,继续来。知道将所有文件读完
- 这时候 它就要收拾刚才找不到的文件了。 怎么收拾呢?
- 就是将这些文件 作为规则的目标,尝试去生成它。 生成一个读进来一个。
- 但凡有一个没搞定,make就马上报错罢工。
- 等到都读完了,就开始常规工作了。
- 这个时候,一旦根据某些规则,将 include所指定的某个文件更新了,也会导致这个文件被重新读入。
- 因为我们可以将整个include 指令看作一个特殊的规则, 只是这个规则没有中间的
:
, include 就是目标,它所指定的文件都是条件. - 这条规则的命令, 即使逐一读取每个条件所对应的文件,并与其所在的Makefile 进行融合。
- 因为我们可以将整个include 指令看作一个特殊的规则, 只是这个规则没有中间的
- include 搜索路径
- 最优先 当前工作路径
- 执行make 命令时 给定的
-I 或 --include-dir
选项所知名的路径
-
Makefile 的命令有一个限制, 就是只能 单行处理bash 脚本
for d in a b c do echo $d/* done > list.txt 在Makefile中用,需要改写成合法的bash格式 for d in a b d ; do echo $$d/* ; done > list.txt
-
注意对变量d的引用上, 采用的是
$$d
, 这样写是为了区别于 Makefile 的变量. -
Makefile 命令修饰符
@
: 不输出命令本身-
: 忽略命令错误+
: 只显示命令 , 而不去执行- 这个看起来用处不大, 不过在编写递归式的Makefile 时会用到
-
- 经简单扩展的变量
:=
复制运算符来定义的变量- 下面 MAKE_DEPEND 的值可能是
gcc -M
, 如果 CC 没定义,则是-M
- 一经定义,就有固定的值
MAKE_DEPEND := $(CC) -M
-
- 经递归扩展的变量
=
delayed expand- 下面 MAKE_DEPEND 的值 会根据 CC 的定义,动态变化
MAKE_DEPEND = $(CC) -M
- 其他的赋值方式
?=
附带条件的变量赋值运算符- 如果左边的变量 之前没有被定义过, 才会赋值
- 如果左边定义过,就跳过
+=
追加赋值- 给变量追加东西,我们可以
A := $(A) B
- 但是如果是 递归扩展的变量呢?
A = $(A) B
- 左递归 无限循环
- 所以需要
+=
来解决这个问题A += B
- 给变量追加东西,我们可以
-
直接我们所说的变量都是 全局变量
- Makefile 也有它的 局部变量 ( 并不是我们印象中的局部变量 ) , 叫 目标专属变量
# myless 需要链接 curses 库 # TinyEdit 不需要 curses 库 LDFLAGS := TinyEdit: ... $(LD) $(LDFLAGS) $^ -o $@ myless : LDFLAGS := -lcurses myless : ... $(LD) $(LDFLAGS) $^ -o $@
-
目标专属变量的值, 在处理对应目标的规则时 会进行改动, 当离开这个规则之后,变量值会被恢复。
-
除了 我们这些自定义的变量和make内建的一些变量之外,make还提供了几个非常有用的专有变量
变量名 | 作用 |
---|---|
MAKE_VERSION | GNU make 版本好 |
CURDIR | make的当前工作路径 |
MAKEFILE_LIST | 在make命令中给定的要生成的目标 |
VARIABLES | 所有已定义的变量名列表, 不包含目标专有变量。 只读 |
-
宏的概念与经递归扩展的变量基本上是一致的。 只是写法不同
-
宏定义的语法:
- 只要将你的命令,写在
...
那里就行了 , 不过要注意命令的写法
define MACRO_NAME ... endef
- 只要将你的命令,写在
-
使用定义好的宏:
$(MACRO_NAME)
or${MACRO_NAME}
-
宏还可以引用参数:
- 使用这样的自动变量
$1, $2, $3 ...
- 使用:
$(call MACRO_NAME [, arg1, ... , argn])
- i.e.
$(call get_files_list,a,b,c)
- i.e.
- 使用这样的自动变量
-
其实 Makefile中已经内置很多非常有用的函数,不过调用方法与我们自定义的函数不一样, 语法是这样的:
$(function arg1,[,argn])
- 明显比我们自定义的要简单。
- 这是因为 Makefile本不打算支持自定义函数的,只是需求太多了,也就得支持了。
- 它并没有采用与 内置函数 相一致的语法, 取而代之的是一个巧妙的方法。
- 就是 提供了一个名为
call
的内置函数。 用这个函数使用一些技巧来扩展宏 - 而且
call
函数不只是对 宏起作用, 对变量同样有效。
-
无论是宏还是函数,都是有返回值的。
- 这个返回值就是宏或函数命令的标准输出。
ifdef, ifndef, ifeq, ifneq
else
endif
- 依赖关系始终是一个特别烦人的事情,尤其是 C的头文件。
- 因为只要修改了头文件, 那么所有引用的 .c 文件都应该被重新编译。
- 要手动维护这些依赖关系 既麻烦又容易出错。
- 如果能够自动维护生成,那就真是轻松愉快了。
- make 和 gcc 通过 狼狈为奸 成就了这种可能。
-
gcc 提供了一个
-M
和-MM
的命令选项- 前者可以输出一个 .c 文件所引入的所有头文件
- 后者则排除掉了 系统提供的头文件,只保留 用户自定义的头文件。
-
它的输出差不多是这样:
- 很熟悉吧,这不就是 Makefilede 目标和条件吗? 可见gcc 与make 的关系不一般了.
# gcc -MM Stack.cpp Stack.o: Stack.cpp Stack.h UMCMacros.h HTTPRequest.h
- 很熟悉吧,这不就是 Makefilede 目标和条件吗? 可见gcc 与make 的关系不一般了.
-
一个例子
SOURCES := $(wildcard *.cpp) all: Stack.o @echo $(SOURCES) , $$$$ -include $(subst .cpp,.d,$(SOURCES)) %.d:%.cpp gcc -MM $< > $@.$$$$; \ sed 's,\($*\)\.o[:]*,\1.o $@:,g' < $@.$$$$ > $@; \ rm -rf $@.$$$$
- 说明
SOURCES := $(wildcard *.cpp)
- 列出当前目录下所有的 cpp 文件
- i.e.
RobotMgr.cpp RobotLogin.cpp Stack.cpp RobotGetUserData.cpp RobotChallenge.cpp HTTPRequest.cpp RobotBattle.cpp
gcc -MM $< > $@.$$$$;
- 根据 cpp 生成 类似 .d.9203 文件,文件的内容类似:
Stack.o: Stack.cpp Stack.h UMCMacros.h HTTPRequest.h
$$
是 bash 里的一个变量,表示 process ID (PID) of the script itself.
- 根据 cpp 生成 类似 .d.9203 文件,文件的内容类似:
- sed 语句 是往 .d 文件中 插入 .d文件名本身
Stack.o Stack.d: Stack.cpp Stack.h UMCMacros.h HTTPRequest.h
- 注意 模版规则 的命令 需要写在 一行
- 多行的话,每一行
$$
生成的数字都会不一样
- 多行的话,每一行
-include $(subst .cpp,.d,$(SOURCES))
- subst 是字符串替换 , 将参数1的内容,替换为参数2, 替换目标是参数3 , 并将结果返回
- 将 SOURCE 变量中的 .cpp 替换为 .d
- 这里 include 就相当于 要引入所有的 .d 文件。 如果找不到,就 执行最后的模式规则来生成 .d 文件。
- 这样之前产生的那些 依赖关系(位于.d 文件中) 就被引入到这个 Makefile 中了.
- subst 是字符串替换 , 将参数1的内容,替换为参数2, 替换目标是参数3 , 并将结果返回
- 为什么 确定依赖关系的规则中 有一个 .d 做目标 ?
- 为了保证当有文件被修改的时候, 不会遗漏依赖关系变化。
- 比如 你在 一个.cpp 的头文件中 又引入了新的头文件. 这时依赖关系 需要更新, 但是我们确没有 .h -> .d 的模式规则。 加入.d 就可以解决这个问题。
- 新版本的 gcc 又配合这个设计,提供了一个新的
-MT
选项。 可以直接在 依赖关系中 加入 .d 文件SOURCES := $(wildcard *.cpp) all: Stack.o @echo $(SOURCES) , $$$$ -include $(subst .cpp,.d,$(SOURCES)) %.d:%.cpp gcc -MM -MT "$@ $(@:.d=.o)" $< > $@
-
Makefile 里 再调用make 去执行另一个 Makefile.
-
为什么要递归呢? 动机很简单:
- 处理单一目录的Makefile 可以非常简单, 目录越多越复杂。
- 给每个目录准备一个 Makefile, 可以很好的降低复杂度。
语法: cd subdir && $(MAKE) 或 $(MAKE) -C subdir
-
这两条命令是等价的。 需要注意的两个地方
- 一个被执行的 Makefile 在执行完毕后, 不需要类似
cd ..
这样的命令, 因为每个命令都有一个独立的 shell 环境 - 永远使用 $(MAKE) 这样的变量来执行make, 这可以保证在一个系统中 安装了 多个版本make时, 使用的make就是你想要的
- 一个被执行的 Makefile 在执行完毕后, 不需要类似
-
Makefile的 caller 和 callee 之间你一定不希望完全没有联系。
- 我们可能需要传递 1 变量 , 2 命令行参数
- 但是 如果什么变量都传递 也会带来不小的麻烦, 所以也就有了下面的这些规则:
- 在命令行上赋值的变量 默认传递, 并且仍以命令行方式传递
- Makefile 中定义的变量 默认不传递
- 用 export 命令可以 明确传递一个变量
- 和 bash 中的 export 不是一回事,不过用法一样
export CC CFLAGS
- unexport 明确不传递某个变量
- 底层 Makefile 对上层 Makefile 传递的变量进行的修改,不会传递回上层。
- 大多数 Linux 上以源代码发行的软件都没有 Makefile, 但是却带有一个 configure 文件
- 这是一个使用bash 脚本写成的程序, 它也是自动生成, configure 的作用就是生成Makefile文件
- configure 常用命令选项
选项名 | 描述 |
---|---|
--prefix=PREFIX |
指定安装路径,默认 /usr/local |
--exec-prefix=EPREFIX |
指定默认的可执行程序安装路径,默认 PREFIX |
--bindir=DIR |
可执行程序安装路径,默认 EPREFIX/bin |
--libcdir=DIR |
库文件安装路径, 默认的是 EPREFIX/lib |
--sysconfdir=DIR |
配置文件安装路径, 默认 PREFIX/etc |
--includedir=DIR |
需要安装的头文件路径, 默认 PREFIX/include |
--disable-FEATURE |
是否关闭某个特性, 等同于 --enable-FEATURE=no |
--enable-FEATURE=[ARG]MAKE_VERSION |
是否开启某个特性, ARG 是 yes 或 no |
- configure 还可以决定 生成软件时 使用什么养的 连接器,编译器,乃至编译器选项
- 这是通过命令中 给定的环境变量实现的
configure常用环境变量 | 描述 |
---|---|
CC | C编译器 |
CFLAGS | C 编译器选项 |
LDFLAGS | 连接器选项 |
CPPFLAGS | c++编译器选项 |
- configure 它是怎么生成的呢? 它又是怎么生成 Makefile的呢?
- 这里就要用到 Autotools 工具了.
all: output
output: input1 input2 input3
command --output $@ --inputs $^
CMake will turn it into
all: output
output: input1 input2 input3
command --output output --inputs input1 input2 input3