makefile教程

我们注意到在k8s的源码根目录下有一个Makefile文件,是k8s的构建方式,我们会分析一下k8s的构建过程,所以我们先学习一下makefile。

make命令执行时,需要一个makefile文件,以告诉make命令需要怎么样的去编译和链接程序。

环境说明:

  • windows环境安装虚拟机
  • 虚拟机使用centos7.9

执行:

yum -y install gcc+ gcc-c++
1

1. 语法规则

一个 Makefile 由一组规则组成。规则通常如下所示:

targets: prerequisites
	command
	command
	command
1
2
3
4
  • 目标 targets 是文件名,以空格分隔。通常,每条规则只有一个。
  • 命令 command 是生成目标的一系列步骤。以制表符开头,不能用空格开头。
  • 先决条件 prerequisites 是文件名(也称为依赖项),以空格分隔。这些文件需要在执行命令之前存在。

上述其实是描述了一个文件依赖关系,即生成一个或多个target依赖于prerequisites,生成规则定义在command中。

示例:

#开头的为注释

blah: blah.o
	cc blah.o -o blah # 第三步

blah.o: blah.c
	cc -c blah.c -o blah.o # 第二步

blah.c:
	echo "int main() { return 0; }" > blah.c # 第一步
1
2
3
4
5
6
7
8

执行:

[root@localhost makefile]# make blah
echo "int main() { return 0; }" > blah.c # 第一步
cc -c blah.c -o blah.o # 第二步
cc blah.o -o blah # 第三步
1
2
3
4

再次执行:

[root@localhost makefile]# make blah
make: `blah' is up to date.
1
2
[root@localhost makefile]# rm -rf blah.o 
[root@localhost makefile]# make blah
cc -c blah.c -o blah.o # 第二步
cc blah.o -o blah # 第三步
1
2
3
4

2. all

如果Makefile 中定义了多个目标,通常定义一个 all 目标来生成所有目标。

all: one two three

one:
	touch one
two:
	touch two
three:
	touch three

clean:
	rm -f one two three
1
2
3
4
5
6
7
8
9
10
11

Makefile中的第一个目标会被作为其默认目标,直接使用make即可,无需指定target

2.1 伪目标

clean:
    rm *.o
1
2

上述的clean并不会真的生成一个clean文件,所以我们称clean为伪目标。“伪目标”的取名不能和文件名重名。

为了避免和文件重名的这种情况,我们可以使用一个特殊的标记“.PHONY”来显式地指明一个目标是“伪目标”,向make说明,不管是否有这个文件,这个目标就是“伪目标”。

示例:

.PHONY: clean
clean:
        rm -f one two three
1
2
3

.PHONY不写其实也会推导出clean是一个伪目标,不去生成文件,但显示的表明clean是一个伪目标是一个好的习惯。

目标可以成为依赖,伪目标同样可以成为依赖

2.2 多行

当命令太长时,反斜杠(“\”)字符使我们能够使用多行

some_file: 
	echo This line is too long, so \
		it is broken up into multiple lines
1
2
3

3. 变量

变量是把一个名字和任意长的字符串关联起来。基本语法如下

MY_VAR=A text string
MY_VAR := A text string
1
2

使用${}$()来引用变量。

示例:

MY_VAR=file1.c file2.c

all:
	echo ${MY_VAR}
1
2
3
4

3.1 求值时机

  • = 仅在使用命名时解析变量值。
  • := 在定义时立即解析变量值。

示例:

# 在下面的echo命令执行时再求值,输出 "later"
one = one ${later_variable}

# 简单的扩展变量,由于 later_variable 未定义,下面不会输出 "later"
two := two ${later_variable}

later_variable = later

all: 
	echo $(one)
	echo $(two)
1
2
3
4
5
6
7
8
9
10
11

=被称为是递归变量(recursive) ,因为其在使用命名时解析,所以不能进行递归定义

:=被称为是简单扩展变量(simply expanded)

比如:

# 简单的扩展变量,若将 := 改为 = 则产生无限循环错误。
one = ${one} there

all: 
	echo $(one)

1
2
3
4
5
6
[root@localhost makefile]# make        
Makefile:1: *** Recursive variable `one' references itself (eventually).  Stop.
1
2

3.2 是否覆盖变量

?= 仅在尚未设置变量时设置变量

示例:

one = hello
one ?= will not be set
two ?= will be set

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

1
2
3
4
5
6
7
8

3.3 空格

行尾的空格不会被删除,但开头的空格会被删除。

示例:

with_spaces = hello   # with_spaces 变量是 hello 末尾有三个空格
after = $(with_spaces)there

nullstring =
space = $(nullstring) # 创建只有一个空格的变量

all: 
	echo "$(after)"
	echo start"$(space)"end
	echo $(nowhere)
1
2
3
4
5
6
7
8
9
10

未定义的变量实际上是一个空字符串!

all: 
	# 未定义的变量实际上是一个空字符串
	echo $(nowhere)
1
2
3

3.4 追加

+= 用于追加

foo := start
foo += more

all: 
	echo $(foo)

1
2
3
4
5
6

2.5 自动变量

makefile定义了一些自动变量,用于自动获取一些值,比如

all: f1.o f2.o

f1.o f2.o:
	echo $@ # 比较常用 相当于一个集合,依次取出并执行命令
# 相当于:
# f1.o:
#	 echo f1.o
# f2.o:
#	 echo f2.o
1
2
3
4
5
6
7
8
9
  • $@ 规则目标的文件名。
  • lt; 第一个先决条件的名称。
  • $? 比目标新的所有先决条件的名称,它们之间有空格。
  • $^ 所有先决条件的名称,它们之间有空格。

更多的参考文档:https://www.gnu.org/software/make/manual/html_node/Automatic-Variables.html

示例:

hey: one two
	echo $@  # 输出hey
	echo $? # 所有比目标新的先决条件 one two
	echo $^ # 所有先决条件

	touch hey

one:
	touch one

two:
	touch two

clean:
	rm -f hey one two

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

4. 通配符

* 称为通配符。

  • * 可以在目标、先决条件或 wildcard 函数(查找指定目录下指定类型的文件)中使用。
  • * 不能在变量定义中直接使用
  • * 没有匹配到文件时,保持原样(除非在 wildcard 函数中运行)
thing_wrong := *.o # Don't do this!  output *.o 
thing_right := $(wildcard *.o)
1
2
thing_wrong := *.o
thing_right := $(wildcard *.o)
all: one two three four

one: 
        echo $(thing_wrong)
two: *.o 
        echo $^

three: 
        echo $(thing_right)

four: 
        echo $(wildcard *.o)
1
2
3
4
5
6
7
8
9
10
11
12
13
14

如果我们的文件名中有通配符,如: * ,那么可以用转义字符 \ ,如 \* 来表示真实的 * 字符,而不是任意的字符串。

5. 隐式规则

make会有一些默认的约定,来帮助我们简化书写。

比如:

blah: blah.o
	cc blah.o -o blah # 第三步

blah.o: blah.c
	cc -c blah.c -o blah.o # 第二步

blah.c:
	echo "int main() { return 0; }" > blah.c # 第一步
1
2
3
4
5
6
7
8

之前我们会经过blah.o这一步,接下来我们省略这一步:

blah: blah.o
	cc blah.o -o blah # 第三步
blah.c:
	echo "int main() { return 0; }" > blah.c # 第一步
1
2
3
4
[root@localhost makefile]# make
echo "int main() { return 0; }" > blah.c # 第一步
cc    -c -o blah.o blah.c
cc blah.o -o blah # 第三步
1
2
3
4

执行后,我们发现多了一步cc -c -o blah.o blah.c

和我们之前写的一样,这种根据依赖自动推导的规则就是隐式规则

