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”选项。
Tips
  • 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
    1. make 会将 makefile 中找到的第一个 target 作为默认 target
    2. make 默认会寻找 Makefile 为名的文件(或者是 makefile ,GNUmakefile 视具体的工具而定),但如果你使用的 -f 参数,则会直接采用指定的文件
    3. 在 makefile 中使用 include 包含外部文件之后,外部文件会直接展开到 include 的位置
    4. 在编写目标的规则脚本前加上 @ 可以在编译的时候不打印规则本身
    5. 正常情况下,规则脚本执行如果报错,则会导致 make 终止运行,但是在规则之前加上 - 可以让 make 忽略报错
    6. 在 make 的时候还可以追加 -n 或者 --just-print 参数,这回打印出目标规则所有的执行语句,但不会执行他们,非常适合调试的时候使用。
    7. 当依赖目标新于目标时,也就是当规则的目标需要被更新时,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 的版本信息
    
原创文章,版权所有,转载请获得作者本人允许并注明出处
我是留白;我是留白;我是留白;(重要的事情说三遍)
posted @ 2021-04-23 00:23  Mojies  阅读(105)  评论(0编辑  收藏  举报