Makefile 学习整理

来源:互联网 发布:js动态删除指定tr 编辑:程序博客网 时间:2024/06/05 10:00
【说明】
    开发人员在阅读或编写 Makefile时,可能会记不起某些知识点。
    此文档目的是希望能起到提醒读者所遗忘的知识点的作用,故在整理时削弱了阅读流畅性,适用于对 Makefile有一定了解的开发人员

【版权声明】
    本文主体参考和整理自 陈皓 的文章《跟我一起写Makefile》。

【背景】
    Makefile决定了整个工程的编译规则。会不会写 Makefile从一个方面说明了一个人是否具备完成大型工程的能力。
    大多数 IDE都有 Makefile。比如 Delphi的 make,VisualC++的 nmake以及 Linux下GNU的 make。

【基本规则】
    Makefile文件包含 2 个部分:依赖关系生成目标文件的方法
    文件内容中的基本单元如下:
    <目标文件> : <依赖文件>    #依赖关系
    [TAB]<命令>    #生成目标文件的方法


    或者写作:
    <目标文件> : <依赖文件> ; <命令>    #将依赖关系和生成目标的方法写成 1 行,<依赖文件>和<命令>之间使用分号隔开

    当<依赖文件>中包含比<目标文件>更新的文件时,<命令>中的语句就会被执行。
    <命令>必须以 TAB 键开始。
    比如:
    study : study.o
    [TAB]gcc -o study study.o
    study.o : study.c study.h
    [TAB]gcc -c study.c
    clean :
    [TAB]rm -f *.o study

【解释器】
    <命令>的语法和shell script 的语法相同,所以同样使用脚本解释器进行解释。
    默认的解释器是 /bin/sh(UNIX的标准Shell)

【注释】
    Makefile只允许行注释。使用符号 # 表示注释的开始。

【变量】
    Makefile允许声明和使用变量,且变量名对大小写敏感。在对变量进行引用时使用小括号() 或花括号{} 将变量名括起来保证安全引用。
    比如我们可以把上面步骤中的Makefile内容修改成下方这样:
    studyNeeded = study.c study.h    #声明变量
    study : study.o
    [TAB]gcc -o study study.o
    study.o : $(studyNeeded)    #引用变量
    [TAB]gcc -c study.c
    clean:
    [TAB]rm -f *o study

    采取这种写法,如果 study.o 的依赖文件有变化,我们只需要修改变量 studyNeeded 的内容就可以了。


    变量赋值符号:

    ?=    条件赋值(conditional variable assignment)。如果变量已经赋值,则不要重新赋值;
    :=     立即赋值(immediately assignment)。变量取值立即展开;
    =      递归展开赋值(recursively expanded assignment)。该变量在使用的时候才对表达式进行展开。

    用法举例:
    nullstring :=
    space := $(nullstring) # 注释符号表明该行结束

    上面这 2 行将 space 定义为一个空格。$(nullstring)表明取变量值,#注释符表明变量定义终止。
    dir := /foo/bar    # 这种定义路径变量的方法包含了 4 个空格,用在 $(dir)/file 这样的情景中将导致意料之外的错误。

    Foo ?= bar
    这句变量声明的含义是,如果 Foo 没有被定义过,那么给变量 Foo 赋值为 bar,否则这条变量声明语句不执行。

    变量嵌套:
    x = y
    y = z
    z = u
    a := $($($(x)))
    上面 4 条语句执行完后,$(a)的值是u

    变量拼接:
    因为变量的值是字符串,所以多个变量的值可以直接拼接。
    a = first
    b = second
    all = $($a_$b)
    于是,$(all)的值就是first_second

    追加变量值:
    使用 += 操作符给变量追加值。如果被操作的变量之前没有定义,那么 += 操作符会进行定义。
    objects = main.o foo.o
    objects += new.o
    这 2 条语句执行之后,$(objects)变成 main.o foo.o new.o

    防止变量重写:
    变量也可以用于存储编译时的参数选项,例如 CFLAGS = -c -g
    有时会在运行 make 时传入命令行参数,为了使 Makefile 中的变量不会被同名的命令行参数替换,可以使用 override 关键字:
    override VAR = VALUE
    如果变量在定义时使用了 override 关键字,那么在向其追加值时也需要使用 override 关键字:
    override VAR += NEWVALUE

    目标变量:
    var : CFLAGS = -g
    所有涉及到生成 var 或由生成 var 衍生出的操作中,$(CFLAGS) 的值均为 -g。同名的环境变量也将被覆盖。

    模式变量:
    %.o : CFLAGS = -O
    所有涉及到生成 .o 后缀文件或由生成 .o 后缀文件衍生出的操作中,$(CFLAGS) 的值均为 -O。同名的环境变量也将被覆盖。

    自动化变量:
    $@    所有的目标文件
    $^    所有的依赖文件
    $<    第一个依赖文件
    $*    模式目标中符号% 之前的所有字符串,也被称为“茎”