隐式规则:

  • 编译C程序时,会自动使用C的编译命令$(CC) -c $(CPPFLAGS) $(CFLAGS)来生成 .o ,比如遇到blah.o依赖,那么会自动找到blah.c 运行命令进行生成
  • 编译C++程序时,使用$(CXX) -c $(CPPFLAGS) $(CXXFLAGS)生成.o,比如遇到blah.o依赖,那么会自动找到blah.cc或者blah.cpp运行命令进行生成

如果我们将上述的程序修改为:

blah: blah.o
blah.c:
	echo "int main() { return 0; }" > blah.c # 第一步
1
2
3
[root@localhost makefile]# make
echo "int main() { return 0; }" > blah.c # 第一步
cc    -c -o blah.o blah.c
cc   blah.o   -o blah
1
2
3
4

我们发现还能正常执行,这又是另一个隐式规则:

  • <n> 目标依赖于 <n>.o ,通过运行C的编译器来运行链接程序生成,其生成命令是: $(CC) $(LDFLAGS) <n>.o $(LOADLIBES) $(LDLIBS)

隐式规则使用的重要变量是:

  • 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文法分析器参数。

比如:

CC = gcc
blah: blah.o
blah.c:
        echo "int main() { return 0; }" > blah.c # 第一步
1
2
3
4
[root@localhost makefile]# make
echo "int main() { return 0; }" > blah.c # 第一步
gcc    -c -o blah.o blah.c
gcc   blah.o   -o blah
1
2
3
4

6. 静态模式规则

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

静态模式规则的语法:

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

匹配 target-pattern 生成 targets, 匹配 prereq-pattern 生成 target-pattern

示例:

比如我们编译一系列.c文件到.o文件

objects = foo.o bar.o all.o
all: $(objects)

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

使用静态模式规则后:

objects = foo.o bar.o all.o
all: $(objects)

$(objects): %.o: %.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

6.1 filter

filter函数可用于静态模式规则以匹配正确的文件。

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
	echo "target: $@ prereq: lt;"
$(filter %.result,$(obj_files)): %.result: %.raw
	echo "target: $@ prereq: lt;" 

%.c %.raw:
	touch $@

clean:
	rm -f $(src_files)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

这个 % 匹配任何非空字符串

7. 双冒号规则

双冒号规则很少使用,但允许为同一个目标定义多个规则。

示例:

如果这些是单冒号,则会打印一条警告,并且只会运行第二组命令。

all: blah

blah::
	echo "hello"

blah::
	echo "hello again"

1
2
3
4
5
6
7
8

8. 命令

8.1 显示与隐藏

在命令之前添加一个@以阻止它被打印。

您也可以运行 make 时使用 -s 参数, 这将为每一行命令添加一个@

all: 
	@echo "This make line will not be printed"
	echo "But this will"
1
2
3

8.2 执行

每个命令都在一个新的 shell 中运行(或者至少效果是这样的)

比如:

all: 
	cd ..
	echo `pwd` # cd不会影响pwd 因为不在一行

	cd ..;echo `pwd` # cd会影响pwd 因为在一行

	cd ..; \
	echo `pwd`

1
2
3
4
5
6
7
8
9

8.3 默认 shell

默认 shell 是/bin/sh. 您可以通过更改变量 SHELL 来更改它:

SHELL=/bin/bash
1

8.4 递归使用make

要递归调用 makefile,请使用$(MAKE)变量代替make

它会为您传递 make 标志并且本身不会受到它们的影响。

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

1
2
3
4
5
6
7
8
9

8.5 创建多个目标

make clean run test运行clean目标,然后run,然后test

8.6 define

define 实际上就是一个命令列表。

示例:

one = export blah="I was set!"; echo $blah

define two
export blah=set
echo $blah
endef

all: 
	@echo "This prints 'I was set'"
	@$(one)
	@echo "This does not print 'I was set' because each command runs in a separate shell"
	@$(two)

1
2
3
4
5
6
7
8
9
10
11
12
13

8.7 特定目标变量

