编译随笔(一)makefile基础知识

编译随笔系列文章目录

1. makefile基础知识


文章目录

  • 编译随笔系列文章目录
  • 前言
  • 参考资料
  • 前置知识
    • 交叉编译链
    • 程序编译
      • 预处理(Preprpcessing)
      • 编译(Compilation)
      • 汇编(Assemble)
      • 链接(Linking)
  • makefile作用
  • makefile介绍
    • make和makefile的关系
    • makefile原则
    • Makefile基本模板
    • make工作方式
    • 示例代码
    • makefile依赖性
  • makefile知识总览
  • make规则
    • 伪目标
    • 编译规则
    • 显示规则
    • 执行规则
    • 出错规则
    • 嵌套规则
    • 变量传递规则
    • 宏定义规则
    • 隐含规则
      • 常见隐含规则
      • 隐含规则使用的变量
        • 关于命令的变量。
        • 关于命令参数的变量
      • 定义规则模式
  • make执行
    • 退出码
    • 指定makefile
    • 指定目标
    • make命令常见参数
      • 不实际执行
      • 更新目标时间
      • 寻找目标
      • 指定文件
  • makefile语法
    • 变量
      • 基本语法
      • 自动化变量
      • 环境变量
      • 目标变量
      • 模式变量
      • 特殊变量
        • VPATH
      • 优化代码
    • 分支
    • 函数
      • 字符串处理函数
      • 文件名操作函数
      • wildcard函数
      • foreach函数
      • if函数
      • call / origin / shell
      • 控制make的函数
    • 文件指示
    • 多目标
      • 静态模式
    • 自动生成依赖性
      • 实际应用
  • 调试手段todo
  • 总结


前言


参考资料

左耳朵耗子《跟我一起写 Makefile》
https://haoel.blog.csdn.net/article/details/2886

[野火]i.MX Linux开发实战指南
https://doc.embedfire.com/linux/imx6/base/zh/latest/linux_app/Makefile_brife.html


前置知识

交叉编译链

当编译器和目标程序都是相同架构的编译过程,被称为本地编译

而当前我们希望的是编译器运行在x86架构平台上,编译生成ARM架构的可执行程序,
这种编译器和目标程序运行在不同架构的编译过程,被称为交叉编译

既然已经有本地编译,为什么需要交叉编译?
这是因为通常编译工具链对编译环境有较高的要求,编译复杂的程序时,可能需要巨大的存储空间以及强大的 CPU 运算能力加快编译速度。
常见的 ARM 架构平台资源有限,无论是存储空间还是 CPU 运算能力,都与 X86 平台相去甚远,特别是对于 MCU 平台,安装编译器根本无从谈起。
有了交叉编译,我们就可以在 PC 上快速编译出针对其他架构的可执行程序。

相对的,能进行架构“交叉”编译过程的编译器,就被称为交叉编译器(Cross compiler)
交叉编译器听起来是个新概念,但在 MCU 开发中一直使用的就是交叉编译器,
例如开发STM32、RT1052所使用的 IDE 软件 Keil(MDK)或IAR,就是在Windows x86架构编译,生成MCU平台的应用程序,最后下载到板子执行。

程序编译

我们日常说的程序编译,实际上由两个部分组成:预处理(Preprpcessing) + 编译(Compile) + 汇编(Assemble) + 链接(Link)。
.c源码文件,经过 编译(compile) 生成.o中间文件,然后再将大量的 .o文件 经过 链接(link) 合成为最后的可执行文件。
S01E01_all_step

预处理(Preprpcessing)

使用预处理器把源文件test.c经过预处理生成test.i文件,
预处理用于将所有的#include头文件以及宏定义替换成其真正的内容。

预处理的命令为:

    gcc -E test.c -o test.i

上述命令中-E是让编译器在预处理之后就退出,不进行后续编译过程;-o是指定输出文件名。
预处理之后得到的仍然是文本文件。test.i文件部分内容截图如下:
S01E02_test_i

编译(Compilation)

使用编译器将预处理文件test.i编译成汇编文件test.s。
编译的命令为:

    gcc -S test.i -o test.s

上述命令中-S让编译器在编译之后停止,不进行后续过程;-o是指定输出文件名。
汇编文件test.s是文本文件,部分内容截图如下:
S01E03_test_s

汇编(Assemble)

使用汇编器将汇编文件test.s转换成目标文件test.o。
汇编过程的命令为:

    gcc -c test.s -o test.o

上述命令中-c、-o让汇编器把汇编文件test.s转换成目标文件test.o。
目标文件test.o是二进制文件,部分内容截图如下:
S01E04_test_o

链接(Linking)

链接过程使用链接器将该目标文件与其他目标文件、库文件、启动文件等链接起来生成可执行文件
链接过程的命令为:

    gcc test.o -o test.exe

makefile作用

当.c文件只有几个时,可以直接使用gcc命令完成编译,如下所示

~/lldp_test $ > gcc lldp_test_DevA.c -o test
~/lldp_test $ > ls -al
总用量 76
drwxr-xr-x  2 rgos rgos  4096 34 08:11 .
drwxrwxr-x 13 rgos rgos  4096 220 08:20 ..
-rwxr--r--  1 rgos rgos  3580 1031 02:40 lldp_test_DevA.c
-rwxrwxr-x  1 rgos rgos 17552 34 08:11 test

当随着实现的功能越来越多,工程的代码与文件量也在不断增加,此时再用gcc + 一串文件的方式进行编译就不合适了。

所以这时候makefile闪亮登场,makefile定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,
甚至于进行更复杂的功能操作,因为makefile就像一个Shell脚本一样,其中也可以执行操作系统的命令。

只要在makefile的同级目录下输入一个 make 命令,即可按照makefile里的内容自动对已修改或是未编译的文件完成编译,极大地提升了开发的效率。

make是一个命令工具,是一个解释makefile中指令的命令工具,一般来说,大多数的IDE都有这个命令,
比如:Delphi的make,Visual C++的nmake,Linux下GNU的make。可见,makefile都成为了一种在工程方面的编译方法。

makefile介绍

make和makefile的关系

  • make工具
    它可以帮助我们找出项目里面修改变更过的文件,并根据依赖关系,找出受修改影响的其他相关文件,然后对这些文件按照规则进行单独的编译,
    这样一来,就能避免重新编译项目的所有的文件。
  • Makefile文件
    上面提到的规则、依赖关系主要是定义在这个Makefile文件中的,我们在其中合理地定义好文件的依赖关系之后,make工具就能精准地进行编译工作。
    S01E05_make_makefile

makefile原则

  • 如果这个工程没有编译过,那么我们的所有C文件都要编译并被链接。
  • 如果这个工程的某几个C文件被修改,那么我们只编译被修改的C文件,并链接目标程序。
  • 如果这个工程的头文件被改变了,那么我们需要编译引用了这几个头文件的C文件,并链接目标程序。

Makefile基本模板

include <filename># comment
target: prerequisites ; commandcommand;...
  • target
    • 可以作为目标文件,用于指定最终的可执行文件或者是中间文件,同样也可以是当标签(Label)使用,用于各个makefile之间进行跳转遍历。
    • 需要顶格书写,前面不能有空格或Tab
  • prerequisites
    • 生成那个target所需要的文件或是目标,也就是所说要实现target前需要完全的依赖。
    • 当prerequisites发现变动,比target新时,command所定义的命令就会被执行。这就是Makefile的规则。也就是Makefile中最核心的内容。
  • command
    • make需要执行的命令,当prerequisites满足后,跳转执行command命令。
    • 要特别注意命令的开头要用 Tab 键,不能使用空格代替,有的编辑器会把Tab键自动转换成空格导致出错,若出现这种情况请检查自己的编辑器配置。
  • comment
    • Makefile中只有行注释,和UNIX的Shell脚本一样,其注释是用 # 字符,这个就像C/C++中的“//”一样。
    • 如果要在Makefile中使用 # 字符,可以用反斜框进行转义,如:/#
  • include
    • filename可以是当前操作系统Shell的文件模式(可以保含路径和通配符)
    • make命令开始时,会把找寻include所指出的其它Makefile,并把其内容安置在当前的位置。
    • 如果文件都没有指定绝对路径或是相对路径的话,make会在当前目录下首先寻找,如果当前目录下没有找到,那么,make还会在下面的几个目录下找:
      • 如果make执行时,有“-I”或“–include-dir”参数,那么make就会在这个参数所指定的目录下去寻找。
      • 如果目录/include(一般是:/usr/local/bin或/usr/include)存在的话,make也会去找。
    • 如果有文件没有找到的话,make会生成一条警告信息,但不会马上出现致命错误。
      • 它会继续载入其它的文件,一旦完成makefile的读取,make会再重试这些没有找到,或是不能读取的文件,如果还是不行,make才会出现一条致命信息。
      • 如果你想让make不理那些无法读取的文件,而继续执行,你可以在include前加一个减号“-”。如:-include <filename>
      • 其表示,无论include过程中出现什么错误,都不要报错继续执行。和其它版本make兼容的相关命令是 sinclude,其作用和这一个是一样的。

make工作方式

  1. 读入所有的Makefile。
  2. 读入被include的其它Makefile。
  3. 初始化文件中的变量。
  4. 推导隐晦规则,并分析所有规则。
  5. 为所有的目标文件创建依赖关系链。
  6. 根据依赖关系,决定哪些目标要重新生成。
  7. 执行生成命令。

示例代码

all: hello_worldhello_world: hello_world.cgcc -Wall -o hello_world hello_world.c hello_printf.cclean:rm -rf hello_world

执行结果如下:

xushiyan@xushiyan:~/Dir_share/my_test/my_makefile/test_for_md$ make
cc    -c -o hello_world.o hello_world.c
gcc -Wall -o hello_world hello_world.c hello_printf.c

makefile依赖性

  1. make 命令会在当前目录下找名字叫 Makefilemakefile 的文件
    如果要指定其它文件作为输入规则,可以通过“-f”参数指定输 入文件,如“make -f 文件名”。
  2. 当执行一个makefile时,不论目标是否会被执行,首先都会去遍历公共区域的内容和所有目标的依赖(prerequisites),而后才是执行具体目标
  3. 若输入的 make 命令里没有指定任何目标,则它会找文件中的第一个目标文件(target),如上文的 all,作为最终的目标文件
    像 clean 这种,没有被第一个目标文件直接或间接关联,那么它后面所定义的命令将不会被自动执行,
    此时可以使用 make clean 的命令来指定目标执行
  4. 当找到目标all后,会去判断all目标的依赖是否有更新,存在且已是最新则不操作
    其中,若存在同名目标,则不会执行操作(all文件不受该规则影响,原因未知),故此时需引入伪目标。
  5. 如果hello_world文件不存在,或是hello_world所依赖的后面的 .o 文件的文件修改时间要比hello_world这个文件新,
    那么,他就会执行后面所定义的命令来生成hello_world这个文件。
  6. make会一层又一层地去找文件的依赖关系,直到最终编译出第一个目标文件,然后不断反递归生成最终目标文件
  7. 在找寻的过程中,如果出现错误,比如最后被依赖的文件找不到,那么make就会直接退出,并报错,
    而对于所定义的命令的错误,或是编译不成功,make根本不理。
    make只管文件的依赖性,即,如果在我找了依赖关系之后,冒号后面的文件还是不在,那么对不起,我就不工作啦。
  8. 如果hello_world所依赖的.o文件也存在,那么make会在当前文件中找目标为.o文件的依赖性,如果找到则再根据那一个规则生成.o文件。
  9. 当然,你的C文件和H文件是存在的啦,如果对应的hello_world.c发生了改动,
    那么make会生成 .o 文件,然后再用 .o 文件生成make的终极任务,也就是执行文件hello_world了。

makefile知识总览

S01E06_makefile_step

make规则

伪目标

当目标名称与依赖文件恰好同名,即正好存在一个文件名为 clean 时,就算修改了相关.c文件,此时再执行 make clean 就无法再次触发编译。

xushiyan@xushiyan:~/Dir_share/my_test/my_makefile/test_for_md$ touch clean
xushiyan@xushiyan:~/Dir_share/my_test/my_makefile/test_for_md$ ls -al
总用量 30
drwxrwx--- 1 root vboxsf  4096 36 14:34 .
drwxrwx--- 1 root vboxsf     0 36 11:27 ..
-rwxrwx--- 1 root vboxsf     0 36 14:34 clean
-rwxrwx--- 1 root vboxsf   174 36 14:18 hello_printf.c
-rwxrwx--- 1 root vboxsf   167 21 16:03 hello_printf.h
-rwxrwx--- 1 root vboxsf 16776 36 14:34 hello_world
-rwxrwx--- 1 root vboxsf   239 21 16:03 hello_world.c
-rwxrwx--- 1 root vboxsf  1552 36 14:34 hello_world.o
-rwxrwx--- 1 root vboxsf   133 36 11:37 Makefile
xushiyan@xushiyan:~/Dir_share/my_test/my_makefile/test_for_md$ make clean
make: “clean”已是最新。
xushiyan@xushiyan:~/Dir_share/my_test/my_makefile/test_for_md$ 

此处存在一个疑问:当创建all文件时,make命令还是会触发编译,原因未知。

xushiyan@xushiyan:~/Dir_share/my_test/my_makefile/test_for_md$ touch all
xushiyan@xushiyan:~/Dir_share/my_test/my_makefile/test_for_md$ 
xushiyan@xushiyan:~/Dir_share/my_test/my_makefile/test_for_md$ make
cc    -c -o hello_world.o hello_world.c
gcc -Wall -o hello_world hello_world.c hello_printf.c
xushiyan@xushiyan:~/Dir_share/my_test/my_makefile/test_for_md$ ls -al
总用量 30
drwxrwx--- 1 root vboxsf  4096 36 14:36 .
drwxrwx--- 1 root vboxsf     0 36 11:27 ..
-rwxrwx--- 1 root vboxsf     0 36 14:36 all
-rwxrwx--- 1 root vboxsf   174 36 14:18 hello_printf.c
-rwxrwx--- 1 root vboxsf   167 21 16:03 hello_printf.h
-rwxrwx--- 1 root vboxsf 16776 36 14:36 hello_world
-rwxrwx--- 1 root vboxsf   239 21 16:03 hello_world.c
-rwxrwx--- 1 root vboxsf  1552 36 14:36 hello_world.o
-rwxrwx--- 1 root vboxsf   133 36 11:37 Makefile
xushiyan@xushiyan:~/Dir_share/my_test/my_makefile/test_for_md$ 

为了避免这种情况,Makefile使用 .PHONY 前缀来区分目标代号和目标文件,并且这种目标代号被称为 伪目标,phony单词翻译过来本身就是假的意思。
也就是说,只要我们不期待生成目标文件,就应该把它定义成伪目标。
“伪目标”并不是一个文件,只是一个标签,由于“伪目标”不是文件,所以make无法生成它的依赖关系和决定它是否要执行。
我们只有通过显示地指明这个“目标”才能让其生效。
只要有这个声明,不管是否有“clean”文件,要运行“clean”这个目标,只有 make clean 这样。

all: hello_worldhello_world: hello_world.cgcc -Wall -o hello_world hello_world.c hello_printf.cclean:rm -rf hello_world.PHONY:clean

执行结果如下所示:

xushiyan@xushiyan:~/Dir_share/my_test/my_makefile/test_for_md$ touch clean
xushiyan@xushiyan:~/Dir_share/my_test/my_makefile/test_for_md$ ls -al
总用量 6
drwxrwx--- 1 root vboxsf 4096 36 15:25 .
drwxrwx--- 1 root vboxsf    0 36 11:27 ..
-rwxrwx--- 1 root vboxsf    0 36 15:25 clean
-rwxrwx--- 1 root vboxsf  174 36 14:18 hello_printf.c
-rwxrwx--- 1 root vboxsf  167 21 16:03 hello_printf.h
-rwxrwx--- 1 root vboxsf  239 21 16:03 hello_world.c
-rwxrwx--- 1 root vboxsf  149 36 15:25 Makefile
xushiyan@xushiyan:~/Dir_share/my_test/my_makefile/test_for_md$ make 
cc    -c -o hello_world.o hello_world.c
gcc -Wall -o hello_world hello_world.c hello_printf.c
xushiyan@xushiyan:~/Dir_share/my_test/my_makefile/test_for_md$ make clean
rm -rf hello_world

编译规则

hello_world目标文件本质上并不是依赖hello_world.c和hello_printf.c文件,而是依赖于hello_main.o和hello_func.o,
把这两个文件链接起来就能得到我们最终想要的hello_main目标文件。
另外,由于make有一条默认规则,当找不到xxx.o文件时,会查找目录下的同名xxx.c文件进行编译
根据这样的规则,我们可把Makefile改修改如下。

