Makefile learn

@think3r 2019-08-13 22:31:16 参考链接:

  1. 跟我一起学 Makefile
  2. GNU make 中文手册.pdf
  3. GNU make - online
  4. 万能 makefile 写法详解,一步一步写一个实用的 makefile
  5. 显示 (gcc)make时实际执行命令

0x00 基础

  • 编译和链接:
    • 一般来说,无论是 C 还是 C++,首先要把源文件编译成中间代码文件,在 Windows 下也就是 .obj 文件,UNIX 下是 .o 文件,即 Object File,这个动作叫做 编译(compile)
      • 编译时,编译器需要的是语法的正确,函数与变量的声明的正确。
      • 如果函数未被声明,编译器会给出一个警告,但可以生成 Object File。
    • 然后再把大量的 Object File 合成执行文件,这个动作叫作 链接(link)
      • 链接时,主要是链接函数和全局变量。链接器并不管函数所在的源文件,只管函数的中间目标文件(Object File).
      • 而在链接程序时,链接器会在所有的 Object File 中找寻函数的实现,如果找不到,那到就会报链接错误码(Linker Error)
  • Makefile 机制:
    1. 如果这个工程没有编译过,那么我们的所有 c 文件都要编译并被链接
    2. 如果这个工程的某几个 c 文件被修改,那么我们只编译被修改的 c 文件,并链接目标程序.
    3. 如果这个工程的头文件被改变了,那么我们需要编译引用了这几个头文件的 c 文件,并链接目标程序。
  • Makefile 格式:
    • 即: prerequisites 中如果有一个以上的文件比 target 文件要新的话, command 所定义的命令就会被执行。

        #makefile 规则
        target ... : prerequisites ...
            command
        ...
      
        # target 可以是一个 object fle(目标文件),也可以是一个执行文件,还可以是一个标签(label)。可以有多个, 可以有通配符,
        # prerequisites 生成该 target 所依赖的文件和/或 target
        # command 该 target 要执行的命令(任意的 shell 命令) 一定要以 tab 开头!
      
    • make 并不管命令是怎么工作的,他只管执行所定义的命令。 make 会比较 targets 文件和 prerequisites 文件的修改日期,如果 prerequisites 文件的日期要比 targets 文件的日期要新,或者target 不存在的话,那么, make 就会执行后续定义的命令。
    • 若冒号后什么也没有,那么, make 就不会自动去找它的依赖性,也就不会自动执行其后所定义的命令。要执行其后的命令,就要在 make 命令后明显得指出这个 label 的名字。
    • 在找寻的过程中,如果出现错误,比如最后被依赖的文件找不到,那么 make 就会直接退出,并报错,
    • 而对于所定义的命令的错误,或是编译不成功, make 根本不理。
  • **`make`** 是如何工作的 / `make` 的执行步骤 :
    1. make 会在当前目录下找名字叫 Makeflemakefle 的文件。大多数的 make 都支持 “makefle” 和 “Makefle” 这两种默认文件名。
      • 也可以指定文件: make -f Make.Linux
    2. 如果找到,读入被 include 的其它 Makefle。
      • 当该文件不存在时,make 会寻找是否有生成它的规则,如果有,则执行其生成命令,然后再尝试读入。(其规则的使用, 参考 `自动生成依赖章节` )
      • 在 include 前加减号 - 可以上 make 忽略其产生的错误,并不输出任何错误信息。
    3. 初始化文件中的变量。
    4. 它会找文件中的第一个目标文件(target),并把这个文件作为最终的目标文件。
      • Makefle 中只应该有一个最终目标,其它的目标都是被这个目标所连带出来的,所以一定要让 make 知道你的最终目标是什么。
    5. 根据 Makefile 机制递归执判断文件依赖, 并执行相关命令;
      • 推导隐晦规则,并分析所有规则
      • 为所有的目标文件创建依赖关系链
      • 根据依赖关系,决定哪些目标要重新生成
      • 执行生成命令
  • Makefile 里包含什么
    1. 显式规则。显式规则说明了如何生成一个或多个目标文件。这是由 Makefle 的书写者明显指出要生成的文件、文件的依赖文件和生成的命令。
    2. 隐晦规则。由于我们的 make 有自动推导的功能,所以隐晦的规则可以让我们比较简略地书写 Makefle,这是由 make 所支持的。
    3. 变量的定义。在 Makefle 中我们要定义一系列的变量,变量一般都是字符串,这个有点像你 C 语言中的宏,当 Makefle 被执行时,其中的变量都会被扩展到相应的引用位置上。
    4. 文件指示。其包括了三个部分,一个是在一个 Makefle 中引用另一个 Makefle,就像 C 语言中的 #include 一样;另一个是指根据某些情况指定 Makefle 中的有效部分,就像 C 语言中的预编译 #if 一样;还有就是定义一个多行的命令。
    5. 注释。 Makefle 中只有行注释,和 UNIX 的 Shell 脚本一样,其注释是用 # 字符,这个就像 C/C++ 中的 // 一样。如果你要在你的 Makefle 中使用 # 字符,可以用反斜杠进行转义,如: \#
  • makefile 的自动推导:
    • 只要 make 看到一个 .o 文件,它就会自动的把 .c 文件加在依赖关系中,如果 make 找到一个whatever.o ,那么 whatever.c 就会是 whatever.o 的依赖文件。

