Home Makefile学习
Post
Cancel

Makefile学习

Learning by Doing.

Makefile学习

Makefile规则

1
2
3
4
tareget ... : prerequisites ...
command
...
...

target是目标文件,可以是Object File 也可以是执行文件,还可以是标签(label), prerequisites是生成target需要的文件或目标

command是make需要执行的指令

如果prerequisites中有任何一个文件比target要新,command中的命令就会被执行

Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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	

上面的代码中,目标文件中包含了执行文件和中间目标文件(即*.o),依赖文件就是目标文件冒号后面的文件。这样就构成了最终的目标文件与原始代码之间的依赖关系。

在依赖关系后,后续的command定义了如何生成目标文件的命令,以tab开头。

clean是一个动作名字,执行其后的命令可以需要在make后手动添加标签,即 make clean,类似地,可以在一个makefile中定义不用的编译或编译无关的命令,如打包/备份等。

变量

在makefile中可以使用变量来简化文件的编写。在makefile中变量可以被视为字符串,类似于C中的宏?

例如:

1
object = main.o kbd.o command.o display.o insert.o searach.o files.o utils.o

则可以通过$(objects)来使用这个变量。这样,上面的代码就可以化简为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
insert.o search.o files.o utils.o
edit : $(objects)
	cc -o edit $(objects)
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 $(objects)

自动推导

make看到.o文件,会自动把.c文件作为他的依赖,并且 cc -c xxx.c也会被自动推导出来。则可进一步化简为:

1
2
3
4
5
6
7
8
9
10
objects = main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
edit : $(objects)
	cc -o edit $(objects)
$(objects) : defs.h
kbd.o command.o files.o : command.h
display.o insert.o search.o files.o : buffer.h
.PHONY : clean
clean :
	rm edit $(objects)

.PHONY说明clean是一个伪目标文件。

.delete_on_error: 如果一条命令返回一个非零返回值,make将会停止执行命令。如果在makefile中添加了delete_on_error,那么在make失败时将会删除掉新编译出的target。

引用其他的Makefile

Makefile中可以使用include关键字将别的Makefile包含进来。include和文件名之间可以用一个或多个空格隔开。

Makefile寻找文件的顺序:

  1. 显式指出的文件位置
  2. 当前目录
  3. 如果make 执行时有-l 或 –include-dir参数,那么make就会在这个参数所指定的目录下去寻找。
  4. 如果<prefix>/include (e.g. /usr/local/bin或/usr/include)存在,make会到该目录下去寻找.

如果有文件没有找到,make会生成警告,但不会立刻退出,而是继续载入其他文件,在完成整个makefile的读取后,make再重试这些没有找到或无法读取的文件。若需要make跳过无法读取的文件,则需要在include前加一个’-‘.

规则

规则包含两个部分,一个是依赖关系,另一个是生成目标的方法。

1
2
3
4
tareget ... : prerequisites ...
command
...
...

Makefile中存在特殊变量VPATH,如果没有指明这个变量,make会在当前的目录中寻找文件的依赖关系。如果在当前目录中没有找到,会到VPATH所指明的目录中寻找。

1
VPATH = src:../headers

如上,makefile将会按照src, ../headers这样的顺序寻找,不同的目录之间用’:’分割。

另一个设置文件搜索路径的方法:使用make的vpath关键字

  1. vpath <pattern> <directories> : 为符合模式pattern的文件指定搜索目录
  2. vpath <pattern> : 清除符合模式pattern的文件的搜索目录
  3. vpath:清除所有被设置好的文件搜索目录

pattern中需要包含%,匹配零或若干字符。例如:

1
vpath %.h ../headers

表示在../headers目录下搜索所有以.h结尾的文件

