Makefile

197 阅读18分钟

1. 关于程序的编译和链接

一般来说,无论是C还是C++,首先要把源文件编译成中间代码文件,在Windows下也就是 .obj 文件,UNIX下是 .o 文件,即Object File,这个动作叫做编译(compile)。然后再把大量的Object File合成可执行文件,这个动作叫作链接(link)

源文件首先会生成中间目标文件,再由中间目标文件生成可执行文件。在编译时,编译器只检测程序语法和函数、变量是否被声明。如果函数未被声明,编译器会给出一个警告,但可以生成Object File。而在链接程序时,链接器会在所有的Object File中找寻函数的实现,如果找不到,那到就会报链接错误码(Linker Error),在VC下,这种错误一般是: Link 2001错误 ,意思说是说,链接器未能找到函数的实现。你需要指定函数的Object File。

2. makefile介绍

首先,我们用一个示例来说明makefile的书写规则,以便给大家一个感性认识。这个示例来源于gnu 的make使用手册,在这个示例中,我们的工程有8个c文件,和3个头文件,我们要写一个makefile来告诉make命令如何编译和链接这几个文件。我们的规则是:

  1. 如果这个工程没有编译过,那么我们的所有c文件都要编译并被链接。
  2. 如果这个工程的某几个c文件被修改,那么我们只编译被修改的c文件,并链接目标程序。
  3. 如果这个工程的头文件被改变了,那么我们需要编译引用了这几个头文件的c文件,并链接目标程序。

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

Makefile 和 makefile

在使用 make 工具时,Makefilemakefile 本质上没有区别,它们都是 make 工具用来定义构建规则的文件。唯一的区别在于文件的名称格式,即大小写不同:

  • Makefile(首字母大写):是传统的、推荐的文件名称,最常见且优先级更高。
  • makefile(全小写):也可以使用,但在风格上不如 Makefile 常用。

工作机制上的区别

  1. 优先级:当在同一目录下存在 Makefilemakefile 时,make 默认会先寻找 Makefile(首字母大写)。如果找不到 Makefile,才会使用 makefile
  2. 风格惯例Makefile 是大小写混合的命名方式,在各个平台上都更常见,符合 Unix 的传统。而 makefile(全小写)在部分项目中也被使用,但并非标准约定。
  3. 便于区分:使用 Makefile 可以让文件在项目中更显眼(尤其在文件较多的情况下),便于识别它是用于 make 构建的文件。

兼容性

在大多数操作系统上,Makefilemakefile 都可以被 make 工具识别,且没有功能性差异。唯一的注意点是:

  • 在大小写敏感的系统(如 Linux 和 macOS)上,Makefilemakefile 是两个不同的文件,而在不区分大小写的文件系统(如 Windows 文件系统)中,它们会被视为同一个文件。

推荐使用方式

为了保持文件的风格统一、清晰明确,通常建议使用 Makefile(首字母大写),这是更常见的命名方式,也是 Unix 系统上默认的约定。

自定义文件名

如果你希望使用其他名字(比如 MyMakefile),可以通过 make -f 选项来指定文件名,例如:

make -f MyMakefile

总结

  • Makefile 是约定俗成的标准,通常优先使用。
  • makefile 可以使用,但不如 Makefile 常见。
  • 两者在功能上没有区别,只有文件名大小写不同。

makefile的规则

在讲述这个makefile之前,还是让我们先来粗略地看一看makefile的规则。

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

    可以是一个object file(目标文件),也可以是一个可执行文件,还可以是一个标签(label)。对于标签这种特性,在后续的“伪目标”章节中会有叙述。

  • prerequisites

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

  • recipe

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

这是一个文件的依赖关系,也就是说,target这一个或多个的目标文件依赖于prerequisites中的文件,其生成规则定义在command中。说白一点就是说:

prerequisites中如果有一个以上的文件比target文件要新的话,recipe所定义的命令就会被执行。

这就是makefile的规则,也就是makefile中最核心的内容。


学习 Makefile 的基本用法,我们可以创建一个简单的项目,包含多个头文件和源文件,通过一个 Makefile 来管理编译流程。以下是一个示例工程及其 Makefile 的配置。

假设我们的项目目录结构如下:

my_project/
├── Makefile
├── main.c
├── add.c
├── sub.c
├── mul.c
├── div.c
└── math.h

文件说明

  1. main.c:主文件,包含 main 函数,调用 add, sub, mul, div 函数。
  2. add.csub.cmul.cdiv.c:各自实现加法、减法、乘法和除法函数。
  3. math.h:声明所有数学运算函数的头文件。

示例代码

  1. math.h(声明函数)

    // math.h
    #ifndef MATH_H
    #define MATH_Hint add(int a, int b);
    int sub(int a, int b);
    int mul(int a, int b);
    int divide(int a, int b);
    ​
    #endif
    
  2. main.c(主程序)

    // main.c
    #include <stdio.h>
    #include "math.h"int main() {
        int a = 10, b = 5;
        printf("Add: %d\n", add(a, b));
        printf("Sub: %d\n", sub(a, b));
        printf("Mul: %d\n", mul(a, b));
        printf("Div: %d\n", divide(a, b));
        return 0;
    }
    
  3. add.c(加法实现)

    // add.c
    #include "math.h"int add(int a, int b) {
        return a + b;
    }
    
  4. sub.c(减法实现)

    // sub.c
    #include "math.h"int sub(int a, int b) {
        return a - b;
    }
    
  5. mul.c(乘法实现)

    // mul.c
    #include "math.h"int mul(int a, int b) {
        return a * b;
    }
    
  6. div.c(除法实现)

    // div.c
    #include "math.h"int divide(int a, int b) {
        if (b != 0) return a / b;
        return 0;
    }
    

创建 Makefile

my_project 目录下创建 Makefile 文件,内容如下:

# Target file name
TARGET = calculator
​
# Source files and object files
SRC = main.c add.c sub.c mul.c div.c
OBJ = main.o add.o sub.o mul.o div.o
​
# Compiler
CC = gcc
CFLAGS = -Wall
​
# Generate target files
$(TARGET): $(OBJ)
    $(CC) -o $(TARGET) $(OBJ)# Generate object files with dependencies
main.o: main.c math.h
    $(CC) $(CFLAGS) -c main.c
add.o: add.c math.h
    $(CC) $(CFLAGS) -c add.c
sub.o: sub.c math.h
    $(CC) $(CFLAGS) -c sub.c
mul.o: mul.c math.h
    $(CC) $(CFLAGS) -c mul.c
div.o: div.c math.h
    $(CC) $(CFLAGS) -c div.c
​
# Clean command to remove the executable and object files
clean:
    rm -f $(TARGET) $(OBJ)

优点

  1. 清晰的依赖关系:每个对象文件 .o 都明确列出了自己的依赖文件(.c.h 文件),便于管理不同 .o 文件的依赖关系。
  2. 便于维护复杂依赖:如果不同的 .c 文件依赖不同的头文件,这种写法会更加清晰,不易出错。

缺点

  1. 冗长:当项目文件增多时,Makefile 会显得冗长,容易增加维护成本。
  2. 修改不便:如果需要修改编译选项或依赖文件,可能需要在多个位置进行调整。

Makefile 说明

  • TARGET:定义生成的可执行文件名为 calculator
  • SRC:定义所有的 .c 源文件。
  • OBJ:定义所有的 .o 中间文件。
  • DEPS:定义依赖的头文件 math.h
  • $(TARGET): $(OBJ):指示 TARGET 文件依赖于所有中间文件 $(OBJ)
  • 每个 .o 文件的生成规则中定义了 .c 文件与 math.h 的依赖关系。
  • clean:定义清理命令,删除所有的中间文件和目标文件。

编译和执行

my_project 目录下执行以下命令:

  1. 编译项目

    make
    

    生成的 calculator 可执行文件将包含所有数学运算功能。

  2. 运行可执行文件

    ./calculator
    
  3. 清理项目

    make clean
    

    这会删除 calculator 和所有 .o 文件。


新版本风格

既然我们的make可以自动推导命令,那么我看到那堆 .o.h 的依赖就有点不爽,那么多的重复的 .h ,能不能把其收拢起来,好吧,没有问题,这个对于make来说很容易,谁叫它提供了自动推导命令和文件的功能呢?来看看最新风格的makefile吧。

这种风格能让我们的makefile变得很短,但我们的文件依赖关系就显得有点凌乱了。鱼和熊掌不可兼得。还看你的喜好了。我是不喜欢这种风格的,一是文件的依赖关系看不清楚,二是如果文件一多,要加入几个新的 .o 文件,那就理不清楚了。

# Target file name
TARGET = calculator
​
# Object files
objects = main.o add.o sub.o mul.o div.o
​
# Compiler and flags
CC = gcc
CFLAGS = -Wall
​
# Rule to build the target executable
$(TARGET): $(objects)
    $(CC) $(CFLAGS) -o $(TARGET) $(objects)# Dependency rules (if all object files depend on math.h)
$(objects): math.h
​
# .PHONY target for cleaning up
.PHONY: clean
clean:
    -rm $(TARGET) $(objects)

优点

  1. 简洁:对象文件依赖和规则写得更简洁,适合依赖关系相对简单的项目。
  2. 便于扩展:增加新的 .c 文件时,只需添加到 objects 列表中,且不会造成 Makefile 代码量急剧增加。

缺点

  1. 依赖关系不明确$(objects): math.h 表示所有 .o 文件都依赖于 math.h,当依赖关系复杂时(如不同 .c 文件依赖不同的 .h 文件),这种写法容易导致不准确的依赖关系。
  2. 难以跟踪复杂项目:在较大的项目中,管理和查看每个 .o 文件的具体依赖项会比较困难。

这种风格能让我们的makefile变得很短,但我们的文件依赖关系就显得有点凌乱了。鱼和熊掌不可兼得。还看你的喜好了。我是不喜欢这种风格的,一是文件的依赖关系看不清楚,二是如果文件一多,要加入几个新的 .o 文件,那就理不清楚了。


清空目录的规则clean

每个Makefile中都应该写一个清空目标文件( .o )和可执行文件的规则,这不仅便于重编译,也很利于保持文件的清洁。这是一个“修养”(呵呵,还记得我的《编程修养》吗)。一般的风格都是:

clean:
    rm edit $(objects)

更为稳健的做法是:

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

前面说过, .PHONY 表示 clean 是一个“伪目标”。而在 rm 命令前面加了一个小减号的意思就是,也许某些文件出现问题,但不要管,继续做后面的事。当然, clean 的规则不要放在文件的开头,不然,这就会变成make的默认目标,相信谁也不愿意这样。不成文的规矩是——“clean从来都是放在文件的最后”。

上面就是一个makefile的概貌,也是makefile的基础,下面还有很多makefile的相关细节,准备好了吗?准备好了就来。


包含其他Makefile

Makefile 中,你可以使用 include 关键字来包含其他 Makefile 文件。这在大型项目中非常有用,因为它允许你将构建逻辑分成多个文件,更加模块化和易于管理。例如,你可以将通用配置、特定模块的编译规则、依赖关系等分开到不同的 Makefile 文件中。

使用 include 关键字

语法非常简单,只需在 Makefile 中使用 include 关键字,指定要包含的文件路径即可:

include path/to/other_makefile.mk

例如:

include common.mk
include moduleA.mk
include moduleB.mk

示例:分割 Makefile 文件

假设你有一个项目结构如下:

project/
├── Makefile
├── common.mk
├── moduleA.mk
├── moduleB.mk
├── src/
│   ├── main.c
│   ├── add.c
│   ├── sub.c
│   └── math.h
  • Makefile:主 Makefile,用于包含其他 Makefile 文件并定义整体构建逻辑。
  • common.mk:包含通用的设置,比如编译器、编译选项等。
  • moduleA.mkmoduleB.mk:定义各自模块的文件和规则。
  1. Makefile(主 Makefile
# Main Makefileinclude common.mk
include moduleA.mk
include moduleB.mk
​
# Define the final target
TARGET = calculator
​
# Build rule for the final target
$(TARGET): $(OBJ)
    $(CC) $(CFLAGS) -o $(TARGET) $(OBJ)# Clean rule
.PHONY: clean
clean:
    rm -f $(TARGET) $(OBJ)
  1. common.mk(通用设置)
# Common settings# Compiler and flags
CC = gcc
CFLAGS = -Wall
​
# Source and object files
SRC = src/main.c src/add.c src/sub.c
OBJ = main.o add.o sub.o
​
# Dependency rule for math.h
$(OBJ): src/math.h
  1. moduleA.mk(模块A的规则)
makefile复制代码# Module A rulesmain.o: src/main.c src/math.h
    $(CC) $(CFLAGS) -c src/main.c -o main.o
  1. moduleB.mk(模块B的规则)
# Module B rules
​
add.o: src/add.c src/math.h
    $(CC) $(CFLAGS) -c src/add.c -o add.o
​
sub.o: src/sub.c src/math.h
    $(CC) $(CFLAGS) -c src/sub.c -o sub.o

工作原理

  1. Makefile 主文件包含 common.mkmoduleA.mkmoduleB.mk
  2. common.mk 定义了通用的变量,比如 CCCFLAGS 和依赖头文件。
  3. moduleA.mkmoduleB.mk 定义了具体的编译规则,例如如何编译 main.oadd.osub.o
  4. 运行 make 时,Makefile 会加载所有包含的文件,整合成一个完整的 Makefile,然后执行构建过程。

注意事项

  • 文件顺序include 的文件顺序可能会影响构建,尤其是当多个文件修改相同变量时,后包含的文件会覆盖先前定义的值。

  • 文件路径:可以使用相对路径或绝对路径来包含 Makefile 文件。

  • 错误处理:如果 make 找不到某个包含的文件,会报错。可以用 -include(或 sinclude)来忽略错误:

    -include optional.mk
    

    这样,如果 optional.mk 不存在,make 会继续执行,不会报错。

通过这种方式,你可以将 Makefile 文件模块化,便于维护和扩展。

.mk 文件本质上也是 Makefile,只是文件扩展名不同。使用 .mk 扩展名是为了便于组织和管理项目中的多个 Makefile 文件,尤其是在大型项目中。

为什么使用 .mk 扩展名?

  1. 模块化管理 在大型项目中,通常会将 Makefile 分解为多个模块化的文件,以便维护和组织。例如,可以将通用设置放在 common.mk 中,把不同模块的编译规则分别放在 moduleA.mkmoduleB.mk 中。
  2. 方便包含 使用 .mk 扩展名可以清楚地表明这些文件是被主 Makefile 包含的模块文件,而不是直接执行的 Makefile。这有助于区分主 Makefile 与辅助 Makefile 文件。
  3. 避免混淆 保留主 Makefile 文件的标准名称(如 Makefilemakefile),并将包含文件命名为 .mk,可以避免在项目中出现多个文件同名为 Makefile 的情况,从而避免混淆。

make的工作方式

  1. 读入所有的Makefile。
  2. 读入被include的其它Makefile。
  3. 初始化文件中的变量。
  4. 推导隐式规则,并分析所有规则。
  5. 为所有的目标文件创建依赖关系链。
  6. 根据依赖关系,决定哪些目标要重新生成。
  7. 执行生成命令。

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

3. 书写规则

规则举例

foo.o: foo.c defs.h       # foo模块
    cc -c -g foo.c

看到这个例子,各位应该不是很陌生了,前面也已说过, foo.o 是我们的目标, foo.cdefs.h 是目标所依赖的源文件,而只有一个命令 cc -c -g foo.c (以Tab键开头)。这个规则告诉我们两件事:

  1. 文件的依赖关系, foo.o 依赖于 foo.cdefs.h 的文件,如果 foo.cdefs.h 的文件日期要比 foo.o 文件日期要新,或是 foo.o 不存在,那么依赖关系发生。
  2. 生成或更新 foo.o 文件,就是那个cc命令。它说明了如何生成 foo.o 这个文件。(当然,foo.c文件include了defs.h文件)

通配符

  1. 通配符在 Makefile 中的用法

Makefile 支持以下三种通配符:

  • * :匹配任意长度的字符。例如,*.c 表示所有 .c 文件。
  • ? :匹配一个字符。例如,file?.c 可以匹配 file1.cfileA.c 等。
  • ~ :在文件路径中使用波浪号(~)来表示用户目录,例如,~/test 表示当前用户的主目录下的 test 目录。

通配符可以用来匹配一系列的文件,但要注意在某些场景下需要加反斜杠 `` 进行转义。比如,如果我们需要使用 * 字符作为文字,而不是通配符,则需要写成 *

  1. 在命令中使用通配符

通配符通常用于清理命令、文件操作等。例如:

clean:
    rm -f *.o

这一行命令会删除当前目录中所有 .o 文件。

还可以在 clean 规则中添加更多操作。例如,如果我们想在删除 .o 文件前查看 main.c 文件内容,可以这样写:

clean:
    cat main.c
    rm -f *.o

这样,make clean 会先显示 main.c 文件内容,然后再删除所有 .o 文件。

  1. 在规则中使用通配符作为依赖

通配符也可以在规则的依赖部分使用。例如:

print: *.c
    lpr -p $?
    touch print

在这个例子中,print 目标依赖于所有 .c 文件。$? 是一个自动化变量,表示自上次构建以来更新过的依赖文件列表(这里的 .c 文件)。lpr -p $? 会打印所有更新的 .c 文件。

  1. 在变量中使用通配符

可以在 Makefile 中定义变量时使用通配符。例如:

objects = *.o

在这里,objects 的值是 *.o,而不是展开的 .o 文件列表。如果希望 *.o 展开为当前目录下的所有 .o 文件,可以使用 wildcard 函数:

objects := $(wildcard *.o)

$(wildcard *.o) 会返回当前目录下所有 .o 文件的列表,因此 objects 的值会是所有 .o 文件的集合。

  1. wildcardpatsubst 函数

Makefile 提供了 wildcardpatsubst 函数,配合使用可以更灵活地处理文件列表:

  • wildcard:用于获取符合条件的文件列表。例如,$(wildcard *.c) 会列出当前目录下的所有 .c 文件。
  • patsubst:用于模式替换,将符合条件的文件名替换成新的形式。例如,$(patsubst %.c, %.o, $(wildcard *.c)) 会将所有 .c 文件替换为相应的 .o 文件。

示例:使用 wildcardpatsubst 自动生成对象文件列表

假设需要列出当前目录下所有的 .c 文件并生成相应的 .o 文件,可以这样写:

# Step 1: 获取所有 .c 文件
sources := $(wildcard *.c)# Step 2: 将所有 .c 文件替换为 .o 文件
objects := $(patsubst %.c, %.o, $(sources))# 生成目标文件
foo: $(objects)
    cc -o foo $(objects)

在这个例子中:

  1. $(wildcard *.c) 获取所有 .c 文件并赋值给 sources
  2. $(patsubst %.c, %.o, $(sources))sources 中所有 .c 文件替换成 .o 文件,生成的文件列表赋值给 objects
  3. foo 目标依赖 objects 列表中的 .o 文件,并链接生成最终的可执行文件 foo

总结

  • 通配符:可以简化规则中对文件的引用。
  • wildcardpatsubst 函数:可以生成和替换文件列表,灵活处理大量文件。
  • 自动化构建:通过 wildcardpatsubst 函数,可以根据文件夹内文件的变化自动更新编译依赖和目标,适合动态或大型项目的管理。

这些技巧能够大大简化 Makefile 的编写,使其更灵活且易于维护。

文件搜索

Makefile 中,文件搜索(文件查找路径)是指 make 查找依赖文件(如头文件或对象文件)的位置。make 提供了几种方法来指定文件的搜索路径,以便在构建过程中自动找到所需的文件。常用的文件搜索方法包括使用 vpath 指令、指定变量中的路径以及使用通配符。

  1. 使用 vpath 指令进行文件搜索

vpathmake 的一个内置指令,用于指定特定类型文件的搜索路径。这样即使文件不在当前目录中,make 也能找到它们。

语法如下:

vpath <pattern> <directories>
  • <pattern> :指定文件的模式,可以是文件的扩展名(如 %.c)或文件名的通配符(如 main*)。
  • <directories> :指定搜索目录列表,用空格分隔多个目录。

示例 1:为 .c 文件指定搜索路径

假设所有的 .c 文件位于 src 目录下,Makefile 在当前目录下,可以使用 vpath 指定 .c 文件的路径:

vpath %.c src

这样,make 在查找 .c 文件时会自动到 src 目录下寻找。

示例 2:为多种文件指定不同的路径

可以为不同类型的文件指定不同的搜索路径:

vpath %.h include
vpath %.c src
vpath %.o obj
  • .h 文件在 include 目录中查找。
  • .c 文件在 src 目录中查找。
  • .o 文件在 obj 目录中查找。
  1. 使用通用路径搜索所有文件

可以使用通配符 * 来为所有文件指定一个通用的搜索路径。例如,如果所有源文件和头文件都存放在 src 目录下:

vpath * src

这样 make 会在 src 目录下查找任何所需的文件。

  1. 使用变量指定文件路径

通过将路径赋值给变量,可以更灵活地管理文件路径,并在规则中使用这些变量。

SRC_DIR = src
INC_DIR = include# 使用变量在规则中指定路径
objects = $(SRC_DIR)/main.o $(SRC_DIR)/add.o
​
$(objects): $(SRC_DIR)/%.o: $(SRC_DIR)/%.c $(INC_DIR)/%.h
    $(CC) $(CFLAGS) -c $< -o $@

这样,文件路径可以统一管理,只需更改变量定义即可调整文件的位置。

  1. 使用 vpathwildcard 结合

wildcard 函数可以与 vpath 结合使用,动态查找指定目录下的所有文件。例如,如果希望在 src 目录下找到所有 .c 文件并将它们转换为 .o 文件,可以这样写:

SRC_DIR = src
​
# 使用 wildcard 查找所有 .c 文件
SRC_FILES := $(wildcard $(SRC_DIR)/*.c)
OBJ_FILES := $(patsubst $(SRC_DIR)/%.c, %.o, $(SRC_FILES))vpath %.c $(SRC_DIR)# 生成目标
target: $(OBJ_FILES)
    $(CC) $(CFLAGS) -o target $(OBJ_FILES)

在这个例子中,wildcard 会在 src 目录下查找所有 .c 文件,并将它们的路径保存到 SRC_FILES 变量中。然后,patsubst.c 文件名替换为 .o 文件,存放在 OBJ_FILES 变量中。

  1. 示例总结

假设你的项目结构如下:

project/
├── Makefile
├── src/
│   ├── main.c
│   ├── add.c
│   └── sub.c
├── include/
│   └── math.h
└── obj/
    ├── main.o
    ├── add.o
    └── sub.o

可以用以下 Makefile 设置路径和搜索规则:

# 变量定义
SRC_DIR = src
INC_DIR = include
OBJ_DIR = obj
​
# 源文件和对象文件
SRC_FILES := $(wildcard $(SRC_DIR)/*.c)
OBJ_FILES := $(patsubst $(SRC_DIR)/%.c, $(OBJ_DIR)/%.o, $(SRC_FILES))# 编译器和标志
CC = gcc
CFLAGS = -Wall -I$(INC_DIR)# vpath 指定路径
vpath %.c $(SRC_DIR)
vpath %.o $(OBJ_DIR)
vpath %.h $(INC_DIR)# 生成目标文件
target: $(OBJ_FILES)
    $(CC) $(CFLAGS) -o target $(OBJ_FILES)# 编译规则
$(OBJ_DIR)/%.o: %.c
    $(CC) $(CFLAGS) -c $< -o $@# 清理规则
.PHONY: clean
clean:
    rm -f $(OBJ_DIR)/*.o target

总结

  • vpath 指令:指定 make 搜索文件的路径,便于组织项目结构。
  • 变量定义路径:使用变量定义文件路径,更加灵活。
  • wildcardpatsubst:结合使用可以自动生成文件列表和路径。

这种方法可以让 Makefile 更加简洁,尤其在目录结构复杂的项目中显得非常高效和清晰。


伪目标

Makefile 中,伪目标(Phony Target)是指不会生成实际文件的目标,它仅仅代表一种操作或任务。伪目标通常用于执行清理操作、打印信息、测试运行等,不涉及实际文件的生成或更新。伪目标可以帮助保持目录整洁,并让 Makefile 处理一些非文件相关的任务。

为什么使用伪目标?

Makefile 中,如果一个目标的名字与某个实际文件相同,make 会认为该目标已经完成,因为文件存在。例如,如果有一个目标叫 clean,而目录中恰好有个文件 cleanmake 会跳过这个规则,因为它认为这个目标已经“完成”。为了避免这种情况,可以使用伪目标。

定义伪目标

Makefile 中,可以通过 .PHONY 指令定义伪目标。例如:

.PHONY: clean

这行代码告诉 makeclean 是一个伪目标,无论目录中是否存在同名文件,都不影响 make 执行 clean 规则。

示例:常见伪目标

  1. 清理目标:删除中间文件和生成文件,保持目录整洁

    .PHONY: clean
    clean:
        rm -f *.o calculator
    
    • make clean 会执行 rm -f *.o calculator,删除所有对象文件和目标文件。
    • 这里使用 .PHONY: clean 是为了确保 make 无论目录中是否存在 clean 文件,都会执行此规则。
  2. 打印信息:用来显示构建信息

    .PHONY: info
    info:
        @echo "This project compiles the calculator program"
    
    • @ 符号抑制了 echo 命令的输出,使其不显示命令本身,只显示结果。
    • 执行 make info 将输出 "This project compiles the calculator program"
  3. 运行测试:用于执行单元测试或检查代码

    .PHONY: test
    test:
        ./calculator --test
    
    • 这个规则在目标文件 calculator 生成后执行,可以运行程序的测试功能。
    • 执行 make test 将调用 ./calculator --test
  4. 重建目标:强制重新编译所有文件

    .PHONY: rebuild
    rebuild: clean all
    
    • make rebuild 会先执行 clean 规则清理文件,然后再执行 all 规则重新编译所有文件。
    • .PHONY: rebuild 确保了 make 会运行该规则,即使目录中有名为 rebuild 的文件。

使用 .PHONY 的优势

  1. 避免文件名冲突:即使目录中有与伪目标同名的文件,make 也会忽略它,只执行规则。
  2. 提高效率make 在执行伪目标时不需要检查文件的时间戳,减少不必要的文件系统操作。
  3. 实现非文件操作:伪目标常用于构建以外的操作,比如清理、测试、安装等,可以让 Makefile 更加灵活。

伪目标示例

以下是一个包含多个伪目标的 Makefile 示例:

# 编译器和选项
CC = gcc
CFLAGS = -Wall
TARGET = calculator
OBJ = main.o add.o sub.o mul.o div.o
​
# 默认目标
all: $(TARGET)# 生成目标文件
$(TARGET): $(OBJ)
    $(CC) $(CFLAGS) -o $(TARGET) $(OBJ)# 清理规则
.PHONY: clean
clean:
    rm -f $(TARGET) $(OBJ)# 打印项目信息
.PHONY: info
info:
    @echo "This Makefile builds the calculator program."# 运行测试
.PHONY: test
test: $(TARGET)
    ./$(TARGET) --test
​
# 重新编译
.PHONY: rebuild
rebuild: clean all

总结

  • 伪目标Makefile 中不会生成实际文件的目标,通常用于清理、测试、显示信息等。
  • 使用 .PHONY 来定义伪目标可以避免同名文件的干扰,使 Makefile 更加灵活。
  • 常见的伪目标包括 cleantestinforebuild 等。

Makefile 中,all 是一个常用的伪目标(phony target),它通常用于定义一个默认的构建目标,即当你直接运行 make 而不指定任何目标时,make 会执行 all 所依赖的其他目标。这种写法常见于多步骤构建流程中。

示例

all: compile link
​
compile:
    gcc -c main.c -o main.o
​
link:
    gcc main.o -o main

在这个例子中:

  • all 是一个伪目标,不指向特定的文件。
  • all 依赖于 compilelink 目标,所以当运行 make 时,会按照顺序先执行 compile,再执行 link
  • make 会自动执行 all 的依赖目标,直到所有依赖都完成。

all 的作用

  • 默认入口:通过设置 all 作为入口,用户可以直接运行 make 而不指定目标,方便使用。
  • 汇总多个目标:可以将多个步骤组合成一个整体,使得项目构建过程更简洁清晰。
  • 可维护性:如果构建步骤增加或改变,只需更新 all 的依赖关系,而不需要改动用户的使用方式。

总结

all 通常作为 Makefile 的默认目标,用于组织和管理其他构建步骤的执行顺序,提供一个便于操作的统一入口。


rebuild

伪目标的存在使得 Makefile 能够执行更多非文件操作,方便了项目的管理和自动化构建流程。在 Makefile 中,rebuild 伪目标的作用是“清理所有生成的文件,然后重新编译整个项目”。它通过组合 cleanall 两个规则来实现这一目的。

让我们逐步解释 rebuild 的工作原理:

rebuild 伪目标的定义

.PHONY: rebuild
rebuild: clean all
  • .PHONY: rebuild:定义 rebuild 为伪目标,以确保 make 将它视为一个纯操作,而不是一个文件。
  • rebuild: clean all:这行代码定义了 rebuild 伪目标依赖于 cleanall 两个目标。

执行 make rebuild 时发生的操作

当运行 make rebuild 时,make 会按以下顺序执行步骤:

  1. clean 目标:先执行 clean 规则,将所有已生成的文件(例如对象文件 .o 和目标文件)删除。

    .PHONY: clean
    clean:
        rm -f $(TARGET) $(OBJ)
    

    clean 的作用是删除编译生成的所有文件,恢复到最初状态。通常,它会删除目标文件 calculator 和所有中间生成的 .o 文件。

  2. all 目标:然后执行 all 规则,重新编译整个项目。

    all: $(TARGET)
    

    all 目标通常是默认的编译目标,它会编译项目中所有需要的文件并生成最终的可执行文件 calculator

因此,make rebuild 实际上是在做两件事情:

  • 先调用 make clean,删除所有已生成的文件。
  • 再调用 make all,重新编译所有的文件,生成新的目标文件。

-rm

-rm 前的减号 - 表示即使删除命令 rm 失败,make 也会继续执行后续的命令,而不会因为这个错误停止。

具体作用

通常,rm 命令在 Makefile 中用于删除文件。如果有些文件不存在,rm 会报错并返回非零退出码,这会导致 make 停止执行。但是在 rm 前加上 -,即使用 -rm,就可以忽略 rm 的错误并继续执行 Makefile 中的其他规则。

示例

.PHONY: clean
clean:
    -rm *.o calculator
    echo "Clean completed."

在这个例子中:

  • 如果目录中没有 .o 文件或 calculator 可执行文件,rm 命令会返回错误。
  • 由于使用了 -rm,即使 rm 失败,make 也会继续执行 echo "Clean completed."

使用场景

使用 -rm 通常在以下场景中会很有帮助:

  • 清理目标:在 clean 规则中,删除文件时可能遇到文件不存在的情况,但你仍希望清理操作能顺利完成。
  • 防止中断:在某些任务失败时继续执行其他任务。

总结

Makefile 中,-rm 可以忽略 rm 命令的错误,使 make 在清理过程中遇到文件不存在或其他错误时不会中断,继续执行后续的命令。


多目标

Makefile 中,多目标规则可以通过变量(例如 $@$<$^)来处理,以便在规则中获取目标名称或依赖文件的名称。多目标可以简化规则的书写,尤其在需要处理大量文件时非常有用。

常用的自动化变量

Makefile 中的自动化变量主要有以下几种:

  • $@ :表示规则中的目标文件(target),即左侧定义的目标名称。
  • $< :表示规则的第一个依赖文件(prerequisite),适用于单个依赖的情况。
  • $^ :表示规则中的所有依赖文件列表,去重后的文件列表。

多目标规则示例

示例 1:使用 $@ 简化规则

假设我们有多个 .c 文件需要分别编译为 .o 文件,可以利用 $@ 表示目标文件名,$< 表示源文件名。例如:

# 编译器和标志
CC = gcc
CFLAGS = -Wall
​
# 多个目标文件
OBJ = main.o add.o sub.o mul.o div.o
​
# 默认目标
all: program# 生成目标文件 program
program: $(OBJ)
    $(CC) $(CFLAGS) -o $@ $^# 使用自动化变量 $@ 和 $< 编译每个 .o 文件
%.o: %.c
    $(CC) $(CFLAGS) -c $< -o $@# 清理规则
.PHONY: clean
clean:
    rm -f program $(OBJ)

在这个例子中:

  • program: $(OBJ) 表示 program 目标依赖于所有对象文件 $(OBJ)

  • $(CC) $(CFLAGS) -o $@ $^ :在编译 program 时,$@ 代表目标文件 program$^ 代表所有依赖的 .o 文件。

  • %.o: %.c

    :是一个模式规则,表示所有的

    .c
    

    文件都可以按照相同方式编译成

    .o
    

    文件。

    • 在模式规则中,$@ 代表生成的 .o 文件(目标),$< 代表对应的 .c 文件(第一个依赖文件)。

示例 2:处理多个目标的不同依赖

如果每个目标文件有不同的依赖文件,可以使用 $@ 来代表目标名称。比如,假设每个 .o 文件依赖于 common.h 文件:

# 编译器和标志
CC = gcc
CFLAGS = -Wall
​
# 多个目标文件
OBJ = main.o add.o sub.o mul.o div.o
​
# 默认目标
all: program# 生成目标文件 program
program: $(OBJ)
    $(CC) $(CFLAGS) -o $@ $^# 使用自动化变量 $@ 和 $< 编译每个 .o 文件
%.o: %.c common.h
    $(CC) $(CFLAGS) -c $< -o $@# 清理规则
.PHONY: clean
clean:
    rm -f program $(OBJ)

在这里,每个 .o 文件依赖于 common.h 文件,%.o: %.c common.h 表示所有 .o 文件在编译时都需要 common.h

自动化变量一览

变量含义
$@当前规则的目标文件,即冒号左边的内容
$<第一个依赖文件,即冒号右边第一个文件
$^所有依赖文件列表,去重
$+所有依赖文件列表,不去重
$?比目标文件更新的所有依赖文件

示例 3:使用 $@ 创建多个执行命令的伪目标

伪目标可以使用 $@ 实现多个独立任务。例如:

.PHONY: clean build run
​
# 使用 $@ 执行不同任务
clean:
    rm -f *.o program
    @echo "$@ completed."
​
build:
    gcc -o program main.c
    @echo "$@ completed."
​
run:
    ./program
    @echo "$@ completed."

在这里,$@ 可以动态获取伪目标名,比如 cleanbuildrun,方便打印任务状态。

总结

  • $@ :表示目标文件,可以用于自动化地表示当前目标,尤其在多目标规则或模式规则中。
  • $< :表示第一个依赖文件,常用于编译单个源文件时指定源文件。
  • $^ :表示所有依赖文件,通常用于链接时指定所有的对象文件。

通过使用这些自动化变量,可以让 Makefile 更加简洁、自动化,提高代码的可维护性


@echo

Makefile 中,@echo 是用于打印信息到终端的命令,同时,@ 符号会抑制命令本身的输出

具体含义

  • echo:这是一个在命令行中常用的命令,用来输出文本。例如,echo "Hello, World!" 会在终端上打印出 Hello, World!
  • @ :在 Makefile 中,任何命令前加上 @make 将不会显示该命令本身。这在需要输出消息但不希望显示命令行时很有用。

示例

.PHONY: build cleanbuild:
    @echo "Building the project..."
    gcc -o program main.c
​
clean:
    @echo "Cleaning up files..."
    rm -f program

执行 make build 的输出

Building the project...
gcc -o program main.c

执行 make clean 的输出

Cleaning up files...

解释

  • buildclean 规则中,@echo 用于输出说明性文本,比如“Building the project…” 和 “Cleaning up files…”,但是不会显示 echo 命令本身。
  • @ 符号隐藏了 echo 命令的显示,因此输出更加简洁、易读。

使用场景

  • 显示任务进度:在编译、清理等操作前输出说明性信息,让用户知道当前正在执行的任务。
  • 避免冗余输出:在 Makefile 中常常有大量命令输出,将一些提示信息隐藏命令行细节,可以让输出更简洁。

总结

@echo 可以在 Makefile 中打印信息,同时通过 @ 符号隐藏 echo 命令本身,保持输出的清晰和简洁。


自动生成依赖性

在大型项目中,手动管理每个源文件(.c 文件)与其依赖的头文件(.h 文件)是一件麻烦且容易出错的事情。为了解决这个问题,Makefile 可以借助编译器的功能,自动生成依赖关系。以下是具体的操作方法和实现步骤:

  1. 自动生成依赖关系的原理
  • 大多数 C/C++ 编译器都支持 -M-MM 选项,用于生成源文件的依赖关系。
  • 例如,gcc -M main.c 会生成 main.o : main.c defs.h,表示 main.o 文件依赖于 main.cdefs.h
  • -M 会包含所有的头文件(包括系统库),而 -MM 只包含项目中的头文件,忽略标准库。

示例

使用 gcc -MM main.c 会得到如下输出:

main.o: main.c defs.h

这表示 main.o 文件的生成依赖于 main.cdefs.h 文件。如果这些文件更新了,make 会重新编译 main.o

那么,编译器的这个功能如何与我们的Makefile联系在一起呢。因为这样一来,我们的Makefile也要根据这些源文件重新生成,让 Makefile 自己依赖于源文件?这个功能并不现实,不过我们可以有其它手段来迂回地实现这一功能。GNU组织建议把编译器为每一个源文件的自动生成的依赖关系放到一个文件中,为每一个 name.c 的文件都生成一个 name.d 的Makefile文件, .d 文件中就存放对应 .c 文件的依赖关系。

于是,我们可以写出 .c 文件和 .d 文件的依赖关系,并让make自动更新或生成 .d 文件,并把其包含在我们的主Makefile中,这样,我们就可以自动化地生成每个文件的依赖关系了。

这里,我们给出了一个模式规则来产生 .d 文件:

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

这个规则的意思是,所有的 .d 文件依赖于 .c 文件, rm -f $@ 的意思是删除所有的目标,也就是 .d 文件,第二行的意思是,为每个依赖文件 $< ,也就是 .c 文件生成依赖文件, $@ 表示模式 %.d 文件,如果有一个C文件是name.c,那么 % 就是 name$$$$ 意为一个随机编号,第二行生成的文件有可能是“name.d.12345”,第三行使用sed命令做了一个替换,关于sed命令的用法请参看相关的使用文档。第四行就是删除临时文件。

总而言之,这个模式要做的事就是在编译器生成的依赖关系中加入 .d 文件的依赖,即把依赖关系:

main.o : main.c defs.h

转成:

main.o main.d : main.c defs.h

于是,我们的 .d 文件也会自动更新了,并会自动生成了,当然,你还可以在这个 .d 文件中加入的不只是依赖关系,包括生成的命令也可一并加入,让每个 .d 文件都包含一个完整的规则。一旦我们完成这个工作,接下来,我们就要把这些自动生成的规则放进我们的主Makefile中。我们可以使用Makefile的“include”命令,来引入别的Makefile文件(前面讲过),例如:

sources = foo.c bar.c
​
include $(sources:.c=.d)

上述语句中的 $(sources:.c=.d) 中的 .c=.d 的意思是做一个替换,把变量 $(sources) 所有 .c 的字串都替换成 .d ,关于这个“替换”的内容,在后面我会有更为详细的讲述。当然,你得注意次序,因为include是按次序来载入文件,最先载入的 .d 文件中的目标会成为默认目标。

4. 书写命令

4.1 显示命令

通常,make会把其要执行的命令行在命令执行前输出到屏幕上。当我们用 @ 字符在命令行前,那么,这个命令将不被make显示出来,最具代表性的例子是,我们用这个功能来向屏幕显示一些信息。如:

@echo 正在编译XXX模块......

当make执行时,会输出“正在编译XXX模块……”字串,但不会输出命令,如果没有“@”,那么,make将输出:

echo 正在编译XXX模块......
正在编译XXX模块......

以下是一些使用 make 控制命令显示的示例:

  1. 使用 @ 符号隐藏命令
build:
    @echo 正在编译XXX模块……
    gcc main.c -o main

执行 make build 时输出:

正在编译XXX模块……

这里只显示了 echo 的输出,没有显示 gcc main.c -o main 的命令本身。

  1. 不使用 @ 符号
build:
    echo 正在编译XXX模块……
    gcc main.c -o main

执行 make build 时输出:

echo 正在编译XXX模块……
正在编译XXX模块……
gcc main.c -o main

这里既显示了命令本身,也显示了命令的输出。

  1. 使用 -n--just-print 参数只显示命令而不执行
build:
    echo 正在编译XXX模块……
    gcc main.c -o main

执行 make -n buildmake --just-print build 时输出:

echo 正在编译XXX模块……
gcc main.c -o main

此时,make 只显示命令,不执行 echogcc,适合调试 Makefile

  1. 使用 -s--silent--quiet 参数全面禁用命令的显示
build:
    echo 正在编译XXX模块……
    gcc main.c -o main

执行 make -s buildmake --silent buildmake --quiet build 时输出:

复制代码

此时 make 完全不显示任何命令或输出,适用于需要安静模式执行的情况。

4.2 命令执行

当依赖目标新于目标时,也就是当规则的目标需要被更新时,make会一条一条的执行其后的命令。需要注意的是,如果你要让上一条命令的结果应用在下一条命令时,你应该使用分号分隔这两条命令。比如你的第一条命令是cd命令,你希望第二条命令得在cd之后的基础上运行,那么你就不能把这两条命令写在两行上,而应该把这两条命令写在一行上,用分号分隔。如:

  • 示例一:
exec:
    cd /home/hchen
    pwd
  • 示例二:
exec:
    cd /home/hchen; pwd

当我们执行 make exec 时,第一个例子中的cd没有作用,pwd会打印出当前的Makefile目录,而第二个例子中,cd就起作用了,pwd会打印出“/home/hchen”。

4.3 命令出错

每当命令运行完后,make会检测每个命令的返回码,如果命令返回成功,那么make会执行下一条命令,当规则中所有的命令成功返回后,这个规则就算是成功完成了。如果一个规则中的某个命令出错了(命令退出码非零),那么make就会终止执行当前规则,这将有可能终止所有规则的执行。

有些时候,命令的出错并不表示就是错误的。例如mkdir命令,我们一定需要建立一个目录,如果目录不存在,那么mkdir就成功执行,万事大吉,如果目录存在,那么就出错了。我们之所以使用mkdir的意思就是一定要有这样的一个目录,于是我们就不希望mkdir出错而终止规则的运行。

为了做到这一点,忽略命令的出错,我们可以在Makefile的命令行前加一个减号 - (在Tab键之后),标记为不管命令出不出错都认为是成功的。如:

clean:
    -rm -f *.o

还有一个全局的办法是,给make加上 -i 或是 --ignore-errors 参数,那么,Makefile中所有命令都会忽略错误。而如果一个规则是以 .IGNORE 作为目标的,那么这个规则中的所有命令将会忽略错误。这些是不同级别的防止命令出错的方法,你可以根据你的不同喜欢设置。

如果想让 make 忽略所有命令的错误并继续执行,可以在运行 make 时使用 -i--ignore-errors 参数。

这样做的效果是,即使某个命令出错(返回非零值),make 也会忽略该错误并继续执行其他命令。

另外,你还可以在 Makefile 中针对某个规则添加 .IGNORE 目标。例如:

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

这会让 make 在执行 clean 规则中的命令时忽略错误,而不影响其他规则。

还有一个要提一下的make的参数的是 -k 或是 --keep-going ,这个参数的意思是,如果某规则中的命令出错了,那么就终止该规则的执行,但继续执行其它规则。

4.4 嵌套Makefile

4.5 定义命名包

如果Makefile中出现一些相同命令序列,那么我们可以为这些相同的命令序列定义一个变量。定义这种命令序列的语法以 define 开始,以 endef 结束,如:

define run-yacc
yacc $(firstword $^)
mv y.tab.c $@
endef

这里,“run-yacc”是这个命令包的名字,其不要和Makefile中的变量重名。在 defineendef 中的两行就是命令序列。这个命令包中的第一个命令是运行Yacc程序,因为Yacc程序总是生成“y.tab.c”的文件,所以第二行的命令就是把这个文件改改名字。还是把这个命令包放到一个示例中来看看吧。

foo.c : foo.y
    $(run-yacc)

我们可以看见,要使用这个命令包,我们就好像使用变量一样。在这个命令包的使用中,命令包“run-yacc”中的 $^ 就是 foo.y$@ 就是 foo.c (有关这种以 $ 开头的特殊变量,我们会在后面介绍),make在执行命令包时,命令包中的每个命令会被依次独立执行。

5. 使用变量

5.1 变量基础

变量在声明时需要给予初值,而在使用时,需要给在变量名前加上 $ 符号,但最好用小括号 () 或是大括号 {} 把变量给包括起来。如果你要使用真实的 $ 字符,那么你需要用 $$ 来表示。

变量可以使用在许多地方,如规则中的“目标”、“依赖”、“命令”以及新的变量中。先看一个例子:

objects = program.o foo.o utils.o
program : $(objects)
    cc -o program $(objects)$(objects) : defs.h

变量会在使用它的地方精确地展开,就像C/C++中的宏一样,例如:

foo = c
prog.o : prog.$(foo)
    $(foo)$(foo) -$(foo) prog.$(foo)

展开后得到:

prog.o : prog.c
    cc -c prog.c

当然,千万不要在你的Makefile中这样干,这里只是举个例子来表明Makefile中的变量在使用处展开的真实样子。可见其就是一个“替代”的原理。

另外,给变量加上括号完全是为了更加安全地使用这个变量,在上面的例子中,如果你不想给变量加上括号,那也可以,但我还是强烈建议你给变量加上括号。

5.2 变量中的变量

为了避免$()造成的变量无限展开问题,我们可以使用make中的另一种用变量来定义变量的方法。这种方法使用的是 := 操作符

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

其等价于:

y := foo bar
x := later

值得一提的是,这种方法,前面的变量不能使用后面的变量,只能使用前面已定义好了的变量。如果是这样:

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

那么,y的值是“bar”,而不是“foo bar”。

还有一个比较有用的操作符是 ?= ,先看示例:

FOO ?= bar

其含义是,如果FOO没有被定义过,那么变量FOO的值就是“bar”,如果FOO先前被定义过,那么这条语将什么也不做,其等价于:

ifeq ($(origin FOO), undefined)
    FOO = bar
endif

5.3 变量高级用法

这里介绍两种变量的高级使用方法,第一种是变量值的替换。

我们可以替换变量中的共有的部分,其格式是 $(var:a=b) 或是 ${var:a=b} ,其意思是,把变量“var”中所有以“a”字串“结尾”的“a”替换成“b”字串。这里的“结尾”意思是“空格”或是“结束符”。

还是看一个示例吧:

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

这个示例中,我们先定义了一个 $(foo) 变量,而第二行的意思是把 $(foo) 中所有以 .o 字串“结尾”全部替换成 .c ,所以我们的 $(bar) 的值就是“a.c b.c c.c”。

另外一种变量替换的技术是以“静态模式”(参见前面章节)定义的,如:

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

5.4 追加变量值

我们可以使用 += 操作符给变量追加值,如:

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

于是,我们的 $(objects) 值变成:“main.o foo.o bar.o utils.o another.o”(another.o被追加进去了)

5.5 override 指令

如果有变量是通过make的命令行参数设置的,那么Makefile文件中对这个变量的赋值会被忽略。如果你想在Makefile文件中设置这类参数的值,那么,你可以使用“override”指令。其语法是:

override <variable>; = <value>;
​
override <variable>; := <value>;

当然,你还可以追加:

override <variable>; += <more text>;

对于多行的变量定义,我们用define指令,在define指令前,也同样可以使用override指令,如:

override define foo
bar
endef

6. 使用条件判断

使用条件判断,可以让make根据运行时的不同情况选择不同的执行分支。条件表达式可以是比较变量的值,或是比较变量和常量的值。

6.1使用条件判断

使用条件判断,可以让make根据运行时的不同情况选择不同的执行分支。条件表达式可以是比较变量的值,或是比较变量和常量的值。

示例

下面的例子,判断 $(CC) 变量是否 gcc ,如果是的话,则使用GNU函数编译目标。

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

可见,在上面示例的这个规则中,目标 foo 可以根据变量 $(CC) 值来选取不同的函数库来编译程序。

我们可以从上面的示例中看到三个关键字: ifeqelseendififeq 的意思表示条件语句的开始,并指定一个条件表达式,表达式包含两个参数,以逗号分隔,表达式以圆括号括起。 else 表示条件表达式为假的情况。 endif 表示一个条件语句的结束,任何一个条件表达式都应该以 endif 结束。

当我们的变量 $(CC) 值是 gcc 时,目标 foo 的规则是:

foo: $(objects)
    $(CC) -o foo $(objects) $(libs_for_gcc)

而当我们的变量 $(CC) 值不是 gcc 时(比如 cc ),目标 foo 的规则是:

foo: $(objects)
    $(CC) -o foo $(objects) $(normal_libs)

当然,我们还可以把上面的那个例子写得更简洁一些:

libs_for_gcc = -lgnu
normal_libs =
​
ifeq ($(CC),gcc)
    libs=$(libs_for_gcc)
else
    libs=$(normal_libs)
endiffoo: $(objects)
    $(CC) -o foo $(objects) $(libs)

6.2 语法

条件表达式的语法为:

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

以及:

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

其中 <conditional-directive> 表示条件关键字,如 ifeq 。这个关键字有四个。

第一个是我们前面所见过的 ifeq

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

比较参数 arg1arg2 的值是否相同。当然,参数中我们还可以使用make的函数。如:

ifeq ($(strip $(foo)),)
<text-if-empty>
endif

这个示例中使用了 strip 函数,如果这个函数的返回值是空(Empty),那么 <text-if-empty> 就生效。

第二个条件关键字是 ifneq 。语法是:

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

其比较参数 arg1arg2 的值是否相同,如果不同,则为真。和 ifeq 类似。

第三个条件关键字是 ifdef 。语法是:

ifdef <variable-name>

如果变量 <variable-name> 的值非空,那到表达式为真。否则,表达式为假。当然, <variable-name> 同样可以是一个函数的返回值。注意, ifdef 只是测试一个变量是否有值,其并不会把变量扩展到当前位置。还是来看两个例子:

示例一:

bar =
foo = $(bar)
ifdef foo
    frobozz = yes
else
    frobozz = no
endif

示例二:

foo =
ifdef foo
    frobozz = yes
else
    frobozz = no
endif

第一个例子中, $(frobozz) 值是 yes ,第二个则是 no

第四个条件关键字是 ifndef 。其语法是:

ifndef <variable-name>

这个我就不多说了,和 ifdef 是相反的意思。

<conditional-directive> 这一行上,多余的空格是被允许的,但是不能以 Tab 键作为开始(不然就被认为是命令)。而注释符 # 同样也是安全的。 elseendif 也一样,只要不是以 Tab 键开始就行了。

特别注意的是,make是在读取Makefile时就计算条件表达式的值,并根据条件表达式的值来选择语句,所以,你最好不要把自动化变量(如 $@ 等)放入条件表达式中,因为自动化变量是在运行时才有的。

而且为了避免混乱,make不允许把整个条件语句分成两部分放在不同的文件中。

7. 函数

7.1 函数的调用语法

函数调用,很像变量的使用,也是以 $ 来标识的,其语法如下:

$(<function> <arguments>)

或是:

${<function> <arguments>}

这里, <function> 就是函数名,make支持的函数不多。 <arguments> 为函数的参数,参数间以逗号 , 分隔,而函数名和参数之间以“空格”分隔。函数调用以 $ 开头,以圆括号或花括号把函数名和参数括起。感觉很像一个变量,是不是?函数中的参数可以使用变量,为了风格的统一,函数和变量的括号最好一样,如使用 $(subst a,b,$(x)) 这样的形式,而不是 $(subst a,b, ${x}) 的形式。因为统一会更清楚,也会减少一些不必要的麻烦。

还是来看一个示例:

comma:= ,
empty:=
space:= $(empty) $(empty)
foo:= a b c
bar:= $(subst $(space),$(comma),$(foo))

在这个示例中, $(comma) 的值是一个逗号。 $(space) 使用了 $(empty) 定义了一个空格, $(foo) 的值是 a b c$(bar) 的定义用,调用了函数 subst ,这是一个替换函数,这个函数有三个参数,第一个参数是被替换字串,第二个参数是替换字串,第三个参数是替换操作作用的字串。这个函数也就是把 $(foo) 中的空格替换成逗号,所以 $(bar) 的值是 a,b,c

7.2 字符串处理函数

subst

$(subst <from>,<to>,<text>)
  • 名称:字符串替换函数

  • 功能:把字串 <text> 中的 <from> 字符串替换成 <to>

  • 返回:函数返回被替换过后的字符串。

  • 示例:

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

feet on the street 中的 ee 替换成 EE ,返回结果是 fEEt on the strEEt


patsubst

$(patsubst <pattern>,<replacement>,<text>)
  • 名称:模式字符串替换函数。

  • 功能:查找 <text> 中的单词(单词以“空格”、“Tab”或“回车”“换行”分隔)是否符合模式 <pattern> ,如果匹配的话,则以 <replacement> 替换。这里, <pattern> 可以包括通配符 % ,表示任意长度的字串。如果 <replacement> 中也包含 % ,那么, <replacement> 中的这个 % 将是 <pattern> 中的那个 % 所代表的字串。(可以用 `` 来转义,以 % 来表示真实含义的 % 字符)

  • 返回:函数返回被替换过后的字符串。

  • 示例:

    $(patsubst %.c,%.o,x.c.c bar.c)
    

把字串 x.c.c bar.c 符合模式 %.c 的单词替换成 %.o ,返回结果是 x.c.o bar.o


strip

$(strip <string>)
  • 名称:去空格函数。

  • 功能:去掉 <string> 字串中开头和结尾的空字符。

  • 返回:返回被去掉空格的字符串值。

  • 示例:

    $(strip a b c )
    

    把字串 a b c 去掉开头和结尾的空格,结果是 a b c


findstring

$(findstring <find>,<in>)
  • 名称:查找字符串函数

  • 功能:在字串 <in> 中查找 <find> 字串。

  • 返回:如果找到,那么返回 <find> ,否则返回空字符串。

  • 示例:

    $(findstring a,a b c)
    $(findstring a,b c)
    

第一个函数返回 a 字符串,第二个返回空字符串


filter

$(filter <pattern...>,<text>)
  • 名称:过滤函数

  • 功能:以 <pattern> 模式过滤 <text> 字符串中的单词,保留符合模式 <pattern> 的单词。可以有多个模式。

  • 返回:返回符合模式 <pattern> 的字串。

  • 示例:

    sources := foo.c bar.c baz.s ugh.h
    foo: $(sources)
        cc $(filter %.c %.s,$(sources)) -o foo
    

    $(filter %.c %.s,$(sources)) 返回的值是 foo.c bar.c baz.s

filter-out

$(filter-out <pattern...>,<text>)
  • 名称:反过滤函数

  • 功能:以 <pattern> 模式过滤 <text> 字符串中的单词,去除符合模式 <pattern> 的单词。可以有多个模式。

  • 返回:返回不符合模式 <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
  • 备注: sort 函数会去掉 <list> 中相同的单词。

word

$(word <n>,<text>)
  • 名称:取单词函数
  • 功能:取字符串 <text> 中第 <n> 个单词。(从一开始)
  • 返回:返回字符串 <text> 中第 <n> 个单词。如果 <n><text> 中的单词数要大,那么返回空字符串。
  • 示例: $(word 2, foo bar baz) 返回值是 bar

wordlist

$(wordlist <ss>,<e>,<text>)
  • 名称:取单词串函数
  • 功能:从字符串 <text> 中取从 <ss> 开始到 <e> 的单词串。 <ss><e> 是一个数字。
  • 返回:返回字符串 <text> 中从 <ss><e> 的单词字串。如果 <ss><text> 中的单词数要大,那么返回空字符串。如果 <e> 大于 <text> 的单词数,那么返回从 <ss> 开始,到 <text> 结束的单词串。
  • 示例: $(wordlist 2, 3, foo bar baz) 返回值是 bar baz

words

$(words <text>)
  • 名称:单词个数统计函数
  • 功能:统计 <text> 中字符串中的单词个数。
  • 返回:返回 <text> 中的单词数。
  • 示例: $(words, foo bar baz) 返回值是 3
  • 备注:如果我们要取 <text> 中最后的一个单词,我们可以这样: $(word $(words <text>),<text>)

firstword

$(firstword <text>)
  • 名称:首单词函数——firstword。
  • 功能:取字符串 <text> 中的第一个单词。
  • 返回:返回字符串 <text> 的第一个单词。
  • 示例: $(firstword foo bar) 返回值是 foo
  • 备注:这个函数可以用 word 函数来实现: $(word 1,<text>)

以上,是所有的字符串操作函数,如果搭配混合使用,可以完成比较复杂的功能。这里,举一个现实中应用的例子。我们知道,make使用 VPATH 变量来指定“依赖文件”的搜索路径。于是,我们可以利用这个搜索路径来指定编译器对头文件的搜索路径参数 CFLAGS ,如:

override CFLAGS += $(patsubst %,-I%,$(subst :, ,$(VPATH)))

如果我们的 $(VPATH) 值是 src:../headers ,那么 $(patsubst %,-I%,$(subst :, ,$(VPATH))) 将返回 -Isrc -I../headers ,这正是cc或gcc搜索头文件路径的参数。

7.3 文件名操作函数

dir

$(dir <names...>)
  • 名称:取目录函数——dir。
  • 功能:从文件名序列 <names> 中取出目录部分。目录部分是指最后一个反斜杠( / )之前的部分。如果没有反斜杠,那么返回 ./
  • 返回:返回文件名序列 <names> 的目录部分。
  • 示例: $(dir src/foo.c hacks) 返回值是 src/ ./

notdir

$(notdir <names...>)
  • 名称:取文件函数——notdir。
  • 功能:从文件名序列 <names> 中取出非目录部分。非目录部分是指最後一个反斜杠( / )之后的部分。
  • 返回:返回文件名序列 <names> 的非目录部分。
  • 示例: $(notdir src/foo.c hacks) 返回值是 foo.c hacks

suffix

$(suffix <names...>)
  • 名称:取後缀函数——suffix。
  • 功能:从文件名序列 <names> 中取出各个文件名的后缀。
  • 返回:返回文件名序列 <names> 的后缀序列,如果文件没有后缀,则返回空字串。
  • 示例: $(suffix src/foo.c src-1.0/bar.c hacks) 返回值是 .c .c

basename

$(basename <names...>)
  • 名称:取前缀函数——basename。
  • 功能:从文件名序列 <names> 中取出各个文件名的前缀部分。
  • 返回:返回文件名序列 <names> 的前缀序列,如果文件没有前缀,则返回空字串。
  • 示例: $(basename src/foo.c src-1.0/bar.c hacks) 返回值是 src/foo src-1.0/bar hacks

addsuffix

$(addsuffix <suffix>,<names...>)
  • 名称:加后缀函数——addsuffix。
  • 功能:把后缀 <suffix> 加到 <names> 中的每个单词后面。
  • 返回:返回加过后缀的文件名序列。
  • 示例: $(addsuffix .c,foo bar) 返回值是 foo.c bar.c

addprefix

$(addprefix <prefix>,<names...>)
  • 名称:加前缀函数——addprefix。
  • 功能:把前缀 <prefix> 加到 <names> 中的每个单词前面。
  • 返回:返回加过前缀的文件名序列。
  • 示例: $(addprefix src/,foo bar) 返回值是 src/foo src/bar

join

$(join <list1>,<list2>)
  • 名称:连接函数——join。
  • 功能:把 <list2> 中的单词对应地加到 <list1> 的单词后面。如果 <list1> 的单词个数要比 <list2> 的多,那么, <list1> 中的多出来的单词将保持原样。如果 <list2> 的单词个数要比 <list1> 多,那么, <list2> 多出来的单词将被复制到 <list1> 中。
  • 返回:返回连接过后的字符串。
  • 示例: $(join aaa bbb , 111 222 333) 返回值是 aaa111 bbb222 333

7.4 foreach

foreach函数和别的函数非常的不一样。因为这个函数是用来做循环用的,Makefile中的foreach函数几乎是仿照于Unix标准Shell(/bin/sh)中的for语句,或是C-Shell(/bin/csh)中的foreach语句而构建的。它的语法是:

$(foreach <var>,<list>,<text>)

这个函数的意思是,把参数 <list> 中的单词逐一取出放到参数 <var> 所指定的变量中,然后再执行 <text> 所包含的表达式。每一次 <text> 会返回一个字符串,循环过程中, <text> 的所返回的每个字符串会以空格分隔,最后当整个循环结束时, <text> 所返回的每个字符串所组成的整个字符串(以空格分隔)将会是foreach函数的返回值。

所以, <var> 最好是一个变量名, <list> 可以是一个表达式,而 <text> 中一般会使用 <var> 这个参数来依次枚举 <list> 中的单词。举个例子:

names := a b c d
​
files := $(foreach n,$(names),$(n).o)

上面的例子中, $(name) 中的单词会被挨个取出,并存到变量 n 中, $(n).o 每次根据 $(n) 计算出一个值,这些值以空格分隔,最后作为foreach函数的返回,所以, $(files) 的值是 a.o b.o c.o d.o

注意,foreach中的 <var> 参数是一个临时的局部变量,foreach函数执行完后,参数 <var> 的变量将不在作用,其作用域只在foreach函数当中。

7.5 if 函数

if函数很像GNU的make所支持的条件语句——ifeq(参见前面所述的章节),if函数的语法是:

$(if <condition>,<then-part>)

或是

$(if <condition>,<then-part>,<else-part>)

可见,if函数可以包含“else”部分,或是不含。即if函数的参数可以是两个,也可以是三个。 <condition> 参数是if的表达式,如果其返回的为非空字符串,那么这个表达式就相当于返回真,于是, <then-part> 会被计算,否则 <else-part> 会被计算。

而if函数的返回值是,如果 <condition> 为真(非空字符串),那个 <then-part> 会是整个函数的返回值,如果 <condition> 为假(空字符串),那么 <else-part> 会是整个函数的返回值,此时如果 <else-part> 没有被定义,那么,整个函数返回空字串。

所以, <then-part><else-part> 只会有一个被计算。

7.6 call函数

call函数是唯一一个可以用来创建新的参数化的函数。你可以写一个非常复杂的表达式,这个表达式中,你可以定义许多参数,然后你可以call函数来向这个表达式传递参数。其语法是:

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

当make执行这个函数时, <expression> 参数中的变量,如 $(1)$(2) 等,会被参数 <parm1><parm2><parm3> 依次取代。而 <expression> 的返回值就是 call 函数的返回值。例如:

reverse =  $(1) $(2)
​
foo = $(call reverse,a,b)

那么, foo 的值就是 a b 。当然,参数的次序是可以自定义的,不一定是顺序的,如:

reverse =  $(2) $(1)
​
foo = $(call reverse,a,b)

此时的 foo 的值就是 b a

需要注意:在向 call 函数传递参数时要尤其注意空格的使用。call 函数在处理参数时,第2个及其之后的参数中的空格会被保留,因而可能造成一些奇怪的效果。因而在向call函数提供参数时,最安全的做法是去除所有多余的空格。

7.7 origin函数

origin函数不像其它的函数,他并不操作变量的值,他只是告诉你你的这个变量是哪里来的?其语法是:

$(origin <variable>)
  • 注意, <variable> 是变量的名字,不应该是引用。所以你最好不要在 <variable> 中使用

    $ 字符。Origin函数会以其返回值来告诉你这个变量的“出生情况”,下面,是origin函数的返回值:

  • undefined

    如果 <variable> 从来没有定义过,origin函数返回这个值 undefined

  • default

    如果 <variable> 是一个默认的定义,比如“CC”这个变量,这种变量我们将在后面讲述。

  • environment

    如果 <variable> 是一个环境变量,并且当Makefile被执行时, -e 参数没有被打开。

  • file

    如果 <variable> 这个变量被定义在Makefile中。

  • command line

    如果 <variable> 这个变量是被命令行定义的。

  • override

    如果 <variable> 是被override指示符重新定义的。

  • automatic

    如果 <variable> 是一个命令运行中的自动化变量。关于自动化变量将在后面讲述。

这些信息对于我们编写Makefile是非常有用的,例如,假设我们有一个Makefile其包了一个定义文件 Make.def,在 Make.def中定义了一个变量“bletch”,而我们的环境中也有一个环境变量“bletch”,此时,我们想判断一下,如果变量来源于环境,那么我们就把之重定义了,如果来源于Make.def或是命令行等非环境的,那么我们就不重新定义它。于是,在我们的Makefile中,我们可以这样写:

ifdef bletch
    ifeq "$(origin bletch)" "environment"
        bletch = barf, gag, etc.
    endif
endif

当然,你也许会说,使用 override 关键字不就可以重新定义环境中的变量了吗?为什么需要使用这样的步骤?是的,我们用 override 是可以达到这样的效果,可是 override 过于粗暴,它同时会把从命令行定义的变量也覆盖了,而我们只想重新定义环境传来的,而不想重新定义命令行传来的。

7.8 shell函数

shell函数也不像其它的函数。顾名思义,它的参数应该就是操作系统Shell的命令。它和反引号“`”是相同的功能。这就是说,shell函数把执行操作系统命令后的输出作为函数返回。于是,我们可以用操作系统命令以及字符串处理命令awk,sed等等命令来生成一个变量,如:

contents := $(shell cat foo)
files := $(shell echo *.c)

注意,这个函数会新生成一个Shell程序来执行命令,所以你要注意其运行性能,如果你的Makefile中有一些比较复杂的规则,并大量使用了这个函数,那么对于你的系统性能是有害的。特别是Makefile的隐式规则可能会让你的shell函数执行的次数比你想像的多得多。

7.9 控制make的函数

make提供了一些函数来控制make的运行。通常,你需要检测一些运行Makefile时的运行时信息,并且根据这些信息来决定,你是让make继续执行,还是停止。

$(error <text ...>)

产生一个致命的错误, <text ...> 是错误信息。注意,error函数不会在一被使用就会产生错误信息,所以如果你把其定义在某个变量中,并在后续的脚本中使用这个变量,那么也是可以的。例如:

示例一:

ifdef ERROR_001
    $(error error is $(ERROR_001))
endif

示例二:

ERR = $(error found an error!).PHONY: errerr: $(ERR)

示例一会在变量ERROR_001定义了后执行时产生error调用,而示例二则在目录err被执行时才发生error调用。

$(warning <text ...>)

这个函数很像error函数,只是它并不会让make退出,只是输出一段警告信息,而make继续执行。

8. make的运行

8.1 make的退出码

make命令执行后有三个退出码:

  • 0

    表示成功执行。

  • 1

    如果make运行时出现任何错误,其返回1。

  • 2

    如果你使用了make的“-q”选项,并且make使得一些目标不需要更新,那么返回2。

Make的相关参数我们会在后续章节中讲述。

8.2 指定Makefile

前面我们说过,GNU make找寻默认的Makefile的规则是在当前目录下依次找三个文件——“GNUmakefile”、“makefile”和“Makefile”。其按顺序找这三个文件,一旦找到,就开始读取这个文件并执行。

当前,我们也可以给make命令指定一个特殊名字的Makefile。要达到这个功能,我们要使用make的 -f 或是 --file 参数( --makefile 参数也行)。例如,我们有个makefile的名字是“hchen.mk”,那么,我们可以这样来让make来执行这个文件:

make –f hchen.mk

如果在make的命令行是,你不只一次地使用了 -f 参数,那么,所有指定的makefile将会被连在一起传递给make执行。

8.3 指定目标

一般来说,make的最终目标是makefile中的第一个目标,而其它目标一般是由这个目标连带出来的。这是make的默认行为。当然,一般来说,你的makefile中的第一个目标是由许多个目标组成,你可以指示make,让其完成你所指定的目标。要达到这一目的很简单,需在make命令后直接跟目标的名字就可以完成(如前面提到的“make clean”形式)

任何在makefile中的目标都可以被指定成终极目标,但是除了以 - 打头,或是包含了 = 的目标,因为有这些字符的目标,会被解析成命令行参数或是变量。甚至没有被我们明确写出来的目标也可以成为make的终极目标,也就是说,只要make可以找到其隐含规则推导规则,那么这个隐含目标同样可以被指定成终极目标。

有一个make的环境变量叫 MAKECMDGOALS ,这个变量中会存放你所指定的终极目标的列表,如果在命令行上,你没有指定目标,那么,这个变量是空值。这个变量可以让你使用在一些比较特殊的情形下。比如下面的例子:

sources = foo.c bar.c
ifneq ( $(MAKECMDGOALS),clean)
    include $(sources:.c=.d)
endif

基于上面的这个例子,只要我们输入的命令不是“make clean”,那么makefile会自动包含“foo.d”和“bar.d”这两个makefile。

使用指定终极目标的方法可以很方便地让我们编译我们的程序,例如下面这个例子:

.PHONY: all
all: prog1 prog2 prog3 prog4

从这个例子中,我们可以看到,这个makefile中有四个需要编译的程序——“prog1”, “prog2”,“prog3”和 “prog4”,我们可以使用“make all”命令来编译所有的目标(如果把all置成第一个目标,那么只需执行“make”),我们也可以使用 “make prog2”来单独编译目标“prog2”。

即然make可以指定所有makefile中的目标,那么也包括“伪目标”,于是我们可以根据这种性质来让我们的makefile根据指定的不同的目标来完成不同的事。在Unix世界中,软件发布时,特别是GNU这种开源软件的发布时,其makefile都包含了编译、安装、打包等功能。我们可以参照这种规则来书写我们的makefile中的目标。

  • all:这个伪目标是所有目标的目标,其功能一般是编译所有的目标。
  • clean:这个伪目标功能是删除所有被make创建的文件。
  • install:这个伪目标功能是安装已编译好的程序,其实就是把目标执行文件拷贝到指定的目标中去。
  • print:这个伪目标的功能是例出改变过的源文件。
  • tar:这个伪目标功能是把源程序打包备份。也就是一个tar文件。
  • dist:这个伪目标功能是创建一个压缩文件,一般是把tar文件压成Z文件。或是gz文件。
  • TAGS:这个伪目标功能是更新所有的目标,以备完整地重编译使用。
  • check和test:这两个伪目标一般用来测试makefile的流程。

当然一个项目的makefile中也不一定要书写这样的目标,这些东西都是GNU的东西,但是我想,GNU搞出这些东西一定有其可取之处(等你的 UNIX下的程序文件一多时你就会发现这些功能很有用了),这里只不过是说明了,如果你要书写这种功能,最好使用这种名字命名你的目标,这样规范一些,规范的好处就是——不用解释,大家都明白。而且如果你的makefile中有这些功能,一是很实用,二是可以显得你的makefile很专业(不是那种初学者的作品)。