学得懂的 Android Framework 教程基础篇之 Make 构建工具入门

本文是在Make 命令教程的基础上进行的演绎和补充。

什么是 Make

代码变成可执行文件,叫做编译(compile);先编译这个,还是先编译那个(即编译的安排),叫做构建(build)。

Make 是最常用的构建工具,诞生于 1977 年,主要用于 C 语言的项目。但是实际上 ,任何只要某个文件有变化,就要重新构建的项目,都可以用 Make 构建。

Make 这个词,英语的意思是"制作"。Make 命令直接用了这个意思,就是要做出某个文件。比如,要做出文件 a.txt,就可以执行下面的命令。

make a.txt

但是,如果你真的输入这条命令,它并不会起作用。因为 Make 命令本身并不知道,如何做出 a.txt,需要有人告诉它,如何调用其他命令完成这个目标。

比如,假设文件 a.txt 依赖于 b.txt 和 c.txt ,是后面两个文件连接(cat命令)的产物。那么,make 需要知道下面的规则。

a.txt: b.txt c.txt
    cat b.txt c.txt > a.txt

也就是说,make a.txt 这条命令的背后,实际上分成两步:第一步,确认 b.txt 和 c.txt 必须已经存在,第二步使用 cat 命令 将这个两个文件合并,输出为新文件。

像这样的规则,都写在一个叫做 Makefile 的文件中,Make 命令依赖这个文件进行构建。

总之,make 只是一个根据指定的 Shell 命令进行构建的工具。它的规则很简单,你规定要构建哪个文件、它依赖哪些源文件,当那些文件有变动时,如何重新构建它。

Makefile 文件的格式

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

<target> : <prerequisites> 
[tab]  <commands> 

上面第一行冒号前面的部分,叫做"目标"(target),冒号后面的部分叫做"前置条件"(prerequisites);第二行必须由一个tab键起首,后面跟着"命令"(commands)。

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

每条规则就明确两件事:构建目标的前置条件是什么,以及如何构建。

我们看个最简单的例子:

test : main.c sub.c sub.h
    gcc -o test main.c sub.c

test 是我们的目标,它依赖于前置条件 main.c sub.c sub.h ,有一下两种情况会执行第二行的命令:

  • test 文件不存在
  • main.c sub.c sub.h 比 test 更新,即修改过

下面就详细讲解,每条规则的这三个组成部分。

1. 目标(target)

一个目标(target)就构成一条规则。目标通常是文件名,指明Make命令所要构建的对象,比如上文的 a.txt 。目标可以是一个文件名,也可以是多个文件名,之间用空格分隔。

除了文件名,目标还可以是某个操作的名字,这称为"伪目标"(phony target)。

clean:
      rm *.o

上面代码的目标是 clean,它不是文件名,而是一个操作的名字,属于"伪目标 ",作用是删除对象文件。

make  clean

但是,如果当前目录中,正好有一个文件叫做 clean,那么这个命令不会执行。因为 Make 发现 clean 文件已经存在,就认为没有必要重新构建了,就不会执行指定的 rm 命令。

为了避免这种情况,可以明确声明 clean 是"伪目标",写法如下。

.PHONY: clean
clean:
        rm *.o temp

声明 clean 是"伪目标"之后,make就不会去检查是否存在一个叫做 clean 的文件,而是每次运行都执行对应的命令。

2. 前置条件(prerequisites)

前置条件通常是一组文件名,之间用空格分隔。它指定了"目标"是否重新构建的判断标准:前置条件修改过,或者说是前置文件比目标文件更新,具体的,前置文件比前置文件的 last-modification 时间戳比目标的时间戳新,"目标"就需要重新构建。(通过 ls -l 可以查看到文件的 last-modification 时间)

我们接下来看一个例子:

result.txt: source.txt
    cp source.txt result.txt

上面代码中,构建 result.txt 的前置条件是 source.txt 。如果当前目录中,source.txt 已经存在,那么 make result.txt 可以正常运行,否则必须再写一条规则,来生成 source.txt 。

source.txt:
    echo "this is the source" > source.txt

上面代码中,source.txt 后面没有前置条件,就意味着它跟其他文件都无关,只要这个文件还不存在,每次调用 make source.txt,它都会生成。

$ make result.txt
$ make result.txt

上面命令连续执行两次 make result.txt。第一次执行会先新建 source.txt,然后再新建 result.txt。第二次执行,Make 发现 source.txt 没有变动(时间戳晚于 result.txt),就不会执行任何操作,result.txt 也不会重新生成。

