Makefile 初探

当项目比较小的时候,我们可以手写gcc编译命令编译源文件,但是当工程稍微大一点,就有点晕了,所以Makefile闪亮登场,用来完成大型工程的编译工作。Makefile中定义了一系列规则,哪些文件先编译,哪些文件后编译,哪些文件需要重新编译,甚至更加复杂的操作,Makefile就像一个shell脚本,包含了一系列的操作,比如编译命令,也可以执行操作系统的命令,Makefile的好处就是自动化编译,一旦写好,只需要一个make命令,整个工程自动编译。

示例

假设有三个.c文件,两个.h文件,如下所示

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

int main(void)
{
    printf("This is main function in main file.\n");
    print1();
    print2();
    return 0;
}

/* file1.c */
#include <stdio.h>

void print1(void)
{
    printf("This is print1 function in file1 file.\n");
}

/* file1.h */
#ifndef _FILE_1_H
#define _FILE_1_H

void print1(void);

#endif

/* file2.c */
#include <stdio.h>

void print2(void)
{
    printf("This is print2 function in file2 file.\n");
}

/* file2.h */
#ifndef _FILE_2_H
#define _FILE_2_H

void print2(void);

#endif

为了自动编译上述文件,Makefile应该如下所示

main: main.o file1.o file2.o
    gcc -o main main.c file1.c file2.c

main.o: main.c file1.h file2.h
    gcc -c main.c

file1.o: file1.c file1.h
    gcc -c file1.c

file2.o: file2.c file2.h
    gcc -c file2.c

clean:
    @echo "cleanning project"
    rm main *.o
    @echo "clean completed"

通过Makefile,只需要在terminal中输入make,就会实现自动编译,生成目标文件以及可执行文件,下面就开始解释下规则

Makefile 规则

target ... : prerequisites ...
    command

规则由三部分组成,target(目标)、prerequisites(先决条件)、command(命令)

  • target: 个目标文件,可以是 Object File,也可以是执行文件。还可以是一个标签(Label),对于标签这种特性,在后续叙述
  • prerequisites: 就是,要生成那个 target 所需要的文件或是目标。
  • command: make 需要执行的命令。(任意的 Shell 命令)

也就是说,这是一个文件的依赖关系,target中的目标文件依赖于prerequisites中的问题件,其生成规则定义在command中。说白一点就是说,prerequisites中如果有一个以上的文件比target文件要新的话,command所定义的命令就会被执行。

Makefile是怎样工作的

默认情况下,我们输入make命令

  • make 会在当前目录下找名字叫“Makefile”或“makefile”的文件.
  • 如果找到,它会找文件中的第一个目标文件(target),在上面的例子中,他会找到main这个文件,并把这个文件作为最终的目标文件,第一个目标为缺省目标。
  • 如果 main 文件不存在,或是 main 所依赖的后面的 .o文件的文件修改时间要比 main这个文件新,那么,他就会执行后面所定义的命令来生成 main 这个文件。
  • 如果 main 所依赖的.o文件也不存在,那么 make 会在当前文件中找目标为.o文件的依赖性,如果找到则再根据那一个规则生成.o文件。(这有点像一个堆栈的过程)
  • 当然源文件.c及.h文件都存在,故会生成.o文件,然后生成目标文件即可执行文件main

Makefile就是通过文件的依赖性,完成自动编译的工作,如果在找寻过程中,出现错误,make就会直接退出并报错,而像clean这种目标,没有被第一个目标文件所关联,那么将不会执行,不过可以显示执行,比如输入命令make clean,完成清除工作。

Makefile中使用变量

在Makefile中可以使用变量简化文件,比如Makefile中

main: main.o file1.o file2.o
    gcc -o main main.c file1.c file2.c

我们可以使用一个变量带表示obj文件

objects = main.o file1.o file2.o

则上述规则就变为

objects = main.o file1.o file2.o
main: $(objects)
    gcc -o main $(objects)

make自动推导

GNU 的 make很强大,它可以自动推导文件以及文件依赖关系后面的命令,于是我们就没必要去在每一个[.o]文件后都写上类似的命令,因为,我们的 make会自动识别,并自己推导命令。

只要 make 看到一个[.o]文件,它就会自动的把[.c]文件加在依赖关系中,如果 make找到一个 whatever.o,那么 whatever.c,就会是 whatever.o 的依赖文件。并且 cc -c whatever.c也会被推导出来,于是,我们的 makefile 再也不用写得这么复杂。我们的是新的 makefile 又出炉了。

objects = main.o file1.o file2.o

main: $(objects)
    gcc -o main $(objects)

main.o: file1.h file2.h

file1.o: file1.h

file2.o: file2.h

.PHONY: clean
clean:
    @echo "cleanning project"
    -rm main *.o
    @echo "clean completed"

这种方法,就是make的隐晦规则。

上面的.PHONY表示clean是个伪目标, 伪目标不是一个文件,只是一个标签,所以make无法生成它的依赖关系和决定它是否执行,当然,为了避免和文件重名,使用特殊标记.PHONY显示指明一个目标是伪目标。

命令rm main *.o前面的-表示执行命令的过程中可能某些文件出现问题,但是忽略它,继续做后面的事。

Makefile 组成

  • 显示规则:说明了如何生成一个或多个的目标文件,由书写者明显指出,要生成的文件,文件的依赖,生成的命令
  • 隐晦规则:make有自动推导的功能,所以隐晦规则可以使我们比较粗略的书写Makefile
  • 变量定义:在Makefile中可以定义一系列变量,变量一般都是字符串,有点类似于C语言的宏,当Makefile被执行时,其中的变量都会被扩展到相应的引用位置上
  • 文件指示:包括三部分
    • 在一个Makefile中引用另一个Makefile,类似于C语言的include
    • 根据某些情况指定Makefile中的有效部分,类似于C语言的条件编译
    • 定义一个多行的命令
  • Makefile只有行注释,#字符,类似于C/C++的//,如果文件中使用#,可以使用转义符#,Makefile中的命令必须以[Tab]键开始

make的工作方式

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

自动生成依赖性

大型工程中可能无法准确得知一个目标的所有依赖文件,并且修改源文件的时候,也要小心的修改Makefile,幸好C/C++编译器支持- M选项,即自动找寻源文件包含的头文件,并生成一个依赖关系。 比如

gcc -M main.c # output: main.o: main.c file1.h file2.h

这样就不必在手动书写若干文件的依赖关系,而是由编译器自动生成,但是如果使用GNU的C/C++编译器,要使用-MM参数,否则会将标准头文件也包含进来

通配符

通配符(wildcard)用来指定一组符合条件的文件名。Makefile 的通配符与 Bash 一致,主要有星号()、问号(?)和 […] 。比如, .o 表示所有后缀名为o的文件。

使用变量

变量基础

变量在声明时需要给予初值,而在使用时,需要给在变量名前加上 ( ) 使 字符,那么你需要用$$来表示

objects = program.o foo.o utils.o
program : $(objects)

变量中的变量

Makefile中有两种方式用变量定义变量的值

= 方法

使用=符号,符号左侧是变量,右侧是变量的值,且右侧变量的值可以定义在文件的任何一处,即可以使用后面定义的值

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

all:
    echo $(foo)

执行make all命令,会打印变量$(foo)的值为Huh?,可见变量可以使用后面的变量定义

但有一个权问题就是可能会导致无限循环的变量展开,当然make有能力检测这样的定义,并会报错

:=方法

使用:=操作符,但是前面的变量定义不能使用后面的变量,只能使用签名已经定义好了的变量

?=操作符
FOO ?= bar # 表示如果FOO未定义,则为bar,否则什么也不错
+=操作符
objects = main.o foo.o bar.o utils.o
objects += another.o  # objects值为 main.o foo.o bar.o utils.o another.o

条件判断

条件表达式的语法为

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

其中表示条件关键字,有四个

  • ifeq(, ): 表示值同为真,值异为假
  • ifneq (, ): 表示值异为真
  • ifdef : 变量值非空,为真
  • ifndef : 与上相反

使用函数

函数调用语法

$(<function> <arguments>) 或者${<function> <arguments>}

字符串处理函数

subst

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

字符串替换函数,把字串中的字符串替换成, 返回被替换后的字符串

比如$(subst ee,EE,feet on the street) ==> fEEt on the strEEt

patsubst 函数

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

模式字符串替换函数, 查找

shell 函数

Shell函数用来执行shell命令,并将系统命令后的输出作为函数返回。

contents := $(shell cat foo)

注意,这个函数会新生成一个shell程序来执行命令,所以要注意运行性能,不要使用太过复杂的规则

隐含规则

隐含规则是一种惯例,迎来运行我们没有明确的规则,隐含规则会使用一些系统变量,我们可以改变系统变量的值定制隐含规则的运行时参数。

隐晦规则命令变量

变量 含义
AR 函数库打开包程序。默认命令是“ar”
AS 汇编语言编译程序。默认命令是“as”
CC C语言编译程序。默认命令是“cc”
CXX C++语言编译程序。默认命令是“g++”
CO 从RCS文件中扩展文件程序。默认命令是“co”
CPP C程序的预处理器(输出是标准输出设备)。默认命令是“$(CC) -E”
FC Fortran和Ratfor的编译器和预处理程序。默认命令是”f77”
GET 从SCCS文件扩展文件的程序。默认命令是“get”
LEX Lex方法分析器程序(针对于C或Ratfor)。默认命令是”lex”
PC Pascal语言编译程序。默认命令是”pc”
YACC Yacc文法分析器(针对C程序)。默认命令是“yacc”
YACCR Yacc文法分析器(针对Ratfor程序)。默认命令是“yacc -r”
MAKEINFO 转换Texinfo源文件(.texi)到info文件程序。默认命令是“makeinfo”
TEX 从TeX源文件创建TeX DVI文件的程序。默认命令是“tex”
WEAVE 转化Web到TeX的程序。默认命令是“weave”
TEXI2DVI 从Texinfo源文件创建TeX DVI文件的程序。默认命令是“texi2dvi”
CWEAVE 转化C Web到TeX的程序。默认命令是“cweave”
TANGLE 转换Web到Pascal语言的程序,默认命令是”tangle“
CTANGLE 转换C Web到C。默认命令是”ctangle“
RM 删除文件命令。默认命令是”rm -f“

隐晦规则命令参数变量

下面的这些变量都是相关上面命令的参数

变量 含义
ARFLAGS 函数库打包程序AR命令的参数。默认值是“rv”
ASFLAGS 汇编语言编译参数(当明显地调用”.s”或”.S”文件时)
CFLAGS C语言编译器参数
CXXFLAGS C++语言编译器参数
COFLAGS RCS命令参数
CPPFLAGS C预处理器参数(C和Fortran编译器也会用到)
FFLAGS Fortran语言编译器参数
GFLAGS SCCS ”get“程序参数
LDFLAGS 连接器参数(如“ld”)
LFLAGS Lex文法分析器参数
PFLAGS Pascal语法编译器参数
RFLAGS Ratfor程序的Fortran编译器参数
YFLAGS Yacc文法分析器参数

模式规则

模式规则用来定义一个隐含规则,在规则中,需要有%字符,表示一个或多个任意字符,在依赖目标中同样可以使用%,只是依赖目标中的%取值,取决于其目标

%的展开发生在变量和函数的展开之后,变量和函数的展开发生在make载入Makefile时,而模式规则的%发生在运行时

模式规则示例
%.o: %.c
    $(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@

该例表示把所有的[.c]文件都编译成[.o]文件,其中, @ <表示了所有依赖目标的挨个值。这些奇怪的变量我们叫”自动化变量”,

自动化变量

把模式中所定义的一系列的文件自动的逐个取出,直至所有的符合模式的文件都取完了。

变量 含义
$@ 表示规则中的所有目标文件的集合。在模式规则中如果有多个目标,“$@”就是匹配于目标中模式定义的集合
$% 仅当目标是函数库文件时,表示规则中的目标成员名,如果目标不是函数库文件(UNIX下是.a,Windows是.lib),其值为空。
$< 依赖目标中的第一个目标名字,如果依赖目标是以模式(即”%“)定义的,则”$<”是符合模式的一系列的文件集
$? 所有比目标新的依赖目标的集合,以空格分隔
$^ 所有依赖目标的集合,以空格分隔。如如果在依赖目标中有多个重复的,则自动去除重复的依赖目标,只保留一份
$+ 同”$^”,也是所有依赖目标的集合,只是它不去除重复的依赖目标。
$* 目标模式中“%”及其之前的部分
$(@D) “@”的目录部分(不以斜杠作为结尾),如果”@”中没有包含斜杠,其值为“.”(当前目录)
$(@F) “@”的文件部分,相当于函数”(notdir $@)”
$(*D) 同”$(@D)”,取文件的目录部分
$(*F) 同”$(@F)”,取文件部分,但不取后缀名
$(%D) 函数包文件成员的目录部分
$(%F) 函数包文件成员的文件名部分
$( 依赖目标中的第一个目标的目录部分
$( 依赖目标中的第一个目标的文件名部分
$(^D) 所有依赖目标文件中目录部分(无相同的)
$(^F) 所有依赖目标文件中文件名部分(无相同的)
$(+D) 所有依赖目标文件中的目录部分(可以有相同的)
$(+F) 所有依赖目标文件中的文件名部分(可以有相同的)
$(?D) 所有被更新文件的目录部分
$(?F) 所有被更新文件的文件名部分

Tips

  • 文件名最好使用Makefile或者makefile
  • 伪目标可以避免和同名文件冲突,并且已知’.PHONY为伪目标,非时间文件,make会跳过隐含规则搜索,会改善性能
  • 面试经常问到赋值操作符=和:=的区别
    • = :可以先使用后定义,这就导致makefile 在全部展开后才能决定变量的值,有可能出现循环递归,无法展开的问题
    • :=:必须先定义后使用,在当前的位置就可以决定变量的值

由于才疏学浅,可能会有些许遗漏和插座,后续会更正修补

参考

猜你喜欢

转载自blog.csdn.net/u011221820/article/details/79767521