Makefile学习笔记

Makefile学习笔记


version : v1.0 「2023.4.12」 最后补充

author: Y.Z.T.

摘要: none

简介: 记录Make的简单特性 , 记录学习的过程




⭐️ 目录





0️⃣ 前言

  • Make是常用的构建工具, 一般是编译一个工程时使用的工具 。

  • 用来描述哪些文件需要编译、哪些需要重新编译的文件就叫做 MakefileMakefile 带来的好处就是可以实现工程的全自动编译

  • make是 一个命令工具,一个解释 makefile 中指令的命令工具

  • 从程序源码到可执行文件 (如.out文件) 的过程就是编译的过程吗,程序的整个编译流程大致分成 几个阶

    段 : 预处理编译汇编链接。(可以参考我之前的一篇笔记程序编译过程



1️⃣ Makefile概况

1.1 gcc与make命令

1.1.1 gcc常用参数

gcc 命令格式如下:

gcc [选项] [文件名字]

主要选项如下:

  • -c:只编译不链接为可执行文件,编译器将输入的.c 文件编译为.o 的目标文件。

  • -o:<输出文件名>用来指定编译结束以后的输出文件名,如果不使用这个选项的话 GCC 默认编译出来的可执行文件名字为 a.out

  • -g:添加调试信息,如果要使用调试工具(如 GDB)的话就必须加入此选项,此选项指示编译的时候生成调试所需的符号信息。(生成debug模式的可执行文件)

  • -O:对程序进行优化编译,如果使用此选项的话整个源代码在编译、链接的的时候都会进

  • 行优化,这样产生的可执行文件执行效率就高。

  • -O2:比-O 更幅度更大的优化,生成的可执行效率更高,但是整个编译过程会很慢。



1.1.2 make常用参数

环境: GNU Make 4.3

常用参数

  • -B : 认为所有的目标都需要更新(重编译),即强制编译所有目标。
  • -f :指定makefile文件;
  • -I <dir>:当包含其他 makefile文件时,利用该选项指定搜索目录;
  • -dDebug模式,相当于--debug=a输出有关文件和检测时间的详细信息。
  • -i :忽略命令执行返回的出错信息;
  • -r:禁止 make 使用任何隐含规则
  • -t : 只是把目标的修改日期变成最新的,也就是阻止生成目标的命令运行。


1.2 Makefile概念

Makefile文件是用来告诉make命令怎么去编译和链接程序的 , Makefile的书写有以下三个规则:

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

注意:

  • Makefile文件的文件名最好使用"Makefile"或"makefile" 。如果要指定命名为其他名字的Makefile文件的话,可以使用make-f--file 参数,如:make -f Make.Linux

1.3 Makefile语法

Makefile 是由件由一系列规则(rules)构成。每条规则的形式如下:

<target>  : <prerequisites>
[tab]<command>
...
  • target目标文件 , 如.o文件或.out文件
  • prerequisites前置条件 , 即要生成那个 target文件 所需要的文件或是目标
  • commandmake 需要执行的命令。(任意的 Shell 命令) , 每条命令前面一定要加tab ,不能是空格。make 命令会为 Makefile 中的每个以 TAB 开始的命令创建一个 Shell 进程去执行。)

注意:

  • 一个目标(target)就构成一条规则。目标通常是文件名,指明Make命令所要构建的对象, 目标可以是一个文

    件名,也可以是多个文件名,之间用空格分隔。

  • 其中第一条规则的目标成为默认目标 , 也即是make命令编译生成的最终目标文件

  • 目标 是必需的,不可省略;前置条件命令 都是可选的,但是两者之中必须至少存在一个。



1.4 示例代码

GUN make 中的一个Makefile程序为例:

  • 该文件描述了一个称为文本编辑器(edit)的可执行文件生成方法, 该文件依靠8个(.o文件);
  • 这8个.o文件有依赖于8个源文件和3个头文件

# 第一条规则说明 , edit这个目标依赖于后面8个.o文件
# gcc命令的作用是将后面8个.o文件链接成可执行文件edit
edit : main.o kbd.o command.o display.o \
       insert.o search.o files.o utils.o
        gcc -o edit main.o kbd.o command.o display.o \
                   insert.o search.o files.o utils.o

# 第二条规则说明 main.o文件依赖于main.c和defs.h两个文件
# gcc命令的作用是不链接编译main.c和defs.h, 生成main.o
main.o : main.c defs.h
        gcc -c main.c
kbd.o : kbd.c defs.h command.h
        gcc -c kbd.c
command.o : command.c defs.h command.h
        gcc -c command.c
display.o : display.c defs.h buffer.h
        gcc -c display.c
insert.o : insert.c defs.h buffer.h
        gcc -c insert.c
search.o : search.c defs.h buffer.h
        gcc -c search.c
files.o : files.c defs.h buffer.h command.h
        gcc -c files.c
utils.o : utils.c defs.h
        gcc -c utils.c


#clean 不是一个文件, 仅仅是一个动作的名称。用于清理目标文件(.o和执行文件)
#它有依赖文件,没有被第一个目标文件直接或间接关联,
#所以它对应的命令不会被自动执行, 可以显性的要求make 执行,即 "make clean命令"
clean :
        rm edit main.o kbd.o command.o display.o \
           insert.o search.o files.o utils.o

如上所示:

  • 上述代码中一共有10个规则。1~4行为第一个规则, 也是 默认目标 即最终的目标文件

  • 反斜杆 / 是换行符的意思, 这样比较便于 Makefile 的易读。

因此由上述程序可以总结出make命令工作的 执行流程:

  • make 会在当前目录下找名字叫Makefilemakefile的文件。

  • 如果找到,它会找文件中的第一个目标文件(target),在上面的例子中,他会找到edit这个文件,并把这个文件作为 最终的目标文件

  • 如果 edit 文件不存在,或是 edit 所依赖的后面的 .o 文件的文件修改时间要比 edit这个文件新,那么,他就会执行后面所定义的命令来生成 edit 这个文件。

  • 如果 edit 所依赖的.o 文件也不存在,或者目标所依赖的文件比目标文件新 (也就是最后修改时间比

    目标文件晚) 的话, 那么 make 会在当前文件中找目标为.o 文件的依赖性,如果找到则再根据那一个规则生成.o 文件。


  • 这就是整个 make 的依赖性,make 会一层又一层地去找文件的依赖关系,直到最终编译出 第一个目标文件

  • make 只管文件的依赖性, 在找寻的过程中,如果出现错误,比如最后被依赖的文件找不到,那么

    make 就会直接退出,并报错。对于定义命令的错误,make不会理会



2️⃣ 简化Makefile

2.1 变量

前面的例子只是一个最简单的Makefile程序, 我们可以使用变量来简化程序

  • Makefile中 , 变量值可以使用在“ 目标”,前置条件命令或是 Makefile 的其它部分中。
  • Makefile 中的变量其实就是 C/C++中的宏 , 当 Makefile 被执行时, 变量都会被 扩展 到相应的引用位置上。
  • 变量的命名可以包含字符数字下划线 (可以是数字开头),但不应该含有 :#= 或是空字符 (空格、回车等)
  • 变量是大小写敏感的 , 可以使用大小写搭配的变量名 , 如:MakeFlags ,避免和系统的变量冲突

例: 以下两段代码是等同的

edit : main.o kbd.o command.o display.o \
       insert.o search.o files.o utils.o
	gcc -o edit main.o kbd.o command.o display.o \
          insert.o search.o files.o utils.o
#变量定义
objects = main.o kbd.o command.o display.o \
          insert.o search.o files.o utils.o

#变量使用
edit: $(objects)
	 gcc -o edit $(objects)


2.1.1变量的使用

Makefile 中变量的引用方法是 $(变量名),比如本例中的$(objects)就是使用变量 objects。括号不加也可以, 但给变量加上括号完全是为了更加安全

地使用这个变量, 最好还是给变量加上括号.



2.1.1.1 变量赋值符

Makefile一共提供了四个赋值运算符 =:=?=+=



2.1.1.1.1 赋值符 “=”

赋值符 =是在 执行的时候进行拓展 , 允许 递归拓展 .

所以在使用=在给变量的赋值的时候,不一定要用已经定义好的值,也可以 使用后面定义的值

如下所示:

a = $(b)
b = $(c)
c = Hello

main:
	@echo $(a)

输出结果:

5c05fcbc167f29d1aa46fcf01f2e686

解释:

  • 赋值符 =是在 执行的时候 才进行拓展 的
  • $(a)的值是 $(b) , $(b)的值是 $(c) , $(c)的值是 “Hello”
  • 说明使用赋值符 =的变量是可以使用后面的变量来进行定义的 , 也就是 变量的值 是取决于它所引用的变量的 最后一次有效值