3. 命令(commands)

命令(commands)表示如何更新目标文件,由一行或多行的 Shell 命令组成。它是构建"目标"的具体指令,它的运行结果通常就是生成目标文件。每行命令之前必须有一个 tab 键。

需要注意的是,每行命令在一个单独的 shell 中执行。这些 Shell 之间没有继承关系。

var-lost:
    export foo=bar
    echo "foo=[$$foo]"

上面代码执行后(make var-lost),取不到 foo 的值。因为两行命令在两个不同的进程执行。一个解决办法是将两行命令写在一行,中间用分号分隔。

var-kept:
    export foo=bar; echo "foo=[$$foo]"

另一个解决办法是在换行符前加反斜杠转义。

var-kept:
    export foo=bar; \
    echo "foo=[$$foo]"

最后一个方法是加上 .ONESHELL:命令。

.ONESHELL:
var-kept:
    export foo=bar; 
    echo "foo=[$$foo]"

Makefile 基础语法

1. 注释

井号(#)在Makefile中表示注释

# 这是注释
result.txt: source.txt
    # 这是注释
    cp source.txt result.txt # 这也是注释

2. 回声(echoing)

正常情况下,make会打印每条命令,然后再执行,这就叫做回声(echoing)

在命令的前面加上@,就可以关闭回声。

test:
    @echo TODO

3. 通配符

通配符(wildcard)用来指定一组符合条件的文件名。Makefile 的通配符与 Bash 一致,主要有星号(*)、问号(?)和 []:

  • * 匹配0个或者是任意个字符
  • ? 匹配任意一个字符
  • [] 我们可以指定匹配的字符放在 “[]” 中

比较常用的就是 * 号

.PHONY:clean
clean:
        rm -f *.o

4. 模式匹配

Make 命令允许对文件名,进行类似正则运算的匹配,主要用到的匹配符是 %。比如,假定当前目录下有 f1.c 和 f2.c 两个源码文件,需要将它们编译为对应的对象文件。

%.o: %.c

等同于下面的写法。

f1.o: f1.c
f2.o: f2.c

使用匹配符 %,可以将大量同类型的文件,只用一条规则就完成构建。

5. 变量和赋值符

Makefile 中允许自定义变量。

txt = Hello World
test:
    @echo $(txt)

上面代码中,变量 txt 等于 Hello World。在使用时,需要给在变量名前加上 $ 符号,但最好用小括号 () 或是大括号 {} 把变量给包括起来。小括号的用法比较常见。

调用 Shell 变量,需要在美元符号前,再加一个美元符号:

test:
    @echo $$HOME
递归展开(Recursively Expanded)

使用 = 来定义的变量是递归展开的 (Recursively Expanded),直到该变量被使用时等号右边的内容才会被展开。而且每次使用该变量时,等号右边的内容都会被重新展开。

概念比较有点拗口,看个例子:

foo = $(bar)
bar = $(ugh)
ugh = Huh?

all:
	echo $(foo)

执行 make all 时,(foo) 被展开成 (bar),(bar) 被展开成 (ugh),(ugh) 被展开成 Huh?,于是最后输出为 Huh? 最终将会打印出变量 (foo)的值为 Huh?

使用这种方法的一个好处是,我们可以把变量的真实值推到后面来定义。

CFLAGS = $(include_dirs) -O
include_dirs = -Ifoo -Ibar

当 CFLAGS 在命令中被展开时,会是-Ifoo -Ibar -O

当然最主要的缺点就是递归定义可能导致出现无限循环展开,尽管 make 能检测出这样的无限循环展开并报错。

CFLAGS = $(CFLAGS) -O

另一个问题就是如果在变量中使用函数,每次展开变量时都要重新执行函数,这种方式会使make运行得非常慢。更糟糕的是,这种用法会使得“wildcard”和“shell”发生不可预知的错误,因为你不知道这两个函数会被调用多少次。

简单展开 (Simply Expanded)

使用 := 来定义的变量是简单展开的 (Simply Expanded)

使用这种方法,读到变量定义这一行时 等号右边立即被展开,引用的所有变量也会被立即展开。

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

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

等价于:

y := foo bar
x := later

使用这种方法可以在变量中引入开头空格。见下面的示例:

nullstring :=
space := $(nullstring) # end of the line

nullstring 是一个 Empty 变量,其中什么也没有,而 space 的值是一个空格。因为在操作符的右边是很难描述一个空格的,这里采用的技术很管用。先用一个 Empty 变量来标明变量的值开始了,而后面采用 # 注释符来表示变量定义的终止,这样,我们可以定义出其值是一个空格的变量。

条件变量赋值

使用 ?= 操作符给变量赋值称为条件变量赋值

FOO ?= bar

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

注意将变量定义为空字符也是定义的一种。

追加变量值

使用 += 操作符给变量赋值,称为追加变量值。有以下几种情况:

对未定义变量使用追加:如果变量之前没有定义过,那么,+= 会自动变成 =,追加变量直接变成递归展开。

对使用 := 方式定义的变量使用追加:如果前面是以简单展开方式 (:=) 定义的变量,那么 += 在将新的值追加到已有变量的值的后面之前,会以简单展开 (:=) 的方式将原来的内容先展开

对使用 = 方式定义的变量使用追加:如果前面是以递归展开方式 (=) 定义的变量,那么 += 在将新的值追加到已有变量的值的后面之后,不会展开原来的内容

CFLAGS = $(includes) -O
…
CFLAGS += -pg   # CFLAGS = $(includes) -O -pg  不会展开

这样我们就可以保留对 includes 的引用,当之后的某个节点完成对 includes 的定义时,当 CFLAGS 被使用时(即 $(CFLAGS)) ,includes 的值才会被展开

内置变量(Implicit Variables)

Make命令提供一系列内置变量,比如,$(CC) 指向当前使用的编译器,$(MAKE) 指向当前使用的 Make 工具。这主要是为了跨平台的兼容性,详细的内置变量清单见手册

自动变量(Automatic Variables)

Make 命令还提供一些自动变量,它们的值与当前规则有关。主要有以下几个。

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

a.txt b.txt: 
    touch $@

等同于下面的写法。

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

$<: 指代第一个前置条件。比如,规则为 t: p1 p2,那么 $< 就指代p1

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

等同于下面的写法:

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

$? 指代比目标更新的所有前置条件,之间以空格分隔。比如,规则为 t: p1 p2,其中 p2 的时间戳比 t 新,$?就指代p2。

$^ 指代所有前置条件,之间以空格分隔。比如,规则为 t: p1 p2,那么 $^ 就指代 p1 p2 。

$* 指代匹配符 % 匹配的部分, 比如% 匹配 f1.txt 中的f1 ,$* 就表示 f1。

判断和循环

Makefile 使用 Bash 语法,完成判断和循环。

#判断当前编译器是否 gcc ,然后指定不同的库文件
ifeq ($(CC),gcc)
  libs=$(libs_for_gcc)
else
  libs=$(normal_libs)
endif

# 循环
LIST = one two three
all:
    for i in $(LIST); do \
        echo $$i; \
    done

# 等同于
all:
    for i in one two three; do \
        echo $$i; \
    done  

函数

函数的使用和变量引用的展开方式相同:

$(function arguments)
${function arguments}

关于函数的使用格式,有以下需要注意的地方:

  • 函数主要分为两类:make 内嵌函数和用户自定义函数。对于 make 内嵌的函数,直接引用就可以了;对于用户自定义的函数,要通过 make 的 call 函数来间接调用。
  • 函数和参数列表之间要用空格隔开,多个参数之间使用逗号隔开。
  • 如果在参数中引用了变量,变量的引用建议和函数引用使用统一格式:要么是一对小括号,要么是一对大括号。

函数使用示例:如果我们想要获取某个目录下所有的 C文件列表,可以使用扩展通配符函数:wildcard

SRC  = $(wildcard *.c)
HEAD = $(wildcard *.h)
all:
    @echo "SRC = $(SRC)"
    @echo "HEAD = $(HEAD)"

在当前目录下,我们新建一些C文件和H文件,然后使用make命令:

# ls
add.c  add.h  hello.c  main.c  makefile  sub.c  sub.h
# make
SRC = hello.c main.c add.c sub.c
HEAD = add.h sub.h

Make 提供了大量的内嵌函数,大大方便了用户 Makefile 的编写。但有时候根据需要,用户也可以自定义一些函数,然后在 Makefile 中引用它们:

PHONY: all

define func
    @echo "pram1 = $(0)"
    @echo "pram2 = $(1)"
endef
all:
    $(call func, hello world)
  • 用户自定义函以 define 开头,endef 结束,给函数传递的参数在函数中使用 ( 0 ) 、 (0)、 (0)(1) 引用,分别表示第1个参数、第2个参数。更多参数,规则类似。

  • 对于用户自定义函数,在 Makefile 中要使用 call 函数间接调用,各个参数之间使用空格隔开

参考资料

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值