Linux学习记录:Makefile

  • 这是本人在学习makefile时的记录,方便日后查询。
  • 所有我遇到的makefile相关的内容都会被记录在这篇笔记中,所以在之后接触到makefile相关的新内容后,会对这篇笔记的内容进行更新。
  • 相关内容学习主要在B站C语言中文网上进行。

零、Makefile简介

  1. 描述了整个工程的编译、链接规则
    · 工程中的哪些源文件需要编译以及如何编译
    · 需要创建哪些库文件以及如何创建这些库文件
    · 如何产生期望得到的最终可执行文件
    · 可以快速的构建和管理工程

  2. make的执行机制
    · makefile文件的命名: Makefilemakefile
    · 在make执行时,会依次寻找GNUmakefile、Makefile、makefile,如未找到则报错,找到则执行此makefile文件。
    · 在执行makefile文件时,make会检测每一条命令的返回值,如果失败的话会报错并终止make,否则会继续进行。
    · 可以使用 make -f 来指定make命令读取的脚本名

  3. makefile的执行流程
    · 从makefile的第一个目标开始执行(从上往下数第一个)
    · 首先看该目标的依赖项,看依赖项是否存在
    · 如果不存在依赖项,则执行命令后结束。
    · 如果存在就先执行依赖项相关目标(然后看依赖项目标是否有依赖项…以此不断寻找最内层,有点dfs的感觉)

PS: 这里简单提一嘴编译和链接的过程…

步骤号 执行前 要干嘛 过程 执行后
1 源文件(.c, .cpp, .h) 预处理器(Preprocessor)进行预处理 引入头文件、进行宏替换等 预编译文件(.i, .ii)
2 预编译文件(.i, .ii) 编译器(Compiler)进行编译处理 比如使用gcc或者g++进行编译 汇编码(.s)
3 汇编码(.s) 汇编程序(Assembler)进行汇编 把汇编码转为机器码 机器码(.o, .obj)
4 机器码(.o, .obj) 链接器(Linker)进行链接 对静态库(.lib, .a)进行连接 得到可执行文件

一、基础语法

基础语法如下:

1
2
3
4
5
6
7
8
target...: prerequisttes...
command...
...

其中:
1.target为目标文件,可以是obj文件也可以是可执行文件
2.prerequisttes为依赖性,即生成目标文件所关联到的文件
3.command为指令,即make所需要执行的指令

1. 单源文件例子

下面是一个简单的单源文件makefile例子:
图1
  如上图所示,在当前文件夹内有两个文件 main.c 和 makefile,而文件中的内容分别如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*
* main.c
* 为简单的打印HelloWorld
*/
#include<stdio.h>

int main()
{
printf("HelloWorld\n");
return 0;
}

/*
* makefile
* 设定目标文件为main,依赖项为main.c,命令语句为gcc main.c -o main
*/
main: main.c
gcc main.c -o main

  如下图,执行make命令后,可以看到它执行了预设的gcc命令,随后正常生成了main可执行文件,并可以正常执行。由此,一个最简单的基础makefile示例就完成了。
图2

2. 多源文件例子

下面是一个简单的多源文件makefile例子:
  如下,是一个文件夹里的四个文件:func.hfunc.cmain.cmakefile,具体内容都列在下方。其中func为一个简单的加法函数,而makefile中涉及了变量和伪目标的概念,具体内容在下面都会记录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/*func.h*/
#ifndef _FUNC_H_
#define _FUNC_H_

int func(int a, int b);

#endif

/*func.c*/
#include"func.h"

int func(int a, int b)
{
return a + b;
}

/*main.c*/
#include<stdio.h>
#include"func.h"

int main()
{
printf("HelloWorld\n1 + 2 = %d\n", func(1, 2));
return 0;
}

/*makefile 涉及到变量和伪目标,具体概念都在下文*/
TARGET = main
cc = gcc
FILE = main.c func.o

TARGET: $(FILE)
$(CC) $(FILE) -w -o $(TARGET)
func.o: func.c
$(CC) -c func.c

.PHONY: clean
clean:
rm $(TARGET) *.o

  如下图,执行make命令,可以看到gcc命令由下向上执行(因为从最内层依赖项开始生成),生成了最终目标文件main,执行main发现func函数正常被调用,说明make成功。
3
  如下图,执行make clean命令,可以看到rm命令被正常执行,删除了main文件和所有.o后缀的文件。说明伪目标clean建立成功。自此该多源文件例子已经完成。
4

二、变量相关

1. 基础

变量简介:
  makefile脚本中可以引入变量来使得编写更加简便以及清晰。变量的声明非常简单,格式为 变量名 = 值 ;而调用变量的格式则为 $(变量名), 由此即可使用变量。

一个简单的例子:
  对上文单文件例子中的makefile进行简单修改,引入一些变量。修改如下:

