Makefile Note (Update in 2022-03-29)
- make file 的组成部分
1. make file 由 变量,目标,规则,注释,函数等部分组成
2. 其中目标则代表编译的目标,如果编译目标的规则并不会生成目标文件时,则需要 .PHONY 指明
3. 变量的复制有如下几种:a = b
,a := b
,a ?= b
,a += b
4. 规则为执行目标生成,或特定任务处理的一组脚本集合,该集合的脚本将会由/bin/sh
指定的 shell 解释器来执行,而且此处的注释也交由解释器解释
5. 常见的函数有:$(error xxx)
,$(warning xxx)
,$(subst from,to,text)
,$(patsubst pattern,replacement,text)
,$(strip string)
,$(findstring find,in)
,$(filter pattern...,text)
,$(sort list)
,$(call ...)
,$(shell ...)
,$(foreach var, list, text)
,$(join ...)
,$(basename ...)
,$(nodir ...)
,$(dir ...)
... - make 返回值
make 总共有三个返回值:0,1,2
0 执行成功
1 执行失败
2 加了-q
选项,并且 make 不需要对一些目标更新 - make 的参数
检查规则-n / --just-print / --dry-run / --recon
不执行参数,这些参数只是打印命令,不管目标是否更新,把规则和连带规则下的命令打印出来,但不执行,这些参数对于我们调试 makefile 很有用处。-t / --touch
把目标文件的时间更新,但不更改目标文件。-q / --question
找目标的意思,也就是说,如果目标存在,那么其什么也不会输出,当然也不会执行编译,如果目标不存在,其会打印出一条出错信息。-W file / --what-if=file / --assume-new=file / --new-file=file
这个参数需要指定一个文件。一般是是源文件(或依赖文件),Make会根据规则推导来运行依赖于这个文件的命令,一般来说,可以和“-n”参数一同使用,来查看这个依赖文件所发生的规则命令。
常规参数-C dir / --dirctory=dir
指定读取makefile的目录。如果有多个“-C”参数,make的解释是后面的路径以前面的作为相对路径,并以最后的目录作为被指定目录。如:“make -C ~hchen/test -C prog”等价于“make -C ~hchen/test/prog”。-f
可以用于指定 make 用于解释的根 makefile, 比如make –f build/platform_x86.mk
-I dir / --include-dir=dir
指定一个被包含 makefile 的搜索目标。可以使用多个 “-I” 参数来指定多个目录。-debug=options
输出 make 的调试信息,有以下几个选项:a / b / v / i / j / m
, make 的-d
参数相当与--debug=a
。-j
指明同时运行命令的个数,如果不指明该参数,则 GNU make 默认同一时间只有一条命令执行。 这个值通常和你 CPU 的核数(n)相关,如果你全速编译可以设置j
(1.2-1.5)*n, 如果你还有其他作业需要处理,请适当调整到j
的值为 n-(1~3)。-i
在执行时忽略所有的错误。-k / --keep-going
出错也不停止运行。如果生成一个目标失败了,那么依赖于其上的目标就不会被执行了。-r
禁止 make 使用任何隐含规则。-R / --no-builtin-variabes
禁止make使用任何作用于变量上的隐含规则。-s / --silent / --quiet
在命令运行时不输出命令的输出。-S / --no-keep-going / --stop
取消 -k 选项的作用。因为有些时候, make 的选项是从环境变量 “MAKEFLAGS” 中继承下来的。 所以你可以在命令行中使用这个参数来让环境变量中的 -k 选项失效。-o file / --old-file=file / --assume-old=file
不重新生成的指定的 file,即使这个目标的依赖文件新于它。-p / --print-data-base
输出 makefile 中的所有数据,包括所有的规则和变量。 这个参数会让一个简单的 makefile 都会输出一堆信息。如果你只是想输出信息而不想执行 makefile,你可以使用make -qp
命令。 如果你想查看执行 makefile 前的预设变量和规则,你可以使用make –p –f /dev/null
。 这个参数输出的信息会包含着你的 makefile 文件的文件名和行号,所以,用这个参数来调试你的 makefile 会是很有用的,特别是当你的环境变量很复杂的时候。-B / --always
更新所有目标(重编译)-e / --environment-overrides
指明环境变量的值覆盖 makefile 中定义的变量的值。-W file / --what-if=file / --new-file=file / --assume-file=file
假定目标 file 需要更新,如果和 “-n” 选项使用,那么这个参数会输出该目标更新时的运行动作。 如果没有 -n 那么就像运行 UNIX 的 touch 命令一样,使得 file 的修改时间为当前时间。--no-print-directory
禁止“-w”选项。
- make 如何知道你的代码应该被更新的?
make 会根据目标依赖的文件的 mtime 是否新于目标文件来判断该目标是否需要重新编译。 如果你的目标脚本执行并没有生成目标文件,或者生成的目标文件并不等于目标名称,则每次执行到该目标时都会重新编译 - 编写目标规则的三种形式
如下所示,但注意, command 如果不是和 target 在同一行,则需要以一个 TAB 作为缩进targets : prerequisites command ... targets : prerequisites command ... targets : prerequisites ; command command ...
- 多目标推到
多目标的编写方式如下,make 会根据变量中目标名称以及后面的规则进行展开,其中$<
会替换成依赖,$@
会替换成目标名称。$(objects): %.o: %.c $(CC) -c $(CFLAGS) $< -o $@
- make 的工作流程
1. 读入所有的Makefile。 2. 读入被include的其它Makefile。 3. 初始化文件中的变量。 4. 推导隐晦规则,并分析所有规则。 5. 为所有的目标文件创建依赖关系链。 6. 根据依赖关系,决定哪些目标要重新生成。 7. 执行生成命令。
- 命令包
命令包以define
开始, 以 endif 结束,如:define run-yacc yacc $(firstword $^) mv y.tab.c $@ endef
在使用的时候后直接在规则里面调用$(run-yacc)
即可
命令包中使用的$@
$^
在实际的调用位置会替换成实际的目标和依赖名 - 条件判断
make 仅支持一种条件判断方式即fieq condition
, 其中最常见的方式是:ifeq ($(VAR),'some_value') ... else ... endif
而 condition 可以是变量和变量的对比,也可以时变量和参数的对比
在 condition 中也可以使用 make 支持的函数
同时还可以使用单变量,有值为真,无值为假 - Tips
- make 会将 makefile 中找到的第一个 target 作为默认 target
-
make 默认会寻找 Makefile 为名的文件(或者是 makefile ,GNUmakefile 视具体的工具而定),但如果你使用的
-f
参数,则会直接采用指定的文件 - 在 makefile 中使用 include 包含外部文件之后,外部文件会直接展开到 include 的位置
-
在编写目标的规则脚本前加上
@
可以在编译的时候不打印规则本身 -
正常情况下,规则脚本执行如果报错,则会导致 make 终止运行,但是在规则之前加上
-
可以让 make 忽略报错 -
在 make 的时候还可以追加
-n
或者--just-print
参数,这回打印出目标规则所有的执行语句,但不会执行他们,非常适合调试的时候使用。 - 当依赖目标新于目标时,也就是当规则的目标需要被更新时,make会一条一条的执行其后的命令。需要注意的是,如果你要让上一条命令的结果应用在下一条命令时,你应该使用分号分隔这两条命令。
- 特殊变量
VPATH
该变量保存一个路径列表,路径与路径用冒号分割,该变量主要指示 make 在进行自动推导的时候搜索文件的目录, make 会按照(当前目录,以及该变量指示目录的顺序)来便利。当然也可以在 makefile 中使用vpath
关键字,这里后面带的目录以空格分割即可
MAKECMDGOALS
该变量存放你在执行 make 指令时传入的目标列表,如你输入的是make clean
, 那么此时该变量的值就为 clean; 不过如果你没有指定,则该变量为空。
$$$$
生成一组随机数字符串
$$
代表 $ 字符 - 变量
使用=
对变量赋值时,在只有使用到该变量的时候才会展开,因此如果你的变量 A 的值包含了另外一个变量 B,而变量 B 的值会在之后发生变化,那么在引用 A 的时候,会使用到 B 改变之后的值
如果要避免上述情况可以使用:=
来进行赋值,使用这种方式赋值,值会在赋值的时候便展开
如果变量定义过长还可以使用+=
来进行追加
其次还有一个赋值
高级用法 1:$(var:a=b)
或者$(foo:%.o=%.c)
, 把 var 变量中以 a 结尾的值替换成 b 结尾的值
如果要定义多行命令可以使用如下方式:define two-lines echo foo echo $(bar) endef
这和命令包非常相似,区别在于,命令包中的规则以 TAB 开头,而这个没有,make 在解释的时候也是依据此来区分的。 - 变量的 override
通常,如无特殊申明,makefile 中的变量是可以在执行 make 的时候携带参数来改变的。
比如,你在 makefile 中定义了一个 var 变量(比如var=old_value
),然后你在对该 makefile 进行编译的时候给 var 传递了一个值:比如make var=new_value
那么此时 var 的值 old_value 就会被 new_value 所取代
而如果你想在 makefile 中忽略参数指定的值,则可以使用 override 指示符,如:override var=old_value
- 目标变量
顾名思义,专为目标定义的变量,该变量会覆盖外部的全局变量,使用方法如下:target: var1=value1 target: var2=value2 target: use $(var1) $(var2) do something
此外,make 还支持模式变量,即可以给符合该模式的所有 target 定义变量,如:%.o: CFLAGS ?= -O2 $(Objects):%.o:%.c $(CC -c $@ $(CFLAGS) $^
makefile 中的函数和变量一样,可以使用大括号来调用,也可以使用小括号来调用,但是为了风格的统一,建议使用同一种风格。
函数的使用基本形式如 $(function_name, arg1,arg2...)
, 下面我们会集中介绍集中常用的函数
- 打印相关
$(warning text...)
编写该函数的地方会打印出 text 字符串做为警告信息,
$(error text...)
编写该函数的地方会打印出目标字符串作为错误信息,并会退出 make 的编译过程 - 外部调用
$(shell shell_centence)
, 该函数主要用于调用 shell 脚本并返回脚本的标准输出
$(call expression,args1,args2...)
, 该函数主要运行 expression 表达式,而后面的 args1, args2 则为该表达式的参数,在表达式中,参数可以使用${1}
,$(2)
的形式引用,如:func = $(shell cd ${1} && find -name *.${2}) $(call func,./src,cpp) $(call func,./src,go)
- 判断
$(if condition,then-part>)
OR$(if condition,then-part,else-part>)
, 和#ifeq
类似,可以包含两个参数,也可以包含三个参数, 使用例子如下:$(if $(filter foo,bar),@echo match is broken,@echo match works)
,viewpdf :=$(if $(filter Darwin,$(uname)), open, evince)
- 字符串处理
$(subst from,to,text)
将字符串 text 中的 from 字符串替换成 to 字符串;
$(patsubst pattern,replacement,text)
将 text 字符串中符合 pattern 格式的字符串替换成 replacement 的串。pattern 中可以使用%
作为通配符,匹配任意长的字符串; 而 replacement 中的%
则表示用 patern 在 text 中匹配到的串;此外,这和我们前面“变量章节”说过的相关知识有点相似。 如$(var:pattern=replacement;)
相当于$(patsubst pattern,replacement,$(var))
, 而$(var: suffix=replacement)
则相当于$(patsubst %suffix,%replacement,$(var))
。
$(strip string)
去掉 string 中开头和结尾的空格
$(findstring find,in)
在 in 代表的字符串中找到 find 这个字符串,如果找到,则会返回 find 字符串,如果没有找到,则会返回空串
$(filter pattern...,text)
以 pattern 过滤 text 中的字符串,返回符合 pattern 模式的字符串
$(filter-out pattern...,text)
该函数返回 text 中去除了符合 pattern 模式的字符串
$(sort list)
返回对 list 重排之后中以单词升序排列的字符串
$(word n,text)
返回字符串 text 中第 n 个单词。如果 n 比 text 中的单词数要大,那么返回空字符串。
$(wordlist startn,endn,text)
从 text 中取出从 startn 到 endn (包含)的单词列表,startn 和 endn 为整形数值, 如果 startn 比 text 中的单词数要大,那么返回空字符串。如果 endn 大于 text 的单词数,那么返回从 startn 开始,到 text 结束的单词串。
$(words text)
统计 text 的单词个数
$(firstword text)
取字符串 text 中的第一个单词。 - 文件处理
$(dir names...)
取出 names... (文件名序列) 中目录部分
$(notdir names...)
取出 names... (文件名序列) 的非目录部分(即最后一个 / 后的名字)
$(suffix names...)
取出 names... (文件名序列) 中文件的后缀(即文件名最后一个.
后的字符串),如果文件没有.
那么不会返回此文件的后缀
$(basename names...)
与 suffix 相反,该函数用于取除后缀意外的文件名部分(未包含前面路径部分)
$(addsuffix suffix,name...)
给 name... 序列中每一个文件追加后缀
$(addprefix prefix,name...)
给 name... 序列中每一个文件追加前缀
$(join list1,list2)
将两个 list 拼接到一起 - 遍历
$(foreeach var,list,text)
把参数 list 中的单词逐一取出放到参数 var 所指定的变量中,然后再执行 text 所包含的表达式。
在查看一些开源的代码时,我们经常会看到一个 makefile 中通常都会包含一些共同的 target, 他们完成的功能也大体相同, 而新手了解这些 target 的体意图之后,会提高自己的分析代码效率,而如果你在开发过程中也遵循这些通识,会让你的代码可阅读性更强,结构更完整。 从而间接的提升你的研发效率。
- all 这个伪目标是所有目标的目标,其功能一般是编译所有的目标
- clean mostlyclean distclean realclean clobber 这些目标则代表功能是删除所有被 make 创建的文件,而具体的差别在于清理的程度。后四者清理的程度相对于 clean 会更多一点,不过这也是工程实际所考虑的问题
- install 这个伪目标功能是安装已编译好的程序,其实就是把目标执行文件拷贝到指定的目标中去。
- print 这个伪目标的功能是例出改变过的源文件。
- tar 这个伪目标功能是把源程序打包备份。也就是一个tar文件
- shar
- dist 个伪目标功能是创建一个压缩文件,一般是把tar文件压成Z文件。或是gz文件
- TAGS 这个伪目标功能是更新所有的目标,以备完整地重编译使用。
- check && test 这两个伪目标一般用来测试makefile的流程。
- gcc 中常用的一些语法
首先是编译,两次面试都碰到这个问题,其实在实际应用中很少用到,但到特殊情况下不知道也很麻烦,从 c 的源码编译到可执行文件经历着这么四个步骤
预处理(生成预处理文件)--> 编译(产生汇编文件) --> 汇编(产生机器可识别文件) --> 链接(将各个分散的文件链接到一起,并生成程序入口)
。通常在编译的时候,可能要自己加入一些指定的库,头文件目录等,其中-I/path
指定头文件目录,-L/path
指定库文件的目录,-lname (ex:-lmath)
指定库文件的名称(可省略 lib 的前缀),-D
指定全局的符号常量(也就是 C 文件中的宏定义常量,在编译预处理的时候将会作为文件中的普通宏定义常量加入计算),-Wall
该选项会发现程序中一系列的常见错误报告,具体包含哪些选项请 man 一下(不知道 man 什么意思,google 一下 ‘linux man’),-Werror
把所有的警告当作错误处理。 - debug 程序时在文件中用到的宏
__DATA__
获取当前日期,__TIME__
获取当前时间,__LINE__
获取打印信息在当前文件中的行号,__FILE__
���取当前代码所在文件的文件名,__STDC__
如果当前编译器符合 ISO 标准,其宏值为1,__STDC_VERSION__
C标准版本(如果版本为 C89 那么值为 199409L,如果版本为 C99 那么值为 199901L),__STDC_HOSTED__
本地系统(hosted,表示拥有完整的标准 C 库)值为1。 - 平台区分(Machine-Dependent)
这里之列举常见的,这些参数也是可在编译不同平台做代码兼容而使用,
x86/Windows
如果在文件中发现这个宏为真,则标识当前编译平台为 x86/Windows 平台,注意有 x86/Windows 和 x86 之分,Darwin
苹果 OSX 平台,MIPS
一看便知,GNU/Linux
同上...., 用法嘛自己去搜索,也可以在 github 上下个多平台兼容的项目下来研究研究。 - C++11 新新特性
在这里只是简单的介绍一下各个特性的关键作用,详细的解析与用法请参考后面的
[link3]
传送门。auto
相当一个泛型变量类型,可用于生成任何变量,变量的类型会由编译器自动分配;decltype
获取一个变量的变量类型,可用于申请新的变量;nullptr
空指针,以前的空指针用 0 表示,但是 0 被隐式的保存为整形,目的就是为了避免此种情况;for
支持像 java 类似的 foreach 特性;Lambda 表达式
相当于 java 中的闭包;make_tuple()
变长参数申请;新类型指针(unique_ptr, shared_ptr, weak_ptr)
;新枚举类型
像 java 一样封装到一个类里面......(参考文档中,这篇很值得看 --> C++开发者都应该使用的10个C++11特性) - gcc 参数
-x // 指定文件所用的语言类型;可选项("c","objective-c","c-header","c++", // "cpp-output","assembler","assembler-with-cpp") -x none // 关闭对任何语言的说明 -M // 输出文档所直接和间接依赖头文件, 用法:gcc -M main.c) -MM // 输出文档所直接依赖的头文件 -O[n] // 不使用`-O'选项时,编译器的目标是减少编译的开销,使编译结果能够调试. // 意味着语句是独立的:如果在两条语句之间用断点中止程序,你可以对任何变量重新 // 赋值,或者在函数体内把程序计数器指到其他语句,以及从源程序中 精确地获取你期待的结果. // 使用了`-O'选项,编译器会试图减少目标码的大小和执行时间. // n=1: 对于大函数,优化编译占用稍微多的时间和相当大的内存. // n=2: 包含 n=1 的情况,几乎执行所有的优化工作, // n=3: 在 n=1 的情况下还打开了 -finline-functions 选项 // n=0: 不优化 .. 详细请移步至 LINK1 or LINK5.
-
查看当前系统中存在那些可以连接到的库
ldconfig -p
,更多选项在下面说明:ldconfig 的参数 -v ldconfig 将扫描以及打印能搜到的目录(包括默认的 /lib,/usr/lib 和配置到 /etc/ld.so.conf 中的)。 -n 仅仅扫描打印指定目录。 -N 不重建 /etc/ld.so.cache 目录,但为指定 -X 选项运行 ldconfig 的时候照样会更新文件的连接。 -X 不更新文件的连接 -f 此参数指定动态库的配置文件,默认是 /etc/ld.so.conf -C 此参数指定动态库的缓存文件,默认是 /etc/ld.so.cache -r 此参数指定动态索引时的根目录,默认为 / ,比如在调用 /etc/ld.so.conf 时实际调用的是 //etc/ld.so.conf -l 进入专家模式手动建立动态库的连接 -p 打印所有共享库的名字 -c 用于指定缓存文件所使用的格式,共有三种:old(老格式),new(新格式)和compat(兼容格式,此为默认格式). -V 打印 ldconfig 的版本信息
![]() |
![]() |