0x01 变量,函数,命令,条件判断

命令

  • 每条规则中的命令和操作系统 Shell 的命令行是一致的。 make 会一按顺序一条一条的执行命令,每条命令的开头必须以 Tab 键开头,除非,命令是紧跟在依赖规则后面的分号后的。在命令行之间中的空格或是空行会被忽略,但是如果该空格或空行是以 Tab 键开头的,那么 make 会认为其是一个空命令。
    • 如果你要让上一条命令的结果应用在下一条命令时,你应该使用分号分隔这两条命令
  • 通常, make 会把其要执行的命令行在命令执行前输出到屏幕上。当我们用 @ 字符在命令行前,那么,这个命令将不被 make 显示出来,
    • echo 使用 -e 可以打印出带颜色的输出;
  • 而在 rm 命令前面加了一个小减号 - 的意思就是,也许某些文件出现问题,但不要管,继续做后面的事。但 make 仍会报错:
    • -mkdir test 的第二次输出:

      mkdir: 无法创建目录 “test”: 文件已存在; make: [clean] 错误 1 (忽略)

    • make 的参数的是 -k 或是 --keep-going ,这个参数的意思是,如果某规则中的命令出错了,那么就终止该规则的执行,但继续执行其它规则。
  • 调试 :
    • 如果 make 执行时,带入 make 参数 -n--just-print,那么其只是显示命令,但不会执行命令,这个功能很有利于我们调试我们的 Makefle,看看我们书写的命令是执行起来是什么样子的或是什么顺序的。
    • 使用 autotools 或者 cmake 时, 你还可以使用 make VERBOSE=1make V=1 来获取到全部的命令.
  • make 参数 -s--silent--quiet 则是全面禁止命令的显示。
  • 大型工程中的嵌套 make :
    • cd subdir && $(MAKE) 或者 $(MAKE) -C subdir
    • 如果你要传递所有的变量,那么,只要一个 export 就行了。后面什么也不用跟,表示传递所有的变量。
    • 有两个变量,一个是 SHELL ,一个是 MAKEFLAGS ,这两个变量不管你是否 export,其总是要传递到下层 Makefle 中,特别是 MAKEFLAGS 变量,其中包含了 make 的参数信息,
    • `MAKEFLAGS = -j` 启动默认多核编译!