注意:

  • 因为赋值 = 是允许递归拓展的 , 所以有可能让 make 陷入无限的变量展开过程中去 , 会让make发出报错
a = $(b)
b = $(a)
  • 如果在 变量中使用 函数 的话 , 会让make在运行时十分缓慢


2.1.1.1.2 赋值符 “:=”

为了避免使用 =赋值符时 , 发送变量无限展开的情况 , 可以使用 赋值符 :=

赋值符 := 是在定义时扩展。

使用赋值符 := 的话 , 前面的变量不能使用后面的变量,只能使用前面已定义好了的变量。

如下所示:

x := foo 
y := $(x) bar 
x := later

等价于

y := foo bar 
x := later

可以看到前面的变量不能使用后面的变量,只能使用前面已定义好了的变量。


赋值符 “:=” 的一种特殊用法

操作符的右边是很难描述一个空格的, 但可以使用 赋值符 := 来定义一个变量 , 其值是一个空格

nullstring := 
space := $(nullstring) # end of the line
  • nullstring 是一个 Empty 变量 , 其值为空
  • space 的值是一个空格
  • 先用一个 Empty 变量来标明变量的值开始了,而后面采用“#”注释符来表示变量定义的终止


2.1.1.1.3 赋值符 “?=”

赋值符 ?= 只有在该变量为空时才设置值。

即当一个变量先前被定义过 , 那么这条命令将什么都不做 , 如果先前没有定义 , 那么变量值就是 赋值符右边的值

如下所示:

FOO ?= bar

其含义是,如果 FOO 没有被定义过,那么变量 FOO 的值就是“bar”,如果 FOO 先前被定义过,那么这条语将什么也不做。


等价于:

#第一句是条件判断语句 , 使用origin函数判断 FOO变量是否被定义过
ifeq ($(origin FOO), undefined) 
FOO = bar 
endif

(origin 函数)


2.1.1.1.4 赋值符 “+=”

赋值符 += 用于将值追加到变量的尾端。


如下所示:

objects = main.o foo.o bar.o utils.o 
objects += another.o

变量 objects 的值为 main.o foo.o bar.o utils.o , 后面我们给它追加了一个another.o

因此变量 objects 变成了main.o foo.o bar.o utils.o another.o


注意:

  • 如果变量之前没有定义过,那么,+=会自动变成 =
  • 如果前面有变量定义,那 += 会继承于前次操作的赋值符。

如下所示

variable := value 
variable += more

等价于

variable := value 
variable := $(variable) more


2.1.1.2变量的值指向另一个变量
x = y 
y = z 
a := $($(x))

如上所示:

  • $(x) 的值是 y,所以 $($(x))就是$(y)
  • $(a)的值就是 z

使用这种 方式可以使用多个变量来 组成一个变量的名字,然后再取其值:

first_second = Hello 
a = first 
b = second 
all = $($a_$b)

如上所示:

这里的$a_$b组成了first_second,于是,$(all)的值就是Hello



2.1.1.3 变量值的替换

可以替换变量中共有的部分:

写法是: 变量名 + 冒号 + 后缀名替换规则

如: $(var:.o=.c) 是将把变量var中所有以.o结尾的文件替换成.c结尾的文件。

它实际上patsubst函数的一种简写形式。( patsubst 函数)


如下所示:

foo := a.o b.o c.o 
bar := $(foo:.o=.c)

这两行代码的意思是 : 将变量foo中所有后缀名.o 替换成.c



2.1.2 自动变量

Make命令还提供一些自动变量,它们的值 与当前规则有关

所谓自动化变量,就是这种变量会把模式中所定义的一系列的文件自动地挨个取出,直至所有的符合模式的文件都取完了。

这种自动化变量只应出现在规则的 命令中。

主要有以下几个:

$@$@指代当前目标,就是Make命令当前构建的那个目标。比如,make foo$@ 就指代foo。
$<$< 指代第一个前置条件。比如,规则为 t: p1 p2,那么$< 就指代p1
$?$? 指代比目标更新的所有前置条件,之间以空格分隔。比如,规则为 t: p1 p2,其中 p2 的时间戳比 t 新,$?就指代p2
$^$^ 指代所有 前置条件,之间以空格分隔。比如,规则为 t: p1 p2,那么 $^ 就指代 p1 p2
$*$* 指代匹配符 % 匹配的部分, 比如% 匹配 f1.txt 中的f1$* 就表示 f1
$(@D) 和 $(@F)$(@D)$(@F) 分别指向 $@目录名文件名。比如,$@src/input.c,那么$(@D) 的值为 src$(@F) 的值为 input.c
$(<D) 和 $(<F)$(<D)$(<F) 分别指向 $<目录名文件名


2.1.2.1 $@

$@ 表示规则中的目标文件集。在模式规则中,如果有多个目标,那么,$@就是匹配于 目标中模式定义的集合。

$@指代当前目标,就是Make命令当前构建的那个目标。比如,make foo$@ 就指代foo。


如下所示:

a.txt b.txt: 
    touch $@

等价于:

a.txt:
    touch a.txt
b.txt:
    touch b.txt


2.1.2.2 $<

$<指的是依赖文件集合中的第一个文件

如果依赖文件是以模式 (即%) 定义的,那么$< 就是符合模式的一系列的文件集合。


如下所示:

a.txt: b.txt c.txt
    cp $< $@ 

等价于:

a.txt: b.txt c.txt
    cp b.txt a.txt 


2.1.2.3 $^

$^ 指代所有 前置条件,之间以空格分隔。

test1.o:test1.c head.c
	gcc -o $@ $^

指的就是test1.c head.c



2.1.2.4 自动化变量示例
dest/%.txt: src/%.txt
    @[ -d dest ] || mkdir dest
    cp $< $@

如上所示:

  • 上面代码将 src 目录下的 txt 文件,拷贝到 dest 目录下。
  • 首先判断 dest 目录是否存在,如果不存在就新建
  • 然后,$< 指代前置文件(src/%.txt)$@ 指代目标文件(dest/%.txt)


2.1.3 内置变量

Make命令提供一系列内置变量, 内置变量分成两种: 一种是命令相关的, 如"CC" ; 另一种是与 参数有关 的, 如"CFLAGS"



2.1.3.1 命令相关的变量
CCC 语言编译程序。默认命令是 cc
CXXC++语言编译程序。默认命令是 g++
CPPC 程序的预处理器(输出是标准输出设备)。默认命令是$(CC) –E
RM删除文件命令。默认命令是 rm –f
AR函数库打包程序。默认命令是ar
AS汇编语言编译程序。默认命令是as
YACCYacc 文法分析器(针对于 C 程序)。默认命令是yacc


2.1.3.2 命令参数相关的变量

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

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


2.2 函数

在 Makefile 中可以使用函数来处理变量, 格式如下

$(函数名 参数集合)
#$(function arguments)
# 或者
${function arguments}


2.2.1 内置函数
  • Makefile 提供了内置函数供我们使用 , 类似于库函数
  • make 所支持的函数也不算很多,不过已经足够我们的操作了。
  • 函数调用后,函数的返回值可以当做变量来使用

以下是常用的内置函数



2.2.1.1 shell函数
  • shell 函数用来执行 shell 命令 ,
  • 它的参数应该就是操作系统 Shell 的命令。
  • shell 函数把执行操作系统命令后的输出作为函数返回。

例如:

files := $(shell echo *.c)


2.2.1.2 wildcard 函数

通配符“%”只能用在规则中,只有在规则中它才会展开.

如果在 变量定义函数使用 时,通配符不会自动展开,这个时候就要用到函数 wildcard


例子:

#用来获取当前目录下所有的.c 文件,类似“%”
$(wildcard *.c)


2.2.1.3 subst 函数

函数 subst 用来完成字符串替换,把字串 <text>中的<from>字符串替换成<to>。

调用形式如下:

$(subst <from>,<to>,<text>)

示例:

$(subst ee,EE,feet on the street)
#把把“feet on the street”中的“ee”替换成“EE”
#返回结果是“fEEt on the strEEt”

例2:

comma:= ,
empty:=
# space变量用两个空变量作为标识符,当中是一个空格
space:= $(empty) $(empty)
foo:= a b c
bar:= $(subst $(space),$(comma),$(foo))
# 把空格替换成"," 返回结果为'a,b,c'


2.2.1.4 patsubst函数

函数 patsubst 用来完成 模式字符 串替换,使用方法如下:

$(patsubst <pattern>,<replacement>,<text>)
  • 查找字符串<text>中的单词是否符合模式<pattern> , 如果匹配就用<replacement>来替换掉
  • <pattern>可以使用通配符%,表示任意长度的字符串
  • 函数返回值就是替换后的字符串。
  • 如果<replacement>中也包涵 %,那么<replacement>中的%将是<pattern>中的那个%所代表的字符串,

示例:

$(patsubst %.c,%.o,a.c b.c c.c)
# 将字符串“a.c b.c c.c”中的所有符合“%.c”的字符串,替换为“%.o”
# 替换完成以后的字符串为“a.o b.o c.o”

注意:

$(patsubst %.o,%.c,$(objects))
#等价于
$(objects:.o=.c)


2.2.1.5 dir函数

函数 dir 用来获取目录,使用方法如下:

$(dir <names…>)

此函数用来从文件名序列<names>中提取出目录部分,返回值是文件名序列<names>的目录

例如:

$(dir </src/a.c>)

提取文件/src/a.c的目录部分,也就是/src



2.2.1.6 notdir 函数

函数notdir 用于取出文件, 即取出文件中的目录部分 . 格式如下:

$(notdir <names...>)
  • 从文件名序列<names>中取出非目录部分。
  • 非目录部分是指最后一个反斜杠(“ /”)之后的部分。

示例:

$(notdir src/foo.c hacks)
#返回值是“foo.c hacks”

2.2.1.7 foreach 函数

foreach 函数用来完成循环 , 格式如下:

$(foreach <var>,<list>,<text>)
  • 参数<list>中的单词逐一取出放到参数<var>所指定的变量中,然后再执行<text>所包含的表达式

  • 每一次<text>会返回一个字符串,循环过程中,<text>的所返回的每个字符串会以空格分隔,最后当整个循环结束时,<text>所返回的每个字符串

    所组成的整个字符串(以空格分隔)将会是 foreach 函数的返回值。

  • 所以,<var>最好是一个变量名,<list>可以是一个表达式

  • 参数是一个临时的局部变量 , 作用域只在 foreach 函数当中

示例:

names := a b c d 
files := $(foreach n,$(names),$(n).o)
  • $(name)中的单词会被挨个取出,并存到变量n
  • $(n).o每次根据 $(n)计算出一个值,这些值以空格分隔,最后作为 foreach 函数的返回
  • 最终$(files)的值是a.o b.o c.o d.o


2.2.1.8 origin函数

origin 函数用于返回变量的来源 , 格式如下:

$(origin <variable>)
  • <variable>是变量的名字,不应该是引用 ,最好不要在<variable>中使用$字符

origin函数的返回值:

“undefined”

如果<variable>从来没有定义过,origin 函数返回这个值***“undefined”***


“default”

如果<variable>是一个默认的定义,比如“CC”这个变量


“environment”

如果<variable>是一个环境变量,并且当 Makefile 被执行时,-e参数没有被打开。


“file”

如果<variable>这个变量被定义在 Makefile 中。


“command line”

如果<variable>这个变量是被命令行定义的


“override”

如果<variable>是被 override 指示符重新定义的。


“automatic”

如果<variable>是一个命令运行中的自动化变量。

示例:

#使用origin函数判断 FOO变量是否被定义过
ifeq ($(origin FOO), undefined) 
...
endif



2.2.1.9 call函数

call函数通常用来调用另一个自定义的make函数,并且会展开里面的内容,同时按照展开的位置解释为makefile语句或者shell语句。

格式如下:

$(call <expression>,<parm1>,<parm2>,<parm3>...)
  • <expression> 是已经定义了的函数名称 , <parm1>,<parm2>,<parm3> 是传递给函数的参数 , 中间以 逗号 隔开
  • 注意,call函数只能在Makefile中调用已经定义的函数,而不能用于调用Shell命令或者 函数。

示例:

PHONY: all

define func
    @echo "pram1 = $(0)"
    @echo "pram2 = $(1)"
endef

all:
    $(call func, num1, num2)

运行结果:

image-20230413213734057

如上所示:

  • make执行这个函数的时候 , $(1)$(2)$(3)参数变量 , 会被参数<parm1>,<parm2>,<parm3>依次 取代
  • $(0) 表示<expression> , 即函数名本身

2.2.2 自定义函数(多行变量)
  • makefile 中可用通过使用define 来定义 多行变量自定义函数 , 以endef 关键字结束
  • 变量的值可以包含 函数命令文字,或是其它变量。
  • 如果用define 定义的命令变量中没有以[TAB] 键开头 , make就不会认为其是命令
  • 自定义的函数一般用于定义 命令的集合,并且运用于 规则中
  • 自定义函数使用 内置函数 call 来调用 , 后面跟自定义函数名及参数

define格式如下

define 函数名
    ...
    ...
endef

示例如下:

PHONY: all

define func1
    @echo "func1 name is $(0)"
endef

define func2
	@echo "pram1 = $(1)"
	@echo "pram2 = $(2)"
endef	
	
all:
    $(call func1)
    $(call func2, num1,num2)

运行结果:

image-20230413215155311

如上所示:

  • 首先定义了一个伪目标all , 接着定义了两个函数func1func2 .
  • func1中通过通过$(0)输出call 的第一个参数,也就是函数名fun1
  • func2中同时输出了第一个参数和第二个参数
  • 注意,自定义函数是一个 过程调用没有任何返回值,这是与Makefile内置函数的区别之处。


3️⃣

3.1 伪目标

3.1.1 介绍
  • 在上面的例子中, 使用了PHONY 来显性的指明了一个伪目标all
  • 伪目标并不是一个文件,只是一个 标签 , 所以 make 无法生成它的依赖关系和决定它是否要执行。
  • 在执行 make 命令的时候通过指定这个伪目标来执行其所在规则的定义的命令. 如:make clean

如下所示:

objects = main.o kbd.o command.o display.o \ 
insert.o search.o files.o utils.o

edit : $(objects) 
	  gcc -o edit $(objects)

#-c标志表示产生目标文件, -o $@表示将输出文件命名为:左边的文件名, $<表示依赖列表中的第一个项(即%.c)。
%.o : %.c
	gcc -c -o $@ $<
 
.PHONY : clean 
clean : 
rm edit $(objects)

如上所示:

有了.PHONY : clean 的声明 , 不管是否存在 “clean” 文件 , 要运行clean 这个目标 , 只能通过 make clean 指令



3.1.2 执行多个目标

可以通过使用 伪目标 , 来实现一次生成多个目标文件的目的


如下所示:

all : prog1 prog2 prog3 
.PHONY : all 
 
prog1 : prog1.o utils.o 
cc -o prog1 prog1.o utils.o 
 
prog2 : prog2.o 
cc -o prog2 prog2.o 
 
prog3 : prog3.o sort.o utils.o 
cc -o prog3 prog3.o sort.o utils.o

说明:

  • 因为Makefile 中的第一个目标会被作为其默认目标 , 通过声明一个all 的伪目标 , 其依赖于其它三个目标prog1 prog2 prog3
  • 由于伪目标的特性是,总是被执行的,所以其依赖的那三个目标就总是不如all这个目标新。
  • 就能达到一次生成多个目标的目的


3.2 模式规则

  • 使用模式规则可以定义一个隐含规则 , 例如: (怎么从所有的[.c]文件生成相应的[.o]文件的规则)

  • 在模式规则中, 至少在规则的目标定义中要包含%,否则,就是一般的规则。

  • 目标中的%表示对文件名的匹配,%表示长度任意的 非空字符串

    例如:

    • %.c就是所有的以.c 结尾的文件,类似于通配符 *
    • a.%.c 就表示以 a.开头,以.c 结束的所有文件
  • 如果%定义在目标中,那么,目标 中的%的值 决定依赖目标 中的%的值

在上面关于 伪目标 的例子中使用了这样一行规则:

#-c标志表示产生目标文件, -o $@表示将输出文件命名为:左边的文件名, $<表示依赖列表中的第一个项(即%.c)。
%.o : %.c
	gcc -c -o $@ $<

这段代码的含义是指出了怎么从所有的[.c]文件生成相应的[.o]文件的规则

%.o 是指将当前目录下所有以.o为后缀的文件当成 目标。同理, 对应的 .c文件为 依赖目标



3.2.1 模式匹配
  • 一般来说,一个目标 的模式有一个有前缀或是后缀的%,或是没有前后缀,直接就是一个"%"。
  • %所匹配的内容叫做 “茎” ,例如%.c所匹配的文件test.ctest就是 “茎”
  • 因为在目标和依赖目标中同时有 % 时,依赖目标会将 % 替换为 , 从而得出文件名。

例如: 有这样一个模式规则

%.o : %.c
# 当模式%.o 匹配文件名为 "test.o"时
# 此时"茎"是 test 
# 此时, 依赖目标中 "%.c"会将 "%" 替换为茎 , 得到文件名"test.c"

当一个模式匹配 包含有斜杠 的文件时 , 在进行模式匹配时, 目录部分 会首先被移开,然后进行匹配,成功后,再把目录加回去。

例如:

  • 有一个模式 e%t , 和文件名 src/eat 匹配 ,则 src/a
  • 依赖目标 转换为文件名的时候 , 中的路径名将被加在前面 , %被替换为 的其余部分
  • 如果依赖目标中 有一个c%r 匹配会得到 src/car


3.3 通配符

  • Makefile中可以使用的通配符有:*?[…]。通配符的使用方法和含义和在shell一样
  • 通配符代替了 一系列文件 , 如*.c表示所以后缀为 c 的文件
  • ~ 波浪号表示当前用户的HOME目录 , 如~/test 就表示 /home/test这个目录

Makefile中,通配符主要用在 两个场合

  • 用在规则的目标和依赖中:

    make在读取Makefile时会自动对其进行匹配处理(通配符展开)。如:

    test: *.o
        gcc -o $@ $^
        
    *.o: *.c
        gcc -c $^
    
  • 用在规则的命令中:

    这时 , 通配符的通配处理在shell执行命令时完成。如:

    clean:
        rm -f *.o
    
  • 除了前面两个以外 , 通配符使用在其他地方, 例如使用在变量当中

    在变量中的通配符 不会进行展开 , 因为在makefile中变量,相当于 C/C++ 中的宏 , 只是简单的进行替换

    如果要让通配符在变量中展开, 可以使用 内置函数wildcard

    # 变量不会展开 , objects的值就是 *.o
    objects = *.o 
    
    # 使用wildcard 函数获取当前目录下所有.c文件
    #$(wildcard *.c) 是用来获取当前目录下所有的.c 文件
    CFILES := $(wildcard *.c)
    


3.3.1 “%” 与 通配符 “*”

%*的主要区别在于 , *是应用在系统中的 , %是应用在Makefile文件中的

前面提到一个 示例:

objects = main.o kbd.o command.o display.o 

edit : $(objects) 
	  gcc -o edit $(objects)

在这个示例中:

  • make命令在构造edit文件时 , 发现需要main.o kbd.o command.o display.o 这几个文件
  • 接下来 , make命令会在Makefile文件中寻找 能匹配 main.o kbd.o command.o display.o 的规则
  • 在分别找到main.o kbd.o command.o display.o 的规则后 , 会运行该规则下的 命令
  • 等到目标edit 的依赖条件齐全之后 , 开始构造edit

以上是一般make在工作时的 执行流程 ,

在工作流程中 , 如果存在 % 符号 ,例如:

objects = main.o kbd.o command.o display.o 

edit : $(objects) 
	  gcc -o edit $(objects)
	  
%.o : %.c
	gcc -c -o $@ $<	  
  • % 符号只会在带着目的 (例如寻找 main.o) , 才会把它要寻找的目标 ( main.o ) 匹配到 % 符号

  • 所以 , 如果只写以下的代码 , make会产生报错

    # 因为此时% 符号并没有目标 , 所以第2~3行定义的这个 规则并不会被执行 , 从而产生报错
    %.o : %.c
    	gcc -c -o $@ $<	  	
    

而如果存在通配符 * , make命令会然后 遍历目录的文件,看是否匹配。找出所有匹配的项目。


总结

虽然 , %* 两个符号的功能看似类似 , 但他们的工作方式是完全不同的



3.4 条件判断

Makefile 中也存在条件判断 , 通过使用条件表达式可以让 make 根据运行时的不同情况选择不同的执行分支

用于条件判断的 关键字有四个: ifeqifneqifdefifndef

语法如下: 语法有两种

<条件关键字>
	<条件为真时执行的语句>
endif

另一种

<条件关键字>
	<条件为真时执行的语句>
else
	<条件为假时执行的语句>
endif


3.4.1 “ifeq” 和 “ifneq”

ifeq 用来判断是否 相等

ifneq 就是判断是否 不相等


用法如下:

# ifeq的用法
ifeq (<参数 1>, <参数 2>)
ifeq ‘<参数 1 >’,‘ <参数 2>’
ifeq “<参数 1>”, “<参数 2>”
ifeq “<参数 1>”, ‘<参数 2>’
ifeq ‘<参数 1>’, “<参数 2>”

#上述用法中都是用来比较“参数 1”和“参数 2”是否相同,如果相同则为真
#“参数 1”和“参数 2”可以为函数返回值。

#ifneq的用法
ifneq (<参数 1>, <参数 2>)
ifneq ‘<参数 1 >’,‘ <参数 2>’
ifneq “<参数 1>”, “<参数 2>”
ifneq “<参数 1>”, ‘<参数 2>’
ifneq ‘<参数 1>’, “<参数 2>”
# ifneq的用法与ifeq类型 , 只是ifneq用来了比较“参数 1”和“参数 2”是否不相等,如果不相等的话就为真。

说明:

参数是可以使用make函数的 , 例如前面提到的一个例子

ifeq ($(origin FOO), undefined) 
FOO = bar 
endif

这个例子中就在 ifeq中使用了 origin 函数 , 用来判断 FOO变量是否被定义过



3.4.2 “ifdef"和"ifndef”

格式如下:

ifdef <variable-name>

ifndef <variable-name>

其中ifdef 用来判断变量<variable-name>的值是否为空 , 非空 这表达式为真 , 否则表达式为假 .

<variable-name> 同样可以是一个函数的 返回值 , ifdefifndef 只是测试一个变量是否有值,其并不会把变量扩展到当前位置。

ifndefifdef 的意思相反


示例如下:

bar = 
foo = $(bar) 
ifdef foo 
frobozz = yes 
else 
frobozz = no 
endif

# 判断foo变量是否为空 , 运行结果是frobozz的值为yes

4️⃣ 示例

例1

给出一个makefile程序 , 用来编译当前目录下的所有文件

all:$(subst .c,.o,$(wildcard *.c))
 
%.o:%.c
    gcc -c -o $@ $<
 
.PHONY : clean
clean:
	rm  *.o
	rm all



例2 存在这样一个目录框架


Makefile 程序

# 目标文件名
Name    		:= obj/ledc
Link_File		:= Timx6ul

# 定义编译命令
Cross_Compile   :=  arm-linux-gnueabihf
CC 				:=  $(Cross_Compile)-gcc
Link    		:= -ld -$(Link_File).lds -o
Elf_Bin 		:= -objcopy -O binary -S

# 头文件路径
Inc_Dirs		:= bsp/led  \
				   drivers  \
				   user     


# 源文件和汇编文件路径
Src_Dirs		:= bsp/led  \
				   project  \
				   user						   


# 将所有头文件前面添加上 参数 "-I" , Makefile语法要求指明头文件目录的时候需要加上"-I"
Include  		:= $(patsubst %, -I %, $(Inc_Dirs))

#获取源文件和汇编文件
S_Files			:= $(foreach dir, $(Src_Dirs), $(wildcard $(dir)/*.S))
C_Files			:= $(foreach dir, $(Src_Dirs), $(wildcard $(dir)/*.c))

# 取出文件中的的非目录部分
S_File_Ndir		:= $(notdir $(S_Files)) 
C_File_Ndir  	:= $(notdir $(C_Files))


#将所有.c , .s 文件替换成.o
S_Obj 			:= $(patsubst %, obj/%, $(S_File_Ndir:.S=.o))
C_Obj			:= $(patsubst %, obj/%, $(C_File_Ndir:.c=.o))
Obj				:= $(S_Obj) $(C_Obj)

# 指定搜索目录
VPATH			:= $(Src_Dirs) $(Inc_Dirs)

$(Name).bin: $(Obj)
	$(Cross_Compile)$(Link) $(Name).elf $^
	$(Cross_Compile)$(Elf_Bin) $(Name).elf $@

$(C_Obj): obj/%.o : %.c
	$(CC) $(Include) -Wall -nostdlib -c -o $@ $<

$(S_Obj): obj/%.o : %.S
	$(CC) -Wall -nostdlib -c -o $@ $<

.PHONY: clean
clean:
	rm obj/*.o obj/*.bin obj/*.elf  


❗️参考文章

Make 命令教程

Makefile中的%标记和系统通配符*的区别

《GUN make使用手册》

《跟我一起写Makefile》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值