Makefile 介绍

前言

本篇属于学习笔记,内容均来自网络。

在 Android 中的编译基本是使用了 Makefile 语法。我们需要对编译语法有基本的认识。下面将介绍基本 Make 命令和 Makefile 语法的内容。

正文

1、编译方式基本概念简介

在介绍之前,我们先来简单了解下一些编译工具

1.1 gcc

gcc 是 GUN Compiler Collection(GUN 编译器套件),可以简单的认为就是编译器。gcc 可以编译很多变成语言,包括 C、C++、Objective-C、Fortran、Java等。

当我们的程序只有一个源文件时,我们就可以用 gcc 命令去编译它。可是,如果我们的程序包含很多个源文件时,用 gcc 命令逐个去编译时,就发现很容易混乱而且工作量大,在此需求下,出现了 make 工具。

1.2 make

make 工具可以认为是一个智能批处理工具,make 工具本身并么有编译和链接的功能,而是用类似于批处理的方式通过调用 makefile 文件中指定的命令来进行编译和链接的。

1.3 makefile

makefile 中定义了编译的命令和规则,make 工具就是根据 makefile 中的命令进行编译和链接的。

在一个工程中,源文件很多,按类型、功能、模块分别被存放在若干目录中,需要按一定顺序、规则进行编译,这时就需要使用到 makefile。

makefile 定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要重新编译,如何进行链接等操作。

makefile 就是“自动化编译”,告诉 make 命令如何编译和链接。

makefile 是 make 工具的配置脚本,默认情况下,make 命令会在当前目录下寻找该文件(按顺序找寻文件名为 “GUNmakefile”、“makefile”、“Makefile” 的文件)。

makefile 在一些简单的工程下可以人工完成,但是当工程非常大的时候,手写 makefile 也是非常麻烦的,并且在更换平台的场景下,makefile 又要重新修改,这时候就出现了 cmake 工具。

1.4 cmake

cmake 是一个强大的自动化配置工具,开源且跨平台的,通过读取脚本文件 CMakeLists.txt 中的规则来构建编译系统。

cmake 默认使用 CMakeLists.txt 作为脚本文件构建编译规则,就像 make 默认使用 makefile 文件一样。

1.5 nmake

nmake 是 Microsoft Visual Studio 中附带命令,需要安装 VS,实际上可以说相当于 Linux 的 make。

2、Makefile 文件格式

Makefile 文件中记录了工程的构建规则。

Makefile 文件中会包含:显示规则、隐晦规则、变量定义、文件知识、注释。

2.1 概述

Makefile 文件由一系列的规则构成,每条规则的格式如下:

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

解释
<目标> : <前置条件>
tab 键起首 <命令>
--------------------------------------------------------------------------------------------------------------------------------
或者 

<target> : <prerequisites> ; <commonds>

解释
<目标> : <前置条件> ; <命令>

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

若 prerequisites 与 commonds 在同一行,需要用 “;” 分隔。若 prerequisites 与 commonds 不在同一行,则 commonds 前面需要用 tab 键开头。

每条规则就命令两件事,构建目标的前置条件是什么,以及如何构建。下面将讲解此格式的组成部分。

2.2 目标 target

target 是目标文件,可以是 ObjectFile,也可以是执行文件,还可以是便签;如果有多个文件,可以用空格隔开;可以使用通配符。

一个目标(target)就构成一条规则。目标通常是文件名,指明 make 命令索要构建的对象,目标可以是一个文件名,也可以是多个文件名,文件名之间用空格分隔。除了文件名,目标还可以是某个操作的名字,这称为 “伪目标”(phony target)。

clean:
	rm *.o

上面的 makefile 规则,定义了一个 clean 命令。它不是文件名,而是一个操作的名字,属于 “伪目标”,作用是删除 .o 文件。

make clean

但是,如果在当前目录下有一个文件叫做 clean。那么这个目标不会执行,因为 make 工具发现 clean 文件已经存在,就认为没有必要重新构建了,就不会执行到上面的规则。

为了避免这种情况,可以明确声明 clean 是一个 “伪目标”,写法如下:

.PHONY: clean
clean:
	rm *.o temp

声明 clean 是 “伪目标” 之后,make 就不会去检查是否存在一个叫做 clean 的文件,而是每次运行都执行对应的命令。像 .PHONY 这样的内容目标名还有很多,可以参考此链接【特殊目标(GNU 制作)

makefile 内置目标 含义
.PHONY 伪目标。将无条件运行其规则。
.SUFFIXES 检查后缀规则的后缀列表
.DEFAULT 未指定任何目标时,使用此配置的规则
.PRECIOUS 此配置项目标如果在执行期间被杀死或中断,则不会删除目标
.INTERMEDIATE 此配置项的目标被视为中间文件
.NOTINTERMEDIATE 此配置项的目标的前置条件从不被视为中间文件
.SECONDARY 此配置项的目标被视为中间文件,知识她们永远不会自动删除,.SECONDART可用于避免在某些异常情况下进行冗余重建
.SECONDEXPANSION 如果在生成文件中的任意位置提及为目标,则在读入所有生成文件后,将再次展开其出现后定义的所有前置条件列表
.DELETE_ON_ERROR 此配置项的目标如果以非零的状态退出,则将删除该规则的目标
.IGNORE 此配置项的目标,将忽略执行这些特定文件的配方时出现的错误
.LOW_RESOLUTION_TIME 此配置项的目标,如果这些文件是由低分辨率时间戳的命令创建的,目标的配方将被忽略
.SILENT 此配置项的目标,在执行这些特定文件之前不会打印用于重新制作这些文件的配方
.EXPORT_ALL_VARIABLES 此配置项的目标,默认情况下将所有变量导出到子进程,这是不带参数使用的替代方法
.NOTPARALLEL 此配置项的目标,此调用中的所有目标都将串行运行,即使给出了选项也是如此
.ONESHELL 此配置项的目标,当构建目标时,配方的所有行都将提供给 shell 的单个调用,但不是单独调用每一行
.POSIX 此配置项的目标,将解析生成文件并以符合 POSIX 的模式运行

如果 make 命令运行时没有指定目标,默认会执行 Makefile 文件的第一个目标。

2.3 前置条件 prerequisites

prerequisites 是依赖文件,是要生成那个 target 所需要的文件或其他 target。

前置条件通常是一组文件名,之间用空格分隔。“前置条件” 指定了 “目标” 是否重新构建的判断标准,只要有一个 ”前置文件“ 不存在,或者有过更新(前置文件的 last-modification 时间戳比目标的时间戳新),”目标“ 就需要重新构建。

result.txt : source.txt
	cp souce.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 也不会重新生成。

如果需要生成多个文件,往往采用下面的写法

source : file1 file2 file3

上面代码中,source 是一个伪目标,只有三个前置文件,没有任何对应的命令

make source

执行 make source 后,就会一次性生成 fil1、file2、file3 三个文件,这比下面的写法要方便很多

make file1
make file2
make file3

2.4 命令 command

commond 是 make 需要执行的命令。

命令(commands)表示如何更新目标文件,由一行或多行的 shell 命令组成。它是构建 “目标” 的具体指令,它的运行结果通常就是生成目标文件。

每行命令之前,必须有一个 TAB 键,如果想用其他键,可以使用内置变量 .RECIPEPREFIX 声明

.RECIPEPREFIX = >
all:
> echo Hello, World

上面代码用 .RECIPEPREFIX 指定 大于号(>) 替代 tab 键,所以,每一行命令的起首变成了大于号,而不是 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]"

3、Makefile文件语法

3.1 注释

在 Makefile 语法中 注释 用 # 表示。

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

3.2 回声 echoing

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

在第一行命令前增加 @ 符号,就可以关闭回声,这样就不会打印命令。

.ONESHELL:
var-kept:
	# 这是注释
	export foo="foood"
	echo "foo=[$$foo]"
	export foo2=$$foo"dd"
	echo "foo2=[$$foo2]"

输出结果

make var-kept

# 这是注释
export foo="foood"
echo "foo=[$$foo]"
export foo2=$$foo"dd"
echo "foo2=[$$foo2]"

foo=[foood]
foo2=[foooddd]

输出结果把所有的命令都输出了,甚至是注释文件都出来了

关闭回声的做法

.ONESHELL:
var-kept:
	@# 这是注释
	export foo="foood"
	echo "foo=[$$foo]"
	export foo2=$$foo"dd"
	echo "foo2=[$$foo2]"

输出结果

make var-kept

foo=[foood]
foo2=[foooddd]

3.3 通配符

通配符(wildcard)用来指定一组符合条件的文件名。Makefile 的通配符与 Bash 一致,主要有星号(*)、问号(?)和 […]。比如,**.o 表示所有后缀名为 o 的文件。

clean:
	rm -rf *.o

3.4 模式匹配

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

%.o:%.c

等同于下面的写法

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

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

3.5 变量和配置符

Makefile 允许使用等号自定义变量。

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

上面代码中,变量 txt 等于 Hello World。调用时,变量需要放在 $() 之中。

调用 Shell 变量,需要在美元符号前,再加一个美元符号,这是因为 Make 命令会对美元符号转义。

test:
	@echo $$HOME

有时,变量的值可能指向另一个变量。

v1 = $(v2)

上面代码中,变量 v1 的值时另一个变量 v2。这时会产生一个问题,v1 的值到底在定义时扩展(静态扩展),还是在运行时扩展(动态扩展)。如果 v2 的值是动态的,这两种扩展方式的结果可能会差异很大。

为了解决类似问题,Makefile 一共提供了四个赋值运算符(=、:=、?=、+=)

# 在执行时扩展,允许递归扩展
VARIABLE = value

# 在定义时扩展
VARIABLE := value

#只有在该变量为空时才设置值
VARIABLE ?= value

#将值追加到变量的尾端
VARIABLE += value

3.6 内置变量 Implicit Variables

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

output:
	$(CC) -o output input.c

3.7 自动变量 Automatic Variables

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

3.7.1 $@

@ ”指当前目标,就是 M a k e 命令当前构建的那个目标。比如 m a k e f o o 的 " @” 指当前目标,就是 Make 命令当前构建的那个目标。比如 make foo 的 " @”指当前目标,就是Make命令当前构建的那个目标。比如makefoo"@" 就指代 foo。

3.7.2 $<

" < " 指代第一个前置条件。比如,规则为 t : p 1 p 2 ,那么“ <" 指代第一个前置条件。比如,规则为 t: p1 p2,那么 “ <"指代第一个前置条件。比如,规则为t:p1p2,那么<” 就指代 p1。

3.7.3 $?

" ? " 指代比目标更新的所有前置条件,之间用空格分隔。比如,规则为 t : p 1 p 2 ,其中 p 2 的时间戳比 t 新,那么“ ?" 指代比目标更新的所有前置条件,之间用空格分隔。比如,规则为 t : p1 p2,其中 p2 的时间戳比 t 新,那么 “ ?"指代比目标更新的所有前置条件,之间用空格分隔。比如,规则为t:p1p2,其中p2的时间戳比t新,那么?” 就指代 p2。

3.7.4 $^

" " 指代所有前置条件,之间用空格分隔。比如 t : p 1 p 2 ,那么“ ^" 指代所有前置条件,之间用空格分隔。比如 t : p1 p2,那么 “ "指代所有前置条件,之间用空格分隔。比如t:p1p2,那么^” 就指代 p1 p2。

3.7.5 $*

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

3.7.6 $(@D) 和 $(@F)

$(@D) 和 $(@F) 分别指向 @ 的目录名和文件名。比如, @ 的目录名和文件名。比如, @的目录名和文件名。比如,@ 是 src/input.c,那么 ( @ D ) 的值为 s r c , (@D) 的值为 src, (@D)的值为src(@F) 的值为 input.c。

3.7.7 $(<D) 和 $(<F)

$(<D) 和 $(<F) 分别指向 $< 的目录名和文件名。

dest/%.txt: src/%.txt
	@[-d dest] || mkdir dest
	cp $< $@

上面代码将 src 目录下的 txt,拷贝到 dest 目录下,首先判断 dest 目录是否存在,如果不存在就新建,然后 “ < " 指代前置文件 ( s r c / <" 指代前置文件 (src/%.txt)," <"指代前置文件(src/@” 指代目标文件 (dest/%.txt)。

3.8 判断和循环

判断

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

循环

# 下面循环遍历 LIST 中的数据,然后输出
LIST = one two three
all:
	for i in $(LIST): do \
		echo $$i; \
	done

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

# 运行结果
one
two
three

3.9 函数

Makefile 还可以使用函数,格式如下

$(function arguments)
# 或者
${function arguments}

Makefile 提供了许多内置函数,可供调用。下面例举出几个常用的内置函数。

3.9.1 shell 函数

shell 函数用来执行 shell 命令

srcfiles := $(shell echo src/{00..99}.txt)
3.9.2 wildcard 函数

wildcard 函数用来在 Makefile 中,替换 Bash 的通配符

srcfiles := $(wildcard src/*.txt)
3.9.3 subst 函数

subst 函数用来文本替换,格式如下

$(subst from, to ,text)

下面的例子将字符串 “feet on the street” 替换成 “fEEt on the strEEt”。

$(subst ee,EE,feet on the street)

下面是一个稍微复杂的例子

comma:=,
empty:=
# space 变量用两个空变量作为表示符,当中是一个空格
space:=$(empty) $(empty)
foo:= a b c
bar:= $(subst $(space),$(comma),$(foo))
# bar is now 'a,b,c'.
3.9.4 patsubst 函数

patsubst 函数用于模式匹配的替换,格式如下

$(patsubst parrern,replacement,text)

下面的例子将文件名 “x.c.c bar.c”,替换成 “x.c.o bar.o”。

$(patsubst %.c,%.o,x.c.c,bar.c)
3.9.5 替换后缀名

替换后缀名函数的写法是:变量名 +冒号 + 后缀名替换规则。实际上是 patsubst 函数的一种简写形式。

min:$(OUTPUT:.js=.min.js)

上面代码的意思是,将变量 OUTPUT 中的后缀名 .js 全部替换成 .min.js

4、Makefile 实例

4.1 执行多个目标

.PHONY: cleanall cleanobj cleandiff

cleanall : cleanobj cleandiff
	rm program

cleanobj :
	rm *.o

cleandiff :
	rm *.diff

上面代码可以调用不同目标,删除不同后缀名的文件,也可以调用一个目标(cleanall),删除所有指定类型的文件。

4.2 编译C语言项目

edit : main.o kbd.o command.o display.o
	cc -o edit main.o kbd.o command.o display.o

main.o : main.c defs.h
	cc -c main.c

kbd.o : kbd.c defs.h command.h
	cc -c command.c

display.o : display.c defs.h
	cc -c display.c

clean :
	rm edit main.o kbd.o command.o display.o

.PHONY: edit clean

猜你喜欢

转载自blog.csdn.net/Yang_Mao_Shan/article/details/131696714
今日推荐