变量

  • Makefle 中的变量其实就是 C/C++ 中的宏。如果你要让通配符在变量中展开,也就是让 objects 的值是所有 .o 的文件名的集合(objects = *.o),那么,你可以这样:
    • 列出一确定文件夹中的所有 .c 文件: objects := $(wildcard *.c)
    • 列出 (1) 中所有文件对应的 .o 文件: objects := $(patsubst %.c,%.o,$(wildcard *.c))
    • 变量是大小写敏感的
    • 推荐使用大小写搭配的变量名,如: MakeFlags。这样可以避免和系统的变量冲突,而发生意外的事情。
    • 变量在声明时需要给予初值,而在使用时,需要给在变量名前加上 $ 符号
    • 变量可以使用在许多地方,如规则中的“目标”、“依赖”、“命令”以及新的变量中。
  • make 支持三个通配符: *?~
    • 波浪号(~ )字符在文件名中也有比较特殊的用途。如果是 ~/test ,这就表示当前用户的 $HOME 目录下的 test 目录。而 ~hchen/test 则表示用户 hchen 的宿主目录下的 test 目录。(这些都是 Unix 下的小知识了, make 也支持)
    • 通配符代替了你一系列的文件,如 *.c 表示所有后缀为 c 的文件。\
  • 模式:
    • 在模式规则中,目标名中需要包含有模式字符 % (一个),包含有模式字符 % 的目标被用来匹配一个文件名,“%”可以匹配任何非空字符串。
    • 规则的依赖文件中同样可以使用 %,依赖文件中模式字符“%”的取值情况由目标中的 % 来决定。例如:对于模式规则 %.o : %.c,它表示的含义是:所有的 .o 文件依赖于对应的 .c 文件。
    • 要注意的是:模式字符 % 的匹配和替换发生在规则中所有变量和函数引用展开之后,变量和函数的展开一般发生在 make 读取 Makefile。
    • 在模式规则中,目标文件是一个带有模式字符 % 的文件,使用模式来匹配目标文件。文件名中的模式字符 % 可以匹配任何非空字符串,除模式字符以外的部分要求一致。例如:
      • %.c 匹配所有以 “.c” 结尾的文件(匹配的文件名长度最少为 3 个字母),
      • s%.c 匹配所有第一个字母为 “s”,而且必须以 “.c” 结尾的文件,文件名长度最小为 5 个字符(模式字符 % 至少匹配一个字符)。
  • Makefile 中 := ?= += = 的区别
  • = 是最基本的赋值, 变量的值是整个 makefil e中 最后 被指定的值。
  • := 是覆盖之前的值, 直接赋值,赋予当前位置的值。
  • ?= 是如果没有被赋值过就赋予等号后面的值
  • += 是添加等号后面的值
  • 有时候需要实现 Makefile 中没有的 -= 功能, 这时候可以采用 shell 命令来实现 :
    • C_Flags -= -Wconversion 等价于下方的 :
    • C_Flags:=$(shell echo $(C_Flags)|sed -s 's/-Wconversion//g')
  • 我们可以替换变量中的共有的部分,其格式是 $(var:a=b) 或是 ${var:a=b} ,其意思是,把变量 “var” 中所有以 “a” 字串 “结尾” 的 “a” 替换成 “b” 字串。这里的 “结尾” 意思是 “空格” 或是 “结束符” 。
    • bar := $(foo:%.o=%.c)
    • 注意 var 和 : 之间不能有空格;
  • 目标变量:
    • 某个目标设置局部变量,这种变量被称为“Target-specifc Variable”,它可以和“全局变量”同名,因为它的作用范围只在这条规则以及连带规则中,所以其值也只在作用范围内有效。而不会影响规则链以外的全局变量的值。
      • prog : C_Flags = -g : 对 prog 目标添加 -g 调试选项;
      • ../calc_base/cpu_calc.o : C_Flags += -O2 : 在编译时, 对 cpu_calc.o 单独进行 -O2 优化.
  • 模式变量:
    • 模式变量的好处就是,我们可以给定一种“模式”,可以把变量定义在符合这种模式的所有目标上。
      • 如: %test.o : C_Flags += -O2, 针对所有 *test.o 文件专门进行 -O2 优化;
  • 自动化变量:
    • $@ : 表示规则中的目标文件集。在模式规则中,如果有多个目标,那么, $@ 就是匹配于目标中模式定义的集合。
    • $% : 仅当目标是函数库文件中,表示规则中的目标成员名。
      • 例如,如果一个目标是 foo.a(bar.o),那么, $% 就是 bar.o , $@ 就是 foo.a 。如果目标不是函数库文件(Unix 下是 .a , Windows下是 .lib ),那么,其值为空。
    • $< : 依赖目标中的第一个目标名字。如果依赖目标是以模式(即 % )定义的,那么 $< 将是符合模式的一系列的文件集。注意,其是一个一个取出来的。
    • $? : 所有比目标新的依赖目标的集合。以空格分隔。
    • $^ : 所有的依赖目标的集合。以空格分隔。如果在依赖目标中有多个重复的,那个这个变量会去除重复的依赖目标,只保留一份。
    • $+ : 这个变量很像 $^ ,也是所有依赖目标的集合。只是它不去除重复的依赖目标。

判断

  • ifeq/ifneq/ifdef、 else 和 endif,
    • 这一行上,多余的空格是被允许的,但是不能以 Tab 键作为开始(不然就被认为是命令)。
<conditional-directive>
<text-if-true>
else
<text-if-false>
endif
  • make 是在读取 Makefle 时就计算条件表达式的值,并根据条件表达式的值来选择语句,
    • 所以, 你最好不要把自动化变量(如 $@ 等)放入条件表达式中,因为自动化变量是在运行时才有的

函数

  • 函数调用后,函数的返回值可以当做变量来使用.
  • 函数的使用方法: $(<function> <arguments>)
    • <function> 就是函数名,
    • <arguments> 为函数的参数,参数间以逗号 , 分隔,而函数名和参数之间以“空格”分隔。
    • 函数调用以 $ 开头,以圆括号或花括号把函数名和参数括起。
  • 字符串处理函数
    • $(subst <from>,<to>,<text>)
      • 名称:字符串替换函数
      • 功能:把字串 <text> 中的 <from> 字符串替换成 <to>
      • 返回:函数返回被替换过后的字符串。
      • demo: $(subst ee,EE,feet on the street)
        • 结果: fEEt on the strEEt
    • $(patsubst <pattern>,<replacement>,<text>)
    • $(strip <string>)
      • 去掉 <string> 字串中开头和结尾的空字符。
    • $(filter <pattern...>,<text>)
      • <pattern> 模式过滤 <text> 字符串中的单词,保留符合模式 <pattern> 的单词。可以有多个模式, 以空格分隔。
  • 文件名操作函数
    • $(dir <names...>)
      • 从文件名序列 <names> 中取出目录部分。目录部分是指最后一个反斜杠(/ )之前的部分。如果没有反斜杠,那么返回 ./
    • $(notdir <names...>)
      • 从文件名序列 <names> 中取出非目录部分。非目录部分是指最後一个反斜杠(/ )之后的部分。
  • $(join <list1>,<list2>)
    • <list2> 中的单词对应地加到 <list1> 的单词后面。如果 <list1> 的单词个数要比<list2> 的多,那么, <list1> 中的多出来的单词将保持原样。如果 <list2> 的单词个数要比 <list1> 多,那么, <list2> 多出来的单词将被复制到 <list1> 中。
  • $(foreach <var>,<list>,<text>)
    • 这个函数的意思是,把参数 <list> 中的单词逐一取出放到参数 <var> 所指定的变量中,然后再执行 <text> 所包含的表达式。每一次 <text> 会返回一个字符串,循环过程中, <text> 的所返回的每个字符串会以空格分隔,最后当整个循环结束时, <text> 所返回的每个字符串所组成的整个字符串(以空格分隔)将会是 foreach 函数的返回值。
  • shell 函数:
    • cur-dir := $(shell pwd)
    • 这个函数会新生成一个 Shell 程序来执行命令,
    • 所以你要注意其运行性能,如果你的 Makefle 中有一些比较复杂的规则,并大量使用了这个函数,那么对于你的系统性能是有害的。
  • -$(subst output,,$@) 中的 $ 表示执行一个 Makefle 的函数,函数名为 subst,后面的为参数。
  • $@ 这个变量表示着目前规则中所有的目标的集合, 就像一个数组.

静态模式 demo

  • 下面的例子中,

    #静态模式
    objects = foo.o bar.o
    all: $(objects)
    $(objects): %.o: %.c
    $(CC) -c $(CFLAGS) $< -o $@
    
    • 指明了我们的目标从 $object 中获取, %.o 表明要所有以 .o 结尾的目标,也就是 foo.o bar.o ,也就是变量 $object 集合的模式,
    • 而依赖模式 %.c 则取模式 %.o 的 % ,也就是 foo bar,并为其加下 .c 的后缀,于是,我们的依赖目标就是 foo.c bar.c 。
    • 而命令中的 $< 和 $@ 则是自动化变量, $< 表示第一个依赖文件, $@ 表示目标集(也就是“foo.o bar.o”)。

      files = foo.elc bar.o lose.o
      $(filter %.o,$(files)): %.o: %.c
      $(CC) -c $(CFLAGS) $< -o $@
      $(filter %.elc,$(files)): %.elc: %.el
      emacs -f batch-byte-compile $<
      

