前言
本文只讲LInux环境下的Makefile基础语法,环境搭建见下链接:
https://blog.csdn.net/weixin_44567668/article/details/125189852
一、概述
学过C语言都知道需要编译才能运行,其实运行的是可执行文件,可能是.obj或者.o文件,而将源文件.c变成可执行文件的过程就是编译,实际上GCC整个编译流程是:预处理、编译、汇编和链接,如使用下面命令进行编译:
gcc main.c -o main
那有人好奇Makefile有什么用?上面知道计算机系统运行需要一个可执行文件,我们只有一个main.c文件只需要一个命令,但是如果有很多个源文件和头文件的时候一个个敲命令,特别是在修改了其中某个文件后,一行行敲命令很显然不现实。而Makefile可以有效的解决这个问题,Makefile本质上相当于一个shell脚本文件,那样你将所有编译命令集中在一起,最后只需要输入make
命令就可以自动化编译,make与Makefile关系如下图:
这就是 make 的执行过程,make 工具就是在 Makefile 中一层一层的查找依赖关系,并执行相应的命令运行gcc编译器,编译出最终的可执行文件
二、示例引用
首先新建一个main.c的源文件,内容如下:
#include "stdio.h"
int main()
{
printf("hello, world! This is a C program.\n");
return 0;
}
然后新建名为Makefile的文件,内容如下:
main:main.o
gcc -o main main.o
main.o:main.c
gcc -c main.c
clean:
rm *.o
rm main
先不管语法后面会讲,然后在命令行里输入make
命令即可执行Makefile,注意main.c、Makefile与终端当前路径是一致的,然后输入./main
运行代码看效果
可以看出就3步:编辑工程代码->编辑Makefile文件->输入make执行编译
三、Makefile规则与运行
3.1 书写格式
- Makefile的语法规则
[目标]:[依赖]
[命令]
由上一小节的例子可以看出就是目标文件指向对应的依赖文件,然后下面就是具体的转换命令。注意:Makefile文件命令行的首行缩进用Tab键,不能使用空格
-
通配符
make支持3种通配符:*
、?
和[]
。通配符代替了一系列文件,如“ *.c ”代表后缀为.c的文件 -
模式规则
模式规则中,至少在规则的目标定定义中要包涵%
,否则就是一般规则,目标中的“%”表示对文件名的匹配,“%”表示长度任意的非空字符串,比如“%.c”就是所有的以.c 结尾的文件,类似与通配符:
%.o : %.c
命令
- 注释
在Makefile里使用#
添加注释,可以用反斜框进行转义,如\#
3.2 命令格式
- 显示命令
- 通常,make会把其要执行的命令行在命令执行前输出到屏幕上。当我们用
@
字符在命令行前,那么,这个命令将不被make显示出来 - 如果make执行时,带入make参数
-n
或--just-print
,那么其只是显示命令,但不会执行命令,这个功能很有利于我们调试我们的 Makefile - 而make参数
-s
或--slient
则是全面禁止命令的显示
- 命令执行
当依赖目标新于目标时,也就是当规则的目标需要被更新时,make 会一条一条的执行其后的命令。需要注意的是,如果你要让上一条命令的结果应用在下一条命令时,你应该使用分号;
分隔这两条命令,如:
exec:
cd /home/hchen;pwd
- 命令出错
有些时候,命令的出错并不表示就是错误的。为了做到这一点,忽略命令的出错,我们可以在 Makefile 的命令行前加一个减号-
clean:
-rm -f *.o
还有一个全局的办法是,给 make 加上-i
或是--ignore-errors
参数,那么,Makefile 中所有命令都会忽略错误。而如果一个规则是以.IGNORE
作为目标的,那么这个规则中的所有命令将会忽略错误。
还有一个要提一下的 make 的参数的是-k
或是--keep-going
,这个参数的意思是,如果某规则中的命令出错了,那么就终目该规则的执行,但继续执行其它规则
- 定义命令包
如果 Makefile 中出现一些相同命令序列,那么我们可以为这些相同的命令序列定义一个变量。定义这种命令序列的语法以define
开始,以endef
结束,如:
define run-yacc
yacc $(firstword $^)
mv y.tab.c $@
endef
这里,“run-yacc”是这个命令包的名字,其不要和 Makefile 中的变量重名。调用示例如下:
foo.c : foo.y
$(run-yacc)
3.3 make的运行
-
make的工作方式
①读入所有的 Makefile。
②读入被 include 的其它 Makefile。
③初始化文件中的变量。
④推导隐晦规则,并分析所有规则。
⑤为所有的目标文件创建依赖关系链。
⑥根据依赖关系,决定哪些目标要重新生成。
⑦执行生成命令。 -
Makefile的文件名
默认的情况下,make 命令会在当前目录下按顺序找寻文件名为“GNUmakefile”、“makefile”、“Makefile”的文件,找到了解释这个文件。在这三个文件名中,最好使用Makefile这个文件名
当 然 , 你 可 以 使 用 别 的 文 件 名 来 书 写 Makefile , 比 如 : “Make.Linux” ,“Make.Solaris”,“Make.AIX”等,如果要指定特定的 Makefile,你可以使用 make 的“-f”和“–file”参数,如:
make -f Make.Linux
make --file Make.AIX
- 文件搜索
正常情况下make只会在当前目录中去找依赖文件和目标文件。但是我们可以使用特殊变量VPATH
就可以去指定路径搜索,如:
VPATH = src:../headers
上面的的定义指定两个目录,“src”和“…/headers”,make 会按照这个顺序进行搜索。目录由“冒号”分隔。(当然,当前目录永远是最高优先搜索的地方)
另一个设置文件搜索路径的方法是使用 make 的vpath
关键字(注意,它是全小写的),这不是变量,这是一个 make 的关键字,如:
#为符合模式<pattern>的文件指定搜索目录<directories>。
vpath <pattern> <directories>
# vapth 使用方法中的<pattern>需要包含“%”字符,如:
vpath %.h ../headers
#清除符合模式<pattern>的文件的搜索目录。
vpath <pattern>
#清除所有已被设置好了的文件搜索目录。
vpath
- 引用其它的 Makefile
在Makefile使用include
关键字可以把别的Makefile包含进来,如:
include foo.make *.mk $(bar)
# 等价于:
include foo.make a.mk b.mk c.mk e.mk f.mk
- 如果make执行时,有
-I
或--include-dir
参数,那么make就会在这个参数 所指定的目录下去寻找 - 如果目录
<prefix>/include
(一般是:/usr/local/bin 或/usr/include)存在的话,make 也会去找。如果有文件没有找到的话,make 会生成一条警告信息,如果你想让make不理那些无法读取的文件,而继续执行,你可以在 include 前加一个减号-
- 如果你的当前环境中定义了环境变量
MAKEFILES
,那么make 会把这个变量中的值做一个类似于 include 的动作。这个变量中的值是其它的 Makefile,用空格分隔。只是,它和 include不同的是,从这个环境变中引入的 Makefile 的“目标”不会起作用,如果环境变量中定义的文件发现错误,make 也会不理。所以建议不要使用这个环境变量
- 变量传递
# 如果你要传递变量到下级 Makefile 中,那么你可以使用这样的声明:
export <variable ...>
#如果你不想让某些变量传递到下级 Makefile 中,那么你可以这样声明:
unexport <variable ...>
如果你要传递所有的变量,那么,只要一个 export 就行了。后面什么也不用跟,表示传递所有的变量。 需要注意的是,有两个变量,一个是 SHELL,一个是 MAKEFLAGS,这两个变量不管你是否 export,其总是要传递到下层 Makefile 中,特别是 MAKEFILES 变量,其中包含了 make的参数信息
- 嵌套执行make
在一些大的工程中,我们会把我们不同模块或是不同功能的源文件放在不同的目录中,我们可以在每个目录中都书写一个该目录的 Makefile。如:
subsystem:
cd subdir && $(MAKE)
#其等价于:
subsystem:
$(MAKE) -C subdir
定义$(MAKE)宏变量的意思是,也许我们的 make 需要一些参数,所以定义成一个变量比较利于维护。这两个例子的意思都是先进入“subdir”目录,然后执行 make 命令
- 伪目标
一般的目标名都是要生成的文件,而伪目标不代表真正的目标名,在执行make命令的时候通过指定这个伪目标来执行其所在规则的定义的命令,如:
clean:
rm *.o temp
此时输入命令make clean
就会执行删除工作。其他伪目标:
伪目标 | 功能 |
---|---|
all | 所有目标的目标,其功能一般是编译所有的目标 |
clean | 删除所有被 make 创建的文件 |
install | 安装已编译好的程序,其实就是把目标执行文件拷贝到指定的 目标中去 |
例出改变过的源文件 | |
tar | 把源程序打包备份 |
dist | 创建一个压缩文件,一般是把 tar 文件压成 Z 文件 |
TAGS | 更新所有的目标,以备完整地重编译使用 |
check和test | 用来测试 makefile 的流程 |
但是如果目录下有一个同名的clean文件,此时输入命令并不会执行删除工作,因此我们可以使用标记.PHONY
来指明目标是伪目标,如:
.PHONY:clean
四、变量
- 变量基础
变量的命名字可以包含字符、数字,下划线(可以是数字开头),但不应该含有“:”、“#”、“=”或是空字符(空格、回车等)。变量是大小写敏感的,“foo”、“Foo”和“FOO”是三个不同的变量名。
变量在声明时需要给予初值,而在调用时需要给在变量名前加上$
符号,但最好用小括号()
或是大括号{}
把变量给包括起来。如果你要使用真实的$
字符,那么你需要用$$
来表示。
定义变量的方式有以下四种:
操作符 | 作用 |
---|---|
= | 延迟赋值,该变量只有在调用的时候,才会被赋值 |
:= | 直接赋值,延迟赋值相反,使用直接赋值的话,变量的值定义时就已经确定了。 |
?= | 若变量值为空,则进行赋值,通常用于设置默认值 |
+= | 追加赋值,可以往变量后面增加新的内容 |
示例如下:
objects = program.o foo.o utils.o #定义变量
program : $(objects) #调用变量
cc -o program $(objects)
- 自动化变量
所谓自动化变量,就是这种变量会把模式中所定义的一系列的文件自动地挨个取出,直至所有的符合模式的文件都取完了。这种自动化变量只应出现在规则的命令中。自动化变量需要配合模式规则%。常用的自动化变量如表:
自动化变量 | 描述 |
---|---|
$@ | 规则中的目标集合,在模式规则中,如果有多个目标的话,“$@”表示匹配模式中定义的目标集合。 |
$% | 当目标是函数库的时候表示规则中的目标成员名,如果目标不是函数库文件,那么其值为空。 |
$< | 依赖文件集合中的第一个文件,如果依赖文件是以模式(即“%”)定义的,那么“$<”就是符合模式的一系列的文件集合。 |
$? | 所有比目标新的依赖目标集合,以空格分开。 |
$^ | 所有依赖文件的集合,使用空格分开,如果在依赖文件中有多个重复的文件,“$^”会去除重复的依赖文件,值保留一份。 |
$+ | 和“$^”类似,但是当依赖文件存在重复的话不会去除重复的依赖文件。 |
$* | 这个变量表示目标模式中"%"及其之前的部分,如果目标是 test/a.test.c,目标模式为 a.%.c,那么“$*”就是 test/a.test。 |
%.o : %.c
gcc -c $<
- 多行变量定义
还有一种设置变量值的方法是使用define
关键字。使用 define 关键字设置变量的值可以有换行,define指示符后面跟的是变量的名字,而重起一行定义变量的值,定义是以endef
关键字结束。如:
define two-lines
echo foo
echo $(bar)
endef
-
环境变量
make 运行时的系统环境变量可以在 make 开始运行时被载入到 Makefile 文件中,但是如果 Makefile 中已定义了这个变量,或是这个变量由 make 命令行带入,那么系统的环境变量的值将被覆盖。(如果 make 指定了“-e”参数,那么,系统环境变量将覆盖 Makefile 中定义的变量)。因此,如果我们在环境变量中设置了CFLAGS
环境变量,那么我们就可以在所有的Makefile 中使用这个变量了。 -
变量高级用法
- 第一种是变量值的替换:我们可以替换变量中的共有的部分,其格式是
$(var:a=b)
或是${var:a=b}
,其意思是,把变量“var”中所有以“a”字串“结尾”的“a”替换成“b”字串。这里的“结尾”意思是“空格”或是“结束符”。 - 第二种是“把变量的值再当成变量”,如:
x = y
y = z
a := $($(x))
五、条件判断
格式如下:
<条件关键字>
<条件为真时执行的语句>
else
<条件为假时执行的语句>
endif
其中条件关键字有 4 个:ifeq
、ifneq
、ifdef
和ifndef
- ifeq用来判断是否相等,格式如下
ifeq (<arg1>, <arg2>)
ifeq '<arg1>' '<arg2>'
ifeq "<arg1>" "<arg2>"
ifeq "<arg1>" '<arg2>'
ifeq '<arg1>' "<arg2>"
- ifneq就是判断是否不相等,格式类似ifeq
- ifdef表示如果“变量名”的值非空,那么表示表达式为真,否则表达式为假
ifdef <变量名>
- ifndef 用法类似,但是含义用户 ifdef 相反
六、函数
- 函数的调用
$(函数名 参数)
#或者
${函数名 参数}
- 字符串处理函数
格式 | 功能 |
---|---|
$(subst from,to,text) | 把字串text中的from字符串替换成to |
$(patsubst pattern,replacement,text) | 查找text中的单词是否符合模式pattern,如果匹配的话,则以replacement替换 |
$(strip string) | 去掉string字串中开头和结尾的空字符 |
$(findstring find,in) | 在字串in中查找find字串 |
$(filter pattern,text) | 以pattern模式过滤text字符串中的单词,保留符合模式pattern的单词 |
$(filter-out pattern,text) | 以pattern模式过滤text字符串中的单词,去除符合模式pattern的单词 |
$(sort list) | 给字符串list>中的单词排序(升序) |
$(word n,text) | 取字符串text中第n个单词。(从一开始) |
$(wordlist s,e,text) | 从字符串text中取从s开始到e的单词串 |
$(words text) | 统计text中字符串中的单词个数 |
$(firstword text) | 取字符串text中的第一个单词 |
- 文件名操作函数
函数名 | 格式 | 功能 |
---|---|---|
dir | $(dir <names…>) | 从文件名序列names中取出目录部分 |
notdir | $(notdir <names…>) | 从文件名序列names中取出非目录部分 |
suffix | $(suffix <names…>) | 从文件名序列names中取出各个文件名的后缀 |
basename | $(basename <names…>) | 从文件名序列names中取出各个文件名的前缀部分 |
addsuffix | $(addsuffix <suffix…>,<names…>) | 把后缀suffix加到names中的每个单词后面 |
addprefix | $(addprefix <prefix…>,<names…>) | 把前缀prefix加到names中的每个单词后面 |
join | $(join <list1…>,<list2…>) | 把list2中的单词对应地加到list1的单词后面 |
- 循环函数
Makefile中的foreach
函数几乎是仿照于 Unix 标准 Shell(/bin/sh)中的 for 语句,或是 C-Shell(/bin/csh)中的 foreach 语句而构建的。它的语法是:
$(foreach <var>,<list>,<text>)
这个函数的意思是,把参数list中的单词逐一取出放到参数var所指定的变量中,然后再执行text所包含的表达式。每一次text会返回一个字符串,循环过程中,text的所返回的每个字符串会以空格分隔,最后当整个循环结束时,text所返回的每个字符串所组成的整个字符串(以空格分隔)将会是 foreach 函数的返回值。
names := a b c d
files := $(foreach n,$(names),$(n).o)
- 条件函数
if
函数很像 GNU 的 make 所支持的条件语句——ifeq,if 函数的语法是:
$(if <condition>,<then-part>)
#或者
$(if <condition>,<then-part>,<else-part>)
可见,if 函数可以包含“else”部分,或是不含。即 if 函数的参数可以是两个,也可以是三个。condition参数是 if 的表达式,如果其返回的为非空字符串,那么这个表达式就相当于返回真,于是,then-part会被计算,否则else-part会被计算。
而 if函数的返回值是,如果condition为真(非空字符串),那个then-part会是整个函数的返回值,如果condition为假(空字符串),那么else-part会是整个函数的返回值,此时如果else-part没有被定义,那么,整个函数返回空字串。
- 定义函数
使用call
函数来创建新的参数化的函数
$(call <expression>,<parm1>,<parm2>,<parm3>...)
当 make 执行这个函数时,expression参数中的变量,会被参数parm1,parm2,parm3依次取代,而expression的返回值就是call函数的返回值。例如:
reverse = $(1) $(2)
foo = $(call reverse,a,b)
- 寻找函数
使用origin
函数来寻找函数出处,格式如下:
$(origin <variable>)
返回值如下:
- undefined:如果variable从来没有定义过
- default:如果variable是一个默认的定义,比如“CC”这个变量
- file:如果variable这个变量被定义在 Makefile 中
- command line:如果variable这个变量是被命令行定义的
- override:如果variable是被 override 指示符重新定义的
- automatic: 如果variable是一个命令运行中的自动化变量
- 其他函数
函数名 | 格式 | 说明 |
---|---|---|
shell | $(shell 命令) | 把执行操作系统命令后的输出作为函数返回 |
error | $(error <text …>) | 产生一个致命的错误,<text …>是错误信息 |
warning | $(warning <text …>) | 只是输出一段警告信息,而 make继续执行 |
七、隐含规则
“隐含规则”也就是一种惯例,make 会按照这种“惯例”心照不喧地来运行,那怕我们的 Makefile 中没有书写这样的规则。例如,把[.c]文件编译成[.o]文件这一规则,你根本就不用写出来,make会自动推导出这种规则,并生成我们需要的[.o]文件。
“隐含规则”会使用一些我们系统变量,我们可以改变这些系统变量的值来定制隐含规则的运行时的参数。如系统变量“CFLAGS”可以控制编译时的编译器参数。当然,你也可以利用 make 的-R
或--no–builtin-variables
参数来取消你所定义的变量对隐含规则的作用。
- 关于命令的变量
变量 | 默认命令 |
---|---|
AR | 函数库打包程序。默认命令是“ar” |
AS | 汇编语言编译程序。默认命令是“as”。 |
CC | C 语言编译程序。默认命令是“cc”。 |
CXX | C++语言编译程序。默认命令是“g++”。 |
CO | 从 RCS 文件中扩展文件程序。默认命令是“co”。 |
CPP | C 程序的预处理器(输出是标准输出设备)。默认命令是“$(CC) –E”。 |
FC | Fortran 和 Ratfor 的编译器和预处理程序。默认命令是“f77”。 |
GET | 从 SCCS 文件中扩展文件的程序。默认命令是“get”。 |
LEX | Lex 方法分析器程序(针对于 C 或 Ratfor)。默认命令是“lex”。 |
PC | Pascal 语言编译程序。默认命令是“pc”。 |
YACC | Yacc 文法分析器(针对于 C 程序)。默认命令是“yacc”。 |
YACCR | Yacc 文法分析器(针对于 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”文件时)。 |
CFLAGS | C 语言编译器参数。 |
CXXFLAGS | C++语言编译器参数。 |
COFLAGS | RCS 命令参数。 |
CPPFLAGS | C 预处理器参数。( C 和 Fortran 编译器也会用到)。 |
FFLAGS | Fortran 语言编译器参数。 |
GFLAGS | SCCS “get”程序参数。 |
LDFLAGS | 链接器参数。(如:“ld”) |
LFLAGS | Lex 文法分析器参数。 |
PFLAGS | Pascal 语言编译器参数。 |
RFLAGS | Ratfor 程序的 Fortran 编译器参数。 |
YFLAGS | Yacc 文法分析器参数。 |
八、GCC命令补充
- 调用数学库用“-L”指定库所在的目录
LIBPATH := -lgcc -L /usr/local/arm/gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf/lib/gcc/arm-linux-gnueabihf/4.9.4
- -Wall选项意思是编译后显示所有警告
- -nostdlib 不连接系统标准启动文件和标准库文件.只把指定的文件传递给连接器
- 加入选项“-fno-builtin”表示不使用内建函数
- 添加了选项“-Wa,-mimplicit-it=thumb”,避免报错thumb(拇指?) 条件指令应该在IT(?)块中
$(CC) -Wall -Wa,-mimplicit-it=thumb -nostdlib -fno-builtin -c -O2 $(INCLUDE) -o $@ $<
- 加入了“-march=armv7-a -mfpu=neon-vfpv4 -mfloat-abi=hard”指令,这些指令用于
指定编译浮点运算的时候使用硬件 FPU
$(CC) -Wall -march=armv7-a -mfpu=neon-vfpv4 -mfloat-abi=hard -Wa,-mimplicit-it=thumb -nostdlib -fno-builtin -c -O2 $(INCLUDE) -o $@ $<
- 自动生成依赖性
大多数的C/C++编译器都支持一个“-M”的选项,即自动找寻源文件中包含的头文件,并生成一个依赖关系,如:
cc -M main.c