隐式规则

  • 编译C代码:n.o通过命令$(CC) -c $(CPPFLAGS) $(CFLAGS),依赖于文件n.c,自动编译出.o文件
  • 编译C++代码:n.o通过命令$(CXX) -c $(CPPFLAGS) $(CXXFLAGS),依赖于文件n.cc或n.cpp,自动编译出.o文件。
  • 链接单个.o文件:n通过$(CC) $(LDFLAGS) n.o $(LOADLIBES) $(LDLIBS),自动编译成可执行文件。

其中使用的变量分别为:

  • CC: 编译C代码使用的编译器,默认为cc
  • CXX: 编译C++代码使用的编译器,默认为g++
  • CFLAGS: C编译器使用的额外参数
  • CXXFLAGS: C++编译器使用的额外参数
  • CPPFLAGS: 传递给C预处理器的额外参数
  • LDFLAGS: 调用链接器时链接器使用的参数

例如:

1
2
3
4
5
6
7
CC = gcc
blah:  blah.o

blah.c:

clean: 
	rm -f blah*

输出为:

1
2
3
> make
gcc    -c -o blah.o blah.c
gcc   blah.o   -o blah

Static Pattern Rules

1
2
targets...: target-pattern: prereq-patterns ...
	commands

The essence is that the given target is matched by the target-pattern (via a % wildcard). Whatever was matched is called the stem. The stem is then substituted into the prereq-pattern, to generate the target’s prereqs.

主要用于自动将.c文件编译成.o文件,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// manual 
objects = foo.o bar.o all.o
all: $(objects)

# These files compile via implicit rules
foo.o: foo.c
bar.o: bar.c
all.o: all.c

all.c:
	echo "int main() { return 0; }" > all.c

%.c:
	touch $@

clean:
	rm -f *.c *.o all
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//auto:
objects = foo.o bar.o all.o
all: $(objects)

# These files compile via implicit rules
# Syntax - targets ...: target-pattern: prereq-patterns ...
# In the case of the first target, foo.o, the target-pattern matches foo.o and sets the "stem" to be "foo".
# It then replaces the '%' in prereq-patterns with that stem
$(objects): %.o: %.c

all.c:
	echo "int main() { return 0; }" > all.c

%.c:
	touch $@

clean:
	rm -f *.c *.o all

区别在于,使用通配符的方式引入了$(objects): %.o: %.c自动为每个.o文件指定了其使用的其依赖的.c文件,而在手动方式中需要依次为每个.o文件指定.c文件,但是似乎只适用于.c文件与.o文件一对一的情况。

filter函数可以被用在static pattern rules中从变量中分离出正确的文件,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
obj_files = foo.result bar.o lose.o
src_files = foo.raw bar.c lose.c

.PHONY: all
all: $(obj_files)

$(filter %.o,$(obj_files)): %.o: %.c   # 取出Object中的.o文件
	echo "target: $@ prereq: $<"
$(filter %.result,$(obj_files)): %.result: %.raw   # 取出object中的.result文件
	echo "target: $@ prereq: $<" 

%.c %.raw:
	touch $@

clean:
	rm -f $(src_files)

Pattern rules

%通配符匹配任何非空字符串,其他的字符匹配其自身。