自动生成依赖

  • 在 Makefle 中,我们的依赖关系可能会需要包含一系列的头文件,但如果是一个比较大型的工程,你必需清楚哪些 C 文件包含了哪些头文件,并且,你在加入或删除头文件时,也需要小心地修改 Makefle,这是一个很没有维护性的工作。
    • 为了避免这种繁重而又容易出错的事情,我们可以使用 C/C++ 编译的一个功能。大多数的 C/C++ 编译器都支持一个 -M 的选项,即自动找寻源文件中包含的头文件,并生成一个依赖关系。
  • 下面介绍 GCC -M 相关选项的用法:
    • -M : 生成文件关联的信息。包含目标文件所依赖的所有源代码, 包含了库的 头文件;
    • -MM : 与 -M 相比, 忽略了库文件;
    • -MMD : 和 -MM 相同,但是输出将导入到 .d 的文件里面, 且编译源文件:
    • -MD : 与 -MMD 类似,只是导出的内容有所不一样,内容与 -M 是一样的, 也会编译源文件;
    • -MF : 指一个文件用于存放生成文件的关联信息,这些信息与 -M-MM 是一样的,所以要与 -M-MM 一些使用,否则会报错,
    • -MT : 指定目标文件名
  • 从上面make的执行过程中可看出,要动态生成依赖关系,只能利用第 2 步读入其它 Makefile 的机制。
    • 那么,我们是否可以先把生成的依赖关系保存到文件,然后再把该文件的内容包含进来?
    • 答案是 Yes! 要利用 makefile 的 include 机制。

示例:

include $(sources:.c=.d)

#所有的 .d 文件依赖于 .c 文件,
%.d: %.c
    # @关键字告诉 make 不输出该行命令;set -e 的作用是,当后面的命令的返回值非0时,立即退出。
    @set -e; \

    # 删除所有的目标,也就是 .d  文件
    rm -f $@; \

    # 为每个依赖文件 $< ,也就是 .c 文件生成依赖文件,$@ 表示模式 %.d 文件,  \
    # $$$$ 为字符串 "$$", 而 $$ 是 shell 的特殊变量,它的值为当前进程号 \
    # (使用进程号为后缀的名称创建临时文件,是shell编程常用做法,这样可保证文件唯一性。)
    $(CC) -MM $(CPPFLAGS) $< > $@.$$$$; \

    # 使用 sed 命令做了一个替换,
    sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \

    #第四行就是删除临时文件。
    rm -f $@.$$$$
  • 下面介绍下上述 sed 的用法:
    • 这条 sed 命令的结构是 s/match/replace/g 。有时为了清晰,可以把每个 / 写成逗号,即这里的格式 s,match,replace,g 。
    • 该命令表示把源串内的 match 都替换成 replace,s 指示 match 可以是正则表达式。
    • g 表示把每行内所有 match 都替换,如果去掉 g,则只有每行的第1处 match 被替换(实际上不需要 g,因为一个 .d 文件中,只会在开头有一个 main.o:)。
    • 这里 match 是正则式;
  • 总而言之,这个模式要做的事就是在编译器生成的依赖关系中加入 .d 文件的依赖,
    • 即: main.o : main.c defs.h -> main.o main.d : main.c defs.h
  • 我们的 .d 文件也会自动更新了,并会自动生成了,当然,你还可以在这个 .d 文件中加入的不只是依赖关系,包括生成的命令也可一并加入,让每个 .d 文件都包含一个完赖的规则。

技巧总结

  • 你希望第二条命令得在 cd 之后的基础上运行,那么你就不能把这两条命令写在两行上,而应该把这两条命令写在一行上,用分号分隔。

  • 通常, make 会把其要执行的命令行在命令执行前输出到屏幕上。当我们用 @ 字符在命令行前,那么,这个命令将不被 make 显示出来,
    • echo 使用 -e 可以打印出带颜色的输出;
  • 而在 rm 命令前面加了一个小减号 - 的意思就是,也许某些文件出现问题,但不要管,继续做后面的事。但 make 仍会报错:

      -mkdir test
      mkdir: 无法创建目录 "test": 文件已存在; make: [clean] 错误 1 (忽略)
    
  • clean 的规则不要放在文件的开头,不然,这就会变成 make 的默认目标,相信谁也不愿意这样。不成文的规矩是——“clean 从来都是放在文件的最后”.
  • 反斜杠(\ )是换行符的意思。这样比较便于 makefle 的阅读。
    • 如果命令太长,你可以使用反斜杠(\ )作为换行符。 make 对一行上有多少个字符没有限制。
  • 一般来说, make 会以 UNIX 的标准 Shell,也就是 /bin/sh 来执行命令。