目录
从工作就接触 make,但还从未仔仔细细看过官方的文档描述和说明,有些小遗憾,现在补充补充;哈哈;好吧,我们开始!
GNU 官方的 make Manual:Top (GNU make)Top (GNU make)https://www.gnu.org/software/make/manual/html_node/index.html
最好最好还是读一读,虽然常用到,但是,总有一些很小的细节需要注意,稍不注意就可能遗漏;
Makefile 本质上来说,是一个用于描述代码的编译、链接、生成最终目标文件的过程和相互依赖的一个文件;当你写好一个 Makefile,并在终端键入 make 指令的时候,此刻,就会按照 Makefile 中指定的规则(依赖)去生成目标文件;
陈皓大神以前写过一篇《跟我一起写 Makefile —— 陈皓》的 pdf,内容非常好,里面也总结了很多于上面这个 GNU 官方的链接的内容,强烈推荐也阅读下这位大佬的文章;
1、make 的行为
当你在终端下敲下 make 的时候,他的行为是什么呢?
1.1、找文件
首先,他会先找当前目录下名为 Makefile 的文件;找到了就用这个文件作为编译、链接的依据;
如果没有找到 Makefile,那么就找名为 makefile 的文件;
1.2、执行 Makefile
找到 Makefile 或者 makefile 后,便开始执行他里面的内容:
1. 找到第一个目标,作为整个 Makefile 的目标;
2. 分析这个目标的依赖,并执行这个目标所定义的命令;
3. 如果这些依赖,还存在其他依赖,那么继续进行展开;
直到最终编译出第一个目标文件;
注意事项:当然,你也可以非主流的使用 Makefile,就是你定义你的 Makefile 的名字为:Make.Code,或者什么其他奇葩的名字,都行,在编译的时候,使用:make -f 这样的命令来指定 Makefile,比如:make -f Make.Code;
当然,并不推荐这样做,最好是定义 Makefile 这个文件;
OK,说完这个文件的命名,接着说下面的内容,这里,我们默认都遵守规矩,都命名为 Makefile;
2、目标
上面的部分,提到一个字眼,就是目标(target),目标简单的说,就是期望生成的内容,比如,你编译 3 个文件,期望生成一个叫 hello 的文件,那么这个 hello 就是我们的目标;
同样,上面提到一个叫依赖关系的字眼,依赖就是,你这个目标,需要包含哪些文件,比如,你要生成一个可执行程序 hello,那么你需要包含一些 object 文件,这些 object 文件就是这个 hello 的依赖;同样,每个 object 文件也可能依赖一些 .c 或者 .h 文件,这些 .c 和 .h 就是这个 object 文件的依赖;
3、格式
Makefile 的基本格式如下:
target ... : prerequisites ...
command
...
在一个 Makefile 中,可以有多个上述这样的组合存在,这种组合用于描述目标、依赖和目标的生成规则;下面我们就来讲述这三个玩意的含义:
target 指的是目标文件,它可以是 object 文件,也可以是最终生成的可执行文件,也可以仅仅是一个标签;
prerequisites 指的是要生成 target 目标所需要的依赖文件;
command 指的是需要执行的 Shell 命令(注意,这里使用 TAB,而不是 Space);
注意:
这里,我们使用 GNU 官方针对 Makefile,由一个例子引出来:
Here is a straightforward makefile that describes the way an executable file called edit depends on eight object files which, in turn, depend on eight C source and three header files.
In this example, all the C files include defs.h, but only those defining editing commands include command.h, and only low level files that change the editor buffer include buffer.h.
我们构建一个和 GNU 一样的例子出来:
GNU 的这个 Makefile 的内容为如下:
edit : main.o kbd.o command.o display.o insert.o search.o files.o utils.o
cc -o edit main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
main.o : main.c defs.h
cc -c main.c
kbd.o : kbd.c defs.h command.h
cc -c kbd.c
command.o : command.c defs.h command.h
cc -c command.c
display.o : display.c defs.h buffer.h
cc -c display.c
insert.o : insert.c defs.h buffer.h
cc -c insert.c
search.o : search.c defs.h buffer.h
cc -c search.c
files.o : files.c defs.h buffer.h command.h
cc -c files.c
utils.o : utils.c defs.h
cc -c utils.c
clean :
rm edit main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
可以看到这 Makefile 里面有很多个前面描述的组合,最终生成的目标是 edit ,可执行文件,其中这个 edit 文件依赖了一大堆 .o 的中间文件,当依赖的中间文件全部 Ready 后,会执行下面的:
cc -o edit main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
即,把这些 .o 文件链接,生成 edit 目标文件;
而这些个 .o 文件又有各自的依赖文件,比如:
main.o : main.c defs.h
cc -c main.c
所以,当你在终端敲入 make 的时候呢,它的行为是解析了这个 Makefile,然后根据依赖,进行先编译 C 文件成为 .o 文件,等 edit 这个目标依赖的 .o 都编译完成后,进行链接,最终形成 edit;
4、Makefile 包含其他 Makefile
在 Makefile 中,可以使用关键字 include ,来包含其他的 Makefile;他的语法是:
include <filename>
如果 make 在一个 Makefile 中,读到了 include,那么 make 就会暂停当前 Makefile 的读取和动作,转而将这个 include 的 Makefile 给读出来执行;
换句话来说,你可以简单的理解成为,如果一个 Makefile A 里面,使用了 include ,包含了其他的 Makefile B,那么在 include 的这一行,相当于把 Makefile B 直接在这一行展开了;
比如:
include aaa.mk bbb.mk
5、环境变量 MAKEFILES
如果当前环境中,定义了环境变量 MAKEFILES 的话,你一定要注意了,在 make 的时候,这个环境变量定义的内容也会被包括进来,如果有时候,你觉得你的 Makefile 总是有怪怪的行为,那么看看你的环境变量里面有没有这个玩意;
6、Wildcard 通配符的使用
做软件的朋友们应该都知道,我们为了提高效率,常常使用通配符,比如 *、?,这种字符,同样,在我们的 Makefile 中也是一样的,为了提高效率,也经常使用通配符;
打个比方,如果一个目录下,有很多 c 文件,你的目标文件需要这些所有的 c 文件一起编译生成;那么你把所有的 .c 文件全部列出来,是不是感觉,缺了点啥高端操作?好吧,我们来看看 Makefile 里面的通配符的使用:
6.1、命令中使用通配符
还记得我们的 Makefile 的规则吗?
target ... : prerequisites ...
command
...
我们可以直接在 command 中,使用通配符,比如(官方的例子):
clean:
rm -f *.o
因为这是 command,会类似像 Shell 一样去执行;
6.2、依赖中使用通配符
print: *.c
lpr -p $?
touch print
这个print目标依赖的所有的 .c 文件,所以这里直接使用 *.c;
6.3、变量定义中使用通配符
6.3.1、使用 *
这个需要特别特别的注意了,你肯定会写成下面这种:
objects = *.o
这种写法是错误的,因为这样写了后,objects 这个变量,相当于直接赋值成为了 “*.o”这个字符串了,记住记住,是字符串,字符串,如果我们想要真正的当前的所有 .o 文件,在变量赋值的时候,正确写法需要使用 wildcard 函数:
objects := $(wildcard *.o)
不得不多说一句,* 这种通配符,是针对我们系统;什么叫针对系统,比如,你一个 Makefile 的文件夹下面,有 test1.c、test2.c、test3.c,那么你使用:
src := $(wildcard *.c)
可以得到这个 Makefile 目录下的所有 .c的所有文件;这个是 Shell 目录下的 * 通配符;
6.3.2、使用 %
% 这种通配符,是 Makefile 语法的通配符,是仅限于这个 Makefile 文件推导后的通配符;虽然都是通配符,% 和 * 用法有点不一样,但是如果你把你的 Makefile 里面写入下面:
%.o:%.c
gcc -o $@ $<
你去执行 make,会出现如下:
Make: *** target not found. stop.
根本原因是:使用 % 通配符,他不是去系统目录下面去找文件,而是看本 Makefile 里面是否有推导出来这个玩意;
所以,改法如下:
all:$(subst .c,.o,$(wildcard *.c))
%.o:%.c
gcc -o $@ $<
上面这段的意义是,先使用 $(wildcard *.c) 去找 Makefile 目录下的所有 .c 文件,然后使用 subst ,将这个 .c 后缀改为 .o 后缀;
这样,Makefile 在推导的时候,会先推导了 all 这个 target 的依赖,得到了这些个 .o,后面就知道了 %o 和 %c 是啥了;
6.3.3、* 和 % 区别
总结起来,他们俩都是通配符,* 针对的是系统目录,文件,行为;而 % 是针对本 Makefile 文件内,经过推导后的变量行为;
也可以参考一下:
7、伪目标 PHONY
前面用了伪目标,现在在来好好说一下这个伪目标;
完全可以从字面上理解这个伪目标,他不是 Makefile 里面的一个真正的目标;比如我们 Makefile 都需要一个 clean 的操作,我们可以把它设置成为一个伪目标:
.PHONY : clean
clean :
rm *.o temp
7.1、没有依赖的伪目标
通常的,类似于我们的 clean 这种伪目标,它是没有任何依赖的,因为它本身就不是目标;这个完全可以理解;用法和上面一样,不再多说了;
7.2、有依赖的伪目标
伪目标,虽然是假的,但是呢,有一种用法还是可以有依赖,就是,如果你想一次性生成多个目标文件,比如,你在一个 Makefile 中,想 make 一下后,生成很多和可执行的文件,那怎么做呢?可以使用如下的方式:
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 的目标(就是你直接敲 make,然后就会找寻第一个目标成为默认的目标),前提是你要把他放到其他的目标前面;
这个例子中,默认目标是 all,是一个伪目标,他依赖了 3 个目标,其实我们就可以通过这样的方式,一次性生成 3 个目标文件;
7.3、伪目标依赖伪目标
伪目标,还可以依赖伪目标,说起来有点绕口,我们直接看官方的例子:
.PHONY: cleanall cleanobj cleandiff
cleanall : cleanobj cleandiff
rm program
cleanobj :
rm *.o
cleandiff :
rm *.diff
可以看到:
1. cleanall cleanobj cleandiff 都被定义成为了伪目标;
2. cleanall 这个伪目标,又依赖了 cleanobj cleandiff 这两个伪目标;
那么,如果你输入 make cleanall,就会触发 cleanobj 和 cleandiff,去删除 *.o 和 *.dff,然后删除 program;
当然,你也可以输入 cleandiff 去删除 *.dff
或者输入 cleanobj 去删除 *.o;
用法比较灵活吧,哈哈;
8、使用变量
Makefile 里面使用变量,绝对是很常用的提升效率的方法,便于维护,结构清晰;灵活使用
这里的变量,我希望扩展出来,不仅仅是 Makefile 文件中定义的变量,GNU make/Makefile 中的变量分为很多类型:
1、环境变量;
2、内置变量;
3、自动变量;
4、Makefile 中定义的变量;
8.1、变量的引用
除了自动化变量,其他变量引用方式是用 $(变量) 或者 ${变量};
8.2、环境变量
在make开始运行时被载入到Makefile文件中,但是如果Makefile中已定义了这个变量,或是这个变量由make命令行带入,那么系统的环境变量的值将被覆盖。(如果make指定了“-e”参数,那么,系统环境变量将覆盖Makefile中定义的变量)比如:PATH,PWD,和前面的 MAKEFILES;
8.3、内置变量/默认变量(default)
make/Makefile 还有很多内置的变量,或者称为默认变量(default),可以在 Shell 环境,通过 make -p 打印出来:
这些内置变量可以直接在 Makefile 中使用,比较常用的内置变量有:
CURDIR:当前目录
MAKE:指代 make
SHELL:当前的 Shell
RM:指代 rm -f
CC:指代 cc,当前 C 编译器
LD:当前的 ld
.........
上面只列举了很少一部分内容, 有兴趣的可以使用在线的 Shell ,往里面敲 make -p 试试看:
菜鸟教程在线编辑器https://www.runoob.com/try/runcode.php?filename=helloworld&type=bash
打印出来的内容差不多是这样的:
# GNU Make 4.2.1
# Built for x86_64-pc-linux-gnu
# Copyright (C) 1988-2016 Free Software Foundation, Inc.
# License GPLv3+: GNU GPL version 3 or later
# This is free software: you are free to change and redistribute it.
# There is NO WARRANTY, to the extent permitted by law.
# Make data base, printed on Wed Feb 9 05:53:57 2022
# Variables
# automatic
# automatic
?F = $(notdir $?)
# default
.SHELLFLAGS := -c
# default
CWEAVE = cweave
# automatic
?D = $(patsubst %/,%,$(dir $?))
# automatic
@D = $(patsubst %/,%,$(dir $@))
# automatic
@F = $(notdir $@)
# makefile
CURDIR := /box
# default
SHELL := /bin/sh
# default
RM = rm -f
# default
CO = co
# environment
_ = /usr/bin/make
# default
PREPROCESS.F = $(FC) $(FFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -F
# default
LINK.m = $(OBJC) $(OBJCFLAGS) $(CPPFLAGS) $(LDFLAGS) $(TARGET_ARCH)
# default
LINK.o = $(CC) $(LDFLAGS) $(TARGET_ARCH)
# default
OUTPUT_OPTION = -o $@
# default
COMPILE.cpp = $(COMPILE.cc)
# makefile
MAKEFILE_LIST :=
# 'override' directive
GNUMAKEFLAGS :=
# default
LINK.p = $(PC) $(PFLAGS) $(CPPFLAGS) $(LDFLAGS) $(TARGET_ARCH)
# default
CC = cc
# default
CHECKOUT,v = +$(if $(wildcard $@),,$(CO) $(COFLAGS) $< $@)
# environment
LIBC_FATAL_STDERR_ = 1
# environment
JUDGE0_HOMEPAGE = https://judge0.com
# default
CPP = $(CC) -E
# default
LINK.cc = $(CXX) $(CXXFLAGS) $(CPPFLAGS) $(LDFLAGS) $(TARGET_ARCH)
# default
MAKE_HOST := x86_64-pc-linux-gnu
# environment
PATH = /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
# default
LD = ld
# default
TEXI2DVI = texi2dvi
# default
YACC = yacc
# default
COMPILE.mod = $(M2C) $(M2FLAGS) $(MODFLAGS) $(TARGET_ARCH)
# default
ARFLAGS = rv
# default
LINK.r = $(FC) $(FFLAGS) $(RFLAGS) $(LDFLAGS) $(TARGET_ARCH)
# default
LINT = lint
# default
COMPILE.f = $(FC) $(FFLAGS) $(TARGET_ARCH) -c
# default
LINT.c = $(LINT) $(LINTFLAGS) $(CPPFLAGS) $(TARGET_ARCH)
# default
YACC.m = $(YACC) $(YFLAGS)
# default
YACC.y = $(YACC) $(YFLAGS)
# default
AR = ar
# default
.FEATURES := target-specific order-only second-expansion else-if shortest-stem undefine oneshell archives jobserver output-sync check-symlink load
# default
TANGLE = tangle
# default
GET = get
# automatic
%F = $(notdir $%)
# default
COMPILE.F = $(FC) $(FFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c
# default
CTANGLE = ctangle
# default
.LIBPATTERNS = lib%.so lib%.a
# default
LINK.C = $(LINK.cc)
# environment
PWD = /box
# default
.LOADED :=
# default
LINK.S = $(CC) $(ASFLAGS) $(CPPFLAGS) $(LDFLAGS) $(TARGET_MACH)
# default
PREPROCESS.r = $(FC) $(FFLAGS) $(RFLAGS) $(TARGET_ARCH) -F
# default
LINK.c = $(CC) $(CFLAGS) $(CPPFLAGS) $(LDFLAGS) $(TARGET_ARCH)
# default
LINK.s = $(CC) $(ASFLAGS) $(LDFLAGS) $(TARGET_MACH)
# environment
HOME = /tmp
# automatic
^D = $(patsubst %/,%,$(dir $^))
# environment
JUDGE0_VERSION = 1.12.0
# environment
MAKELEVEL := 0
# default
COMPILE.m = $(OBJC) $(OBJCFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c
# default
MAKE = $(MAKE_COMMAND)
# environment
SHLVL = 2
# default
AS = as
# default
PREPROCESS.S = $(CC) -E $(CPPFLAGS)
# default
COMPILE.p = $(PC) $(PFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c
# default
MAKE_VERSION := 4.2.1
# default
FC = f77
# makefile
.DEFAULT_GOAL :=
# automatic
%D = $(patsubst %/,%,$(dir $%))
# default
WEAVE = weave
# default
MAKE_COMMAND := make
其中,明确表示了变量的类型,写着 automatic 的是自动变量,写着 default 的是默认变量,写着 environment 的是环境变量;
8.4、自动变量
Makefile 定义了一串自动化变量,用起来也非常方便:
$@:表示目标文件的名称,包含扩展名
$^:表示所有的依赖文件,以空格隔开,不重复
$<:表示第一个依赖文件的名称
$+:表示所有的依赖文件,空格隔开,可以重复
$*:表示目标文件的名称,不包含扩展名
$?:依赖项中,所有比目标文件新的依赖文件
打个比方:
test : test.o test1.o test2.o
gcc -o $@ $^
test.o : test.c test.h
gcc -o $@ $<
test1.o : test1.c test1.h
gcc -o $@ $<
test2.o : test2.c test2.h
gcc -o $@ $<
这个规则模式中用到了 "$@" 、"$<" 和 "$^" 这三个自动化变量:
每一个 "$@" 都代表的是目标文件
“$^” 代表的是所有依赖的文件,展开也就是 test.o test1.o test2.o
“$<”代表的是依赖文件中的第一个,也就是那些个 .c 的文件;
我们在执行 make 的时候,make 会自动识别命令中的自动化变量,并自动实现自动化变量中的值的替换。
8.5、Makefile 中自行定义变量
除了上面这些已经有了的,可以直接使用的变量以外,我们还可以在 Makefile 中自己定义想要用到的变量;定义变量有几种方式:
- 简单赋值 ( := ) 编程语言中常规理解的赋值方式,只对当前语句的变量有效。
- 递归赋值 ( = ) 变量的值是整个Makefile中最后被指定的值。
- 条件赋值 ( ?= ) 如果变量未定义,则使用符号中的值定义变量。如果该变量已经赋值,则该赋值语句无效。
- 追加赋值 ( += ) 原变量用空格隔开的方式追加一个新值。
8.4.1、"="
比较常用的使用 "=" 号定义一个变量:
objects = program.o foo.o utils.o
program : $(objects)
cc -o program $(objects)
$(objects) : defs.h
引用变量时候可以用 $(objects) 或者是 ${objects};
变量 objects 会在 $(objects) 的地方,精确的展开;
有时候,= 号赋值,容易被搞错,比如:
x = foo
y = $(x) bar
x = later
最后 y 的值是 later bar,而不是 foo bar,因为 Makefile 展开后,会以最后一个赋值的 x 当做最终的值;
8.4.2、":="
:= 这种赋值语句,就和我们正常理解的 C 语言的变量赋值含义完全一样,举个例子:
x := foo
y := $(x) bar
x := later
和上面的 = 号赋值不一样,使用 := 这种赋值,可以理解为顺序执行,y 的值是 foo bar,x 的值开始是 foo,最后的值是 later;
如果是这种情况:
y := $(x) bar
x := foo
即,在 y 赋值之前,没有定义 x,那么 y 的值等于 bar;
8.4.3、"?="
?= 的赋值很好理解,如果变量未定义,则使用符号中的值定义变量。如果该变量已经赋值,则该赋值语句无效;
比如:
FOO ?= bar
如果在这条语句之前,FOO 被定义了,那么这条语句将啥都不做,否则,会将 bar 赋值给 FOO;
8.4.4、"=+"
这个应该比较熟悉了,而且没有任何歧义;指的是在之前的基础之上,在累加上其他变量;
x:=foo
y:=$(x)b
x+=$(y)
最后 y 的值等于 foob,x 的值等于 foo foob;
8.4.5、变量替换
我们可以替换变量中的共有的部分,其格式是“(var:a=b)”或是“{var:a=b}”,其意思是,把变量“var”中所有以“a”字串“结尾”的“a”替换成“b”字串。这里的“结尾”意思是“空格”或是“结束符”,这玩意就很有用,怎么说呢?我们来看看例子:
foo := a.o b.o c.o
bar := $(foo:.o=.c)
这个示例中,我们先定义了一个“(foo)”变量,而第二行的意思是把“(foo)”中所有以“.o”字串“结尾”全部替换成“.c”,所以我们的“$(bar)”的值就是“a.c b.c c.c”。
还有一种写法:
foo:=a.c b.c d.c
obj:=$(foo:%.c=%.o)
All:
@echo $(obj)
其中的 % 是通配符的意思;前面已经做过解释,不在赘述;
8.4.6、变量的传递
一个大型的软件工程,一般会有多个 Makefile 组成,那么就涉及到一个问题,上一级的 Makefile 定义的变量,下一级 Makefile(子目录下的 Makefile) 是否能用?
一般情况下,有些变量,在顶层的 Makefile 定义好了后, 直接往下传就行了,下面不需要在定义;比如:交叉编译工具链,目录结构,CPU 结构体系等等;
变量的传递,使用关键字 export:如果你想传递一个变量到下级 Makefile,那么:
export <variable ...>
比如,你定义好了你的交叉编译工具链:
CROSS_COMPILE = /usr/local/arm/arm-2009q3/bin/arm-none-linux-gnueabi-
这个变量肯定需要往下一级 Makefile 传递(因为要编译嘛):
export CROSS_COMPILE
如果不希望变量被传递到下一级 Makefile,使用 unexport 关键字:
unexport <variable ...>
9、多 Makefile 层次嵌套
在正式项目,或者大型的项目工程中,常常使用多 Makefile 组合来描述整个软件系统的编译,通常情况下,代码顶层有一个 Makefile,我们管它叫做总控 Makefile,其余的子目录,也有他自己的 Makefile;
一般的,子目录的 Makefile 描述的是这个目录下的文件的编译规则,顶层 Makefile 统领所有;那就涉及到了顶层 Makefile 如何调用子目录的 Makefile;
GNU make 中,使用
$(MAKE) -C <Dir>
其中,$(MAKE) 指的是引用了 MAKE 这个变量的值,MAKE 这个变量是一个内置变量(默认变量),他相当于 "make" 这个字符串;
所以 $(MAKE) -C <Dir> 展开为:
make -C <Dir>
这个 make -C 是 make 指令的 options,意思是:进入到目录 DIR,然后执行 make
比如,你一个顶层 Makefile,有一个文件夹 subdir,使用如下命令进入这个文件夹,生成目标:
subsystem:
$(MAKE) -C subdir
如果你有多个文件夹,那么可以使用 for 循环:
SUBDIRS=foo bar baz
subdirs:
for dir in $(SUBDIRS); do\
$(MAKE) -C $$dir; \
done
此时,每次 make 进入文件夹的时候都会打印:
make[1]:Entering directory xxxx
执行完毕改条 $(MAKE) -C 后,退出文件夹:
make[1]:Leaving directory xxxx
10、条件判断
Makefile 支持条件判断语句,类似于 C 语言的 #if...#else...#endif 和 #ifdef...#else...#endif
关键字 | 功能 |
---|---|
ifeq | 判断参数是否不相等,相等为 true,不相等为 false。 |
ifneq | 判断参数是否不相等,不相等为 true,相等为 false。 |
ifdef | 判断是否有值,有值为 true,没有值为 false。 |
ifndef | 判断是否有值,没有值为 true,有值为 false。 |
10.1、ifeq 和 ifneq
他们的用法类似于 C 语言的 #if...#else...#endif,条件判断的使用方式如下:
ifeq (ARG1, ARG2)
ifeq 'ARG1' 'ARG2'
ifeq "ARG1" "ARG2"
ifeq "ARG1" 'ARG2'
ifeq 'ARG1' "ARG2"
官方的例子为判断当前的编译器 CC 是不是 gcc:
libs_for_gcc= -lgnu
normal_libs=
foo:$(objects)
ifeq($(CC),gcc)
$(CC) -o foo $(objects) $(libs_for_gcc)
else
$(CC) -o foo $(objects) $(noemal_libs)
endif
ifneq 的用法和 ifeq 完全一样,只不过逻辑相反,不在多说;
10.2、ifdef 和 ifndef
他们的用法类似于 C 语言的 #ifdef...#else...#endif,条件判断的使用方式如下:
ifdef variable-name
它的主要功能是判断变量的值是不是为空,实例 1:
bar =
foo = $(bar)
all:
ifdef foo
@echo yes
else
@echo no
endif
打印 yes
实例 2:
foo=
all:
ifdef foo
@echo yes
else
@echo no
endif
打印 no;
通过两个实例对比说明:通过打印 "yes" 或 "no" 来演示执行的结果。我们执行 make 可以看到实例 1打印的结果是 "yes" ,实例 2打印的结果是 "no" 。其原因就是在实例 1 中,变量“foo”的定义是“foo = $(bar)”。虽然变量“bar”的值为空,但是“ifdef”的判断结果为真,这种方式判断显然是有不行的,因此当我们需要判断一个变量的值是否为空的时候需要使用“ifeq" 而不是“ifdef”。
特别注意的是,make是在读取Makefile时就计算条件表达式的值,并根据条件表达式的值来选择语句,所以,你最好不要把自动化变量(如“$@”等)放入条件表达式中,因为自动化变量是在运行时才有的。
11、使用函数
Makefile 中支持使用函数,而且 Makefile 中预定义了很多对我们高效写 Makefile 有帮助的函数;函数的调用方法为:
$(<function> <arguments>) 或者是 ${<function> <arguments>}
function:函数名
arguments:参数
不同的函数,名字不一样,参数的个数也不尽相同,函数与参数之间使用空格分隔,多个参数之间使用逗号分隔;
11.1、字符串处理函数
11.1.1、subst
subst 字符串替换函数,它的格式如下:
$(subst <from>,<to>,<text>)
函数的功能是把字符串中的 form 替换成 to,返回值为替换后的新字符串。实例:
OBJ=$(subst ee,EE,feet on the street)
all:
@echo $(OBJ)
执行 make 命令,把“feet on the street”中的“ee”替换成“EE”,我们得到的值是“fEEt on the strEEt”
11.1.2、patsubst
patsubst 模式字符串替换函数,它的格式如下:
$(patsubst <pattern>,<replacement>,<text>)
函数功能是查找 text 中的单词是否符合模式 pattern,如果匹配的话,则用 replacement 替换。返回值为替换后的新字符串。实例:
OBJ=$(patsubst %.c,%.o,1.c 2.c 3.c)
all:
@echo $(OBJ)
<pattern>可以包括通配符 “%”,表示任意长度的字串。 如果<replacement>中也包含“%”, 那么, <replacement> 中的这个“%”将是<pattern>中的那个“%”所代表的字串;执行 make 命令,我们可以得到的值是 "1.o 2.o 3.o",这些都是替换后的值;
11.1.3、strip
strip 去空格函数;函数使用格式如下:
$(strip <string>)
函数的功能是去掉字符串的开头和结尾的字符串,并且将其中的多个连续的空格合并成为一个空格。返回值为去掉空格后的字符串。实例:
OBJ=$(strip a b c)
all:
@echo $(OBJ)
去掉空格后,输出内容为:“a b c”。这个只是除去开头和结尾的空格字符,并且将字符串中的空格合并成为一个空格;
11.1.4、findstring
findstring 查找字符串函数,函数使用格式如下:
$(findstring <find>,<in>)
函数的功能是查找 in 中的 find ,如果我们查找的目标字符串存在,返回值为目标字符串,如果不存在就返回空。实例:
OBJ=$(findstring a,a b c)
all:
@echo $(OBJ)
在 "a b c" 中查找 a,返回 a;
OBJ=$(findstring a,b c)
all:
@echo $(OBJ)
在 "b c" 中查找 a,返回空;
11.1.5、filter
filter 过滤函数,函数使用格式如下:
$(filter <pattern>,<text>)
函数的功能是过滤出 text 中符合模式 pattern 的字符串,可以有多个 pattern 。返回值为过滤后的字符串。实例:
sources := foo.c bar.c baz.s ugh.h
foo: $(sources)
cc $(filter %.c %.s,$(sources)) -o foo
$(filter %.c %.s,$(sources)) 过滤出了 foo.c bar.c baz.s ;
11.1.6、filter-out
filter-out 反向过滤函数,函数使用格式如下:
$(filter-out <pattern>,<text>)
函数的功能是功能和 filter 函数正好相反,但是用法相同。去除符合模式 pattern 的字符串,保留符合的字符串。返回值是保留的字符串。实例:
objects=main1.o foo.o main2.o bar.o
mains=main1.o main2.o
$(filter-out $(mains),$(objects))
返回值是“foo.o bar.o”
11.1.7、sort
sort 排序函数,函数使用格式如下:
$(sort <list>)
函数的功能是将<list>中的单词排序(升序)。返回值为排列后的字符串。实例:
$(sort foo bar lose)
返回 “bar foo lose”
注意:sort会去除重复的字符串
11.1.8、word
word 取单词函数,函数使用格式如下:
$(word <n>,<text>)
函数的功能是取出函数据<text>中的第n个单词。返回值为我们取出的第 n 个单词。实例:
$(word 2, foo bar baz)
返回值是“bar”
11.1.9、wordlist
wordlist 取单词串函数,函数使用格式如下:
$(wordlist <s>,<e>,<text>)
函数的功能是从字符串<text>中取从<s>开始到<e>的单词串。<s>和<e>是一个数字;返回字符串<text>中从<s>到<e>的单词字串。如果<s>比<text>中的单词数要大,那么返回空字符串。如果<e>大于<text>的单词数,那么返回从<s>开始,到<text>结束的单词串。示例:
$(wordlist 2, 3, foo bar baz)
返回值是“bar baz”
11.1.10、words
words 单词个数统计函数;函数使用格式如下:
$(words <text>)
统计<text>中字符串中的单词个数,统计<text>中字符串中的单词个数;
$(words, foo bar baz)
返回值是“3”;
如果我们要取<text>中最后的一个单词,我们可以这样:$(word $(words <text>),<text>)。
11.1.11、firstword
firstword 首单词函数,函数使用格式如下:
$(firstword <text>)
取字符串<text>中的第一个单词;示例:
$(firstword foo bar)
返回值是“foo”
11.2、文件名相关函数
获取文件的路径,去除文件的路径,取出文件前缀或后缀等等;下面我们要介绍的函数主要是处理文件名的。每个函数的参数字符串都会被当做一个或是一系列的文件名来对待。
11.2.1、dir
dir 取目录函数,函数使用格式如下:
$(dir <names>)
函数的功能是从文件名序列 names 中取出目录部分,如果没有 names 中没有 "/" ,取出的值为 "./" 。返回值为目录部分,指的是最后一个反斜杠之前的部分。如果没有反斜杠将返回“./”。实例:
OBJ=$(dir src/foo.c hacks)
all:
@echo $(OBJ)
name 字段有2个,所以 make 后返回为:“src/ ./”
11.2.2、notdir
notdir 取文件函数,函数使用格式如下:
$(notdir <names>)
函数的功能是从文件名序列 names 中取出非目录的部分。非目录的部分是最后一个反斜杠之后的部分。返回值为文件非目录的部分。实例:
OBJ=$(notdir src/foo.c hacks)
all:
@echo $(OBJ)
返回的值是“foo.c hacks”
11.2.3、suffix
suffix 取后缀名函数,函数使用格式如下:
$(suffix <names>)
函数的功能是从文件名序列中 names 中取出各个文件的后缀名。返回值为文件名序列 names 中的后缀序列,如果文件没有后缀名,则返回空字符串。实例:
OBJ=$(suffix src/foo.c src-1.0/bar.c hacks)
all:
@echo $(OBJ)
返回值是 ".c .c"
注意,这里取的后缀,是带了那个点. 的
11.2.4、basename
basename 取前缀函数,函数使用格式如下:
$(basename <names>)
函数的功能是从文件名序列 names 中取出各个文件名的前缀部分。返回值为被取出来的文件的前缀名,如果文件没有前缀名则返回空的字符串。实例:
OBJ=$(basename src/foo.c src-1.0/bar.c hacks)
all:
@echo $(OBJ)
返回的值是 src/foo src-1.0/bar hacks
11.2.5、addsuffix
addsuffix 添加后缀名函数,函数使用格式如下:
$(addsuffix <suffix>,<names>)
函数的功能是把后缀 suffix 加到 names 中的每个单词后面。返回值为添加上后缀的文件名序列。实例:
$(addsuffix .c,foo bar)
返回值是“foo.c bar.c”
值得一提的是,如果文件名存在后缀名,依然会加上:
OBJ=$(addsuffix .c,src/foo.c hacks)
all:
@echo $(OBJ)
返回 “sec/foo.c.c hack.c”
11.2.6、addprefix
addprefix 添加前缀名函数,函数使用格式如下:
$(addperfix <prefix>,<names>)
函数的功能是把前缀 prefix 加到 names 中的每个单词的前面。返回值为添加上前缀的文件名序列。实例:
OBJ=$(addprefix src/, foo.c hacks)
all:
@echo $(OBJ)
返回 "src/foo.c src/hacks",这是一种添加路径的方法;
11.2.7、join
join 链接函数,函数使用格式如下:
$(join <list1>,<list2>)
函数功能是把 list2 中的单词对应的拼接到 list1 的后面。
- 如果 list1 的单词要比 list2 的多,那么,list1 中多出来的单词将保持原样,
- 如果 list1 中的单词要比 list2 中的单词少,那么 list2 中多出来的单词将保持原样。
返回值为拼接好的字符串。实例:
OBJ=$(join src car,abc zxc qwe)
all:
@echo $(OBJ)
返回值是 “srcabc carzxc qwe”。很显然<list1>中的文件名比<list2>的少,所以多出来的保持不变
11.2.8、wildcard
wildcard 获取匹配模式文件名函数,命令使用格式如下:
$(wildcard PATTERN)
函数的功能是列出当前目录下所有符合模式的 PATTERN 格式的文件名。返回值为空格分隔并且存在当前目录下的所有符合模式 PATTERN 的文件名。实例:
OBJ=$(wildcard *.c *.h)
all:
@echo $(OBJ)
得到当前函数下所有的 ".c " 和 ".h" 结尾的文件。这个函数通常跟的通配符 "*" 连用,使用在依赖规则的描述的时候被展开;
11.3、循环函数
11.3.1、foreach
foreach 是用来做循环的函数,他的语法是:
$(foreach <var>,<list>,<text>)
把参数<list>中的单词逐一取出放到参数<var>所指定的变量中,然后再执行<text>所包含的表达式。每一次<text>会返回一个字符串,循环过程中,<text>的返所返回的每个字符串会以空格分割,最后当整个循环结束的时候,<text>所返回的每个字符串所组成的整个字符串(以空格分隔)将会是 foreach 函数的返回值。所以<var>最好是一个变量名,<list>可以是一个表达式,而<text>中一般会只用<var>这个参数来一次枚举<list>中的单词。
实例:
name:=a b c d
files:=$(foreach n,$(names),$(n).o)
all:
@echo $(files)
$(files) 返回值是 “a.o b.o c.o d.o”
注意,foreach中的<var>参数是一个临时的局部变量,foreach函数执行完后,参数<var>的变量将不在作用,其作用域只在foreach函数当中。
11.4、条件函数
11.4.1、if
Makefile 中使用 if 函数和 make 支持的 ifeq 条件语句非常类似,只不过这个 if 是函数;他的语法有两种,分别是带else的和不带else的:
$(if <condition>,<then-part>)
$(if <condition>,<then-part>,<else-part>)
即if函数的参数可以是两个,也可以是三个;
<condition>参数是 if 的表达式,如果其返回的为非空字符串,那么这个表达式就相当于返回真,于是,<then-part>会被计算,否则<else-part>会被计算
而if函数的返回值是,如果<condition>为真(非空字符串),那个<then-part>会是整个函数的返回值,如果<condition>为假(空字符串),那么<else-part>会是整个函数的返回值,此时如果<else-part>没有被定义,那么,整个函数返回空字串;
实例:
OBJ:=foo.c
OBJ:=$(if $(OBJ),$(OBJ),main.c)
all:
@echo $(OBJ)
函数的值是 foo.c,如果变量 OBJ 的值为空的话,我们得到的 OBJ 的值就是main.c
11.5、创建函数
11.5.1、call
call 函数用来创建一个新的函数(功能比较弱),可以支持自定义多个参数;他的语法是:
$(call <expression>,<parm1>,<parm2>,<parm3>,...)
当 make 执行这个函数的时候,expression参数中的变量$(1)、$(2)、$(3)等,会被参数parm1,parm2,parm3依次取代。而expression的返回值就是 call 函数的返回值。
举例 1:
reverse = $(1) $(2)
foo = $(call reverse,a,b)
all:
@echo $(foo)
foo 的值就是“a b”。当然,参数的次序可以是自定义的,不一定是顺序的,
举例 2:
reverse = $(2) $(1)
foo = $(call reverse,a,b)
all:
@echo $(foo)
此时的 foo 的值就是“b a”
11.6、origin 函数
origin 函数不像其他的函数,它并不操作变量的值,它只是告诉你这个变量是哪里来的,比较抽象的概念,下面举例子来介绍:
他的语法是:
$(origin <variable>)
注意: variable 是变量的名字,不应该是引用,所以最好不要在 variable 中使用“$”字符。origin 函数会员其返回值来告诉你这个变量的“出生情况”。
origin 的返回值有固定的几个:
- “undefined”:如果<variable>从来没有定义过,函数将返回这个值。
- “default”:如果<variable>是一个默认的定义,比如说“CC”这个变量。
- “environment”:如果<variable>是一个环境变量并且当Makefile被执行的时候,“-e”参数没有被打开。
- “file”:如果<variable>这个变量被定义在Makefile中,将会返回这个值。
- “command line”:如果<variable>这个变量是被命令执行的,将会被返回。
- “override”:如果<variable>是被override指示符重新定义的。
- “automatic”:如果<variable>是一个命令运行中的自动化变量。
这些信息对于我们编写 Makefile 是非常有用的,例如假设我们有一个 Makefile ,其包含了一个定义文件Make.def,在Make.def中定义了一个变量bletch,而我们的环境变量中也有一个环境变量bletch,我们想去判断一下这个变量是不是环境变量,如果是我们就把它重定义了。如果是非环境变量,那么我们就不重新定义它。于是,我们在 Makefile 中,可以这样写:
ifdef bletch
ifeq "$(origin bletch)" "environment"
bletch = barf,gag,etc
endif
endif
11.7、shell 函数
shell 函数,顾名思义,可以理解为调用了操作系统的 shell 命令,这样,在 Makefile 中就可以使用诸如什么 awk,sed,cat,这样的指令,比如:
contents := $(shell cat foo)
files := $(shell echo *.c)
注意,这个函数会新生成一个Shell程序来执行命令,所以你要注意其运行性能,如果你的Makefile中有一些比较复杂的规则,并大量使用了这个函数,那么对于你的系统性能是有害的。特别是Makefile的隐晦的规则可能会让你的shell函数执行的次数比你想像的多得多。
12、make 命令参数和选项
前面说的都是 Makefile 以及直接在命令行敲入 make 命令,其实 make 命令是可以带参数的(前面为了执行多个 Makefile 使用了 make -C),make 的格式如下:
make [-f Makefile] [option] [target]
可以通过 -f 指定使用哪个 Makefile,可以带 option,或者指定目标(比如 make clean);
make 的所有的定义,可以使用:
make -h 帮助。更详细帮助可以查看 man make
来查看:
Usage: make [options] [target] ...
Options:
-b, -m Ignored for compatibility.
-B, --always-make Unconditionally make all targets.
-C DIRECTORY, --directory=DIRECTORY
Change to DIRECTORY before doing anything.
-d Print lots of debugging information.
--debug[=FLAGS] Print various types of debugging information.
-e, --environment-overrides
Environment variables override makefiles.
--eval=STRING Evaluate STRING as a makefile statement.
-f FILE, --file=FILE, --makefile=FILE
Read FILE as a makefile.
-h, --help Print this message and exit.
-i, --ignore-errors Ignore errors from recipes.
-I DIRECTORY, --include-dir=DIRECTORY
Search DIRECTORY for included makefiles.
-j [N], --jobs[=N] Allow N jobs at once; infinite jobs with no arg.
-k, --keep-going Keep going when some targets can't be made.
-l [N], --load-average[=N], --max-load[=N]
Don't start multiple jobs unless load is below N.
-L, --check-symlink-times Use the latest mtime between symlinks and target.
-n, --just-print, --dry-run, --recon
Don't actually run any recipe; just print them.
-o FILE, --old-file=FILE, --assume-old=FILE
Consider FILE to be very old and don't remake it.
-O[TYPE], --output-sync[=TYPE]
Synchronize output of parallel jobs by TYPE.
-p, --print-data-base Print make's internal database.
-q, --question Run no recipe; exit status says if up to date.
-r, --no-builtin-rules Disable the built-in implicit rules.
-R, --no-builtin-variables Disable the built-in variable settings.
-s, --silent, --quiet Don't echo recipes.
-S, --no-keep-going, --stop
Turns off -k.
-t, --touch Touch targets instead of remaking them.
--trace Print tracing information.
-v, --version Print the version number of make and exit.
-w, --print-directory Print the current directory.
--no-print-directory Turn off -w, even if it was turned on implicitly.
-W FILE, --what-if=FILE, --new-file=FILE, --assume-new=FILE
Consider FILE to be infinitely new.
--warn-undefined-variables Warn when an undefined variable is referenced.
参数选项 | 功能 |
---|---|
-b,-m | 忽略,提供其他版本 make 的兼容性 |
-B,--always-make | 强制重建所有的规则目标,不根据规则的依赖描述决定是否重建目标文件。 |
-C DIR,--directory=DIR | 在读取 Makefile 之前,进入到目录 DIR,然后执行 make。当存在多个 "-C" 选项的时候,make 的最终工作目录是第一个目录的相对路径。 |
-d | make 在执行的过程中打印出所有的调试信息,包括 make 认为那些文件需要重建,那些文件需要比较最后的修改时间、比较的结果,重建目标是用的命令,遗憾规则等等。使用 "-d" 选项我们可以看到 make 构造依赖关系链、重建目标过程中的所有的信息。 |
--debug[=OPTIONS] | make 执行时输出调试信息,可以使用 "OPTIONS" 控制调试信息的级别。默认是 "OPTIONS=b" ,"OPTIONS" 的可值为以下这些,首字母有效:all、basic、verbose、implicit、jobs、makefile。 |
-e,--enveronment -overrides | 使用环境变量定义覆盖 Makefile 中的同名变量定义。 |
-f=FILE,--file=FILE, --makefile=FILE | 指定文件 "FILE" 为 make 执行的 Makefile 文件 |
-p,--help | 打印帮助信息。 |
-i,--ignore-errors | 执行过程中忽略规则命令执行的错误。 |
-I DIR,--include-dir=DIR | 指定包含 Makefile 文件的搜索目录,在Makefile中出现另一个 "include" 文件时,将在 "DIR" 目录下搜索。多个 "-i" 指定目录时,搜索目录按照指定的顺序进行。 |
-j [JOBS],--jobs[=JOBS] | 可指定同时执行的命令数目,没有 "-j" 的情况下,执行的命令数目将是系统允许的最大可能数目,存在多个 "-j" 目标时,最后一个目标指定的 JOBS 数有效。 |
-k,--keep-going | 执行命令错误时不终止 make 的执行,make 尽最大可能执行所有的命令,直至出现知名的错误才终止。 |
-l load,--load-average=[=LOAD],--max-load[=LOAD] | 告诉 make 在存在其他任务执行的时候,如果系统负荷超过 "LOAD",不在启动新的任务。如果没有指定 "LOAD" 的参数 "-l" 选项将取消之前 "-l" 指定的限制。 |
-n,--just-print,--dry-run | 只打印执行的命令,但是不执行命令。 |
-o FILE,--old-file=FILE, --assume-old=FILE | 指定 "FILE"文件不需要重建,即使是它的依赖已经过期;同时不重建此依赖文件的任何目标。注意:此参数不会通过变量 "MAKEFLAGS" 传递给子目录进程。 |
-p,--print-date-base | 命令执行之前,打印出 make 读取的 Makefile 的所有数据,同时打印出 make 的版本信息。如果只需要打印这些数据信息,可以使用 "make -qp" 命令,查看 make 执行之前预设的规则和变量,可使用命令 "make -p -f /dev/null" |
-q,-question | 称为 "询问模式" ;不运行任何的命令,并且无输出。make 只返回一个查询状态。返回状态 0 表示没有目标表示重建,返回状态 1 表示存在需要重建的目标,返回状态 2 表示有错误发生。 |
-r,--no-builtin-rules | 取消所有的内嵌函数的规则,不过你可以在 Makefile 中使用模式规则来定义规则。同时选项 "-r" 会取消所有后缀规则的隐含后缀列表,同样我们可以在 Makefile 中使用 ".SUFFIXES",定义我们的后缀名的规则。"-r" 选项不会取消 make 内嵌的隐含变量。 |
-R,--no-builtin-variabes | 取消 make 内嵌的隐含变量,不过我们可以在 Makefile 中明确定义某些变量。注意:"-R" 和 "-r" 选项同时打开,因为没有了隐含变量,所以隐含规则将失去意义。 |
-s,--silent,--quiet | 取消命令执行过程中的打印。 |
-S,--no-keep-going, --stop | 取消 "-k" 的选项在递归的 make 过程中子 make 通过 "MAKEFLAGS" 变量继承了上层的命令行选项那个。我们可以在子 make 中使用“-S”选项取消上层传递的 "-k" 选项,或者取消系统环境变量 "MAKEFLAGS" 中 "-k"选项。 |
-t,--touch | 和 Linux 的 touch 命令实现功能相同,更新所有的目标文件的时间戳到当前系统时间。防止 make 对所有过时目标文件的重建。 |
-v,version | 查看make的版本信息。 |
-w,--print-directory | 在 make 进入一个子目录读取 Makefile 之前打印工作目录,这个选项可以帮助我们调试 Makefile,跟踪定位错误。使用 "-C" 选项时默认打开这个选项。 |
--no-print-directory | 取消 "-w" 选项。可以是 用在递归的 make 调用的过程中 ,取消 "-C" 参数的默认打开 "-w" 的功能。 |
-W FILE,--what-if=FILE, --new-file=FILE, --assume-file=FILE | 设定文件 "FILE" 的时间戳为当前的时间,但不更改文件实际的最后修改时间。此选项主要是为了实现对所有依赖于文件 "FILE" 的目标的强制重建。 |
--warn-undefined-variables | 在发现 Makefile 中存在没有定义的变量进行引用时给出告警信息。此功能可以帮助我们在调试一个存在多级嵌套变量引用的复杂 Makefile。但是建议在书写的时候尽量避免超过三级以上的变量嵌套引用。 |
比较常用的有:
-j 或者 --jobs:并行编译;
-k 或者 --keep-going:如果某规则中的命令出错了,那么就终目该规则的执行,但继续执行其它规则
-w 或者 --print-directory:打印退出、进入目录;
make: Entering directory `/u/gnu/make'
make: Leaving directory `/u/gnu/make'.
-i 或者 --ignore-errors:Makefile中所有命令都会忽略错误
-n 或者 --just-print:只是显示命令,但不会执行命令,利于我们调试我们的 Makefile
-s 或者 --slient:则是全面禁止命令的显示
参考和借鉴资料: