知识图库
Java知识库
JDK线程池实现原理
Java中的强、软、弱、虚引用
深入拆解Java虚拟机
01 开篇词 | 为什么我们要学习Java虚拟机?
02 Java代码是怎么运行的?
03 Java的基本类型
04 Java虚拟机是如何加载Java类的?
05 JVM是如何执行方法调用的?(上)
06 JVM是如何执行方法调用的?(下)
7 JVM是如何处理异常的?
Java面试常见问题整理
Java面试常见问题-Java 基础篇
Java面试常见问题-Jvm篇
Java面试常见问题-并发篇
Android知识库
Kotlin编程第一课
1 开篇词 | 入门Kotlin有多容易,精通Kotlin就有多难
2 Kotlin基础语法:正式开启学习之旅
3 面向对象:理解Kotlin设计者的良苦用心
4 Kotlin原理:编译器在幕后干了哪些“好事”?
5 实战:构建一个Kotlin版本的四则运算计算器
6 object关键字:你到底有多少种用法?
7 扩展:你的能力边界到底在哪里?
8 高阶函数:为什么说函数是Kotlin的“一等公民”?
9 实战:用Kotlin写一个英语词频统计程序
10 加餐一 | 初识Kotlin函数式编程
11 委托:你为何总是被低估?
12 泛型:逆变or协变,傻傻分不清?
13 注解与反射:进阶必备技能
14 实战:用Kotlin实现一个网络请求框架KtHttp
15 加餐二 | 什么是“表达式思维”?
16 加餐三 | 什么是“不变性思维”?
17 加餐四 | 什么是“空安全思维”?
18 春节刷题计划(一)| 当Kotlin遇上LeetCode
19 春节刷题计划(二)| 一题三解,搞定版本号判断
20 春节刷题计划(三)| 一题双解,搞定求解方程
21 春节刷题计划(四)| 一题三解,搞定分式加减法
22 什么是“协程思维模型”?
23 如何启动协程?
24 挂起函数:Kotlin协程的核心
25 Job:协程也有生命周期吗?
26 Context:万物皆为Context?
27 实战:让KtHttp支持挂起函数
28 期中考试 | 用Kotlin实现图片处理程序
29 题目解答 | 期中考试版本参考实现
30 Channel:为什么说Channel是“热”的?
31 Flow:为什么说Flow是“冷”的?
32 select:到底是在选择什么?
33 并发:协程不需要处理同步吗?
34 异常:try-catch居然会不起作用?坑!
35 实战:让KtHttp支持Flow
36 答疑(一)| Java和Kotlin到底谁好谁坏?
37 集合操作符:你也会“看完就忘”吗?
38 协程源码的地图:如何读源码才不会迷失?
39 图解挂起函数:原来你就是个状态机?
40 加餐五 | 深入理解协程基础元素
41 launch的背后到底发生了什么?
42 Dispatchers是如何工作的?
43 CoroutineScope是如何管理协程的?
44 图解Channel:如何理解它的CSP通信模型?
45 图解Flow:原来你是只纸老虎?
46 Java Android开发者还会有未来吗?
47 Kotlin与Jetpack简直是天生一对!
48 用Kotlin写一个GitHub Trending App
49 结课测试 | “Kotlin编程第一课”100分试卷等你来挑战!
50 结束语 | 不忘初心
Android Framework 教程—基础篇
01 Ubuntu 使用快速入门
02 Make 构建工具入门
03 理解 Unicode UTF-8 UTF-16 UTF-32
04 Linux Shell 脚本编程入门1——核心基础语法
05 SeAndroid 使用极速上手
06 理解 C++ 的 Memory Order
07 AOSP 极速上手
08 系统开发工具推荐
09 添加 Product
运动相关知识
爱上跑步
01 开篇词 | 跑步,不那么简单的事儿
02 跑两步就喘了,是不是我不适合跑步?
03 正确的跑步姿势是什么样的?
04 为什么跑步要先热身?
05 怎样制定你的第一个10公里跑步计划?
06 快跑和慢跑,哪个更燃脂?
07 普通跑步者应该如何选择跑鞋?
08 买跑步装备,不要踩这些坑儿
09 跑步前到底应不应该吃东西?
10 跑步到底伤不伤膝盖?
11 参加了20场马拉松,我是如何准备的?
12 除了马拉松,还能参加哪些跑步赛事?
13 热点问题答疑 :跑完第二天浑身疼,还要不要继续跑?
健身房计划
[DeepSeek]减脂塑形计划
【DeepSeek】训练周期安排
每日餐饮热量控制
减脂期间食物推荐避坑指南
HarmonyOS知识库
其他知识类目
心理学相关
如何学点心理学——关于非专业人士学心理学的一点建议
投射性认同
-
+
首页
02 Make 构建工具入门
本文是在[Make 命令教程](https://www.ruanyifeng.com/blog/2015/02/make.html "Make 命令教程")的基础上进行的演绎和补充。 ### 1. 什么是 Make 代码变成可执行文件,叫做编译(compile);先编译这个,还是先编译那个(即编译的安排),叫做构建(build)。 Make 是最常用的构建工具,诞生于 1977 年,主要用于 C 语言的项目。但是实际上 ,任何只要某个文件有变化,就要重新构建的项目,都可以用 Make 构建。 Make 这个词,英语的意思是"制作"。Make 命令直接用了这个意思,就是要做出某个文件。比如,要做出文件 a.txt,就可以执行下面的命令。 ```shell make a.txt ``` 但是,如果你真的输入这条命令,它并不会起作用。因为 Make 命令本身并不知道,如何做出 a.txt,需要有人告诉它,如何调用其他命令完成这个目标。 比如,假设文件 a.txt 依赖于 b.txt 和 c.txt ,是后面两个文件连接(cat命令)的产物。那么,make 需要知道下面的规则。 ```shell 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 命令进行构建的工具。它的规则很简单,你规定要构建哪个文件、它依赖哪些源文件,当那些文件有变动时,如何重新构建它。 ### 2. Makefile 文件的格式 Makefile文件由一系列规则(rules)构成。每条规则的形式如下 ```shell <target> : <prerequisites> [tab] <commands> ``` 上面第一行冒号前面的部分,叫做"目标"(target),冒号后面的部分叫做"前置条件"(prerequisites);第二行必须由一个tab键起首,后面跟着"命令"(commands)。 "目标"是必需的,不可省略;"前置条件"和"命令"都是可选的,但是两者之中必须至少存在一个。 每条规则就明确两件事:构建目标的前置条件是什么,以及如何构建。 我们看个最简单的例子: ```shell 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 更新,即修改过 下面就详细讲解,每条规则的这三个组成部分。 ### 2.1 目标(target) 一个目标(target)就构成一条规则。目标通常是文件名,指明Make命令所要构建的对象,比如上文的 a.txt 。目标可以是一个文件名,也可以是多个文件名,之间用空格分隔。 除了文件名,目标还可以是某个操作的名字,这称为"伪目标"(phony target)。 ```shell clean: rm *.o ``` 上面代码的目标是 clean,它不是文件名,而是一个操作的名字,属于"伪目标 ",作用是删除对象文件。 ```shell make clean ``` 但是,如果当前目录中,正好有一个文件叫做 clean,那么这个命令不会执行。因为 Make 发现 clean 文件已经存在,就认为没有必要重新构建了,就不会执行指定的 rm 命令。 为了避免这种情况,可以明确声明 clean 是"伪目标",写法如下。 ```shell .PHONY: clean clean: rm *.o temp ``` 声明 clean 是"伪目标"之后,make就不会去检查是否存在一个叫做 clean 的文件,而是每次运行都执行对应的命令。 ### 2.2 前置条件(prerequisites) 前置条件通常是一组文件名,之间用空格分隔。它指定了"目标"是否重新构建的判断标准:前置条件修改过,或者说是前置文件比目标文件更新,具体的,前置文件比前置文件的 last-modification 时间戳比目标的时间戳新,"目标"就需要重新构建。(通过 ls -l 可以查看到文件的 last-modification 时间) 我们接下来看一个例子: ```shell result.txt: source.txt cp source.txt result.txt ``` 上面代码中,构建 result.txt 的前置条件是 source.txt 。如果当前目录中,source.txt 已经存在,那么 make result.txt 可以正常运行,否则必须再写一条规则,来生成 source.txt 。 ```shell source.txt: echo "this is the source" > source.txt ``` 上面代码中,source.txt 后面没有前置条件,就意味着它跟其他文件都无关,只要这个文件还不存在,每次调用 make source.txt,它都会生成。 ```shell $ make result.txt $ make result.txt ``` 上面命令连续执行两次 make result.txt。第一次执行会先新建 source.txt,然后再新建 result.txt。第二次执行,Make 发现 source.txt 没有变动(时间戳晚于 result.txt),就不会执行任何操作,result.txt 也不会重新生成。 ### 2.3 命令(commands) 命令(commands)表示如何更新目标文件,由一行或多行的 Shell 命令组成。它是构建"目标"的具体指令,它的运行结果通常就是生成目标文件。每行命令之前必须有一个 tab 键。 需要注意的是,每行命令在一个单独的 shell 中执行。这些 Shell 之间没有继承关系。 ```shell var-lost: export foo=bar echo "foo=[$$foo]" ``` 上面代码执行后(make var-lost),取不到 foo 的值。因为两行命令在两个不同的进程执行。一个解决办法是将两行命令写在一行,中间用分号分隔。 ```shell var-kept: export foo=bar; echo "foo=[$$foo]" ``` 另一个解决办法是在换行符前加反斜杠转义。 ```shell var-kept: export foo=bar; \ echo "foo=[$$foo]" ``` 最后一个方法是加上 .ONESHELL:命令。 ```shell.ONESHELL: var-kept: export foo=bar; echo "foo=[$$foo]" ``` ### 3. Makefile 基础语法 #### 3.1 注释 井号(#)在 Makefile 中表示注释 ```shell # 这是注释 result.txt: source.txt # 这是注释 cp source.txt result.txt # 这也是注释 ``` #### 3.2 回声(echoing) 正常情况下,make会打印每条命令,然后再执行,这就叫做回声(echoing) 在命令的前面加上@,就可以关闭回声。 ```shell test: @echo TODO ``` #### 3.3 通配符 通配符(wildcard)用来指定一组符合条件的文件名。Makefile 的通配符与 Bash 一致,主要有星号(*)、问号(?)和 []: - * 匹配0个或者是任意个字符 - ? 匹配任意一个字符 - [] 我们可以指定匹配的字符放在 "[]" 中 比较常用的就是 * 号 ```shell .PHONY:clean clean: rm -f *.o ``` #### 3.4 模式匹配 Make 命令允许对文件名,进行类似正则运算的匹配,主要用到的匹配符是 %。比如,假定当前目录下有 f1.c 和 f2.c 两个源码文件,需要将它们编译为对应的对象文件。 ```shell %.o: %.c ``` 等同于下面的写法。 ```shell f1.o: f1.c f2.o: f2.c ``` 使用匹配符 %,可以将大量同类型的文件,只用一条规则就完成构建。 #### 3.5 变量和赋值符 Makefile 中允许自定义变量。 ```shell txt = Hello World test: @echo $(txt) ``` 上面代码中,变量 txt 等于 Hello World。在使用时,需要给在变量名前加上 $ 符号,但最好用小括号 () 或是大括号 {} 把变量给包括起来。小括号的用法比较常见。 调用 Shell 变量,需要在美元符号前,再加一个美元符号: ```shell test: @echo $$HOME ``` ##### 3.5.1 递归展开(Recursively Expanded) 使用 = 来定义的变量是递归展开的 (Recursively Expanded),直到该变量被使用时等号右边的内容才会被展开。而且每次使用该变量时,等号右边的内容都会被重新展开。 概念比较有点拗口,看个例子: ```shell foo = $(bar) bar = $(ugh) ugh = Huh? all: echo $(foo) ``` 执行 make all 时,(foo) 被展开成 (bar),(bar) 被展开成 (ugh),(ugh) 被展开成 Huh?,于是最后输出为 Huh? 最终将会打印出变量 (foo)的值为 Huh? 使用这种方法的一个好处是,我们可以把变量的真实值推到后面来定义。 ```shell CFLAGS = $(include_dirs) -O include_dirs = -Ifoo -Ibar ``` 当 CFLAGS 在命令中被展开时,会是-Ifoo -Ibar -O 当然最主要的缺点就是递归定义可能导致出现无限循环展开,尽管 make 能检测出这样的无限循环展开并报错。 ```shell CFLAGS = $(CFLAGS) -O ``` 另一个问题就是如果在变量中使用函数,每次展开变量时都要重新执行函数,这种方式会使make运行得非常慢。更糟糕的是,这种用法会使得“wildcard”和“shell”发生不可预知的错误,因为你不知道这两个函数会被调用多少次。 ##### 3.5.2 简单展开 (Simply Expanded) 使用 := 来定义的变量是简单展开的 (Simply Expanded) 使用这种方法,读到变量定义这一行时 等号右边立即被展开,引用的所有变量也会被立即展开。 前面的变量不能使用后面的变量,只能使用前面已定义好了的变量。 ```shellx := foo y := $(x) bar x := later ``` 等价于: ```shell y := foo bar x := later ``` 使用这种方法可以在变量中引入开头空格。见下面的示例: ```shell nullstring := space := $(nullstring) # end of the line ``` nullstring 是一个 Empty 变量,其中什么也没有,而 space 的值是一个空格。因为在操作符的右边是很难描述一个空格的,这里采用的技术很管用。先用一个 Empty 变量来标明变量的值开始了,而后面采用 # 注释符来表示变量定义的终止,这样,我们可以定义出其值是一个空格的变量。 ##### 3.5.3 条件变量赋值 使用 ?= 操作符给变量赋值称为条件变量赋值 ```shell FOO ?= bar ``` 如果 FOO 没有被定义过,那么变量 FOO 的值被定义为 bar,如果 FOO 先前被定义过,那么这条语将什么也不做。 注意将变量定义为空字符也是定义的一种。 ?= 是递归展开的 ```shell x := foo y ?= $(x) bar x := later echo $y ``` 这里输出 later bar ##### 3.5.4 追加变量值 使用 += 操作符给变量赋值,称为追加变量值。有以下几种情况: 对未定义变量使用追加:如果变量之前没有定义过,那么,+= 会自动变成 =,追加变量直接变成递归展开。 对使用 := 方式定义的变量使用追加:如果前面是以简单展开方式 (:=) 定义的变量,那么 += 在将新的值追加到已有变量的值的后面之前,会以简单展开 (:=) 的方式将原来的内容先展开 对使用 = 方式定义的变量使用追加:如果前面是以递归展开方式 (=) 定义的变量,那么 += 在将新的值追加到已有变量的值的后面之后,不会展开原来的内容 ```shell CFLAGS = $(includes) -O … CFLAGS += -pg # CFLAGS = $(includes) -O -pg 不会展开 ``` 这样我们就可以保留对 includes 的引用,当之后的某个节点完成对 includes 的定义时,当 CFLAGS 被使用时(即 $(CFLAGS)) ,includes 的值才会被展开 ### 4. 内置变量(Implicit Variables) Make命令提供一系列内置变量,比如,$(CC) 指向当前使用的编译器,$(MAKE) 指向当前使用的 Make 工具。这主要是为了跨平台的兼容性,详细的内置变量清单见手册。 ### 5. 自动变量(Automatic Variables) Make 命令还提供一些自动变量,它们的值与当前规则有关。主要有以下几个。 $@: 指代当前目标,就是 Make 命令当前构建的那个目标。比如,make foo 的 $@ 就指代 foo。 ```shell a.txt b.txt: touch $@ ``` 等同于下面的写法。 ```shell a.txt b.txt: touch a.txt b.txt ``` $<: 指代第一个前置条件。比如,规则为 t: p1 p2,那么 $< 就指代p1 ```shell a.txt: b.txt c.txt cp $< $@ ``` 等同于下面的写法: ```shell a.txt: b.txt c.txt cp b.txt a.txt ``` $? 指代比目标更新的所有前置条件,简单的说就是修改过的前置条件,之间以空格分隔。比如,规则为 t: p1 p2,其中 p2 的时间戳比 t 新,$?就指代p2。 看个例子: ```shell result.txt : a.txt b.txt cat $? ``` 接下来同时创建三个文件: ```shell touch a.txt b.txt result.txt ``` 执行 make result.txt,什么都不会做: ```shell make result.txt make: 'result.txt' is up to date. ``` 接着修改 a.txt 的内容如下: ```shell this is a.txt ``` 再次执行 make result.txt: ```shell make result.txt cat a.txt this is a.txt ``` 这里的 $? 就代表了 a.txt ,因为 a.txt 比 result.txt 更新 $^ 指代所有前置条件,之间以空格分隔。比如,规则为 t: p1 p2,那么 $^ 就指代 p1 p2 。看个例子: ```shell result.txt : a.txt b.txt cat $^ ``` 准备工作: ```shell touch result.txt #修改 a.txt 的内容如下: this is a.txt #修改 b.txt 的内容如下: this is b.txt ```` 执行 make result.txt: ```shell make result.txt cat a.txt b.txt this is a.txt this i b.txt ``` $* 指代匹配符 % 匹配的部分, 比如% 匹配 f1.txt 中的f1 ,$* 就表示 f1。 ### 6. 判断和循环 Makefile 使用 Bash 语法,完成判断和循环。 ```shell #判断当前编译器是否 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 ``` ### 7. 函数 Makefile 中,函数主要分为两类:Make 内嵌函数和用户自定义函数 接下来我们来看一下 Make 内嵌函数的使用。 如果我们想要获取某个目录下所有的 C 文件列表,可以使用扩展通配符函数:wildcard ```shell SRC = $(wildcard *.c) HEAD = $(wildcard *.h) all: @echo "SRC = $(SRC)" @echo "HEAD = $(HEAD)" ``` 在当前目录下,我们新建一些C文件和H文件,然后使用 make 命令: ```shell # 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 ``` wildcard 还可用于判断文件是否存在: ```shell #判断文件是否存在 files := main.o # main.o 存在 files 的值为 main.o # main.o 不存在 files 的值为空 files := $(wildcard $(files)) ``` foreach 函数用于循环,使用方法如下: ```shell names := a b c d # $(name)中的单词会被挨个取出,并存到变量“n”中,“$(n).o”每次根据“$(n)”计算出一个值,这些值以空格分隔,最后作为foreach函数的返回,所以,$(files)的值是“a.o b.o c.o d.o”。 files := $(foreach n,$(names),$(n).o) ``` Make 提供了大量的内嵌函数,大大方便了用户 Makefile 的编写。但有时候根据需要,用户也可以自定义一些函数,然后在 Makefile 中引用它们: ```shell 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 函数间接调用,各个参数之间使用空格隔开 ### 参考资料 Make 命令教程 《嵌入式 Linux 应用开发完全手册 韦东山》 Makefile学习笔记之变量定义与赋值 Makefile 函数 ### 参考资料 Make 命令教程 《嵌入式 Linux 应用开发完全手册 韦东山》 Makefile学习笔记之变量定义与赋值 Makefile 函数
嘿手大叔
2024年11月1日 16:01
转发文档
收藏文档
上一篇
下一篇
手机扫码
复制链接
手机扫一扫转发分享
复制链接
Markdown文件
分享
链接
类型
密码
更新密码