Makefile基础

一个工程文件的编译规则,描述了整个工程的编译和链接等规则。其中包含了那些文件需要编译,那些文件不需要编译,那些文件需要先编译,那些文件需要后编译,那些文件需要重建等等.

Makefile 可以使得我们的项目工程的编译变得自动化,不需要每次都手动输入一堆源文件和参数。

Makefile 描述的是文件编译的相关规则,它的规则主要是两个部分组成,分别是依赖的关系执行的命令,结构如下:

targets : prerequisites
    command

或者

targets : prerequisites; command
    command
  • targets:规则的目标,可以是 Object File(一般称它为中间文件),也可以是可执行文件,还可以是一个标签;
  • prerequisites:是我们的依赖文件,要生成 targets 需要的文件或者是目标。可以是多个,也可以是没有;
  • command:make 需要执行的命令(任意的 shell 命令)。可以有多条命令,每一条命令占一行。

注意: 我们的目标和依赖文件之间要使用冒号分隔开,command 的开始一定要使用Tab键

示例:

test:test.c
    gcc -o test test.c

上述代码实现的功能就是编译 test.c 文件,通过这个实例可以详细的说明 Makefile 的具体的使用。其中 test 是的目标文件,也是我们的最终生成的可执行文件。依赖文件就是 test.c 源文件,重建目标文件需要执行的操作是gcc -o test test.c。这就是 Makefile 的基本的语法规则的使用。

工作流程

当我们在执行 make 条命令的时候,make 就会去当前文件下找要执行的编译规则,也就是 Makefile 文件。我们编写 Makefile 的时可以使用的文件的名称 "GNUmakefile" 、"makefile" 、"Makefile" ,make 执行时回去寻找 Makefile 文件,找文件的顺序也是这样的。

main:main.o test1.o test2.o
    gcc main.o test1.o test2.o -o main
main.o:main.c test.h
    gcc -c main.c -o main.o
test1.o:test1.c test.h
    gcc -c test1.c -o test1.o
test2.o:test2.c test.h
    gcc -c test2.c -o test2.o

我们编译项目文件的时候,默认情况下,make 执行的是 Makefile 中的第一规则(Makefile 中出现的第一个依赖关系),此规则的第一目标称之为“最终目标”或者是“终极目标”。

在 shell 命令行执行的 make 命令,就可以得到可执行文件 main 和中间文件 main.o、test1.o 和 test2.o,main 就是我们要生成的最终文件。通过 Makefile 我们可以发现,目标 main"在 Makefile 中是第一个目标,因此它就是 make 的终极目标,当修改过任何 C 文件后,执行 make 将会重建终极目标 main。

它的具体工作顺序是:当在 shell 提示符下输入 make 命令以后。 make 读取当前目录下的 Makefile 文件,并将 Makefile 文件中的第一个目标作为其执行的“终极目标”,开始处理第一个规则(终极目标所在的规则)。在我们的例子中,第一个规则就是目标 "main" 所在的规则。规则描述了 "main" 的依赖关系,并定义了链接 ".o" 文件生成目标 "main" 的命令;make 在执行这个规则所定义的命令之前,首先处理目标 "main" 的所有的依赖文件(例子中的那些 ".o" 文件)的更新规则(以这些 ".o" 文件为目标的规则)。

对这些 ".o" 文件为目标的规则处理有下列三种情况

  • 目标 ".o" 文件不存在,使用其描述规则创建它;
  • 目标 ".o" 文件存在,目标 ".o" 文件所依赖的 ".c" 源文件 ".h" 文件中的任何一个比目标 ".o" 文件“更新”(在上一次 make 之后被修改)。则根据规则重新编译生成它;
  • 目标 ".o" 文件存在,目标 ".o" 文件比它的任何一个依赖文件(".c" 源文件、".h" 文件)“更新”(它的依赖文件在上一次 make 之后没有被修改),则什么也不做。

通过上面的更新规则我们可以了解到中间文件的作用,也就是编译时生成的 ".o" 文件。作用是检查某个源文件是不是进行过修改,最终目标文件是不是需要重建。我们执行 make 命令时,只有修改过的源文件或者是不存在的目标文件会进行重建,而那些没有改变的文件不用重新编译,这样在很大程度上节省时间,提高编程效率。小的工程项目可能体会不到,项目工程文件越大,效果才越明显。

当然 make 命令能否顺利的执行,还在于我们是否制定了正确的的依赖规则,当前目录下是不是存在需要的依赖文件,只要任意一点不满足,我们在执行 make 的时候就会出错。所以完成一个正确的 Makefile 不是一件简单的事情。

清除工作目录中的过程文件:

以在编写 Makefile 文件的时候会在末尾加上这样的规则语句:

.PHONY:clean
clean:
    rm -rf *.o test

其中 "*.o" 是执行过程中产生的中间文件,"test" 是最终生成的执行文件。我们可以看到 clean 是独立的,它只是一个伪目标(在《Makefile伪目标》的章节中详细介绍),不是具体的文件。不会与第一个目标文件相关联,所以我们在执行 make 的时候也不会执行下面的命令。在shell 中执行 "make clean" 命令,编译时的中间文件和生成的最终目标文件都会被清除,方便我们下次的使用。

编译与链接

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

  • 编译:编译时,编译器需要的是语法的正确,函数与变量的声明的正确。对于后者,通常是你需要告诉编译器头文件的所在位置(头文件中应该只是声明,而定义应该放在C/C++文件中),只要所有的语法正确,编译器就可以编译出中间目标文件。一般来说,每个源文件都应该对应于一个中间目标文件(O文件或是OBJ文件)。
  • 链接: 主要是链接函数和全局变量,所以,我们可以使用这些中间目标文件(O文件或是OBJ文件)来链接我们的应用程序。链接器并不管函数所在的源文件,只管函数的中间目标文件(Object File),在大多数时候,由于源文件太多,编译生成的中间目标文件太多,而在链接时需要明显地指出中间目标文件名,这对于编译很不方便,所以,我们要给中间目标文件打个包,在Windows下这种包叫“库文件”(Library File),也就是 .lib 文件,在UNIX下,是Archive File,也就是 .a 文件。

变量

Makefile 文件中定义变量的基本语法如下:

变量的名称=值列表

变量的名称可以由大小写字母、阿拉伯数字和下划线构成。等号左右的空白符没有明确的要求,因为在执行 make 的时候多余的空白符会被自动的删除。至于值列表,既可以是零项,又可以是一项或者是多项。如:

OBJ=main.o test.o test1.o test2.o
test:$(OBJ)
      gcc -o test $(OBJ)

变量的来源

  • 文件:变量可以被定义在makefile中,或是被makefile引入(include指令)
  • 命令行: 可以直接在make命令行上定义或者重新定义变量: $ make CFLAG=-g CPPFLAGS='DBSD -DDEBUG',每个命令行参数中所包含的等号,都是一个变量赋值运算符。在命令行上,每个变量赋值运算符的右边部分必须是一个单独的shell参数。如果变量的值含有空格,则必须为参数加上括号或是引号。 命令行上变量的赋值将会覆盖掉环境变量以及makefile中的赋值结果。如果要使makefile中的变量覆盖命令行中的变量,可以在makefile中的变量前加override指令。
  • 环境: 当make启动时,所有来自环境的变量都会被定义为make的变量。这些变量具有非常低的优先级,makefile文件或命令行参数的赋值结果都会覆盖环境变量的值。但是,可以使用--environment-overrides(或-e)命令行选项,让环境变量覆盖相应的makefile变量。当make被递归调用时,有若干来自上层make的变量会通过环境传递给下层的make。默认只有原来就来自环境的变量会被导出到下层的环境中。可以使用export指令导出任何变量。
  • 自动创建: make会在执行一个规则的命令脚本之前立即创建自动变量。注意自动变量创建的时机。

变量的赋值

知道了如何定义,下面我们来说一下 Makefile 的变量的四种基本赋值方式:

  • 简单赋值 ( := ) 编程语言中常规理解的赋值方式,只对执行操作后的语句有效。
x:=foo
y:=$(x)b
x:=new
test:
      @echo "y=>$(y)"
      @echo "x=>$(x)"

在 shell 命令行执行make test我们会看到:

y=>foob
x=>new
  • 递归赋值 ( = ) 赋值语句可能影响多个变量,所有目标变量相关的其他变量都受影响。
x=foo
y=$(x)b
x=new
test:
      @echo "y=>$(y)"
      @echo "x=>$(x)"

在 shell 命令行执行make test我们会看到:

y=>newb
x=>new
  • 条件赋值 ( ?= ) 如果变量未定义,则使用符号中的值定义变量。如果该变量已经赋值,则该赋值语句无效。
  • 追加赋值 ( += ) 原变量用空格隔开的方式追加一个新值。

变量的使用

调用变量的时候可以用 "$(VALUE_LIST)" 或者是 "${VALUE_LIST}" 来替换,这就是变量的引用

OBJ=main.o test.o test1.o test2.o
test:$(OBJ)
      gcc -o test $(OBJ)

打印变量

使用info/warning/error增加调试信息

  • 方法1: $(info, "here add the debug info")
    但是此不能打印出.mk的行号
  • 方法2: $(warning "here add the debug info")
  • 方法3: $(error "error: this will stop the compile"), 这个可以停止当前makefile的编译
  • 方法4: 打印变量的值 $(info $(TARGET_DEVICE) )
  • 使用echo增加调试信息(echo只能在target:后面的语句中使用,且前面是个TAB)
## 方法1: 
    @echo "start the compilexxxxxxxxxxxxxxxxxxxxxxx"

## 方法2: 
    @echo $(files)i

条件判断

条件语句可以根据一个变量的值来控制 make 执行或者时忽略 Makefile 的特定部分,条件语句可以是两个不同的变量或者是常量和变量之间的比较。
注意: 条件语句只能用于控制 make 实际执行的 Makefile 文件部分,不能控制规则的 shell 命令执行的过程。

ifeq 和 ifneq

ifeq 和 ifneq, 判断参数是否不相等,格式如下:

ifeq (ARG1, ARG2)
ifeq 'ARG1' 'ARG2'
ifeq "ARG1" "ARG2"
ifeq "ARG1" 'ARG2'
ifeq 'ARG1' "ARG2"

示例如下:

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

条件语句中使用到三个关键字“ifeq”、“else”、“endif”。其中:“ifeq”表示条件语句的开始,并指定一个比较条件(相等)。括号和关键字之间要使用空格分隔,两个参数之间要使用逗号分隔。参数中的变量引用在进行变量值比较的时候被展开。“ifeq”,后面的是条件满足的时候执行的,条件不满足忽略;“else”表示当条件不满足的时候执行的部分,不是所有的条件语句都要执行此部分;“endif”是判断语句结束标志,Makefile 中条件判断的结束都要有。

ifdef 和 ifndef

ifdef 和 ifndef 判断是否有值,使用格式如下:

ifdef VARIABLE-NAME

它的主要功能是判断变量的值是不是为空,

实例1:

bar =
foo = $(bar)
all:
ifdef foo
    @echo yes
else
    @echo  no
endif

实例2:

foo=
all:
ifdef foo
    @echo yes
else
    @echo  no
endif

通过两个实例对比说明:通过打印 "yes" 或 "no" 来演示执行的结果。我们执行 make 可以看到实例 1打印的结果是 "yes" ,实例 2打印的结果是 "no" 。其原因就是在实例 1 中,变量“foo”的定义是“foo = $(bar)”。虽然变量“bar”的值为空,但是“ifdef”的判断结果为真,这种方式判断显然是有不行的,因此当我们需要判断一个变量的值是否为空的时候需要使用“ifeq" 而不是“ifdef”。

注意:
在 make 读取 Makefile 文件时计算表达式的值,并根据表达式的值决定判断语句中的哪一个部分作为此 Makefile 所要执行的内容。因此在条件表达式中不能使用自动化变量,自动化变量在规则命令执行时才有效,更不能将一个完整的条件判断语句分卸在两个不同的 Makefile 的文件中。在一个 Makefile 中使用指示符 "include" 包含另一个 Makefile 文件。

伪目标

伪目标它并不会创建目标文件,只是想去执行这个目标下面的命令。伪目标的存在可以帮助我们找到命令并执行。

使用伪目标有两点原因:

  • 避免我们的 Makefile 中定义的只执行的命令的目标和工作目录下的实际文件出现名字冲突。
  • 提高执行 make 时的效率,特别是对于一个大型的工程来说,提高编译的效率也是我们所必需的。

使用场景1

我们先来看一下第一种情况的使用:

clean:
    rm -rf *.o test

当工作目录下不存在以 clean 命令的文件时,在 shell 中输入 make clean 命令,命令 rm -rf *.o test 总会被执行 ,这也是我们期望的结果。
如果当前目录下存在文件名为 clean 的文件时情况就会不一样了,当我们在 shell 中执行命令 make clean,由于这个规则没有依赖文件,所以目标被认为是最新的而不去执行规则所定义的命令。因此命令 rm 将不会被执行。

为解决上述问题,将一个目标声明称伪目标的方法是将它作为特殊的目标.PHONY的依赖,如

.PHONY:clean

这样 clean 就被声明成一个伪目标,无论当前目录下是否存在 clean 这个文件,当我们执行 make clean 后 rm 都会被执行。而且当一个目标被声明为伪目标之后,make 在执行此规则时不会去试图去查找隐含的关系去创建它。这样同样提高了 make 的执行效率,同时也不用担心目标和文件名重名而使我们的编译失败。

.PHONY:clean
clean:
    rm -rf *.o test

使用场景2

伪目标的另一种使用的场合是在 make 的并行和递归执行的过程中,此情况下一般会存在一个变量,定义为所有需要 make 的子目录。对多个目录进行 make 的实现,可以在一个规则的命令行中使用 shell 循环来完成。如下:

SUBDIRS=foo bar baz
subdirs:
    for dir in $(SUBDIRS);do $(MAKE) -C $dir;done

但是这种实现方法存在以下几个问题:

  • 当子目录执行 make 出现错误时,make 不会退出。就是说,在对某个目录执行 make 失败以后,会继续对其他的目录进行 make。
  • 另外一个问题就是使用这种 shell 循环方式时,没有用到 make 对目录的并行处理功能由于规则的命令时一条完整的 shell 命令,不能被并行处理。

Makefile调试

执行make时,添加-d参数

make -d

参考

posted @ 2021-11-09 20:46  可酷可乐  阅读(202)  评论(0编辑  收藏  举报