1
2
3
4
5
6
7
8
9
10
/*修改前*/
main: main.c
gcc main.c -o main

/*修改后*/
TARGET = main
cc = gcc
FILE = main.c
$(TARGET): $(FILE)
$(CC) $(FILE) -o $(TARGET)

  如下图,在修改完makefile后,make仍可正常进行。通过变量,可以方便日后的修改,比如说想把gcc换成g++,就可以把编译器设为变量,想更换编译器时直接修改变量值即可,可以大大减少修改量。
在这里插入图片描述

2. 一些变量相关符号

符号 意义
= 是最基本的赋值
:= 是覆盖之前的值
?= 是如果没有被赋值过就赋予等号后面的值
+= 是添加等号后面的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
之前一直纠结makefile中“=”和“:=”的区别到底有什么区别,因为给变量赋值时,两个符号都在使用。
看一篇博客,无意中发现作者对于这个问题做了很好的解答。

1、“=”

make会将整个makefile展开后,再决定变量的值。
也就是说,变量的值将会是整个makefile中最后被指定的值。看例子:

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

在上例中,y的值将会是 xyz bar ,而不是 foo bar 。

2、“:=”

“:=”表示变量的值决定于它在makefile中的位置,而不是整个makefile展开后的最终值。

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

在上例中,y的值将会是 foo bar ,而不是 xyz bar 了。

三、伪目标

  通常makefile中第一个目标为最终目标,后续目标和最终目标有依赖关系。但是有时候想要执行清空生成的文件等一些单独执行的命令时,很明显这些命令并不会生成目标文件,由此和生成最终目标也没有必要关系,需要与普通的command进行区分,这时就出现了伪目标的概念。
  伪目标不是一个输出文件,而是一个标签。在执行make指令时,并不会主动执行伪目标的命令(因为伪目标没有依赖项),想要执行伪目标就必须使用命令 make 伪目标名 或者把伪目标放到makefile最上面。而显式声明伪目标的语法为 .PHONY: 伪目标名,随后设定调用此伪目标时执行的命令即可。

一个简单的例子:
  对上文单文件例子中的makefile进行简单修改(多文件例子中已经存在clean),在末尾引入伪目标,使其可以删除掉生成的main可执行文件。修改如下:

1
2
3
4
5
6
7
8
9
/*修改后*/
TARGET = main
cc = gcc
FILE = main.c
$(TARGET): $(FILE)
$(CC) $(FILE) -o $(TARGET)
.PHONY: clean
clean:
rm $(TARGET)

  如下图,在执行伪目标clean后,删除掉了设定好的最终目标main。在make后,再次生成最终目标main文件。
在这里插入图片描述

四、make嵌套执行

  在大的工程会把源文件分为很多个目录,为了逻辑上的简单,会为每个子目录写一个makefile文件,而最上层的makefile文件被称为总控makefile。通过执行总控makefile,即可自动执行下层的makefile文件,从而使得项目总体进行make操作。

下面是一个例子:
  我在当前文件夹内创建了三个新文件夹:main存放main.c源码、func存放func.h/c源码、build中存放obj文件和最终可执行文件。并且三个文件夹内都有自己的makefile文件,而当前文件夹下有总控makefile文件。源码如下:

  • ./func:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    /* func.h */
    #ifndef _FUNC_H_
    #define _FUNC_H_

    int func(int a, int b);

    #endif

    /* func.c */
    #include"func.h"

    int func(int a, int b)
    {
    return a + b;
    }

    /* makefile */
    CC = gcc

    func.o: func.c
    $(CC) -c func.c -o func.o
    cp func.o ../build/func.o

    .PHONY: clean
    clean:
    rm *.o
  • ./main:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    /* main.c */
    #include<stdio.h>
    #include"../func/func.h"

    int main()
    {
    printf("HelloWorld\n1 + 2 = %d\n", func(1, 2));
    return 0;
    }

    /* makefile */
    CC = gcc

    main.o: main.c
    $(CC) -c main.c -o main.o
    cp main.o ../build/main.o

    .PHONY: clean
    clean:
    rm *.o
  • ./build:
1
2
3
4
5
6
7
8
CC = gcc

test: func.o main.o
$(CC) func.o main.o -I ../func -o test

.PHONY: clean
clean:
rm test *.o
  • ./:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    .PHONY: all
    all:
    cd func;make
    cd main;make
    cd build;make

    .PHONY: clean
    clean:
    cd build;make clean
    cd main;make clean
    cd func;make clean
    5

  可以看到上面四部分文件存在一个总控makefile和三个子makefile,通过make最终在build文件夹内生成可执行文件test,而test输出HelloWorld已经一个简单的加法式子。
6
  由上图可以看到,在执行make命令后,其按顺序依次进入各个文件夹并执行make命令(蓝框内),在make完成后,挨个查看各个文件夹,发现文件均正常生成。最后执行可执行文件test,发现正常执行,说明本次make成功。
7
  由上图可以看到,在执行make clean命令后,其按顺序进入各个文件夹并执行make clean命令,随后相关文件均被清除。自此该例子完成,其已经实现了make嵌套的功能。

五、条件判断

1. ifeq / ifneq

关键字 作用
ifeq 判断参数是否不相等,相等为 true,不相等为 false。
ifneq 判断参数是否不相等,不相等为 true,相等为 false。
  • 使用方式如下,ifeqifneq使用方法相同。
1
2
3
4
5
ifeq (ARG1, ARG2)
ifeq 'ARG1' 'ARG2'
ifeq "ARG1" "ARG2"
ifeq "ARG1" 'ARG2'
ifeq 'ARG1' "ARG2"
  • 例子如下,例子中进行判定,若编译器为gcc则链接gnu库,否则不链接库。其中用到了ifeqelseendif
1
2
3
4
5
6
7
8
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

2. ifdef / ifndef

关键字 作用
ifdef 判断是否有值,有值为 true,没有值为 false。
ifndef 判断是否有值,没有值为 true,有值为 false。
  • 使用方法如下,ifdefifndef 使用方法相同。
1
ifdef VARIABLE-NAME
  • 两个例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*  例1  */
bar =
foo = $(bar)
all:
ifdef foo
@echo yes
else
@echo no
endif

/* 例2 */
foo =
all:
ifdef foo
@echo yes
else
@echo no
endif

  通过执行make可以发现例1打印的结果是yes,例2打印的结果是no。其原因就是例1中变量foo的定义是foo = $(bar),虽然变量bar的值为空,但是ifdef的判断结果为真。这种方式判断显然是不行的,所以当需要判断一个变量的值是否为空的时候需要使用ifeq而不是ifdef。如下即可进行判空:

1
#ifeq(变量,) 

六、shell特殊变量

变量名 意义
$@ 表示目标文件
$^ 表示所有的依赖文件
$< 表示第一个依赖文件
$? 表示比目标还要新的依赖文件列表

七、通配符

  在makefile文件中也可以使用通配符来模糊指定,比如说上面就存在rm *.o来删除所有的.o文件,通配符的具体用法如下:

通配符 使用说明
* 匹配0个或者是任意个字符
? 匹配任意一个字符
[] 可以指定匹配的字符放在 “[]” 中
  但是有一点需要注意,当需要在变量中使用通配符时,比如想要用变量OBJ来指向所有.c文件,写成OBJ = *.c是错误的,因为这里是把OBJ的值设为一个叫*.c的文件。这里需要使用关键字wildcard来告诉系统这里用到了通配符,所以正确写法是OBJ = $(wildcard *.c),这算是通配符相关的一个重点。
  • 还有一个和通配符*相类似的字符,这个字符是%,也是匹配任意个字符,使用在相关规则当中。
    1
    2
    3
    4
    test:test.o test1.o
    gcc -o $@ $^
    %.o:%.c
    gcc -o $@ $^
      如上面这个例子, %.o把需要的所有的.o文件组合成为一个列表,从列表中挨个取出的每一个文件,则%表示取出来文件的文件名(不包含后缀),然后找到文件中和 %名称相同的.c文件,执行下面的命令,直到列表中的文件全部被取出来为止。
      这个属于Makefile中静态模规则:规则存在多个目标,并且不同的目标可以根据目标文件的名字来自动构造出依赖文件。与多规则目标的意思相近,但是又不相同。

八、目标文件搜索

1. 一般搜索 VPATH

语法如下:

1
2
3
4
5
6
7
8
9
10
11
//单文件:
VPATH := src

//多文件:
VPATH := src car
VPATH := src:car

//例子:
VPATH=src car
test:test.o
gcc -o $@ $^

  注意:无论你定义了多少路径,make 执行的时候会先搜索当前路径下的文件,当前目录下没有我们要找的文件,才去 VPATH 的路径中去寻找。如果当前目录下有我们要使用的文件,那么 make 就会使用我们当前目录下的文件。

2. 选择搜索vpath

区别:VPATH 是搜索路径下所有的文件,而 vpath 更像是添加了限制条件,会过滤出一部分再去寻找。语法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
vpath支持模糊搜索
%.c 代表.c文件

//单文件:
vpath test.c src //在src目录下寻找test.c文件

//多文件:
vpath test.c src car //在src car目录下寻找test.c文件
vpath test.c src : car

//清除符合文件 test.c 的搜索目录
vpath test.c

//清除所有已被设置的文件搜索路径
vpath

九、include文件包含

  当 make 读取到 include关键字的时候,会暂停读取当前的 Makefile,而是去读include包含的文件,读取结束后再继读取当前的 Makefile 文件。其使用的具体方式如下:

1
include <filenames>

其中 filenames 是 shell 支持的文件名(可以使用通配符表示的文件)。

  使用时,通常用-include来代替include来忽略文件不存在或者是无法创建的错误提示,使用格式如下:

1
-include <filename>

使用方法和 “include” 的使用方法相同。

  • 使用include <filenames>,make 在处理程序的时候,文件列表中的任意一个文件不存在的时候或者是没有规则去创建这个文件的时候,make 程序将会提示错误并保存退出。
  • 使用-include <filenames>,当包含的文件不存在或者是没有规则去创建它的时候,make 将会继续执行程序,只有真正由于不能完成终极目标重建的时候我们的程序才会提示错误保存退出。

include通常使用在以下的场合:

  • 在一个工程文件中,每一个模块都有一个独立的 Makefile 来描述它的重建规则。它们需要定义一组通用的变量定义或者是模式规则。通用的做法是将这些共同使用的变量或者模式规则定义在一个文件中,需要的时候用include包含这个文件。
  • 当根据源文件自动产生依赖文件时,我们可以将自动产生的依赖关系保存在另一个文件中。然后在 Makefile 中包含这个文件。

十、字符串处理函数

1. patsubst

1
2
3
4
5
6
7
8
9
$(patsubst <pattern>,<replacement>,<text>)

函数说明:函数功能是查找 text 中的单词是否符合模式 pattern,如果匹配的话,
则用 replacement 替换。返回值为替换后的新字符串。

实例: //执行 make 命令,我们可以得到的值是 "1.o 2.o 3.o"
OBJ=$(patsubst %.c,%.o,1.c 2.c 3.c)
all:
@echo $(OBJ)

2. subst

1
2
3
4
5
6
7
8
 $(subst <from>,<to>,<text>)

函数说明:函数的功能是把字符串中的 form 替换成 to,返回值为替换后的新字符串。

实例: //执行 make 命令,我们得到的值是"fEEt on the strEEt"
OBJ=$(subst ee,EE,feet on the street)
all:
@echo $(OBJ)

3. strip

1
2
3
4
5
6
7
8
9
 $(strip <string>)

函数说明:函数的功能是去掉字符串的开头和结尾的字符串,
并且将其中的多个连续的空格合并成为一个空格。返回值为去掉空格后的字符串。

实例: //执行完 make 之后,结果是“a b c”
OBJ=$(strip a b c)
all:
@echo $(OBJ)

4. findstring

1
2
3
4
5
6
7
8
9
 $(findstring <find>,<in>)

函数说明:函数的功能是查找 in 中的 find ,如果我们查找的目标字符串存在。
返回值为目标字符串,如果不存在就返回空。

实例: //执行 make 命令,得到的返回的结果就是"a"
OBJ=$(findstring a,a b c)
all:
@echo $(OBJ)

5. filter

1
2
3
4
5
6
7
8
9
 $(filter <pattern>,<text>)

函数说明:函数的功能是过滤出 text 中符合模式 pattern 的字符串,
可以有多个 pattern 。返回值为过滤后的字符串。

实例: //执行 make 命令,我们得到的值是"1.c 2.o"
OBJ=$(filter %.c %.o,1.c 2.o 3.s)
all:
@echo $(OBJ)

6. filter-out

1
2
3
4
5
6
7
8
9
 $(filter-out <pattern>,<text>)

函数说明:函数的功能是功能和 filter 函数正好相反,但是用法相同。
去除符合模式 pattern 的字符串,保留符合的字符串。返回值是保留的字符串。

实例: //执行 make 命令,打印的结果是"3.s"
OBJ=$(filter-out 1.c 2.o ,1.o 2.c 3.s)
all:
@echo $(OBJ)

7. sort

1
2
3
4
5
6
7
8
9
10
 $(sort <list>)

函数说明:函数的功能是将 <list> 中的单词排序(升序)。返回值为排列后的字符串。

实例: //执行 make 命令,我们得到的值是"bar foo lost"
OBJ=$(sort foo bar foo lost)
all:
@echo $(OBJ)

注意:sort会去除重复的字符串。

8. word

1
2
3
4
5
6
7
8
9
 $(word <n>,<text>)

函数说明:函数的功能是取出函数 <text> 中的第n个单词。
返回值为我们取出的第 n 个单词。

实例: //执行 make 命令,我们得到的值是"2.c"
OBJ=$(word 2,1.c 2.c 3.c)
all:
@echo $(OBJ)

十一、一些小东西

1. @echo

  在makefile中执行echo命令(例echo "HelloWorld")时,会出现回显情况,即先输出一次echo命令(echo "HelloWorld"),再输出相关内容(HelloWorld)。
  为了避免这种情况,可以使用@echo命令(例@echo "HelloZHJ"),此时就只会输出内容(HelloZHJ)了。