all: one = cool

all: 
	echo one is defined: $(one)

other:
	echo one is nothing: $(one)

1
2
3
4
5
6
7
8
%.c: one = cool

blah.c: 
	echo one is defined: $(one)

other:
	echo one is nothing: $(one)

1
2
3
4
5
6
7
8

9. 条件

9.1 ifelse

foo = ok

all:
ifeq ($(foo), ok)
	echo "foo equals ok"
else
	echo "nope"
endif

1
2
3
4
5
6
7
8
9

判空:

nullstring =
foo = $(nullstring) 

all:
ifeq ($(strip $(foo)),)
	echo "foo is empty after being stripped"
endif
1
2
3
4
5
6
7

判断变量是否定义

bar =
foo = $(bar)

all:
ifdef foo
	echo "foo is defined"
endif

1
2
3
4
5
6
7
8

10. 函数

函数主要用于文本处理。 使用$(fn, arguments)${fn, arguments}调用函数。

10.1 subst

用法是$(subst FROM,TO,TEXT),即将TEXT中的东西从FROM变为TO

示例:

bar := ${subst not, totally, "I am not superman"}
all: 
	@echo $(bar)
1
2
3
comma := ,
empty:=
space := $(empty) $(empty)
foo := a b c
bar := $(subst $(space),$(comma),$(foo)) # 注意$(foo)前面不要有空格 否则会当做字符串的一部分

all: 
	@echo $(bar)
1
2
3
4
5
6
7
8

10.2 pathsubst

模式字符串替换函数。

格式: $(patsubst <pattern>,<replacement>,<text> )

查找<text>中的单词是否符合模式<pattern>,如果匹配的话,则以<replacement>替换。

替换引用$(text:pattern=replacement)是对此的简写。

示例:

foo := a.o b.o l.a c.o
one := $(patsubst %.o,%.c,$(foo)) # %代表任意字符
two := $(foo:%.o=%.c)
three := $(foo:.o=.c)

all:
	echo $(one)
	echo $(two)
	echo $(three)
1
2
3
4
5
6
7
8
9

10.3 foreach

$(foreach var,list,text): 将一个单词列表(由空格分隔)转换为另一个单词列表。

list代表单词列表,var设置列表中每个单词, text针对每个单词进行扩展。

示例:

foo := who are you
bar := $(foreach wrd,$(foo),$(wrd)!)
all:
	@echo $(bar) 
1
2
3
4

10.4 if

if检查第一个参数是否为非空。如果是,则运行第二个参数,否则运行第三个。

foo := $(if this-is-not-empty,then!,else!)
empty :=
bar := $(if $(empty),then!,else!)

all:
	@echo $(foo)
	@echo $(bar)

1
2
3
4
5
6
7
8

10.5 call

$(call VARIABLE,PARAM,PARAM,...):在执行时,将它的参数"PARAM"依次赋给VARIABLE中的临时变量"$(1)","$(2)"等等.

$(0)是获取VARIABLE变量名称。

示例:

sweet_new_fn = Variable Name: $(0) First: $(1) Second: $(2) Empty Variable: $(3)
all:
	@echo $(call sweet_new_fn, go, tigers)
1
2
3

10.5 shell

shell函数就是调用shell

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

11. 包含

include 指令告诉 make 读取一个或多个其他 makefile。

语法:

include filenames...
1

12. vpath

使用 vpath 指定某些先决条件存在的位置。

格式:vpath <pattern> <directories, space/colon separated>

<pattern>可以有一个%,它匹配任何零个或多个字符。

比如:

vpath %.h ../headers
1

代表要求make在“../headers”目录下搜索所有以 .h 结尾的文件。前提是当前目录没有找到。

示例:

vpath %.h ../headers
some_binary: blah.h
        touch some_binary
blah:
        mkdir ../headers
        touch ../headers/blah.h
clean:
        rm -rf ../headers
        rm -f some_binary
1
2
3
4
5
6
7
8
9