all: hello_worldhello_world: hello_world.o hello_printf.ogcc -Wall -o hello_world hello_world.o hello_printf.oclean:rm -rf hello_world.PHONY:clean

执行结果如下:

xushiyan@xushiyan:~/Dir_share/my_test/my_makefile/test_for_md$ make
cc    -c -o hello_world.o hello_world.c
cc    -c -o hello_printf.o hello_printf.c
gcc -Wall -o hello_world hello_world.o hello_printf.o
xushiyan@xushiyan:~/Dir_share/my_test/my_makefile/test_for_md$ ls -al
总用量 34
drwxrwx--- 1 root vboxsf  4096 36 15:36 .
drwxrwx--- 1 root vboxsf     0 36 11:27 ..
-rwxrwx--- 1 root vboxsf   174 36 14:18 hello_printf.c
-rwxrwx--- 1 root vboxsf   167 21 16:03 hello_printf.h
-rwxrwx--- 1 root vboxsf  1696 36 15:36 hello_printf.o
-rwxrwx--- 1 root vboxsf 16776 36 15:36 hello_world
-rwxrwx--- 1 root vboxsf   239 21 16:03 hello_world.c
-rwxrwx--- 1 root vboxsf  1552 36 15:36 hello_world.o
-rwxrwx--- 1 root vboxsf   164 36 15:35 Makefile
xushiyan@xushiyan:~/Dir_share/my_test/my_makefile/test_for_md$

显示规则

通常,make会把其要执行的命令行在命令执行前输出到屏幕上。
当我们用“@”字符在命令行前,那么,这个命令将不被make显示出来,最具代表性的例子是,我们用这个功能来像屏幕显示一些信息。如:

    @echo 正在编译XXX模块......

当make执行时,会输出“正在编译XXX模块…”字串,但不会输出命令,如果没有“@”,那么,make将输出:

    echo 正在编译XXX模块......正在编译XXX模块......

如果make执行时,带入make参数 -n--just-print,那么其只是显示命令,但不会执行命令,
这个功能很有利于我们调试我们的Makefile,看看我们书写的命令是执行起来是什么样子的或是什么顺序的。

而make参数“-s”或“–slient”则是全面禁止命令的显示。

执行规则

如果你要让上一条命令的结果应用在下一条命令时,你应该使用分号分隔这两条命令。
比如你的第一条命令是cd命令,你希望第二条命令得在cd之后的基础上运行,
那么你就不能把这两条命令写在两行上,而应该把这两条命令写在一行上,用分号分隔。如:

first: # 如果要让上一条命令的结果应用在下一条命令时,使用分号分隔这两条命令。cd /sbinpwdcd /sbin; pwd

执行结果如下:

# 如果要让上一条命令的结果应用在下一条命令时,使用分号分隔这两条命令。
cd /sbin
pwd
/home/xushiyan/Dir_share/my_test/my_makefile/hello_world
cd /sbin; pwd
/sbin

出错规则

每当命令运行完后,make会检测每个命令的返回码,
如果命令返回成功,那么make会执行下一条命令,当规则中所有的命令成功返回后,这个规则就算是成功完成了。
如果一个规则中的某个命令出错了(命令退出码非零),那么make就会终止执行当前规则,这将有可能终止所有规则的执行。

有些时候,命令的出错并不表示就是错误的。
例如mkdir命令,我们一定需要建立一个目录,如果目录不存在,那么mkdir就成功执行,万事大吉,如果目录存在,那么就出错了。
我们之所以使用mkdir的意思就是一定要有这样的一个目录,于是我们就不希望mkdir出错而终止规则的运行。

为了做到这一点,忽略命令的出错,我们可以在Makefile的命令行前加一个减号“-”(在Tab键之后),标记为不管命令出不出错都认为是成功的

还有一个全局的办法是,给make加上“-i”或是“–ignore-errors”参数,那么,Makefile中所有命令都会忽略错误。
而如果一个规则是以“.IGNORE”作为目标的,那么这个规则中的所有命令将会忽略错误。
这些是不同级别的防止命令出错的方法,你可以根据你的不同喜欢设置。

还有一个要提一下的make的参数的是“-k”或是“–keep-going”,
这个参数的意思是,如果某规则中的命令出错了,那么就终目该规则的执行,但继续执行其它规则。

嵌套规则

在一些大的工程中,我们会把我们不同模块或是不同功能的源文件放在不同的目录中,
我们可以在每个目录中都书写一个该目录的Makefile,这有利于让我们的Makefile变得更加地简洁,
而不至于把所有的东西全部写在一个Makefile中,这样会很难维护我们的Makefile,
这个技术对于我们模块编译和分段编译有着非常大的好处。

例如,我们有一个子目录叫subdir,这个目录下有个Makefile文件,来指明了这个目录下文件的编译规则。
那么我们总控的Makefile可以这样书写:

subsystem:cd subdir && $(MAKE)# 等价于
subsystem:$(MAKE) -C subdir

我们把这个Makefile叫做“总控Makefile”

还有一个在“嵌套执行”中比较有用的参数,“-w”或是“–print-directory”会在make的过程中输出一些信息,让你看到目前的工作目录
比如,如果当前make目录是“/home/xushiyan/Dir_share/my_test/my_makefile/test_for_md/02”,
如果我们使用“make -w”来执行,那么当进入该目录时,我们会看到:

    make: 进入目录“/home/xushiyan/Dir_share/my_test/my_makefile/test_for_md/02”

而在完成make后离开目录时,我们会看到:

    make: 离开目录“/home/xushiyan/Dir_share/my_test/my_makefile/test_for_md/02”

当你使用“-C”参数来指定make下层Makefile时,“-w”会被自动打开的。
如果参数中有“-s”(“–slient”)或是“–no-print-directory”,那么,“-w”总是失效的。

变量传递规则

总控Makefile的变量可以传递到下级的Makefile中(如果你显示的声明),
但是不会覆盖下层的Makefile中所定义的变量,除非指定了“-e”参数。

如果你想(或者不想)传递变量到下级Makefile中,那么你可以使用这样的声明:

export <variable ...>
export variable = value
#等价于
variable = value
export variableunexport <variable ...>

如果你要传递所有的变量,那么,只要一个export就行了。后面什么也不用跟,表示传递所有的变量。

需要注意的是,有两个变量,一个是SHELL,一个是MAKEFLAGS,这两个变量不管你是否export,其总是要传递到下层Makefile中,
特别是MAKEFILES变量,其中包含了make的参数信息,如果我们执行“总控Makefile”时有make参数或是在上层Makefile中定义了这个变量,
那么MAKEFILES变量将会是这些参数,并会传递到下层Makefile中,这是一个系统级的环境变量

但是make命令中的有几个参数并不往下传递,它们是 -C -f -h -o -W ,如果你不想往下层传递参数,那么,你可以这样来:

subsystem:cd subdir && $(MAKE) MAKEFLAGS=

如果你定义了环境变量MAKEFLAGS,那么你得确信其中的选项是大家都会用到的,
如果其中有“-t”,“-n”,和“-q”参数,那么将会有让你意想不到的结果,或许会让你异常地恐慌。

宏定义规则

如果Makefile中出现一些相同命令序列,那么我们可以为这些相同的命令序列定义一个变量。
语法如下:

define ls_sbin_all
pwd
ls -al /sbin
endefall:$(ls_sbin_all)

这里,ls_sbin_all 是这个命令包的名字,其不要和Makefile中的变量重名。
要使用这个命令包,我们就好像使用变量一样,使用 $(xx) 的方式调用。

其工作方式和“=”操作符一样。变量的值可以包含函数、命令、文字,或是其它变量。
因为命令需要以[Tab]键开头,所以如果你用define定义的命令变量中没有以[Tab]键开头,那么make就不会把其认为是命令。

隐含规则

“隐含规则”也就是一种惯例,make会按照这种“惯例”心照不喧地来运行,那怕我们的Makefile中没有书写这样的规则。

如果书写了自己的规则,那么make就不会自动推导并调用隐含规则,它会按照我们写好的规则忠实地执行。

“隐含规则”会使用一些我们系统变量,我们可以改变这些系统变量的值来定制隐含规则的运行时的参数。
如系统变量“CFLAGS”可以控制编译时的编译器参数。

我们还可以通过“模式规则”的方式写下自己的隐含规则。
用“后缀规则”来定义隐含规则会有许多的限制。
使用“模式规则”会更回得智能和清楚,但“后缀规则”可以用来保证我们Makefile的兼容性。

在make的“隐含规则库”中,每一条隐含规则都在库中有其顺序,越靠前的则是越被经常使用的,
所以,这会导致我们有些时候即使我们显示地指定了目标依赖,make也不会管。

常见隐含规则

  1. 编译C程序的隐含规则。
    “.o”的目标的依赖目标会自动推导为“.c”,并且其生成命令是“$(CC) –c $(CPPFLAGS) $(CFLAGS)”

  2. 编译C++程序的隐含规则。
    “.o”的目标的依赖目标会自动推导为“.cc”或是“.C”,并且其生成命令是“$(CXX) –c $(CPPFLAGS) $(CFLAGS)”。(建议使用“.cc”作为C++源文件的后缀,而不是“.C”)

  3. 编译Pascal程序的隐含规则。
    “.o”的目标的依赖目标会自动推导为“.p”,并且其生成命令是“$(PC) –c $(PFLAGS)”。

  4. 编译Fortran/Ratfor程序的隐含规则。
    “.o”的目标的依赖目标会自动推导为“.r”或“.F”或“.f”,并且其生成命令是:
    “.f” “$(FC) –c ( F F L A G S ) ”“ . F ”“ (FFLAGS)” “.F” “ (FFLAGS)”“.F”“(FC) –c $(FFLAGS) ( C P P F L A G S ) ”“ . f ”“ (CPPFLAGS)” “.f” “ (CPPFLAGS)”“.f”“(FC) –c $(FFLAGS) $(RFLAGS)”

  5. 预处理Fortran/Ratfor程序的隐含规则。
    “.f”的目标的依赖目标会自动推导为“.r”或“.F”。这个规则只是转换Ratfor或有预处理的Fortran程序到一个标准的Fortran程序。其使用的命令是:
    “.F” “$(FC) –F $(CPPFLAGS) ( F F L A G S ) ”“ . r ”“ (FFLAGS)” “.r” “ (FFLAGS)”“.r”“(FC) –F $(FFLAGS) $(RFLAGS)”

  6. 编译Modula-2程序的隐含规则。
    “.sym”的目标的依赖目标会自动推导为“.def”,并且其生成命令是:“$(M2C) $(M2FLAGS) ( D E F F L A G S ) ”。“ < n . o > ”的目标的依赖目标会自动推导为“ < n > . m o d ”,并且其生成命令是:“ (DEFFLAGS)”。“<n.o>” 的目标的依赖目标会自动推导为“<n>.mod”,并且其生成命令是:“ (DEFFLAGS)<n.o>的目标的依赖目标会自动推导为<n>.mod,并且其生成命令是:(M2C) $(M2FLAGS) $(MODFLAGS)”。

  7. 汇编和汇编预处理的隐含规则。
    “.o” 的目标的依赖目标会自动推导为“.s”,默认使用编译品“as”,并且其生成命令是:“$(AS) ( A S F L A G S ) ”。“ < n > . s ”的目标的依赖目标会自动推导为“ < n > . S ”,默认使用 C 预编译器“ c p p ”,并且其生成命令是:“ (ASFLAGS)”。“<n>.s” 的目标的依赖目标会自动推导为“<n>.S”,默认使用C预编译器“cpp”,并且其生成命令是:“ (ASFLAGS)<n>.s的目标的依赖目标会自动推导为<n>.S,默认使用C预编译器cpp,并且其生成命令是:(AS) $(ASFLAGS)”。

  8. 链接Object文件的隐含规则。
    “”目标依赖于“.o”,通过运行C的编译器来运行链接程序生成(一般是“ld”),其生成命令是:“$(CC) $(LDFLAGS) .o $(LOADLIBES) $(LDLIBS)”。这个规则对于只有一个源文件的工程有效,同时也对多个Object文件(由不同的源文件生成)的也有效。例如如下规则:

    x : y.o z.o

并且“x.c”、“y.c”和“z.c”都存在时,隐含规则将执行如下命令:

    cc -c x.c -o x.occ -c y.c -o y.occ -c z.c -o z.occ x.o y.o z.o -o xrm -f x.orm -f y.orm -f z.o

如果没有一个源文件(如上例中的x.c)和你的目标名字(如上例中的x)相关联,那么,你最好写出自己的生成规则,不然,隐含规则会报错的。

  1. Yacc C程序时的隐含规则。
    “.c”的依赖文件被自动推导为“n.y”(Yacc生成的文件),其生成命令是:“$(YACC) $(YFALGS)”。(“Yacc”是一个语法分析器,关于其细节请查看相关资料)

  2. Lex C程序时的隐含规则。
    “.c”的依赖文件被自动推导为“n.l”(Lex生成的文件),其生成命令是:“$(LEX) $(LFALGS)”。(关于“Lex”的细节请查看相关资料)

  3. Lex Ratfor程序时的隐含规则。
    “.r”的依赖文件被自动推导为“n.l”(Lex生成的文件),其生成命令是:“$(LEX) $(LFALGS)”。

  4. 从C程序. Yacc文件或Lex文件创建Lint库的隐含规则。
    “.ln” (lint生成的文件)的依赖文件被自动推导为“n.c”,其生成命令是:“$(LINT) $(LINTFALGS) $(CPPFLAGS) -i”。对于“.y”和“.l”也是同样的规则。

隐含规则使用的变量

在隐含规则中的命令中,基本上都是使用了一些预先设置的变量。
你可以在你的makefile中改变这些变量的值,或是在make的命令行中传入这些值,或是在你的环境变量中设置这些值,
无论怎么样,只要设置了这些特定的变量,那么其就会对隐含规则起作用。
当然,你也可以利用make的“-R”或“–no–builtin-variables”参数来取消你所定义的变量对隐含规则的作用。

我们可以把隐含规则中使用的变量分成两种:

关于命令的变量。

一种是命令相关的,如“CC”;一种是参数相的关,如“CFLAGS”。
下面是所有隐含规则中会用到的变量:

AR函数库打包程序。默认命令是“ar”。
AS汇编语言编译程序。默认命令是“as”。
CCC语言编译程序。默认命令是“cc”。
CXXC++语言编译程序。默认命令是“g++”。
CO从 RCS文件中扩展文件程序。默认命令是“co”。
CPPC程序的预处理器(输出是标准输出设备)。默认命令是“$(CC) –E”。
FCFortran 和 Ratfor 的编译器和预处理程序。默认命令是“f77”。
GET从SCCS文件中扩展文件的程序。默认命令是“get”。
LEXLex方法分析器程序(针对于C或Ratfor)。默认命令是“lex”。
PCPascal语言编译程序。默认命令是“pc”。
YACCYacc文法分析器(针对于C程序)。默认命令是“yacc”。
YACCRYacc文法分析器(针对于Ratfor程序)。默认命令是“yacc –r”。
MAKEINFO转换Texinfo源文件(.texi)到Info文件程序。默认命令是“makeinfo”。
TEX从TeX源文件创建TeX DVI文件的程序。默认命令是“tex”。
TEXI2DVI从Texinfo源文件创建军TeX DVI 文件的程序。默认命令是“texi2dvi”。
WEAVE转换Web到TeX的程序。默认命令是“weave”。
CWEAVE转换C Web 到 TeX的程序。默认命令是“cweave”。
TANGLE转换Web到Pascal语言的程序。默认命令是“tangle”。
CTANGLE转换C Web 到 C。默认命令是“ctangle”。
RM删除文件命令。默认命令是“rm –f”。
关于命令参数的变量

下面的这些变量都是相关上面的命令的参数。如果没有指明其默认值,那么其默认值都是空。

ARFLAGS函数库打包程序AR命令的参数。默认值是“rv”。
ASFLAGS汇编语言编译器参数。(当明显地调用“.s”或“.S”文件时)。
CFLAGSC语言编译器参数。
CXXFLAGSC++语言编译器参数。
COFLAGSRCS命令参数。
CPPFLAGSC预处理器参数。( C 和 Fortran 编译器也会用到)。
FFLAGSFortran语言编译器参数。
GFLAGSSCCS “get”程序参数。
LDFLAGS链接器参数。(如:“ld”)
LFLAGSLex文法分析器参数。
PFLAGSPascal语言编译器参数。
RFLAGSRatfor 程序的Fortran 编译器参数。
YFLAGSYacc文法分析器参数。

定义规则模式

https://blog.csdn.net/haoel/article/details/2898

make执行

退出码

