Makefile基础及进阶

Makefile 概述

makefile 是一种用于自动化编译和构建程序的脚本文件,特别是在Unix-like系统中。它定义了如何将源代码编译成可执行文件或其他目标文件。通过 make 工具读取 Makefile 中的规则,自动处理源文件的依赖关系和编译顺序,从而简化和自动化了构建过程。

编译与链接

在编译过程中,源文件(如 .c 文件)会被编译成中间目标文件(在Unix下是 .o 文件)。这个步骤称为编译。编译器检查语法和声明,生成目标文件。如果源文件包含函数或变量的声明而没有定义,编译器会发出警告,但仍会生成目标文件。

链接过程将所有目标文件(.o 文件)合并成一个最终的可执行文件。在链接时,链接器负责解决函数和变量的实际定义,并检查是否所有的引用都有对应的实现。如果链接器找不到某个函数的实现,就会报错。

Make的作用

make 是一个自动化构建工具,通过读取 Makefile,make 能够自动处理文件的依赖关系,执行编译和链接命令。它可以减少手动编译的繁琐步骤,提高开发效率。在开发大型工程时,编写 Makefile 让 make 工具管理编译过程,可以大大简化构建流程。

Makefile 的好处包括:

  • 自动化编译:只需运行 make 命令,即可自动编译整个工程。

  • 依赖管理:自动处理文件间的依赖关系,确保只有在源文件修改后才重新编译相关的目标文件。

  • 提高效率:避免了重复编译和手动管理编译过程的麻烦。

Makefile介绍

makefile来告诉make命令如何编译和链接这几个文件。我们的规则是:

  1. 如果这个工程没有编译过,那么我们的所有c文件都要编译并被链接。

  2. 如果这个工程的某几个c文件被修改,那么我们只编译被修改的c文件,并链接目标程序。

  3. 如果这个工程的头文件被改变了,那么我们需要编译引用了这几个头文件的c文件,并链接目标程序。

只要我们的makefile写得够好,所有的这一切,我们只用一个make命令就可以完成,make命令会自动智能地根据当前的文件修改的情况来确定哪些文件需要重编译,从而自动编译所需要的文件和链接目标程序。

Makefile内容

Makefile里主要包含了五个东西:显式规则、隐式规则、变量定义、指令和注释。

  1. 显式规则。显式规则说明了如何生成一个或多个目标文件。这是由Makefile的书写者明显指出要生成的文件、文件的依赖文件和生成的命令。

  2. 隐式规则。由于我们的make有自动推导的功能,所以隐式规则可以让我们比较简略地书写Makefile,这是由make所支持的。

  3. 变量的定义。在Makefile中我们要定义一系列的变量,变量一般都是字符串,这个有点像你C语言中的宏,当Makefile被执行时,其中的变量都会被扩展到相应的引用位置上。

  4. 指令。其包括了三个部分,一个是在一个Makefile中引用另一个Makefile,就像C语言中的include一样;另一个是指根据某些情况指定Makefile中的有效部分,就像C语言中的预编译#if一样;还有就是定义一个多行的命令。有关这一部分的内容,我会在后续的部分中讲述。

  5. 注释。Makefile中只有行注释,和UNIX的Shell脚本一样,其注释是用 # 字符,这个就像C/C++中的 // 一样。如果你要在你的Makefile中使用 # 字符,可以用反斜杠进行转义,如: \#

值得一提的是,在Makefile中的命令,必须要以 Tab 键开始。

Make的工作方式

GNU的make工作时的执行步骤如下:

  1. 读入所有的Makefile。

  2. 读入被include的其它Makefile。

  3. 初始化文件中的变量。

  4. 推导隐式规则,并分析所有规则。

  5. 为所有的目标文件创建依赖关系链。

  6. 根据依赖关系,决定哪些目标要重新生成。

  7. 执行生成命令。

1-5步为第一个阶段,6-7为第二个阶段。第一个阶段中,如果定义的变量被使用了,那么,make会把其展开在使用的位置。但make并不会完全马上展开,make使用的是拖延战术,如果变量出现在依赖关系的规则中,那么仅当这条依赖被决定要使用了,变量才会在其内部展开。

Makefile中使用变量

edit : main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
cc -o edit main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o

main.o : main.c defs.h
cc -c main.c
kbd.o : kbd.c defs.h command.h
cc -c kbd.c
command.o : command.c defs.h command.h
cc -c command.c
display.o : display.c defs.h buffer.h
cc -c display.c
insert.o : insert.c defs.h buffer.h
cc -c insert.c
search.o : search.c defs.h buffer.h
cc -c search.c
files.o : files.c defs.h buffer.h command.h
cc -c files.c
utils.o : utils.c defs.h
cc -c utils.c
clean :
rm edit main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o

反斜杠( \ )是换行符的意思。

声明一个变量,叫 objectsOBJECTSobjsOBJSobj 或是 OBJ ,反正不管什么啦,只要能够表示obj文件就行了。我们在makefile一开始就这样定义:

objects = main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o

改良版makefile就变成下面这个样子:

objects = main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o

edit : $(objects)
cc -o edit $(objects)
main.o : main.c defs.h
cc -c main.c
kbd.o : kbd.c defs.h command.h
cc -c kbd.c
command.o : command.c defs.h command.h
cc -c command.c
display.o : display.c defs.h buffer.h
cc -c display.c
insert.o : insert.c defs.h buffer.h
cc -c insert.c
search.o : search.c defs.h buffer.h
cc -c search.c
files.o : files.c defs.h buffer.h command.h
cc -c files.c
utils.o : utils.c defs.h
cc -c utils.c
clean :
rm edit $(objects)

如果有新的 .o 文件加入,只需简单地修改一下 object

让Make自动推导

GNU Make 具有强大的隐式规则功能,可以自动推导文件之间的依赖关系及其对应的构建命令。这样,你无需为每个目标文件明确指定构建命令,Make 可以自动处理这些任务。

隐式规则的工作原理

  1. 自动推导依赖

    • GNU Make 能够自动推导 .o 文件的生成规则。比如,它知道 .o 文件通常由同名的 .c 文件编译生成。
  2. 自动生成命令

    • 当 Make 发现一个 .o 文件时,它会自动将 .c 文件作为依赖,并使用类似 cc -c whatever.c 的命令进行编译。

简化后的 Makefile 示例

使用 GNU Make 的隐式规则,可以简化 Makefile,避免为每个目标文件都写上类似的命令。以下是一个简化后的 Makefile 示例:

makefileCopy code# 定义目标文件
objects = main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o

# 定义最终目标
edit: $(objects)
cc -o edit $(objects)

# 指定头文件依赖(不再需要显式的编译规则)
main.o: defs.h
kbd.o: defs.h command.h
command.o: defs.h command.h
display.o: defs.h buffer.h
insert.o: defs.h buffer.h
search.o: defs.h buffer.h
files.o: defs.h buffer.h command.h
utils.o: defs.h

# 定义伪目标
.PHONY: clean

# 清理规则
clean:
rm -f edit $(objects)

另一种的写法

objects = main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o

edit : $(objects)
cc -o edit $(objects)

$(objects) : defs.h
kbd.o command.o files.o : command.h
display.o insert.o search.o files.o : buffer.h

.PHONY : clean
clean :
rm edit $(objects)

清空目录的规则

每个Makefile中都应该写一个清空目标文件( .o )和可执行文件的规则,这不仅便于重编译,也很利于保持文件的清洁。

.PHONY : clean
clean :
-rm edit $(objects)

包含其它Makefile

在 Makefile 中,include 指令用于将其他 Makefile 的内容包含到当前 Makefile 中。这种机制类似于 C 语言中的 #include 指令。通过 include,你可以将多个 Makefile 文件的内容合并到一个主 Makefile 中,从而实现代码的重用和组织。

include <filenames>...
  • <filenames> 可以是当前操作系统 Shell 的文件模式,包括路径和通配符。

  • include 前面可以有空格,但不能使用 Tab 键开始。

  • include<filenames> 之间可以用一个或多个空格隔开。

假设你有以下 Makefile 文件:a.mkb.mkc.mk,以及一个文件 foo.make,还有一个变量 $(bar),其包含了 bishbash,那么以下语句:

include foo.make *.mk $(bar)

等价于:

include foo.make a.mk b.mk c.mk bish bash

寻找文件的路径

make 在处理 include 指令时,会按照以下顺序查找文件:

  1. 当前目录:首先在当前目录下查找指定的文件。

  2. 指定的目录:如果使用了 -I--include-dir 参数,则会在这些目录下查找。

  3. 默认目录

    • <prefix>/include(如 /usr/local/bin
    • /usr/gnu/include
    • /usr/local/include
    • /usr/include

环境变量 .INCLUDE_DIRS 也会影响 make 查找的目录列表。避免使用 -I 参数覆盖默认目录,以免使 make 忘记已设定的包含目录。

错误处理

  • 如果指定的文件无法找到,make 会发出警告,但不会立即终止执行。

  • make 会继续处理其他文件,直到完成 Makefile 的读取。如果无法找到文件或读取失败,make 才会报致命错误。

  • 要忽略无法读取的文件并继续执行,可以使用减号 -

    -include <filenames>...

    或者为了兼容其他 make 版本,可以使用 sinclude 代替 -include。:

环境变量MAKEFILES

在使用 make 工具时,环境变量 MAKEFILES 可以指定一系列 Makefile 文件,这些文件的内容会在执行 make 时被自动引入。该变量中的值是由空格分隔的多个 Makefile 文件名。这种行为类似于 include 指令。

  1. 自动引入make 会将 MAKEFILES 环境变量中指定的文件内容自动包含进来,就像在 Makefile 中使用 include 指令一样。

  2. 默认目标忽略:从 MAKEFILES 环境变量引入的 Makefile 文件中的“默认目标”不会被执行。

  3. 错误忽略:如果指定的文件存在错误,make 不会中断执行,而是继续处理其他文件或目标。

如果环境变量 MAKEFILES 设置为:

export MAKEFILES="common.mk utilities.mk"

那么 make 在执行时会自动将 common.mkutilities.mk 中的内容包含进来。

  • 全局影响:一旦定义了 MAKEFILES 环境变量,所有的 make 执行都会受到这些文件的影响。这可能导致意外的行为或难以追踪的错误。

  • 调试:如果遇到 Makefile 的异常行为,检查是否定义了 MAKEFILES 环境变量,并确保其内容正确。

  • 避免使用:通常不建议使用 MAKEFILES 环境变量,因为它会影响所有 make 的执行过程。为避免混淆和难以追踪的错误,最好在 Makefile 中显式使用 include 指令来包含其他 Makefile 文件。

  • 检查:在调试 Makefile 问题时,检查环境变量以确认是否有定义 MAKEFILES,以帮助识别潜在的问题来源。

Makefile文件名

默认情况下,make 命令会按以下顺序在当前目录下寻找 Makefile 文件:

  1. GNUmakefile

  2. makefile

  3. Makefile

建议使用 Makefile 作为文件名,因为它在排序上靠近其他重要文件(如 README)。GNUmakefile 文件名仅被 GNU make 支持,其他版本的 make 可能不识别。

你也可以使用其他文件名,如 Make.SolarisMake.Linux。要指定特定的 Makefile,可以使用 -f--file 参数

make -f Make.Solaris
make --file Make.Linux

可以使用多个 -f--file 参数来指定多个 Makefile。

书写规则

语法

target ... : prerequisites ...
recipe
...
...
  • target

    可以是一个object file(目标文件),也可以是一个可执行文件,还可以是一个标签(label)。

  • prerequisites

    生成该target所依赖的文件和/或target。

  • recipe

    该target要执行的命令(任意的shell命令)。

通配符

make支持三个通配符: *?~ 。这是和Unix的B-Shell是相同的。

波浪号( ~ )字符在文件名中也有比较特殊的用途。如果是 ~/test ,这就表示当前用户的 $HOME 目录下的test目录。而 ~hchen/test 则表示用户hchen的宿主目录下的test 目录。

  1. * (星号): 匹配任意数量的字符
    删除所有以 .o 结尾的文件

    clean:
    rm -f *.o
  2. ? (问号): 匹配单个字符
    匹配所有文件名为单个字符后加 .c 的文件

    compile:
    gcc -o output ?.c
  3. ~ (波浪号): 表示当前用户的 $HOME 目录
    进入用户主目录下的 test 目录

    gohome:
    cd ~/test

文件搜寻

Makefile中可以通过VPATHvpath关键字设置文件搜索路径:

  1. VPATH: 指定全局搜索目录,多个目录用冒号分隔。

    “src”和“…/headers”,make会按照这个顺序进行搜索

    VPATH = src:../headers
  2. vpath: 更灵活,支持为不同类型的文件指定不同的搜索目录

    使用方法有三种:

    • vpath <pattern> <directories>

      为符合模式的文件指定搜索目录

    • vpath <pattern>

      清除符合模式的文件的搜索目录。

    • vpath

      清除所有已被设置好了的文件搜索目录。

    vpath使用方法中的需要包含 % 字符。 % 的意思是匹配零或若干字符,(需引用 % ,使用 \ )例如, %.h 表示所有以 .h 结尾的文件

    要求make在“…/headers”目录下搜索所有以 .h 结尾的文件

    vpath %.h ../headers
    vpath %.c src

伪目标

  • 伪目标只是一个标签,通常用于Makefile中没有依赖文件的目标。伪目标不会生成实际的文件,只有在用make命令显式调用时才会执行。

  • 例如,clean是一个常见的伪目标,用于清理生成的文件。因为伪目标本身不生成文件,所以当目录下有与伪目标同名的文件时,执行make命令会出现错误。这时,我们可以使用伪目标来避免冲突。

SRC = $(wildcard *.c)
OBJ = $(patsubst %.c, %.o, $(SRC))

ALL: hello.out

hello.out: $(OBJ)
gcc $< -o $@

$(OBJ): $(SRC)
gcc -c $< -o $@

clean:
rm -rf $(OBJ) hello.out

.PHONY: clean ALL

通常也会把ALL设置成伪目标

多目标

Makefile的规则中的目标可以不止一个,其支持多目标

bigoutput littleoutput : text.g
generate text.g -$(subst output,,$@) > $@

上述规则等价于:

bigoutput : text.g
generate text.g -big > bigoutput
littleoutput : text.g
generate text.g -little > littleoutput

其中, -$(subst output,,$@) 中的 $ 表示执行一个Makefile的函数,函数名为subst,后面的为参数。关于函数,将在后面讲述。这里的这个函数是替换字符串的意思, $@ 表示目标的集合,就像一个数组, $@ 依次取出目标,并执于命令。

静态模式

静态模式规则是Makefile中的一个强大特性,用于简化多目标文件的规则定义。它使得我们能够为一组目标文件定义共同的编译规则,从而避免重复书写相似的规则。

<targets ...> : <target-pattern> : <prereq-patterns ...>
<commands>
...
  • <targets ...>: 定义了一系列目标文件,可以使用通配符来匹配多个目标文件。

  • <target-pattern>: 目标模式,指定目标文件的名称模式。通常使用 % 表示通配符。

  • <prereq-patterns ...>: 依赖模式,指定依赖文件的名称模式。通常使用 % 来与目标模式进行匹配。

  • <commands>: 执行的命令,用于生成目标文件。

示例一

objects = foo.o bar.o

all: $(objects)

$(objects): %.o: %.c
$(CC) -c $(CFLAGS) $< -o $@

# 扩展后的规则等价于:
foo.o : foo.c
$(CC) -c $(CFLAGS) foo.c -o foo.o
bar.o : bar.c
$(CC) -c $(CFLAGS) bar.c -o bar.o

示例二

files = foo.elc bar.o lose.o

$(filter %.o,$(files)): %.o: %.c
$(CC) -c $(CFLAGS) $< -o $@
$(filter %.elc,$(files)): %.elc: %.el
emacs -f batch-byte-compile $<
  1. $(filter %.o,$(files)):

    • filter 函数过滤 files 列表,得到 .o 文件(bar.olose.o)。
    • 对这些 .o 文件应用规则 %.o: %.c,即每个 .o 文件由相应的 .c 文件生成。
  2. $(filter %.elc,$(files)):

    • filter 函数过滤 files 列表,得到 .elc 文件(foo.elc)。
    • 对这些 .elc 文件应用规则 %.elc: %.el,即每个 .elc 文件由相应的 .el 文件生成。

扩展后的规则等价于:

bar.o : bar.c
$(CC) -c $(CFLAGS) bar.c -o bar.o
lose.o : lose.c
$(CC) -c $(CFLAGS) lose.c -o lose.o
foo.elc : foo.el
emacs -f batch-byte-compile foo.el

自动生成依赖性

在大型工程中,手动维护头文件的依赖关系是一项繁琐且容易出错的工作。为了简化这一过程,我们可以利用编译器的自动依赖生成功能。

大多数 C/C++ 编译器都支持 -M 选项来自动找寻源文件中包含的头文件并生成依赖关系。例如:

gcc -MM main.c

输出示例:

main.o: main.c defs.h

对于 GNU 编译器,-MM 参数会忽略标准库头文件,仅生成用户头文件的依赖关系。

为了将这些自动生成的依赖关系集成到 Makefile 中,我们可以创建一个模式规则来生成 .d 文件。这些 .d 文件将存储每个源文件的依赖关系。例如:

%.d: %.c
@set -e; rm -f $@; \
$(CC) -M $(CPPFLAGS) $< > $@.$$$$; \
sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \
rm -f $@.$$$$

这个规则的工作流程如下:

  1. 删除旧的 .d 文件。

  2. 使用编译器生成新的依赖文件。

  3. 使用 sed 命令将生成的依赖关系格式化成 .d 文件的格式。

  4. 删除临时文件。

.d 文件集成到主 Makefile 中,可以通过 include 指令引入这些文件:

sources = foo.c bar.c

include $(sources:.c=.d)

这里,$(sources:.c=.d) 会将 foo.cbar.c 替换为 foo.dbar.d。这样,主 Makefile 会自动包含每个 .d 文件,确保所有依赖关系得到更新。

使用变量

在Makefile中,变量类似于C/C中的宏,用于代表文本字符串。在执行Makefile时,这些变量会自动展开。与C/C不同的是,你可以在Makefile中修改变量的值。变量可以在“目标”、“依赖目标”、“命令”或Makefile的其他部分中使用。

变量的命名

  • 可以包含字符、数字和下划线(下划线可以在数字开头),但不应包含 :#= 或空字符(空格、回车等)。

  • 变量名是大小写敏感的。例如,fooFooFOO 是不同的变量名。

  • 推荐使用大小写搭配的变量名,如 MakeFlags,以避免与系统变量冲突。

变量的基础

定义和使用变量

  • 定义变量:

    variable = value
  • 使用变量:

    $(variable)

    ${variable}
  • 如果需要在变量中使用 $ 字符,需用 $$ 表示。

变量中的变量

使用其他变量定义变量

  • 简单使用 =

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

    all:
    echo $(foo)
  • 使用 := 进行即时展开:

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

递归定义和即时展开

递归定义(不推荐)

CFLAGS = $(CFLAGS) -O

即时展开(推荐)

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

其他变量操作

定义空格变量

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

条件赋值

FOO ?= bar

高级用法

变量值替换

foo := a.o b.o c.o
bar := $(foo:.o=.c)

模式替换

foo := a.o b.o c.o
bar := $(foo:%.o=%.c)

变量的值再当作变量

x = y
y = z
a := $($(x))

复杂示例

x = variable1
variable2 := Hello
y = $(subst 1,2,$(x))
z = y
a := $($($(z)))

追加变量值

使用 += 追加值

objects = main.o foo.o bar.o utils.o
objects += another.o

override 指令

覆盖命令行或环境变量

override variable := value

多行变量

使用 define 定义多行变量

define two-lines
echo foo
echo $(bar)
endef

环境变量

使用系统环境变量

  • 如果定义了系统环境变量,则在Makefile中也可以使用。

  • 使用 -e 参数可以让环境变量覆盖Makefile中的变量。

目标变量

定义目标特定变量

prog : CFLAGS = -g
prog : prog.o foo.o bar.o
$(CC) $(CFLAGS) prog.o foo.o bar.o

模式变量

定义模式特定变量

%.o : CFLAGS = -O

使用条件判断

Makefile 中,条件判断允许根据运行时的不同情况选择不同的执行分支。这是实现更复杂的构建逻辑时非常有用的功能。以下是有关如何在 Makefile 中使用条件判断的说明。

以下示例演示了如何根据 $(CC) 变量的值来选择不同的编译选项:

libs_for_gcc = -lgnu
normal_libs =

foo: $(objects)
ifeq ($(CC),gcc)
$(CC) -o foo $(objects) $(libs_for_gcc)
else
$(CC) -o foo $(objects) $(normal_libs)
endif

在这个例子中,如果 $(CC) 的值是 gcc,则会将 $(libs_for_gcc) 作为库文件来编译目标 foo。否则,会使用 $(normal_libs) 作为库文件。

语法

条件表达式的语法如下:

<conditional-directive>
<text-if-true>
endif

或者:

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

其中 <conditional-directive> 是条件关键字。主要有以下四种:

  1. ifeq: 比较两个参数的值是否相同。

    ifeq (<arg1>, <arg2>)
    ifeq '<arg1>' '<arg2>'
    ifeq "<arg1>" "<arg2>"
    ifeq "<arg1>" '<arg2>'
    ifeq '<arg1>' "<arg2>"

    例如:

    ifeq ($(strip $(foo)),)
    <text-if-empty>
    endif
  2. ifneq: 比较两个参数的值是否不同。

    ifneq (<arg1>, <arg2>)
    ifneq '<arg1>' '<arg2>'
    ifneq "<arg1>" "<arg2>"
    ifneq "<arg1>" '<arg2>'
    ifneq '<arg1>' "<arg2>"
  3. ifdef: 检查一个变量是否被定义且值非空。

    ifdef <variable-name>

    例如:

    bar =
    foo = $(bar)
    ifdef foo
    frobozz = yes
    else
    frobozz = no
    endif
  4. ifndef: 检查一个变量是否未定义或值为空。

    ifndef <variable-name>

注意事项

  • <conditional-directive> 行上,多余的空格是允许的,但不能以 Tab 键开始。

  • 注释符 # 也是安全的。

  • make 在读取 Makefile 时计算条件表达式的值,所以最好不要将自动化变量(如 $@)放入条件表达式中,因为这些变量在运行时才会被定义。

  • make 不允许将条件语句分成两个部分放在不同的文件中。

Makefile 中使用函数可以让你的构建规则更加灵活和智能。make 支持一些基本的函数,用于处理变量和字符串。以下是 make 中的主要函数及其用法的详细整理:

使用函数

函数调用语法

函数调用的基本语法是:

$<function>(<arguments>)

${<function>(<arguments>)}
  • <function> 是函数名

  • <arguments> 是函数参数,参数间以逗号分隔

字符串处理函数

  • subst: 字符串替换

    $(subst <from>,<to>,<text>)

    替换 <text> 中的 <from><to>。例如:

    $(subst ee,EE,feet on the street)  # 结果: fEEt on the strEEt
  • patsubst: 模式匹配替换

    $(patsubst <pattern>,<replacement>,<text>)

    按模式 <pattern> 替换 <text> 中的部分。示例:

    $(patsubst %.c,%.o,x.c.c bar.c)  # 结果: x.c.o bar.o
  • strip: 去除首尾空格

    $(strip <string>)

    去除 <string> 开头和结尾的空格。示例:

    $(strip a b c )  # 结果: a b c
  • findstring: 查找子串

    $(findstring <find>,<in>)

    查找 <in> 中是否存在 <find>。示例:

    $(findstring a,a b c)  # 结果: a
    $(findstring a,b c) # 结果: (空)
  • filter: 按模式过滤

    $(filter <pattern...>,<text>)

    过滤 <text> 中符合 <pattern> 的单词。示例:

    sources := foo.c bar.c baz.s ugh.h
    $(filter %.c %.s,$(sources)) # 结果: foo.c bar.c baz.s
  • filter-out: 反过滤

    $(filter-out <pattern...>,<text>)

    去除 <text> 中符合 <pattern> 的单词。示例:

    objects=main1.o foo.o main2.o bar.o
    mains=main1.o main2.o
    $(filter-out $(mains),$(objects)) # 结果: foo.o bar.o
  • sort: 排序

    $(sort <list>)

    <list> 中的单词进行升序排序。示例:

    $(sort foo bar lose)  # 结果: bar foo lose
  • word: 取单词

    $(word <n>,<text>)

    获取 <text> 中第 <n> 个单词。示例:

    $(word 2, foo bar baz)  # 结果: bar
  • wordlist: 取单词串

    $(wordlist <ss>,<e>,<text>)

    <text> 中从第 <ss> 到第 <e> 个单词。示例:

    $(wordlist 2, 3, foo bar baz)  # 结果: bar baz
  • words: 单词个数

    $(words <text>)

    统计 <text> 中的单词数量。示例:

    $(words foo bar baz)  # 结果: 3
  • firstword: 取首单词

    $(firstword <text>)

    <text> 中的第一个单词。示例:

    $(firstword foo bar)  # 结果: foo

文件名操作函数

  • dir: 取目录

    $(dir <names...>)

    提取 <names> 中的目录部分。示例:

    $(dir src/foo.c hacks)  # 结果: src/ ./
  • notdir: 取文件名

    $(notdir <names...>)

    提取 <names> 中的文件名部分。示例:

    $(notdir src/foo.c hacks)  # 结果: foo.c hacks
  • suffix: 取后缀

    $(suffix <names...>)

    提取 <names> 中的文件后缀。示例:

    $(suffix src/foo.c src-1.0/bar.c hacks)  # 结果: .c .c
  • basename: 取前缀

    $(basename <names...>)

    提取 <names> 中的文件前缀。示例:

    $(basename src/foo.c src-1.0/bar.c hacks)  # 结果: src/foo src-1.0/bar hacks
  • addsuffix: 添加后缀

    $(addsuffix <suffix>,<names...>)

    <names> 中的每个文件名添加 <suffix>。示例:

    $(addsuffix .c,foo bar)  # 结果: foo.c bar.c
  • addprefix: 添加前缀

    $(addprefix <prefix>,<names...>)

    <names> 中的每个文件名添加 <prefix>。示例:

    $(addprefix src/,foo bar)  # 结果: src/foo src/bar
  • join: 连接

    $(join <list1>,<list2>)

    <list2> 中的单词连接到 <list1> 的每个单词后面。示例:

    $(join aaa bbb , 111 222 333)  # 结果: aaa111 bbb222 333

控制 make的函数

  • error: 报错并终止

    $(error <text ...>)

    输出错误信息并停止执行。示例:

    $(error An error occurred!)  # 输出错误并终止
  • warning: 警告

    $(warning <text ...>)

    输出警告信息但不终止执行。示例:

    $(warning This is a warning!)  # 输出警告

其他函数

  • call: 创建参数化函数

    $(call <expression>,<parm1>,<parm2>,...,<parmn>)

    使用 <expression> 和参数 <parm1>, <parm2>, … 创建新的字符串。示例:

    reverse = $(2) $(1)
    foo = $(call reverse,a,b) # 结果: b a
  • origin: 获取变量来源

    $(origin <variable>)

    返回 <variable> 的来源类型。示例:

    $(origin CC)  # 结果: default
  • shell: 执行 Shell 命令

    $(shell <command>)

    执行 <command> 并返回其输出。示例:

    $(shell ls)  # 执行 'ls' 命令并返回结果

make的运行

make的退出码

  • 0: 成功执行

  • 1: 执行过程中出现错误

  • 2: 使用了 -q 选项且某些目标不需要更新

指定 Makefile

默认情况下,make 会查找当前目录下的 GNUmakefilemakefileMakefile。你可以使用 -f--file 参数来指定其他 Makefile。例如:

make -f hchen.mk

如果指定多个 -f 参数,make 会按顺序读取所有指定的 Makefile。

指定目标

  • 默认情况下,make 执行第一个目标。你可以指定其他目标,例如:

    make clean
  • MAKECMDGOALS 环境变量包含指定的终极目标列表,如果没有指定目标,该变量为空。

检查规则

  • -n, --just-print, --dry-run, --recon: 不执行命令,只打印出命令序列。

  • -t, --touch: 更新目标文件的时间戳,但不真正编译。

  • -q, --question: 检查目标是否需要更新,不执行命令。

  • -W <file>, --what-if=<file>, --assume-new=<file>, --new-file=<file>: 假定指定的文件需要更新,常与 -n 参数结合使用。

make的常用参数

  • -b, -m: 忽略与其他版本 make 的兼容性。

  • -B, --always-make: 认为所有目标都需要更新。

  • -C <dir>, --directory=<dir>: 指定 Makefile 所在目录。

  • -d: 输出调试信息,相当于 --debug=a

  • -e, --environment-overrides: 环境变量值覆盖 Makefile 中的变量值。

  • -f <file>, --file=<file>, --makefile=<file>: 指定需要执行的 Makefile。

  • -i, --ignore-errors: 执行时忽略所有错误。

  • -I <dir>, --include-dir=<dir>: 指定包含 Makefile 的搜索目录。

  • -j [<jobsnum>], --jobs[=<jobsnum>]: 指定同时运行的命令数量。

  • -k, --keep-going: 出错时继续执行。

  • -l <load>, --load-average[=<load>]: 指定命令运行的负载。

  • -p, --print-data-base: 输出 Makefile 中的所有数据。

  • -r, --no-builtin-rules: 禁止使用隐含规则。

  • -s, --silent, --quiet: 运行时不输出命令。

  • -t, --touch: 使目标文件的时间戳更新。

  • -v, --version: 输出 make 程序的版本信息。

  • -w, --print-directory: 输出运行 Makefile 之前和之后的目录信息。

  • --warn-undefined-variables: 输出未定义变量的警告信息。

隐含规则

隐含规则概述

隐含规则是 make 提供的一种机制,允许自动推导目标文件的生成规则,而无需显式地在 Makefile 中定义这些规则。它们是 make 内部预设的一些规则,用于处理常见的编译和构建任务。理解隐含规则可以帮助你简化 Makefile,避免重复的规则定义。

隐含规则的基本使用

当你在 Makefile 中只定义了目标和最终的依赖目标而没有明确列出如何生成依赖目标的规则时,make 会自动根据隐含规则来生成这些依赖目标。例如,在下述 Makefile 中:

foo: foo.o bar.o
cc -o foo foo.o bar.o $(CFLAGS) $(LDFLAGS)

在这个例子中,Makefile 中没有定义 foo.obar.o 的生成规则。make 会自动推导出这些目标的规则,通常是通过隐含规则将 .c 文件编译为 .o 文件。

隐含规则的默认行为

make 内部有一套默认的隐含规则,这些规则处理各种文件后缀的编译和转换。例如:

  • 编译 C 程序<n>.o 的目标依赖于 <n>.c 文件,使用 $(CC) -c $(CPPFLAGS) $(CFLAGS) 命令生成目标文件。

  • 编译 C++ 程序<n>.o 的目标依赖于 <n>.cc<n>.cpp<n>.C 文件,使用 $(CXX) -c $(CPPFLAGS) $(CXXFLAGS) 命令生成目标文件。

  • 编译 Fortran 程序<n>.o 的目标依赖于 <n>.f<n>.F<n>.r 文件,使用相应的 Fortran 编译器命令生成目标文件。

这些规则可以通过 -r--no-builtin-rules 选项被禁用,但某些规则可能仍会根据后缀列表(如 .o.c)生效。

使用模式规则定义隐含规则

你可以自定义隐含规则,使用模式规则定义规则的模式,其中 表示匹配文件名的一部分。例如:

%.o: %.c
$(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@

这个规则的意义是,任何 .o 文件的生成都依赖于同名的 .c 文件,并且会使用指定的编译命令生成目标文件。$@ 表示目标文件,$< 表示第一个依赖文件。

自动化变量

自动化变量用于在规则中表示特定的文件名和文件集合。这些变量可以帮助你编写通用的规则,处理不同的目标和依赖文件。常见的自动化变量包括:

  • $@:表示规则中的目标文件。

  • $<:表示第一个依赖文件。

  • $^:表示所有的依赖文件(去重)。

  • $*:表示目标文件名中模式中 及其之前的部分部分。

隐含规则链

隐含规则链指的是一个目标的生成可能依赖于多个隐含规则的组合。例如,make 可能会先使用 Yacc 隐含规则将 .y 文件转换为 .c 文件,然后再使用 C 编译的隐含规则将 .c 文件编译为 .o 文件。

伪目标和特殊规则

你可以使用伪目标来控制 make 的行为,例如:

  • .INTERMEDIATE:声明中间目标,make 在生成最终目标后会删除这些中间文件。

  • .SECONDARY:声明中间目标,make 不会自动删除这些文件。

  • .PRECIOUS:声明被隐含规则生成的中间文件不会被删除。

这些伪目标帮助你控制文件的生成和删除行为,以适应不同的构建需求。

通过理解和利用隐含规则、模式规则及自动化变量,你可以使 Makefile 更加简洁和高效,同时减少手动维护规则的复杂性。

使用make更新函数库文件

  1. 函数库文件(归档文件)

    • 由多个对象文件(.o 文件)组成。
    • 使用 ar 命令打包,例如 ar cr libfoo.a foo.o
  2. 函数库文件的成员定义

    • 可以使用 archive(member) 的形式指定成员。
    • 多个成员用空格分开,或使用通配符来定义,如 foolib(*.o)
  3. 隐含规则

    • make 会将 a(m) 形式的目标变成 m,然后寻找对应的规则。
    • 例如,make foo.a(bar.o) 会尝试生成 bar.o,如果没有规则,make 会使用内建规则来编译 bar.c 生成 bar.o
  4. 自动化变量

    • $% 是函数库文件的自动化变量。
  5. 后缀规则

    • 使用 .c.a 后缀规则来生成归档文件。
    • 后缀规则的等效形式也可以使用隐含规则。
  6. 注意事项

    • 并行执行(-j 参数)时要小心,因为多个 ar 命令可能会损坏归档文件。

Reference

[1] 跟我一起写Makefile:https://seisman.github.io/how-to-write-makefile/index.html

------------------------------- 本文结束啦❤感谢您阅读-------------------------------
赞赏一杯咖啡