【自动推导】
    如果 make 找到一个 test.o 目标文件,那么相应的 test.c 依赖文件就会被自动推导出来,命令 gcc -c test.c 也会被自动推导出来
    所以形如下面这样的语句:
    test : test.o
    [TAB]gcc -o test test.o
    test.o : test.c test.h 
    [TAB]gcc -c test.c
    可以缩写为:
    test.o : test.h

【引用其它Makefile】
    在 Makefile 中使用关键字 include 可以在当前位置包含别的 Makefile 文件。关键字 include 与文件名、文件名与文件名之间使用空格隔开。
    语法为:include <filename> <variables>
    例如:include foo.nake *.mk $(bar)

    环境变量 MAKEFILES 也有类似的作用。如果当前环境中定义了环境变量 MAKEFILES,那么 make 会把这个变量中的值做一个类似 include 的动作。这个变量中的值是其它的 Makefile,用空格分隔。
    不推荐使用 MAKEFILES 环境变量,因为她会影响所有的 Makefile。
    如果 Makefile 不能按照预期工作,可以检查一下是不是当前环境中定义了环境变量 MAKEFILES 在从中作梗。

【依赖文件的位置】
    在工程比较大时,通常将源文件进行分类,存放在不同的目录中。
    可以使用特殊变量 VPATH 保存源文件的存放路径。比如:
    VPATH = src:../headers
    上面的语句声明了 src 和 ../headers 两个目录,目录由冒号分隔。make会先在当前目录下搜索依赖文件,若没找到,再按照声明的目录顺序查找依赖文件。

    还可以使用关键字 vpath 分别指定不同依赖文件的不同路径。比如:
    vpath %.h ../headers    在 ../headers 中搜索所有文件名以 .h 结尾的依赖文件
    vpath %.c ../src:src    依次在 ../src 和 src 中搜索所有文件名以 .c 结尾的依赖文件
    vpath %.h     清除前一次设置的 .h 文件的搜索路径
    vpath    清除所有已经设置的文件搜索路径

【伪目标】
    伪目标既可作为目标文件,也可作为依赖文件。
    使用关键字 .PHONY 声明伪目标。比如:
    .PHONY : clean
    clean :
    [TAB]rm *.o temp

    或者,作为目标文件:
    all : prog1 prog2    #因为Makefile的第一个目标会被作为默认目标,所以应该将all写在第一行,其后再写.PHONY:all
    .PHONY : all
    prog1 : prog1.o utils.o
    [TAB]gcc -o prog1 prog1.o utils.o
    prog2 : prog2.o utils.o
    [TAB]gcc -o prog2 prog2.o utils.o

    或者,作为依赖文件:
    .PHONY : cleanall cleanobj cleandiff
    cleanall : cleanobj cleandiff    #伪目标既作目标文件,又作依赖文件
    [TAB]rm program
    cleanobj :
    [TAB]rm *.o
    cleandiff:
    [TAB]rm *.diff

【静态模式】
    静态模式可以让Makefile的功能更加灵活。格式如下:
    <目标文件集合> : <目标模式集合> : <依赖模式集合>
    [TAB]<命令>

    举个例子:
    objects = foo.o bar.o
    all : $(objects)
    $(objects) : %.o : %.c
    $(CC) -c $(CFLAGS) $< -o $@

    其中,$< 表示依赖模式集合,即foo.c bar.c,$@表示目标目标模式集合,即foo.o bar.o
    等价于:
    foo.o : foo.c
    [TAB]$(CC) -c $(CFLAGS) foo.c -o foo.o
    bar.o : bar.c
    [TAB]$(CC) -c $(CFLAGS) bar.c -o bar.o

    当我们的 %.o 有几百个时,这种 静态模式规则 的写法能极大提高效率。

    另一个例子:
    files = foo.elc bar.o lose.o
    $(filter %.o, $(files)) : %.o : %.c
    [TAB]$(CC) -c $(CFLAGS) $< -o $@
    $(filter %.elc, $(files)) : %.elc : %.el
    [TAB]emacs -f batch-byte-compile $<

    其中,$(filter %.o, $(files)) 表示调用 Makefile的 filter函数,将 $(files)中模式为 %.o 的内容过滤出来。

【自动生成依赖关系】
    如果 main.c 中存在语句 #include "defs.h",那么 main.o的依赖关系为 main.o : main.c defs.h
    在 C文件中包含的头文件增加或减少时,如果手动修改 Makefile的依赖关系,工作将很繁琐,效率也极低。
    在使用 gcc 进行编译时带上 -M 参数可以让编译器自动寻找 C文件所包含的头文件,并生成依赖关系。
    以 main.c 为例,执行命令:
    gcc -M main.c
    这条命令的输出是:
    main.o : main.c defs.h


    如果使用的是GNU的C/C++编译器,需要使用参数 -MM ,否则 -M 参数会把一些标准库的头文件也包含进来。

【命令的执行】
    如果要让上一条命令的结果应用在下一条命令时,应该把这 2 条命令写在一行上,并使用分号隔开,而不是写在两行上。
    例如:
    exec:
    [TAB]cd /home/testUsr; pwd

    使用 @ 作为命令的开头可以关闭命令回显,从而只打印命令执行的结果。

    make会检测每条命令执行之后的返回码。如果命令执行出错,返回码将被置为非零值,make将会终止执行当前规则。
    在[TAB]之后,紧接命令的位置添加符号 - 可以让 make忽略该条命令执行错误。
    例如:
    clean:
    [TAB]-rm -f *.o
    如果 rm 不带-前缀,当 .o文件不存在时命令 make clean将报错并停止;带 - 前缀的该命令则不会。

【模块化】
    在大的工程中,不同功能的代码分属不同目录,每个目录都编写一个对应的特定 Makefile,再由一个总控 Makefile进行嵌套调用。
    这有利于让我们的 Makefile更加简洁,更容易维护。

    使用 export <variables ...> 命令将变量传递到下级 Makefile中;使用 unexport <variables ...> 命令防止将变量传递到下一级 Makefile中。

    使用 make -w 参数让编译器在切换工作目录时打印出类似于“make: Entering directory '/home/testUsr/gnu/make'”的切换信息。
    当使用 -C 参数指定 make 的下层 Makefile时, -w 参数会被自动打开。

【流程控制】
    条件语句:
    条件判断语句中允许多余的空格,但不能以[TAB]键开始。
    make 在读取 Makefile时就计算条件表达式的值,而自动化变量(比如 $@、$^等)在运行时才出现,所以不要把自动化变量写在条件表达式里。

    ifeq ($(CC), gcc)    # 如果编译器是gcc
        libs = $(libs_for_gcc)    # 根据判断结果选择不同的库
    else
        libs = $(normal_libs)
    endif
    foo : $(objects)
    $(CC) -o foo $(objects) $(libs)

    ifneq ($(CC), gcc)    # 如果编译器不是gcc
        ...
    endif

    ifdef VAR    # 如果定义了VAR
        ...
    endif

    ifndef VAR    # 如果没有定义VAR
        ...
    endif

【make函数】
    调用形式:$(<function> <arguments>)
    函数调用以符号 $ 开头。函数名与参数间使用空格进行分隔。参数间使用逗号进行分隔。参数可以使用变量。

    字符串处理函数:
    $(subst <from>, <to>, <text>)    把<text>中的<from>字符串替换成<to>字符串。返回值为替换后的字符串
    $(patsubst <pattern>, <replacement>, <text>)    将<text>中符合模式<pattern>的字符串使用<replacement>字符串替换。返回值是替换后的字符串。
    $(strip <string>)    去掉<string>字符串中开头和结尾的空格。返回值是去掉首尾空格后的字符串。
    $(findstring <find>, <in>)    在字符串<in>中查找<find>字符串。如果找到,返回<find>字符串,否则返回空字符串。
    $(filter <pattern ...>, <text>)    将<text>中符合模式<pattern ...>的字符串过滤出来。返回值是过滤出的字符串。
    $(filter-out <pattern ...>, <text>)    去除<text>中符合模式<pattern ...>的字符串。返回值是不符合<pattern ...>模式的字符串。
    $(sort <list>)    将<list>中的数据以升序排序。返回值是排序后的字符串。
    $(word <n>, <text>)    取出<text>中以空格作为分隔的第 n 个单词(从 1 开始计数),若 n 大于<text>中单词个数,则返回空字符串。
    $(wordlist <s>, <e>, <text>)    取出<text>字符串中从第<s>个到第<e>个的单词,若<s>大于总单词个数,则返回空字符串,若<e>大于总单词个数,则返回从第<s>个到最后一个单词。
    $(words <text>)    返回<text>中的单词个数。
    $(firstword <text>)    返回<text>中的第 1 个单词。

    文件名操作函数:
    $(dir <filenames ...>)    返回<filenames ...>中所有文件各自所在的目录。
    $(notdir <dirnames ...>)    返回<dirnames ...>中所有文件各自末尾的文件名。
    $(suffix <filenames ...>)    返回<filenames ...>中所有文件各自的后缀。若没有后缀,则返回空字符串。
    $(basename <filenames ...>)    返回<filenames ...>中所有文件各自的前缀。若没有前缀(比如隐藏文件),则返回空字符串。
    $(addsuffix <suffix>, <filenames ...>)    给<filenames ...>中所有文件添加<suffix>后缀。
    $(addprefix <prefix>, <filenames ...>)    给<filenames ...>中所有文件添加<prefix>前缀。
    $(join <list1>, <list2>)    将<list2>中的各字符串添加到对应位置的<list1>中的各字符串后。比如 $(join aaa bbb, 111 222 333) 的返回值为 aaa111 bbb222 333

    控制函数:
    $(foreach <var>, <list>, <text>)    把参数<list>中的单词逐一取出放到<var>中,再执行<text>中的表达式。比如 $(foreach n, $(names), $(n).o) 将为 $(names)中的每个字符串添加 .o后缀,并将全部添加后缀后的单词作为一个整体字符串返回。
    $(if <condition>, <then-part>, <else-part>)    作用即字面意思。
    $(call <expression>, <param1>, <param2>, <param3> ...)    调用<expression>并使用<param1>、<param2>、<param3>依次取代<expression>中的第 1 个变量、第 2 个变量、第 3 个变量。
    $(origin <variable>)    获取变量<variable>的性质。其返回值如下:
        undefined    表示<variable>没有被定义
        default    表示<variable>是一个默认变量
        file    表示<variable>在 Makefile中被定义
        command line    表示<variable>在命令行被定义
        override    表示<variable>被override关键字定义
        automatic    表示<variable>是一个自动化变量
        environment    表示<variable>是一个环境变量
    $(shell <command> <arguments>)    调用 shell命令

【GNU规范】
    伪目标名称:
    all    所有目标的目标,功能一般是编译所有的目标。
    clean    删除所有被 make创建的文件。
    install    安装已编译好的程序,即把可执行文件拷贝到指定的目录中。
    print    列出改变过的文件。
    tar    把源程序打包备份成一个 .tar文件。
    dist    创建一个压缩文件。一般是将 .tar文件压缩成 .z文件或 .gz文件。
    TAGS    更新所有的目标,以备完整的重编译使用。
    check/test    测试 Makefile的执行流程。





0 0
原创粉丝点击