make命令执行后有三个退出码:

  • 0 —— 表示成功执行。
  • 1 —— 如果make运行时出现任何错误,其返回1。
  • 2 —— 如果你使用了make的“-q”选项,并且make使得一些目标不需要更新,那么返回2。

指定makefile

make -f test.mk
如果在make的命令行是,你不只一次地使用了“-f”参数,那么,所有指定的makefile将会被连在一起传递给make执行。

指定目标

一般来说,make的最终目标是makefile中的第一个目标,而其它目标一般是由这个目标连带出来的。这是make的默认行为。

任何在makefile中的目标都可以被指定成终极目标,但是除了以“-”打头,或是包含了“=”的目标,
因为有这些字符的目标,会被解析成命令行参数或是变量。
甚至没有被我们明确写出来的目标也可以成为make的终极目标,
也就是说,只要make可以找到其隐含规则推导规则,那么这个隐含目标同样可以被指定成终极目标。

在Unix世界中,软件发布时,特别是GNU这种开源软件的发布时,其makefile都包含了编译、安装、打包等功能。
我们可以参照这种规则来书写我们的makefile中的目标。

“all”这个伪目标是所有目标的目标,其功能一般是编译所有的目标。
“clean”这个伪目标功能是删除所有被make创建的文件。
“install”这个伪目标功能是安装已编译好的程序,其实就是把目标执行文件拷贝到指定的目标中去。
“print”这个伪目标的功能是例出改变过的源文件。
“tar”这个伪目标功能是把源程序打包备份。也就是一个tar文件。
“dist”这个伪目标功能是创建一个压缩文件,一般是把tar文件压成Z文件。或是gz文件。
“TAGS”这个伪目标功能是更新所有的目标,以备完整地重编译使用。
“check”和“test”这两个伪目标一般用来测试makefile的流程。

make命令常见参数

不实际执行

这些参数只是打印命令,不管目标是否更新,把规则和连带规则下的命令打印出来,但不执行,这些参数对于我们调试makefile很有用处。

    “-n”“--just-print”“--dry-run”“--recon”

更新目标时间

这个参数的意思就是把目标文件的时间更新,但不更改目标文件。
也就是说,make假装编译目标,但不是真正的编译目标,只是把目标变成已编译过的状态。

    “-t”“--touch”

寻找目标

这个参数的行为是找目标的意思,
如果目标存在,那么其什么也不会输出,当然也不会执行编译,如果目标不存在,其会打印出一条出错信息。

    “-q”“--question”

指定文件

这个参数需要指定一个文件。
一般是是源文件(或依赖文件),Make会根据规则推导来运行依赖于这个文件的命令,
一般来说,可以和“-n”参数一同使用,来查看这个依赖文件所发生的规则命令。

    “-W <file>”“--what-if=<file>”“--assume-new=<file>”“--new-file=<file>”

makefile语法

变量

使用C自动编译成*.o的默认规则有个缺陷,
由于没有显式地表示*.o依赖于.h头文件,假如我们修改了头文件的内容,那么*.o并不会更新,这是不可接受的。
并且默认规则使用固定的 cc 进行编译,假如我们想使用 ARM-GCC 进行交叉编译,那么系统默认的 cc 会导致编译错误。
要解决这些问题并且让Makefile变得更加通用,需要引入变量和分支进行处理。

基本语法

  1. 变量定义

    “=”  :延时赋值,该变量只有在调用的时候,才会被赋值
    “:=” :直接赋值,与延时赋值相反,使用直接赋值的话,变量的值定义时就已经确定了。
    “?=” :若变量的值为空,则进行赋值,通常用于设置默认值。
    “+=” :追加赋值,可以往变量后面增加新的内容。
    
  2. 使用变量

    $(变量名)
    
  3. 示例代码

    VAR_A = FILEA
    VAR_B = $(VAR_A)
    VAR_C := $(VAR_A)
    VAR_A += FILEB
    VAR_D ?= FILED
    .PHONY:check
    check:@echo "VAR_A:"$(VAR_A)@echo "VAR_B:"$(VAR_B)@echo "VAR_C:"$(VAR_C)@echo "VAR_D:"$(VAR_D)
    

执行结果如下:

xushiyan@xushiyan:~/Dir_share/my_test/my_makefile/test_for_md/02$ make
VAR_A:FILEA FILEB
VAR_B:FILEA FILEB
VAR_C:FILEA
VAR_D:FILED
xushiyan@xushiyan:~/Dir_share/my_test/my_makefile/test_for_md/02$ 

自动化变量

符号意义
$@匹配目标文件
$% @ 类似,但 @类似,但 @类似,但%仅匹配“库”类型的目标文件
$<依赖中的第一个目标文件
$^所有的依赖目标,如果依赖中有重复的,只保留一份
$+所有的依赖目标,即使依赖中有重复的也原样保留
$?所有比目标要新的依赖目标

环境变量

make运行时的系统环境变量可以在make开始运行时被载入到Makefile文件中,
但是如果Makefile中已定义了这个变量,或是这个变量由make命令行带入,那么系统的环境变量的值将被覆盖。
(如果make指定了“-e”参数,那么,系统环境变量将覆盖Makefile中定义的变量)

因此,如果我们在环境变量中设置了“CFLAGS”环境变量,那么我们就可以在所有的Makefile中使用这个变量了。
这对于我们使用统一的编译参数有比较大的好处。
如果Makefile中定义了CFLAGS,那么则会使用Makefile中的这个变量,
如果没有定义则使用系统环境变量的值,一个共性和个性的统一,很像“全局变量”和“局部变量”的特性。

当make嵌套调用时,上层Makefile中定义的变量会以系统环境变量的方式传递到下层的Makefile中。
当然,默认情况下,只有通过命令行设置的变量会被传递。
而定义在文件中的变量,如果要向下层Makefile传递,则需要使用exprot关键字来声明。

尽量别使用,会影响整个编译环境中所有的makefile变量。

目标变量

可以为某个目标设置局部变量,这种变量被称为“Target-specific Variable”,它可以和“全局变量”同名,
因为它的作用范围只在这条规则以及连带规则中,所以其值也只在作用范围内有效。而不会影响规则链以外的全局变量的值。

其语法是:

    <target ...> : <variable-assignment><target ...> : overide <variable-assignment>

这个特性非常的有用,当我们设置了这样一个变量,这个变量会作用到由这个目标所引发的所有的规则中去。如:

prog : CFLAGS = -g
prog : prog.o foo.o bar.o$(CC) $(CFLAGS) prog.o foo.o bar.oprog.o : prog.c$(CC) $(CFLAGS) prog.cfoo.o : foo.c$(CC) $(CFLAGS) foo.cbar.o : bar.c$(CC) $(CFLAGS) bar.c

在这个示例中,不管全局的 ( C F L A G S ) 的值是什么,在 p r o g 目标,以及其所引发的所有规则中( p r o g . o f o o . o b a r . o 的规则), (CFLAGS)的值是什么, 在prog目标,以及其所引发的所有规则中(prog.o foo.o bar.o的规则), (CFLAGS)的值是什么,在prog目标,以及其所引发的所有规则中(prog.ofoo.obar.o的规则),(CFLAGS)的值都是“-g”

模式变量

模式变量的好处就是,我们可以给定一种“模式”,可以把变量定义在符合这种模式的所有目标上。
同样,模式变量的语法和“目标变量”一样.
我们知道,make的“模式”一般是至少含有一个 % 的,所以,我们可以以如下方式给所有以 [.o] 结尾的目标定义目标变量:

    %.o : CFLAGS = -O

特殊变量

VPATH

在一些大的工程中,有大量的源文件,我们通常的做法是把这许多的源文件分类,并存放在不同的目录中。
所以,当make需要去找寻文件的依赖关系时,你可以在文件前加上路径,但最好的方法是把一个路径告诉make,让make在自动去找。

如果没有指明这个变量,make只会在当前的目录中去找寻依赖文件和目标文件。
如果定义了这个变量,那么,make就会在当前目录找不到的情况下,到所指定的目录中去找寻文件了。

VPATH = src:../headers

上面的的定义指定两个目录,“src”和“…/headers”,make会按照这个顺序进行搜索。
目录由“冒号”分隔。(当然,当前目录永远是最高优先搜索的地方)

另一个设置文件搜索路径的方法是使用make的“vpath”关键字(注意,它是全小写的),
这不是变量,这是一个make的关键字,这和上面提到的那个VPATH变量很类似,但是它更为灵活。
它可以指定不同的文件在不同的搜索目录中。
这是一个很灵活的功能。它的使用方法有三种:

  • vpath
    为符合模式的文件指定搜索目录。
  • vpath
    清除符合模式的文件的搜索目录。
  • vpath
    清除所有已被设置好了的文件搜索目录。

