摘要

make、Makefile

介绍

本质

  1. target ...: prerequisites ...
  2. command
  3. ...

当target文件不存在或prerequisites文件更新时间比target文件更新时执行command命令。

内容

组成

显式规则

隐式规则

变量

文件指示

注释

解决方案

1. 同时生成多个可执行文件

本质:使用伪目标

  1. .PHONY: all
  2. all: producer customer
  3. producer: producer.o
  4. gcc -o producer producer.o
  5. customer: customer.o
  6. gcc -o customer customer.o
  7. .PHONY: clean
  8. clean:
  9. rm -rf *.o
  10. rm producer
  11. rm customer

参考

  1. 跟我一起学Makefile.pdf

C #Cpp #编译 #Gcc #Makefile #make #编译工具

Makefile实战-20211030 · 语雀 (yuque.com)

如何学习Makefile

Makefile是为了帮助C/C++自动化编译的工具,方便进行项目管理

背景

核心

采用目标(target)依赖(dependency)关系来思考所需解决的问题。
![[Pasted image 20211106230713.png]]
目标就是指要干什么,或说运行 make 后生成什么
依赖是告诉 make 如何去做以实现目标
在 Makefile 中,目标和依赖是通过规则/命令(rule)来表达的

工作原理

基本单元就是规则
规则 = 目标 + 依赖 + 命令
![[Pasted image 20211106230904.png]]
描述:
当 make 得到目标后,先找到定义目标的规则,然后运行规则中的命令来达到构建目标的目的

make 处理规则的活动图
如图所示,当中的构建依赖目标(build dependent target(s))这一活动(注意是活动,而不是动作)就是重复图 1.15 所示的同样的活动,你可以看作是对图 1.15 活动图的递归调用。而运行命令构建目标(run command to build target)则是一个动作,是由命令所组成的动作。活动与动作的区别是,动作是只做一件事(但是可以有多个命令),而活动可以包括多个动作。
![[Pasted image 20211107014704.png]]

案例

  1. all: test
  2. @echo "Hello World"
  3. test:
  4. @echo "Just for test!"

![[Pasted image 20211107014814.png]]

make 是如何决定哪些目标(这里是文件)是需要重新编译的
通过文件的时间戳!当 make 在运行一个规则时,我们前面已经提到了目标和先决条件之间的依赖关系,make 在检查一个规则时,采用的方法是:如果先决条件中相关的文件的时间戳大于目标的时间戳,即先决条件中的文件比目标更新,则知道有变化,那么需要运行规则当中的命令重新构建目标。这条规则会运用到所有与我们在 make时指定的目标的依赖树中的每一个规则。

对于make 工具,一个文件是否改动不是看文件大小,而是其时间戳。

编写思路

思想:先用面向依赖关系的方法想清楚,所要写的 Makefile 需要表达什么样的依赖关系

基本需求

  1. simple项目

  2. complicated项目
    1. 所有的目标文件放入源程序所在目录的 objs 子目录中
    2. 所有最终生成的可执行程序放入源程序所在目录的 exes 子目录中
    3. 引入用户头文件

步骤
simple(简单项目)

  1. 构建依赖关系图(依赖树)
    ![[Pasted image 20211107015458.png]]
    更精确表达—>
    ![[Pasted image 20211107015525.png]]
  2. 规则表示依赖关系(即其中的每一个带箭头的虚线)
    Makefile
    ![[Pasted image 20211107015716.png]]
    依赖关系与规则的映射图
    ![[Pasted image 20211107015749.png]]
  3. 简化
    使用自动变量简化目标与依赖
    使用模式(编译所有的源文件,从.c.o
    使用函数
    complicated(复杂项目)

语法

规则

每一个规则可以包含很多条命令
一个规则是由目标(targets)先决条件(prerequisites)以及命令(commands)所组成的。
目标和先决条件之间表达的就是依赖关系(dependency),这种依赖关系指明在构建目标之前,必须保证先决条件先满足(或构建)。而先决条件可以是其它的目标,当先决条件是目标时,其必须先被构建出来。还有就是一个规则中目标可以有多个,当存在多个目标,且这一规则是 Makefile 中的第一个规则时,如果我们运行 make 命令不带任何目标,那么规则中的第一个目标将被视为是缺省目标。
功能就是指明 make 什么时候以及如何来为我们重新创建目标

![[Pasted image 20211107014431.png]]
规则的语法

  1. targets : prerequisites
  2. command

目标

  1. 一个 Makefile 中可以定义多个目标
  2. 目标可以生成文件
  3. make检查目标时会先检查是否存在同名文件

伪目标(phony target)

解决目标与存在文件同名

make 并不会将其当作一个文件来处理,而只是当作一个概念上的目标。

假目标可以采用.PHONY 关键字来定义,需要注意的是其必须是大写字母

依赖

依赖目标在 Makefile 中又被称之为先决条件
从左到右的先后顺序先构建规则中所依赖的每一个目标

命令

每一行命令之前必须用 TAB 键
在命令前加了一个‘@’。 这一符号告诉 make,在运行时不要将这一行命令显示出来

  1. all:
  2. @echo "Hello World"

对于规则中的每一个命令,make 都是在一个新的 Shell 上运行它的,如果希望多个命令在同一个 Shell 中运行,则需要用‘;’将这些命令连起来。当命令很长时,为了方便阅读,我们需要将一行命令分成多行,这需要用‘\’

  1. .PHONY: all
  2. all:
  3. @mkdir test
  4. @cd test
  5. @mkdir subtest

在同个目录中创建了 test 和 subtest 两个目录

  1. Makefile
  2. .PHONY: all
  3. all:
  4. @mkdir test ; \
  5. cd test ; \
  6. mkdir subtest

生成了test/subtest目录

执行方式

make:
以 Makefile 文件中定义的第一个目标作为这次运行的目标。这“第一个”目标也称之为默认目标(和是不是all没有关系)。
输出:命令(一行)+结果(一行)
make all(目标):

  1. all存在
  2. all不存在
    make 变量=变量值

变量

使用变量来使得它更简洁、更具可维护性

定义
变量名 = 变量值
通过在 make 命令行中定义变量的方式从而覆盖 Makefile 中所定义的变量的值

常见变量名
命令:大写命令 = 命令 eg RM = rm
命令选项:XXXFLAGS = -rf
CC
CCFLAGS = -g -o
EXE
DIR
SRCS = .c
OBJS =
.o

采用“+=”操作符对变量进行赋值的方法
objects = main.o foo.o bar.o utils.o
objects += another.o
等价
objects = main.o foo.o bar.o utils.o
objects := $(objects) another.o

引用
Makefile - 图1%0A%E9%AB%98%E7%BA%A7%E5%BC%95%E7%94%A8%0A#card=math&code=%28%E5%8F%98%E9%87%8F%E5%90%8D%29%0A%E9%AB%98%E7%BA%A7%E5%BC%95%E7%94%A8%0A&id=DlquN)(变量名:.o=.c):在赋值的同时完成后缀替换操作(还可使用函数patsubst实现)

自动变量(预定义变量)

简化目标和先决条件

自动变量的值是在命令处理阶段才被赋值,不能用于条件语法中

在 Makefile 中$具有特殊的意思,因此,如果想采用 echo 输出$,则必需用两个连着的$。还有就是,$@对于 Shell 也有特殊的意思,我们需要在$$@之前再加一个脱字符\

$@用于表示一个规则中的目标(目标的完整名称)。当我们的一个规则中有多个目标时,Makefile - 图2^则表示的是规则中的所有先择条件。(所有的依赖文件,以空格分开,不包含重复的依赖文件)Makefile - 图3%如果目标是归档成员,则该变量表示目标的归档成员名称Makefile - 图4+所有的依赖文件,以空格分开,并以出现的先后为序,可能包含重复的依赖文件$*`不包含扩展名的目标文件名称

测试

  1. .PHONY: all
  2. all: first second third
  3. @echo "\$$@ = $@"
  4. @echo "$$^ = $^"
  5. @echo "$$< = $<"
  6. first second third:

特殊变量

MAKE
表示的是make 命令名是什么。我们需要在 Makefile 中调用另一个 Makefile 时需要用到这个变量,采用这种方式,有利于写一个容易移植的 Makefile。
MAKECMDGOALS
表示用户输入的目标
当我们只运行 make 命令时,虽然根据 Makefile 的语法,第一个目标将成为缺省目标,即 all 目标,但 MAKECMDGOALS 仍然是空

变量类别

  1. 递归扩展变量
    只用一个“=”符号定义的变量,我们称之为递归扩展变量(recursively expanded variable)
    递归扩展变量的引用是递归的
    好处:
    自动扩展
    坏处
    不能重复赋值(造成死循环)
    CFLAGS = $(CFLAGS) -O 不允许
  2. 简单扩展变量(simply expanded variables)

“:=”操作符来定义的。对于这种变量,make 只对其进行一次扫描和替换

  1. 条件赋值符“?=”
    当变量以前没有定义时,就定义它并且将左边的值赋值给它,如果已经定义了那么就不再改变其值。条件赋值类似于提供了给变量赋缺省值的功能

变量来源

  1. 自定义
  2. 自动变量(根据规则上下文自动获取)
  3. 执行make命令时定义:make 变量=值(覆盖,若不想覆盖,使用override指令,override 变量名 = 值
  4. 来自Shell环境变量:export 变量=值

模式

应用同一个模式规则,类似使用通配符%

  1. %.o: %.c
  2. gcc -o $@ -c $^

函数

语法

addprefix
用来在给字符串中的每个子串前加上一个前缀
$(addprefix prefix, names...)
eg:$(addprefix objs/, $(without_dir))
适用:移动特定文件到指定目录中(重命名文件路径)

filter
用于从一个字符串中,根据模式得到满足模式的字符串(过滤,正则表达式)
$(filter pattern..., text)
eg
$(filter %.c %.s, $(sources))
适用:过滤指定模式文件(保留pattern)

filter-out
用于从一个字符串中根据模式滤除一部分字符串
$(filter-out pattern..., text)
eg
$(filter-out main%.o, $(objects))
(不保留pattern),与filter功能相反(互补)

patsubst
用来进行字符串替换(可前缀、后缀)
$(patsubst pattern, replacement, text)

strip
用于去除变量中的多余的空格
$(strip strings...)

wildcard
通配符函数
$(wildcard pattern)

其他

  1. 文件的存放目录可以是任意
  2. 命令前的缩进:必须只有 TAB(即你键盘上的 TAB键),且至少有一个 TAB,而不能用空格代替

提高

常用伪目标

clean

  1. .PHONY: clean
  2. clean:
  3. $(RM) $(RMFLAGS) $(DIRS)
  4. $(RM) *.o
  5. $(RM) $(EXES)

install

  1. .PHONY: install
  2. install:
  3. $(CP) $(EXES) $(PATH)

创建目录

在编译项目之前希望用于存放文件的目录先准备好(自动创建),即只有目录创建功能的 Makefile

依赖关系图
![[Pasted image 20211107033056.png]]
会报错:make: *** No rule to make target 'objs', needed by 'all'. Stop.

  1. .PHONY: all
  2. MKDIR = mkdir
  3. DIRS = objs exes
  4. all: $(DIRS)
  5. $(MKDIR) $@

——>

  1. .PHONY: all
  2. MKDIR = mkdir
  3. DIRS = objs exes
  4. all:
  5. $(MKDIR) $DIRS

无依赖关系,执行make,第一次成功创建目录,但第2次报错,丢失Makefile特性

  1. mkdir: cannot create directory objs’: File exists
  2. mkdir: cannot create directory exes’: File exists
  3. make: *** [Makefile:5: all] Error 1

——>
![[Pasted image 20211107033133.png]]

  1. .PHONY: all
  2. MKDIR = mkdir
  3. DIRS = objs exes
  4. all: $(DIRS)
  5. $(DIRS):
  6. $(MKDIR) $@

objs 和 exes 即是目标又是目录(第2次make时认为 objs 和 exes 目标都是最新,无需执行)
make: Nothing to be done for 'all'.

移动文件

一般用于将目标文件移动到objs/子目录,可执行文件移动到exes/子目录
提供库时移动相关头文件到include/子目录中等等

  1. 修改中间目标文件的生成目录:使用addprefix函数添加目录前缀
  2. 修改构建中间目标的目标名
    eg
  1. DIR_OBJS = objs
  2. SRCS = $(wildcard *.c)
  3. OBJS = $(SRCS:.c=.o)
  4. OBJS := $(addprefix $(DIR_OBJS)/, $(OBJS))
  5. $(DIR_OBJS)/%.o: %.c
  6. $(CC) -o $@ -c $^

复杂依赖

自动链接依赖的头文件
为每一个源文件通过采用 gcc 和 sed生成一个依赖关系文件,这些文件我们采用.dep 后缀结尾,并存放在新的子目录deps中

如何在 make 时,动态的生成文件的依赖关系(gcc)
gcc -M XXX.c
直接或是间接包含的头文件
gcc -MM XXX.c ——> 结果可以构成一个目标: 依赖
不列出对于系统头文件的依赖关系
当将目标文件放入子目录objs/时,使用
gcc -MM XXX.c | sed 's,\(.*\)\.o[ :]*,objs/\1.o: ,g'
结果为:objs/XXX.o: XXX.c XXX.h

在生成依赖关系时,其实我们并不需要 gcc 去编译,只要进行预处理就行(gcc -E),可以避免在生成依赖关系时出现没有必要的 warning,以及提高依赖关系的生成效率。

  1. **Makefile**
  2. .PHONY: all clean
  3. MKDIR = mkdir
  4. RM = rm
  5. RMFLAGS = -fr
  6. CC = gcc
  7. DIR_OBJS = objs
  8. DIR_EXES = exes
  9. DIR_DEPS = deps
  10. DIRS = $(DIR_OBJS) $(DIR_EXES) **$(DIR_DEPS)**
  11. EXE = complicated
  12. EXE := $(addprefix $(DIR_EXES)/, $(EXE))
  13. SRCS = $(wildcard *.c)
  14. OBJS = $(SRCS:.c=.o)
  15. OBJS := $(addprefix $(DIR_OBJS)/, $(OBJS))
  16. DEPS = $(SRCS:.c=.dep)
  17. DEPS := $(addprefix $(DIR_DEPS)/, $(DEPS))
  18. all: $(DIRS) $(DEPS) $(EXE)
  19. $(DIRS):
  20. $(MKDIR) $@
  21. $(EXE): $(OBJS)
  22. $(CC) -o $@ $^
  23. $(DIR_OBJS)/%.o: %.c foo.h
  24. $(CC) -o $@ -c $^
  25. $(DIR_DEPS)/%.dep: %.c
  26. @echo "Making $@ ..."
  27. @set -e; \
  28. $(RM) $(RMFLAGS) $@.tmp ; \
  29. $(CC) -E -MM $^ > $@.tmp ; \
  30. sed 's,\(.*\)\.o[ :]*,objs/\1.o: ,g' < $@.tmp > $@ ; \
  31. $(RM) $(RMFLAGS) $@.tmp
  32. clean:
  33. $(RM) $(RMFLAGS) $(DIRS)

存在以下更改:

  • 增加了 DIR_DEPS 变量,用于保存需要创建的 deps 目录名,以及将这一变量的值加入到DIR 变量中。
  • 删除了目标文件模式规则中对于 foo.h 文件的依赖,与此同时,我们将这个规则中的$<变成了$^
  • 增加了 DEPS 变量用于存放依赖文件。
  • 为 all 目标增加了对于 DEPS 的依赖。
  • 增加了一个用于创建依赖文件的模式规则。有这一规则的命令当中,我们使用了 gcc 的-E和-MM 选项来获取依赖关系,为了最终生成依赖文件,中间采用了一个临时文件。为了增加可读性,在生成一个依赖文件时,会在终端上打印类似“Making foo.dep …”这样的提示信息。在这个规则中,set -e 的作用是告诉 BASH Shell 当生成依赖文件的过程中出现任何错误时,就直接退出。最强终的表现就是 make 会告诉我们出错了,从而停止后面的 make工作。如果不进行这一设置,当构建依赖文件出现错误时,make 还会继续后面的工作,这是我们所不希望的。同样,你可以试着将 set -e 去掉,然后故意在 foo.c 或是 main.c 中植入一个错误,观察一下 make 此时的行为是什么。

包含文件

include关键字
include $(DEPS)

make 对于 include 的处理是先于 all 目标的构建的
忽略include出错:在 Makefile 中,如果在 include 前加上一个-号,当 make 处理这一包含指示时,如果文件不存在就会忽略这一错误
-include $(DEPS)

处理流程
当 make 看到 include 指令时,会先找一下有没有这个文件,如果有则读入。接着,make 还会看一看对于包含进来的文件,在 Makefile 中是否存在规则来更新它。如果存在,则运行规则去更新需被包含进来的文件,当更新完了之后再将其包含进来

有了这些信息之后,我们需要对 Makefile 的依赖关系进行调整,即将 deps 目录的创建放在构建依赖文件之前。其改动就是在依赖文件的创建规则当中增加对 deps 目录的信赖,且将其当作是第一个先决条件。采用同样的方法,我们将所有的目录创建都放到相应的规则中去

依赖关系

gcc -MM foo.c | sed 's,\(.*\)\.o[ :]*,objs/\1.o deps/foo.dep: ,g'
只要在依赖文件的构建规则中多增加依赖文件自身这个目标

依赖关系:*.o依赖.dep,不直接依赖.h
添加依赖关系:.dep依赖.h和*.c(通过只要在依赖文件的构建规则中多增加依赖文件自身这个目标即可)
解决了添加的头文件仅修改了“预处理语句”,使得.o不依赖.h,

条件语法

conditional-directive:ifdef、ifeq、ifndef /ifneq

conditional-directive
text-if-true
endif

conditional-directive
text-if-true
else
text-if-false
endif

conditional-directive
text-if-one-is-true
else conditional-directive
text-if-true
else
text-if-false
endif

ifeq/ifneq arg1 arg2
(arg1, arg2)
“arg1”, “arg2” 不区分单双引号(可混搭,4种)

ifdef/ifndef 变量

解决执行重复执行make clean时,还会先构建依赖文件,接着再删除
ifneq ($(MAKECMDGOALS), clean)
-include $(DEPS)
endif

目的:

  1. 无须重新编译,可继续编译(即不用每次必须先make clean后再make)
  2. 能准确监控所有代码文件,只要发生变更,都会进行重新编译,而不会遗漏

准备

安装
Linux
sudo yum install make -y
Windows
Cgywin
验证
make -v

Makefile新手?千万别错过了《驾驭Makefile》洛奇看世界-CSDN博客驾驭makefile

拓展

自动生成工具:Cmake、Autotools
Autotools使用详细解读[转载]_我的博客-CSDN博客_autotools