1
2
%.o : %.c
	$(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@

完整的makefile版本:

1
2
3
4
5
6
7
8
9
10
11
CC = gcc
obj_files = blah.o test.o
.PHONY: all

all: $(obj_files)
	
$(obj_files): %.o: %.c
%.c:
等价于
%.o: %.c 
	$(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@

双冒号规则:

使用双冒号允许为一个target定义多条指令,如果使用单冒号,将会报warnning 并且只执行第二条指令

伪目标

如果需要一次性生成多个可执行文件,但只要输入一个make,并且所有的目标文件都写在一个makefile中,可以通过伪目标来实现:

1
2
3
4
5
6
7
8
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

如上,声明了一个all伪目标,其依赖于其他三个目标。由于伪目标总是被执行,所以其依赖的三个目标总是不如all新,因此另外三条规则总是会被执行,于是就达到了一口气生成多个目标的目的。

书写命令

默认的shell为/bin/sh,可以在makefile开头通过设置变量SHELL来改变

一、 显示命令

make默认将执行的命令行在命令执行前输出到屏幕,若使用@在命令行前,则这个命令将不被显示。

若在make执行时带入参数-n 或 –just-print,则只是显示命令而不执行,可用于调试makefile,查看命令的执行顺序。

make -s 或 –slient可以全面禁止命令的显示

二、命令执行

如果让下一条命令在上一条命令执行的基础上进行,则应该使用’;’分隔这两条命令,而不是换行。例如:

1
2
exec:
	cd /home/hchen; pwd

中断或中止make,如果在执行make时在键盘输入ctrl+c,make将会删除其刚刚生成的targets

三、命令出错

忽视命令出错,即无论正确不正确都认为是正确的,则在命令前加’-‘。例如:

1
2
clean:
	-rm -f *.o

或给Make添加 -i或 –ignore-errors,忽略所有命令的报错

make -k 或 –keep-going,表示如果某条规则的命令出错了,那么终结该命令的执行,但继续执行其他规则

四、嵌套执行make

把不同模块或不同功能的源文件放在不同的目录中,可以在每个目录中都书写一个该目录的Makefile。例如,有一个子目录subdir,这个目录下有Makefile指明了这个目录下文件的编译规则,则父目录的Makefile可以这样, 其中应该使用$(MAKE)而不是使用make,这样传递给被调用的make的参数将不会影响正在使用的make:

1
2
3
4
5
6
7
8
new_contents = "hello:\n\ttouch inside_file"
all:
	mkdir -p subdir
	printf $(new_contents) | sed -e 's/^ //' > subdir/makefile
	cd subdir && $(MAKE)
	
clean:
	rm -rf subdir

在Make中使用export

export的语法与sh相同,功能相似,但是并不相关

export的功能是在上级makefile中创建的变量可以供被调用的下级makefile使用。

define

可以将相同的一组命令序列打包为一个特定的命令。

1
2
3
4
define run-yacc
yacc $(firstword $^)
mv y.tab.c $@
endef

之后可以通过

1
2
foo.c:foo.y
	$(run-yacc)

执行定义的命令. define的命令在一个分离的shell中执行。

命令行参数和override

使用override标识符来通过命令行覆盖定义的变量,例如 在命令行中make option_one=hi:

1
2
3
4
5
6
override option_one = did_override

option_two = not_override
all:
	echo $(option_one)
	echo $(option_two)

Target-specific variables

1
2
3
4
5
6
7
all: one = cool

all: 
	echo one is defined: $(one)

other:
	echo one is nothing: $(one)

使用变量

变量在声明时需要指定初值,使用时需要加上$,例如:

1
2
3
4
objects = program.o foo.o utils.o
program : $(objects)
	cc -o program $(objects)
$(objects) : defs.h

自动变量以及通配符

自动变量含义
$@target name
$<dependency 第一个文件的名字
$^全部dependencies 名字,无重复
$?prerequisites中比target更新的文件名
$+全部dependencies名字,有重复,适用于在链接器中重复值有意义的情况

变量嵌套:可以把变量的真实值放到后面定义。例如:

1
2
include_dirs := -Ifoo -Ibar
CFLAGS := $(include_dirs) -O

这样前面的变量不能使用后面的变量,而只能使用他前面定义好了的变量

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

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

将foo中的.o全部替换为.c

静态模式替换:依赖于被替换字符串中有相同的模式,模式中必须包含一个%字符。例如:

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

追加变量值:使用+=,例如:

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

变量定义

makefile中存在两种风格的变量定义:

  • recursive: (=) 在命令被使用时产生而不是在被定义时生成完整的变量
  • simply expanded: (:=) 在命令被定义时生成完整的变量

(可能看起来比较抽象,看看例子就懂了)

1
2
3
4
5
6
7
8
9
10
# Recursive variable. print "later"
one = one ${later_variable}
# Simply expanded variable. Will not print "later"
two := two ${later_variable}

later_variable = later

all:
	echo $(one)
	echo $(two)

因为在one被定义时,仍然保持为one ${later_variable},直到echo使用$(one)时才会查找${later_variable}对应的值。而two使用simply expanded定义,在定义时,到上面去查找latere_variable对应的值,而此时later_variable并没有被定义,为空值,因此two = two

因为此种特性,simply expanded可以用来向一个变量追加值,而recursive definitions将会抛出一个infinite loop error.

除了simplye expanded之外,还可以使用+=向变量追加值,两者的区别在于,+=默认使用一个空格分隔,而simply expanded可以自行调整空格的数量,见下例:

1
2
3
4
5
6
7
one = hello
one := ${one} three //输出为hello three

one := ${one}three //输出为hellothree

foo := start
foo +=moree  //输出为start moree

?=仅仅在变量没有被定义时才会产生定义,e.g.:

1
2
3
4
5
6
7
one = hello
one ?= will not be set
two ?= will be set

all:
	echo $(one)
	echo $(two)

每一行末尾的空格不会被删除,而开头的空格会被删除,如果需要使用空格的变量,可以通过$(nullstring)来完成。

这里有个疑问:行末尾是指什么?在下面的例子中,with_spaces=hello ,最后输出的hello自带几个空格,而单独输出nullstring为”“,输出spacew为” “, 可以认为所谓的行开头指的是等号后面开始一直到本行结束,因此变量等于单个空格会直接被忽略而作为空值,若需要使用空格则需要再加一层引用。

1
2
3
4
5
6
7
8
9
10
11
with_spaces = hello   # with_spaces has many spaces after "hello"
after = $(with_spaces)

nullstring = 
space = $(nullstring) # Make a variable with a single space.

all: 
	echo "$(snullstring)"
	echo "$(space)"
	echo "$(after)"
	echo start"$(space)"end

Makefile中的条件判断

if/else

1
2
3
4
5
6
7
foo = ok
all:
ifeq ($(foo), ok)
	echo "foo equals ok"
else 
	echo "nope"
endif

判断是否为空

1
2
3
4
5
6
7
8
9
10
nullstring = 
foo = $(nullstring)

all:
ifeq ($(strip $(foo)), )
	echo "foo is empty after being stripped"
endif
ifeq ($(nullstring), )
	echo "nullstring doesn't even have spaces"
endif

判断变量是否已被定义

1
2
3
4
5
6
7
8
9
bar =
foo = $(bar)
all:
ifdef foo
	echo "foo is defined"
endif
ifndef bar
	echo "buf bar is not"
endif

$(makeflags)

$(makeflags)表示make时通过命令行传递的参数

Functions

$(fn, arguments), arguments之间不应当添加额外的空格, 除非需要。

String Substitution

$(patsubst pattern,replacement,text):从text中选择与pattern匹配的单词,将其替换为replacement。其中pattern可以包含‘%’,如果replacement中也包含%,那么%将会被其对应的字符串替换。仅仅pattern中的第一个%会以这种方式被替换,后续的%将会保留。

foreach

$(forrach var,list,text):其中var是list中的每个单词,对于list中的每个var,将其替换为text

if

$(if this-is-not-empty,then!,else!),判断第一个参数是否为空值,若非空,则执行then,否则执行else.

call

makefile通过创建变量来定义函数,并且通过$(0)等向函数传递参数。

1
2
3
4
5
sweet_new_fn = Variable Name: $(0) First: $(1) Second: $(2) Empty Variable: $(3)

all:
	# Outputs "Variable Name: sweet_new_fn First: go Second: tigers Empty Variable:"
	@echo $(call sweet_new_fn, go, tigers)

shell function

1
2
all:
	echo $(shell ls -la)

REFERENCE

[1]全网最牛Linux内核Makefile系统文件详解(纯文字代码) - 知乎 (zhihu.com)

[2]Makefile Tutorial By Example

This post is licensed under CC BY 4.0 by the author.