vapth使用方法中的 <pattern> 需要包含 % 字符。% 的意思是匹配零或若干字符,例如,%.h 表示所有以 .h 结尾的文件。
<pattern> 指定了要搜索的文件集,而 <directories> 则指定了 <pattern> 的文件集的搜索的目录。
例如:

    vpath %.h ../headers

该语句表示,要求make在“…/headers”目录下搜索所有以“.h”结尾的文件。(如果某文件在当前目录没有找到的话)

我们可以连续地使用vpath语句,以指定不同搜索策略。
如果连续的vpath语句中出现了相同的,或是被重复了的,那么,make会按照vpath语句的先后顺序来执行搜索。

    vpath %.c foo:barvpath %   blish

表示“.c”结尾的文件,先在“foo”目录,然后是“bar”目录,最后才是“blish”目录。

优化代码

TARGET = test_target
CC = gcc
CFLAGS = -I.
DEPS = hello_printf.h
OBJS = hello_world.o hello_printf.o
# 通过替换.c后缀生成对象文件列表
# SOURCES=hello_world.c hello_printf.c
# OBJECTS=$(SOURCES:.c=.o)all: test_target# hello_world: hello_world.o hello_printf.o
#    gcc -Wall -o hello_world hello_world.o hello_printf.o$(TARGET): $(OBJS)$(CC) -o $@ $^ $(CFLAGS)%.o: %.c $(DEPS)$(CC) -c -o $@ $< $(CFLAGS).PHONY:clean
clean:rm -rf *.o $(TARGET)

执行结果如下:

xushiyan@xushiyan:~/Dir_share/my_test/my_makefile/test_for_md/01$ make 
gcc -c -o hello_world.o hello_world.c -I.
gcc -c -o hello_printf.o hello_printf.c -I.
gcc -o test_target hello_world.o hello_printf.o -I.
xushiyan@xushiyan:~/Dir_share/my_test/my_makefile/test_for_md/01$ ls -al
总用量 38
drwxrwx--- 1 root vboxsf  4096 36 16:28 .
drwxrwx--- 1 root vboxsf  4096 36 16:02 ..
-rwxrwx--- 1 root vboxsf   174 36 14:18 hello_printf.c
-rwxrwx--- 1 root vboxsf   167 21 16:03 hello_printf.h
-rwxrwx--- 1 root vboxsf  1696 36 16:28 hello_printf.o
-rwxrwx--- 1 root vboxsf   239 21 16:03 hello_world.c
-rwxrwx--- 1 root vboxsf  1552 36 16:28 hello_world.o
-rwxrwx--- 1 root vboxsf   490 36 16:28 Makefile
-rwxrwx--- 1 root vboxsf 16776 36 16:28 test_target
xushiyan@xushiyan:~/Dir_share/my_test/my_makefile/test_for_md/01$ 

分支

语法如下,其中参数arg1和arg2可以是变量或者是常量:

# ifneq/ifdef/ifndef同理
ifeq(arg1, arg2)
分支1
else
分支2
endif

多余的空格是被允许的,但是不能以[Tab]键做为开始(不然就被认为是命令)

特别注意的是,make是在读取Makefile时就计算条件表达式的值,并根据条件表达式的值来选择语句,
所以,最好不要把自动化变量(如“$@”等)放入条件表达式中,因为自动化变量是在运行时才有的。

而且,为了避免混乱,make不允许把整个条件语句分成两部分放在不同的文件中。

举例如下:

# ?= 变量为空才赋值
ARCH ?= x86ifeq ($(ARCH),x86)
CC = gcc
else
CC = arm-linux-gnueabihf-gcc
endif

可以通过 make ARCH=arm 完成对变量的指定

函数

调用函数的方法跟变量的使用类似,以“ ( ) ”或“ ()”或“ (){}”符号包含函数名和参数,具体语法如下:

$(函数名 参数)
#或者使用花括号
${函数名 参数}

下列介绍一些常见的makefile函数

字符串处理函数

  1. subst函数

    把字串 中的字符串替换成。
    函数返回被处理过后的字符串

    $(subst <from>,<to>,<text>)$(subst $$(FREERTOS_DIR),,${SRCS})
    # 将${SRCS}中的$(FREERTOS_DIR)部分替换为空字符串。
    
  2. patsubst函数

    查找 中的单词(单词以“空格”、“Tab”或“回车”“换行”分隔)是否符合模式,如果匹配的话,则以替换。
    这里,可以包括通配符“%”,表示任意长度的字串。
    如果中也包含“%”,那么,中的这个“%”将是中的那个“%”所代表的字串。
    (可以用“/”来转义,以“/%”来表示真实含义的“%”字符)
    函数返回被处理过后的字符串

    $(patsubst <pattern>,<replacement>,<text>)$(patsubst %.c,%.o,x.c.c bar.c)
    # 把字串“x.c.c bar.c”符合模式[%.c]的单词替换成[%.o],返回结果是“x.c.o bar.o”
    
  3. strip函数

    去掉字串中开头和结尾的空字符。
    函数返回被处理过后的字符串

    $(strip <string>)$(strip a b c )
    # 把字串“a b c ”去到开头和结尾的空格,结果是“a b c”。
    
  4. findstring函数

    在字串中查找字串。
    如果找到,那么返回,否则返回空字符串。

    $(findstring <find>,<in>)$(findstring a,a b c)
    $(findstring a,b c)
    # 第一个函数返回“a”字符串,第二个返回“”字符串(空字符串)
    
  5. filter函数

    以模式过滤 字符串中的单词,保留符合模式的单词。可以有多个模式。
    函数返回被处理过后的字符串

    $(filter <pattern...>,<text>)sources := foo.c bar.c baz.s ugh.h
    foo: $(sources)cc $(filter %.c %.s,$(sources)) -o foo
    # 返回值是“foo.c bar.c baz.s”。
    
  6. filter-out函数

    以模式过滤 字符串中的单词,去除符合模式的单词。可以有多个模式。
    函数返回被处理过后的字符串

    $(filter-out <pattern...>,<text>)objects=main1.o foo.o main2.o bar.o
    mains=main1.o main2.o
    $(filter-out $(mains),$(objects)) 
    # 返回值是“foo.o bar.o”。
    
  7. sort函数

    给字符串中的单词排序(升序)。
    sort函数会去掉中相同的单词。

    $(sort <list>)$(sort foo bar lose)
    # 返回“bar foo lose” 。
    
  8. word函数

    取字符串 中第个单词。(从一开始)
    返回字符串 中第个单词。如果比 中的单词数要大,那么返回空字符串。

    $(word <n>,<text>)$(word 2, foo bar baz)
    # 返回值是“bar”
    
  9. wordlist函数

    从字符串 中取从 开始到的单词串。和是一个数字。
    返回字符串 中从 到的单词字串。
    如果中的单词数要大,那么返回空字符串。
    如果大于 的单词数,那么返回从 开始,到 结束的单词串。

    $(wordlist <s>,<e>,<text>) $(wordlist 2, 3, foo bar baz)
    # 返回值是“bar baz”。
    
  10. words函数

    统计 中字符串中的单词个数。
    返回 中的单词数。

    $(words <text>)$(words, foo bar baz)
    # 返回值是“3”。# 如果我们要取<text>中最后的一个单词,我们可以这样:
    $(word $(words <text>),<text>)。
    
  11. firstword函数

    取字符串 中的第一个单词。
    返回字符串 的第一个单词。

    $(firstword <text>)$(firstword foo bar)
    # 返回值是“foo”。
    # 这个函数可以用word函数来实现:$(word 1,<text>)。
    

文件名操作函数

  1. dir函数

    从文件名序列中取出目录部分。目录部分是指最后一个反斜杠(“/”)之前的部分。如果没有反斜杠,那么返回“./”。
    返回文件名序列的目录部分。

    $(dir <names...>)$(dir src/foo.c hacks)
    # 返回值是“src/ ./”。
    
  2. notdir函数

    从文件名序列中取出非目录部分。非目录部分是指最后一个反斜杠(“/”)之后的部分。
    返回文件名序列的非目录部分

    $(notdir <names...>)$(notdir src/foo.c hacks)
    # 返回值是“foo.c hacks”。
    
  3. suffix函数

    从文件名序列中取出各个文件名的后缀。
    返回文件名序列的后缀序列,如果文件没有后缀,则返回空字串。

    $(suffix <names...>)$(suffix src/foo.c src-1.0/bar.c hacks)
    # 返回值是“.c .c”。
    
  4. basename函数

    从文件名序列中取出各个文件名的前缀部分。
    返回文件名序列的前缀序列,如果文件没有前缀,则返回空字串。

    $(basename <names...>)$(basename src/foo.c src-1.0/bar.c hacks)
    # 返回值是“src/foo src-1.0/bar hacks”。
    
  5. addsuffix函数

    把后缀加到中的每个单词后面。
    返回加过后缀的文件名序列。

    $(addsuffix <suffix>,<names...>)$(addsuffix .c,foo bar)
    # 返回值是“foo.c bar.c”。
    
  6. addprefix函数

    把前缀加到中的每个单词后面。
    返回加过前缀的文件名序列。

    $(addprefix <prefix>,<names...>)$(addprefix src/,foo bar)
    # 返回值是“src/foo src/bar”。
    
  7. join函数

    把中的单词对应地加到的单词后面。
    如果的单词个数要比的多,那么,中的多出来的单词将保持原样。
    如果的单词个数要比多,那么,多出来的单词将被复制到中。
    返回连接过后的字符串。

    $(join <list1>,<list2>)$(join aaa bbb , 111 222 333)
    # 返回值是“aaa111 bbb222 333”。
    

wildcard函数

用于获取文件列表,并使用空格分隔开。

$(wildcard 匹配规则)# 在sources目录下有hello_func.c、hello_main.c、test.c文件
# 执行如下函数
$(wildcard sources/*.c)
# 函数的输出为:
sources/hello_func.c sources/hello_main.c sources/test.c

foreach函数

用来做循环
把参数中的单词逐一取出放到参数所指定的变量中,然后再执行 所包含的表达式。
每一次 会返回一个字符串,循环过程中, 的所返回的每个字符串会以空格分隔,
最后当整个循环结束时, 所返回的每个字符串所组成的整个字符串(以空格分隔)将会是foreach函数的返回值。

所以,最好是一个变量名,可以是一个表达式,而 中一般会使用 这个参数来依次枚举中的单词。

$(foreach <var>,<list>,<text>)names := a b c d
files := $(foreach n,$(names),$(n).o)
# $(files)的值是“a.o b.o c.o d.o”

foreach中的参数是一个临时的局部变量,foreach函数执行完后,参数的变量将不在作用,其作用域只在foreach函数当中。

if函数

参数是if的表达式,
如果其返回的为非空字符串,那么这个表达式就相当于返回真,于是,会被计算,否则会被计算。

而if函数的返回值是,
如果为真(非空字符串),那个会是整个函数的返回值,
如果为假(空字符串),那么会是整个函数的返回值,此时如果没有被定义,那么,整个函数返回空字串。

所以,和只会有一个被计算。

$(if <condition>,<then-part>,<else-part>)

call / origin / shell

控制make的函数

make提供了一些函数来控制make的运行。
通常,你需要检测一些运行Makefile时的运行时信息,并且根据这些信息来决定,你是让make继续执行,还是停止。

  1. error函数

    产生一个致命的错误,<text …>是错误信息。
    注意,error函数不会在一被使用就会产生错误信息,
    所以如果你把其定义在某个变量中,并在后续的脚本中使用这个变量,那么也是可以的。

    $(error <text ...>)ifdef ERROR_001
    $(error error is $(ERROR_001))
    endif
    # 示例一会在变量ERROR_001定义了后执行时产生error调用ERR = $(error found an error!)
    .PHONY: err
    err: ; $(ERR)
    # 示例二则在目录err被执行时才发生error调用。
    
  2. warning函数

    这个函数很像error函数,只是它并不会让make退出,只是输出一段警告信息,而make继续执行。

    $(warning <text ...>)
    

文件指示

其包括了三个部分,
一个是在一个Makefile中引用另一个Makefile,就像C语言中的include一样;
另一个是指根据某些情况指定Makefile中的有效部分,就像C语言中的预编译#if一样;
还有就是定义一个多行的命令。

多目标

Makefile的规则中的目标可以不止一个,其支持多目标,有可能我们的多个目标同时依赖于一个文件,并且其生成的命令大体类似。
于是我们就能把其合并起来。

bigoutput littleoutput : text.ggenerate text.g -$(subst output,,$@) > $@bigoutput : text.ggenerate text.g -big > bigoutput
littleoutput : text.ggenerate text.g -little > littleoutput

静态模式

静态模式可以更加容易地定义多目标的规则,可以让我们的规则变得更加的有弹性和灵活。
语法如下:

<targets ...>: <target-pattern>: <prereq-patterns ...><commands>...

targets定义了一系列的目标文件,可以有通配符。是目标的一个集合。
target-parrtern是指明了targets的模式,也就是的目标集模式。
prereq-parrterns是目标的依赖模式,它对target-parrtern形成的模式再进行一次依赖目标的定义。

看个例子

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.c bar.c ),$@ 表示目标集(也就是foo.o bar.o)。
于是,上面的规则展开后等价于下面的规则

foo.o : foo.c$(CC) -c $(CFLAGS) foo.c -o foo.o
bar.o : bar.c$(CC) -c $(CFLAGS) bar.c -o bar.o

试想,如果我们的 %.o 有几百个,那种我们只要用这种很简单的“静态模式规则”就可以写完一堆规则,实在是太有效率了。
“静态模式规则”的用法很灵活,如果用得好,那会一个很强大的功能。再看一个例子:

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

$(filter %.o,$(files)) 表示调用Makefile的filter函数,过滤 $filter 集,只要其中模式为“%.o”的内容。

自动生成依赖性

如果是一个比较大型的工程,你必需清楚哪些C文件包含了哪些头文件,并且,你在加入或删除头文件时,也需要小心地修改Makefile,这是一个很没有维护性的工作。
为了避免这种繁重而又容易出错的事情,我们可以使用C/C++编译的一个功能。
大多数的C/C++编译器都支持一个“-M”的选项,即自动找寻源文件中包含的头文件,并生成一个依赖关系。
例如,如果我们执行下面的命令:

    cc -M main.c

其输出是:

    main.o : main.c defs.h

如果使用GNU的C/C++编译器,得用“-MM”参数,不然,“-M”参数会把一些标准库的头文件也包含进来。
举个例子

first: $(info "!!!!! first")$(info "!!!!! first start")# 打印文件依赖,-M会包含标准库头文件gcc -M hello_world.cgcc -MM hello_world.c

对应执行结果如下

xushiyan@xushiyan:~/Dir_share/my_test/my_makefile/hello_world$ make -f test.mk first 
"!!!!! first"
"!!!!! first start"
# 打印文件依赖,-M会包含标准库头文件
gcc -M hello_world.c
hello_world.o: hello_world.c /usr/include/stdc-predef.h \/usr/include/stdio.h \/usr/include/x86_64-linux-gnu/bits/libc-header-start.h \/usr/include/features.h /usr/include/x86_64-linux-gnu/sys/cdefs.h \/usr/include/x86_64-linux-gnu/bits/wordsize.h \/usr/include/x86_64-linux-gnu/bits/long-double.h \/usr/include/x86_64-linux-gnu/gnu/stubs.h \/usr/include/x86_64-linux-gnu/gnu/stubs-64.h \/usr/lib/gcc/x86_64-linux-gnu/9/include/stddef.h \/usr/lib/gcc/x86_64-linux-gnu/9/include/stdarg.h \/usr/include/x86_64-linux-gnu/bits/types.h \/usr/include/x86_64-linux-gnu/bits/timesize.h \/usr/include/x86_64-linux-gnu/bits/typesizes.h \/usr/include/x86_64-linux-gnu/bits/time64.h \/usr/include/x86_64-linux-gnu/bits/types/__fpos_t.h \/usr/include/x86_64-linux-gnu/bits/types/__mbstate_t.h \/usr/include/x86_64-linux-gnu/bits/types/__fpos64_t.h \/usr/include/x86_64-linux-gnu/bits/types/__FILE.h \/usr/include/x86_64-linux-gnu/bits/types/FILE.h \/usr/include/x86_64-linux-gnu/bits/types/struct_FILE.h \/usr/include/x86_64-linux-gnu/bits/stdio_lim.h \/usr/include/x86_64-linux-gnu/bits/sys_errlist.h hello_printf.h
gcc -MM hello_world.c
hello_world.o: hello_world.c hello_printf.h

实际应用

没看懂
https://blog.csdn.net/haoel/article/details/2890

调试手段todo


总结

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/730150.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

如何使用Postman创建Mock Server?

这篇文章将教会大家如何利用 Postman&#xff0c;通过 Mock 的方式测试我们的 API。 什么是 Mock Mock 是一项特殊的测试技巧&#xff0c;可以在没有依赖项的情况下进行单元测试。通常情况下&#xff0c;Mock 与其他方法的主要区别就是&#xff0c;用于取代代码依赖项的模拟对…

CVPR2023 | 提升图像去噪网络的泛化性,港科大上海AILab提出 MaskedDenoising,已开源!

作者 | 顾津锦 首发 | AIWalker 链接 | https://mp.weixin.qq.com/s/o4D4mNM3jL6sYuhUC6VgoQ 当前深度去噪网络存在泛化能力差的情况&#xff0c;例如&#xff0c;当训练集噪声类型和测试集噪声类型不一致时&#xff0c;模型的性能会大打折扣。作者认为其原因在于网络倾向于过度…

Python实现极限学习机分类模型(ELMClassifier算法)项目实战

说明&#xff1a;这是一个机器学习实战项目&#xff08;附带数据代码文档视频讲解&#xff09;&#xff0c;如需数据代码文档视频讲解可以直接到文章最后获取。 1.项目背景 极限学习机&#xff08;ELMClassifier&#xff09;算法是一种基于单隐层前馈神经网络&#xff08;SLFN…

【C语言】操作符相关知识点

移位操作符 << 左移操作符 >>右移操作符 左移操作符 移位规则&#xff1a; 左边抛弃、右边补0 右移操作符 移位规则&#xff1a; 首先右移运算分两种&#xff1a; 1.逻辑移位 左边用0填充&#xff0c;右边丢弃 2.算术移位 左边用原该值的符号位填充&#xff0c;…

上门服务小程序|上门服务系统成品功能包含哪些?

随着移动互联网的快速发展&#xff0c;上门服务小程序成为了一种创新的家政服务模式。它不仅为用户带来了极大的便利&#xff0c;还能在提高服务效率和质量方面发挥作用。通过上门服务小程序&#xff0c;用户可以轻松预约按摩或理疗服务&#xff0c;无需繁琐操作&#xff0c;只…

knife4j生产环境禁止打开页面

Knife4j是一个集Swagger2 和 OpenAPI3为一体的增强解决方案&#xff0c;官网地址&#xff1a;Knife4j 集Swagger2及OpenAPI3为一体的增强解决方案. | Knife4j 考虑到安全性问题&#xff0c;在实际服务部署到生产环境后就需要禁用到swagger页面的展示&#xff0c;这个时候只需…

类和对象(1)(至尊详解版)

相信对于大家而言&#xff0c;对于类和对象都会是一头雾水吧&#xff01;什么是类&#xff1f;或者你有对象吗&#xff1f;那么本期的内容呢&#xff1f;就由我来为大家再次增加对于它们的理解&#xff0c;由于水平上的原因&#xff0c;可能会存在不当之处&#xff0c;敬请读者…

类与对象(三)--static成员、友元

文章目录 1.static成员1.1概念&#x1f3a7;面试题✒️1.2static的特性&#x1f3a7;1.3思考&#x1f3a7; 2.友元2.1什么是友元&#xff1f;&#x1f3a7;2.2两种友元关系&#xff1a;&#x1f3a7; 1.static成员 1.1概念&#x1f3a7; &#x1f50e; static关键字用于声明类…

Jmeter性能测试 -1

之前讲的Jmeter算不上是性能测试&#xff0c;只是用Jmeter做接口测试。现在我们开始进入实际的性能测试。开始前你应该对Jmeter有了一定的了解&#xff0c;把前面Jmeter内容看一遍应该可以入门了。 Jmeter与locust locust除了可以做接口的性能测试以外&#xff0c;做性能测试…

ubuntu18.04编译OpenCV-3.4.19+OpenCV_contrib-3.4.19

首先确保安装了cmake工具 安装opencv依赖文件 sudo apt-get install build-essential sudo apt-get install git libgtk-3-dev pkg-config libavcodec-dev libavformat-dev libswscale-dev sudo apt-get install python3-dev python3-numpy libtbb2 libtbb-dev libjpeg-dev li…

树莓派(Raspberry Pi)常见的各种引脚介绍

本文将为您详细讲解树莓派&#xff08;Raspberry Pi&#xff09;常见的各种引脚&#xff0c;以及它们的特点、区别和优势。树莓派是一款非常受欢迎的单板计算机&#xff0c;它拥有多个 GPIO&#xff08;通用输入输出&#xff09;引脚&#xff0c;这些引脚可以用于各种电子项目和…

【C++】C++模板基础知识篇

个人主页 &#xff1a; zxctscl 文章封面来自&#xff1a;艺术家–贤海林 如有转载请先通知 文章目录 1. 泛型编程2. 函数模板2.1 函数模板概念2.2 函数模板格式2.3 函数模板的原理2.4 函数模板的实例化2.5 模板参数的匹配原则 3. 类模板3.1 类模板的定义格式3.2 类模板的实例化…

金智维售前总监屈文浩,将出席“ISIG-RPA超级自动化产业发展峰会”

3月16日&#xff0c;第四届「ISIG中国产业智能大会」将在上海中庚聚龙酒店拉开序幕。本届大会由苏州市金融科技协会指导&#xff0c;企智未来科技&#xff08;RPA中国、AIGC开放社区、LowCode低码时代&#xff09;主办。大会旨在聚合每一位产业成员的力量&#xff0c;深入探索R…

问题:前端获取long型数值精度丢失,后面几位都为0

文章目录 问题分析解决 问题 通过接口获取到的数据和 Postman 获取到的数据不一样&#xff0c;仔细看 data 的第17位之后 分析 该字段类型是long类型问题&#xff1a;前端接收到数据后&#xff0c;发现精度丢失&#xff0c;当返回的结果超过17位的时候&#xff0c;后面的全…

通知:T3学员领取资料391-395

各位T3学员∶本周VBA技术资料增加391-395讲&#xff0c;看到通知后联络我&#xff08;微&#xff1a;VBA6337&#xff09;免费领取资料。成果来之不易&#xff0c;您更新后请说声谢谢&#xff0c;感恩我的成果。 MF391&#xff1a;选择同颜色的单元格 MF392&#xff1a;连接范…

图文并茂的讲清楚Linux零拷贝技术

今天我们来聊一聊Linux零拷贝技术&#xff0c;今天我们以一个比较有代表性的技术sendfile系统调用为切入点&#xff0c;详细介绍一下零拷贝技术的原理。 1.零拷贝技术简介 Linux零拷贝技术是一种优化数据传输的技术&#xff0c;它可以减少数据在内核态和用户态之间的拷贝次数&…

图论入门题题解

✨欢迎来到脑子不好的小菜鸟的文章✨ &#x1f388;创作不易&#xff0c;麻烦点点赞哦&#x1f388; 所属专栏&#xff1a;刷题_脑子不好的小菜鸟的博客-CSDN博客 我的主页&#xff1a;脑子不好的小菜鸟 文章特点&#xff1a;关键点和步骤讲解放在 代码相应位置 拓扑排序 / 家谱…

寄存器(CPU工作原理)

文章目录 寄存器(CPU工作原理)1. 通用寄存器2. 字在寄存器中的存储3. 几条汇编指令4. 物理地址5. 16位结构的CPU6. 8086CPU给出物理地址的方法7. 段的概念8. 段寄存器9 . CS和IP10. 修改CS、IP的指令11. 代码段 寄存器(CPU工作原理) 一个典型的CPU由运算器、控制器、寄存器等器…

【好书推荐-第十期】《AI绘画教程:Midjourney使用方法与技巧从入门到精通》

&#x1f60e; 作者介绍&#xff1a;我是程序员洲洲&#xff0c;一个热爱写作的非著名程序员。CSDN全栈优质领域创作者、华为云博客社区云享专家、阿里云博客社区专家博主、前后端开发、人工智能研究生。公众号&#xff1a;洲与AI。 &#x1f388; 本文专栏&#xff1a;本文收录…

005-事件捕获、冒泡事件委托

事件捕获、冒泡&事件委托 1、事件捕获与冒泡2、事件冒泡示例3、阻止事件冒泡4、阻止事件默认行为5、事件委托6、事件委托优点 1、事件捕获与冒泡 2、事件冒泡示例 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /…