《跟我一起写makefile》读书笔记

前言

本文为《跟我一起写makefile》读书笔记,是自己在学makefile时候自己感觉比较全面的学习资料。

关于程序的编译和链接

概述

  • makefile关系到了整个工程的编译规则。

  • 一个工程中的源文件不计其数,并且按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为makefile就像一个Shell脚本一样,其中也可以执行操作系统的命令。

  • “自动化编译”—只要一个make命令,整个工程就能完全自动编译。

关于程序的编译和链接

编译
链接
.c文件
.o文件
.exe执行文件
  • 首先要把源文件编译成中间代码文件,在Windows下也就是 .obj 文件,UNIX下是 .o 文件,即Object File,这个动作叫做==编译(compile)。==
  • 然后再把大量的Object File合成执行文件,这个动作叫作链接(link)。链接时,主要是链接函数和全局变量。链接时,主要是链接函数和全局变量。
    • 在大多数时候,由于源文件太多,编译生成的中间目标文件太多,而在链接时需要明显地指出中间目标文件名,这对于编译很不方便。所以,我们要给中间目标文件打个包,在Windows下这种包叫“库文件”(Library File),也就是==.lib 文件==,在UNIX下,是Archive File,也就是==.a 文件==。

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

maekfile介绍

makefile介绍

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

makefile规则

target ... : prerequisites ...
    command
    ...
    ...
  • target[目标]:可以是一个object file(目标文件),也可以是一个执行文件,还可以是一个标签(label)。

  • prerequisites[依赖]:生成该target所依赖的文件和/或target

  • command[命令]:该target要执行的命令(任意的shell命令)

  • makefile规则:

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

示例

edit : main.o kbd.o command.o display.o \
        insert.o search.o files.o utils.o
    cc -o edit main.o kbd.o command.o display.o \
        insert.o search.o files.o utils.o

main.o : main.c defs.h
    cc -c main.c
kbd.o : kbd.c defs.h command.h
    cc -c kbd.c
command.o : command.c defs.h command.h
    cc -c command.c
display.o : display.c defs.h buffer.h
    cc -c display.c
insert.o : insert.c defs.h buffer.h
    cc -c insert.c
search.o : search.c defs.h buffer.h
    cc -c search.c
files.o : files.c defs.h buffer.h command.h
    cc -c files.c
utils.o : utils.c defs.h
    cc -c utils.c
clean :
    rm edit main.o kbd.o command.o display.o \
        insert.o search.o files.o utils.o
  • 反斜杠( \ )是换行符的意思。这样比较便于makefile的阅读。
  • 我们可以把这个内容保存在名字为“makefile”或“Makefile”的文件中,然后在该目录下直接输入命令 make 就可以生成执行文件edit
  • 如果要删除执行文件和所有的中间目标文件,那么,只要简单地执行一下 make clean 就可以了。
  • 生成目标文件的操作系统命令,一定要以一个 ==Tab==键作为开头
  • make会比较targets文件和prerequisites文件的修改日期,如果prerequisites文件的日期要比targets文件的日期要,或者target不存在的话,那么,make就会执行后续定义的命令
  • clean 不是一个文件,它只不过是一个动作名字,其冒号后什么也没有,那么,make就不会自动去找它的依赖性,也就不会自动执行其后所定义的命令。

make是如何工作的

输入make命令:

  • 1.make会在当前目录下名字叫**“Makefile”或“makefile”**的文件。
  • 2.如果找到,它会文件中的第一个目标文件(target),在上面的例子中,他会找到“edit”这个文件,并把这个文件作为最终的目标文件。
  • 3.如果edit文件不存在,或是edit所依赖的后面的 .o 文件的文件修改时间要比 edit 这个文件新,那么,他就会执行后面所定义的命令来生成 edit 这个文件。
  • 4.如果 edit 所依赖的 .o 文件也不存在,那么make会在当前文件中找目标为 .o 文件的依赖性,如果找到则再根据那一个规则生成 .o 文件。(这有点像一个堆栈的过程)
  • 5.当然,你的C文件和H文件是存在的啦,于是make会生成 .o 文件,然后再用 .o 文件生成make的终极任务,也就是执行文件 edit 了。

makefile中使用变量

edit : main.o kbd.o command.o display.o \
        insert.o search.o files.o utils.o
    cc -o edit main.o kbd.o command.o display.o \
        insert.o search.o files.o utils.o
  • 如果我们的工程需要加入一个新的 .o 文件,那么我们需要在两个地方加(应该是三个地方,还有一个地方在clean中)。
  • 为了makefile的易维护,在makefile中我们可以使用变量。makefile的变量也就是一个字符串,理解成C语言中的宏可能会更好。
  • 我们==声明一个变量==,叫 objectsOBJECTSobjsOBJSobj 或是 OBJ ,够表示obj文件。就可以很方便地在我们的makefile中以 ==$(objects) 的方式来使用这个变量==了
objects = main.o kbd.o command.o display.o \
    insert.o search.o files.o utils.o

edit : $(objects)
    cc -o edit $(objects)
main.o : main.c defs.h
    cc -c main.c
...

让makefile自动推导

  • make很强大,它可以自动推导文件以及文件依赖关系后面的命令

  • 只要make看到一个 .o 文件,它就会自动的把 .c 文件加在依赖关系中,如果make找到一个whatever.o ,那么 whatever.c 就会是 whatever.o 的依赖文件。并且 cc -c whatever.c 也会被推导出来.

  • .PHONY 表示 clean 是个伪目标文件。

objects = main.o kbd.o command.o display.o \
    insert.o search.o files.o utils.o

edit : $(objects)
    cc -o edit $(objects)

main.o : defs.h
kbd.o : defs.h command.h
command.o : defs.h command.h
display.o : defs.h buffer.h
insert.o : defs.h buffer.h
search.o : defs.h buffer.h
files.o : defs.h buffer.h command.h
utils.o : defs.h

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

合并同类项

让我们的makefile变得很简单,但我们的文件依赖关系就显得有点凌乱。

objects = main.o kbd.o command.o display.o \
    insert.o search.o files.o utils.o

edit : $(objects)
    cc -o edit $(objects)

$(objects) : defs.h
kbd.o command.o files.o : command.h
display.o insert.o search.o files.o : buffer.h

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

清空目标文件的规则

  • 每个Makefile中都应该写一个清空目标文件( .o 和执行文件)的规则,这不仅便于重编译,也很利于保持文件的清洁。

  • .PHONY 表示 clean 是一个“伪目标”。而在 rm 命令前面加了一个小减号的意思就是,也许某些文件出现问题,但不要管,继续做后面的事。

  • clean 的规则不要放在文件的开头,“clean从来都是放在文件的最后”。

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

makefile里有什么

Makefile里主要包含了五个东西:显式规则、隐晦规则、变量定义、文件指示和注释。

  1. 显式规则。显式规则说明了如何生成一个或多个目标文件。这是由Makefile的书写者明显指出要生成的文件、文件的依赖文件和生成的命令
  2. 隐晦规则。由于我们的make有自动推导的功能,所以隐晦的规则可以让我们比较简略地书写 Makefile,这是由make所支持的。
  3. 变量的定义。在Makefile中我们要定义一系列的变量变量一般都是字符串,这个有点像你C语言中的宏,当Makefile被执行时,其中的变量都会被扩展到相应的引用位置上。
  4. 文件指示。其包括了三个部分,一个是在一个Makefile中引用另一个Makefile,就像C语言中的include一样;另一个是指根据某些情况指定Makefile中的有效部分,就像C语言中的预编译#if一样;还有就是定义一个多行的命令。
  5. 注释。Makefile中只有行注释,和UNIX的Shell脚本一样,其注释是用 # 字符,这个就像C/C++中的 // 一样。如果你要在你的Makefile中使用 # 字符,可以用反斜杠进行转义,如: \#

最后,还值得一提的是,在Makefile中的命令,必须要以 Tab 键开始

makefile文件名

  • 默认的情况下,make命令会在当前目录下按顺序找寻文件名为“GNUmakefile”、“makefile”、“Makefile”的文件,找到了解释这个文件。在这三个文件名中,最好使用==“Makefile”==这个文件名。大多数的make都支持”makefile”和“Makefile”这两种默认文件名。

  • 如果要指定特定的Makefile,你可以使用make的 -f--file 参数,如: make -f Make.Linuxmake --file Make.AIX

引用其他的makefile

  • 在Makefile使用 include 关键字可以把别的Makefile包含进来,这很像C语言的 #include ,被包含的文件会原模原样的放在当前文件的包含位置。
include <filename>
  • filename 可以是当前操作系统Shell的文件模式(可以包含路径和通配符)。

  • include 前面可以有一些空字符,但是==绝不能是 Tab 键开始==。 include<filename> 可以用一个或多个空格隔开。

  • make命令开始时,会找寻 include 所指出的其它Makefile,并把其内容安置在当前的位置。

    • 如果make执行时,有==-I--include-dir 参数==,那么make就会在这个参数所指定的目录下去寻找。
    • 如果目录 <prefix>/include (一般是: /usr/local/bin/usr/include)存在的话,make也会去找。
  • 如果有文件没有找到的话,make会生成一条警告信息,但不会马上出现致命错误。它会继续载入其它的文件,一旦完成makefile的读取,make会再重试这些没有找到,或是不能读取的文件,如果还是不行,make才会出现一条致命信息。如果你想让make不理那些无法读取的文件,而继续执行,你可以在include前加一个减号“-”。

    -include <filename>
    

环境变量MAKEFILES

当前环境中定义了环境变量 MAKEFILES,那么,make会把这个变量中的值做一个类似于 include 的动作。这个变量中的值是其它的Makefile,用空格分隔。只是,它和 include 不同的是,从这个环境变量中引入的Makefile的“目标”不会起作用

make的工作方式

make工作时的执行步骤如下:(想来其它的make也是类似)

  • 1.读入所有的Makefile。
  • 2.读入被include的其它Makefile。
  • 3.初始化文件中的变量。
  • 4.推导隐晦规则,并分析所有规则。
  • 5.为所有的目标文件创建依赖关系链。
  • 6.根据依赖关系,决定哪些目标要重新生成。
  • 7.执行生成命令。
  • 1-5步为第一个阶段。第一个阶段中,如果定义的变量被使用了,那么,make会把其展开在使用的位置。但make并不会完全马上展开,make使用的是拖延战术,如果变量出现在依赖关系的规则中,那么仅当这条依赖被决定要使用了,变量才会在其内部展开。

  • ,6-7为第二个阶段。

书写规则

规则包含两个部分,一个是依赖关系,一个是生成目标的方法。

在Makefile中,规则的顺序是很重要的,因为,Makefile中只应该有一个最终目标,其它的目标都是被这个目标所连带出来的,所以一定要让make知道你的最终目标是什么。一般来说,定义在Makefile中的目标可能会有很多,但是==第一条规则中的目标将被确立为最终的目标==。如果第一条规则中的目标有很多个,那么,第一个目标会成为最终的目标。make所完成的也就是这个目标。

规则的语法

targets : prerequisites ; command
    command
    ...
  • targets是文件名,以空格分开,可以使用通配符。一般来说,我们的目标基本上是一个文件,但也有可能是多个文件。

  • command是命令行,如果其不与“target:prerequisites”在一行,那么,必须以==Tab 键开头==,如果和prerequisites在一行,那么可以用分号做为分隔。(见上)

  • prerequisites也就是目标所依赖的文件(或依赖目标)。如果其中的某个文件要比目标文件要新,那么,目标就被认为是“过时的”,被认为是需要重生成的。

  • 一般来说,make会以UNIX的标准Shell,也就是 /bin/sh 来执行命令。

在规则中使用通配符

当在使用命令行时,有很多时间都用来查找你所需要的文件,如ls find等。 S h e l l提供了一套完整的字符串模式匹配规则,或者称之为元字符,当s h e l l遇到上述字符时,就会把它们当作特殊字符,而不是文件名中的普通字符,这样用户就可以用它们来匹配相应的文件名,这可以称为通配符。

  • 通配符与正则表达式是有区别的,简单来说:通配符是用来通配的,正则表达式是用来匹配字符串的;

  • 在文本过滤工具里,都是用正则表达式,比如像awk,sed,等,是针对文件的内容的。

  • 而通配符多用在文件名上,比如查找find,ls,cp,等等。

  • make支持三个通配符: *?~
  • 波浪号( ~ )字符在文件名中也有比较特殊的用途。如果是 ~/test ,这就表示当前用户的 $HOME 目录下的test目录。
  • 通配符*代替了你一系列的文件,如 *.c 表示所有后缀为c的文件。
  • 转义字符 \ ,如 \* 来表示真实的 * 字符,而不是任意长度的字符串。
clean:
    cat main.c
    rm -f *.o

在编译完后看看main.c的源代码

objects = *.o
print: *.c
    lpr -p $?
    touch print

通配符也可以在我们的规则中,也样可以用在变量中。

  • 在Makefile规则中,通配符会被自动展开。但变量的定义和函数引用时,通配符将失效这种情况下如果需要通配符有效,就需要使用函数“wildcard”,它的用法是:$(wildcard PATTERN…) 。

    objects := $(wildcard *.o)
    
  • 一个例子

    • wildcard : 扩展通配符
    • notdir : 去除路径
    • patsubst :替换通配符

    • 建立一个测试目录

      |----------+ test

      |------------| a.c

      |------------|b.c

      |------------+ sub

      |---------------|sa.c

      |---------------|sb.c

    src=$(wildcard *.c ./sub/*.c)
    dir=$(notdir $(src))
    obj=$(patsubst %.c,%.o,$(dir) )
    
    all:
    @echo $(src)
    @echo $(dir)
    @echo $(obj)
    @echo "end"
    

    输出

    a.c b.c ./sub/sa.c ./sub/sb.c    #wildcard把 指定目录 ./ 和 ./sub/ 下的所有后缀是c的文件全部展开。
    a.c b.c sa.c sb.c                #notdir把展开的文件去除掉路径信息
    a.o b.o sa.o sb.o				 #在$(patsubst %.c,%.o,$(dir) )中,patsubst把$(dir)中的变量符合后缀是.c的全部替换成.o,任何输出。
    

    或者输出可以使用

    obj=$(dir:%.c=%.o)
    
    • makefile里的替换引用规则:

      $(var:a=b) 或 $  {var:a=b}
      
      • 它的含义是把变量var中的每一个值结尾用b替换掉a

文件搜索

  • VPATH

    Makefile文件中的特殊变量 VPATH 就是完成这个功能的,如果没有指明这个变量,make只会在当前的目录中去找寻依赖文件和目标文件。如果定义了这个变量,那么,make就会在当前目录找不到的情况下,到所指定的目录中去找寻文件了。

    VPATH = src:../headers
    

    上面的定义指定两个目录,“src”和“…/headers”,make会按照这个顺序进行搜索。目录由==“冒号”分隔==。(当然,当前目录永远是最高优先搜索的地方

  • vpath

    另一个设置文件搜索路径的方法是使用make的“vpath”关键字(注意,它是全小写的),这不是变量,这是一个make的关键字,这和上面提到的那个VPATH变量很类似,但是它更为灵活。它可以指定不同的文件在不同的搜索目录中。

    vpath <pattern> <directories>
    

    为符合模式的文件指定搜索目录。

    vpath <pattern>
    

    清除符合模式的文件的搜索目录。

    vpath
    

    清除所有已被设置好了的文件搜索目录。

    vapth使用方法中的需要包含 % 字符。% 的意思是匹配零或若干字符,(需引用 % ,使用 \ )例如, %.h 表示所有以 .h 结尾的文件。**指定了要搜索的文件集,而则指定了< pattern>的文件集的搜索的目录。**例如:

    vpath %.c foo:bar
    vpath %   blish
    

    .c 结尾的文件,先在“foo”目录,然后是“bar”目录,最后才是“blish”目录。

伪目标

  • 我们也应该提供一个清除它们的“目标”以备完整地重编译而用。 (以“make clean”来使用该目标)因为,我们并不生成“clean”这个文件。“伪目标”并不是一个文件**,只是一个标签**,由于“伪目标”不是文件,所以make无法生成它的依赖关系和决定它是否要执行。我们只有通过显式地指明这个“目标”才能让其生效。当然,“伪目标”的取名不能和文件名重名,不然其就失去了“伪目标”的意义了。

    当然,为了避免和文件重名的这种情况,我们可以使用一个特殊的标记“.PHONY”来显式地指明一个目标是“伪目标”,向make说明,不管是否有这个文件,这个目标就是“伪目标”。

    .PHONY : clean
    clean :
      rm *.o temp
    

  • ```ma
    .PHONY : cleanall cleanobj cleandiff

    cleanall : cleanobj cleandiff
    rm program

    cleanobj :
    rm *.o

    cleandiff :
    rm *.diff

    
    “make cleanall”将清除所有要被清除的文件。“cleanobj”和“cleandiff”这两个伪目标有点像“子程序”的意思。我们可以输入“make cleanall”和“make cleanobj”和“make cleandiff”命令来达到清除不同种类文件的目的。
    
    

多目标

Makefile的规则中的目标可以不止一个,其支持多目标,有可能我们的多个目标同时依赖于一个文件,并且其生成的命令大体类似。我们可以使用一个自动化变量 $@ (关于自动化变量,将在后面讲述),这个变量表示着目前规则中所有的目标的集合。

bigoutput littleoutput : text.g
    generate text.g -$(subst output,,$@) > $@

等价于

bigoutput : text.g
    generate text.g -big > bigoutput
littleoutput : text.g
    generate text.g -little > littleoutput

-$(subst output,,$@) 中的 $ 表示执行一个Makefile的函数,函数名为subst,后面的为参数。这里的这个函数是替换字符串的意思, $@ 表示目标的集合,就像一个数组, $@ 依次取出目标,并执于命令。

静态模式

<targets ...> : <target-pattern> : <prereq-patterns ...>
    <commands>
    ...
  • targets定义了一系列的目标文件,可以有通配符。是目标的一个集合。
  • target-pattern是指明了targets的模式,也就是的目标集模式。
  • prereq-patterns是目标的依赖模式,它对target-pattern形成的模式再进行一次依赖目标的定义。

objects = foo.o bar.o

all: $(objects)

$(objects): %.o: %.c
    $(CC) -c $(CFLAGS) $< -o $@
目标从$ object中获取, `%.o` 表明要所有以 `.o` 结尾的目标,也就是 `foo.o bar.o` ,也就是变量 `$ object` 集合的模式,而依赖模式 `%.c` 则取模式 `%.o` 的 `%` ,也就是 `foo bar` ,并为其加下 `.c`的后缀,于是,我们的依赖目标就是 `foo.c bar.c` 。而命令中的 `$<` 和 `$@`则是自动化变量, `$<` 表示第一个依赖文件, `$@` 表示目标集(也就是“foo.o bar.o”)。于是,上面的规则展开后等价于下面的规则:
foo.o : foo.c
    $(CC) -c $(CFLAGS) foo.c -o foo.o
bar.o : bar.c
    $(CC) -c $(CFLAGS) bar.c -o bar.o

自动生成依赖

大多数的C/C++编译器都支持一个“-M”的选项,即自动找寻源文件中包含的头文件,并生成一个依赖关系。

cc -M main.c

其输出是:

main.o : main.c defs.h

于是由编译器自动生成的依赖关系,这样一来,你就不必再手动书写若干文件的依赖关系,而由编译器自动生成了。需要提醒一句的是,如果你使用GNU的C/C++编译器,你得-MM 参数,不然,``-M` 参数会把一些标准库的头文件也包含进来

书写命令

  • make会一按顺序一条一条的执行命令,每条命令的开头必须Tab 键开头
  • 在命令行之间中的空格或是空行会被忽略,但是如果该空格或空行是以Tab键开头的,那么make会认为其是一个空命令

显示命令

  • make会把其要执行的命令行在命令执行前输出到屏幕上。当我们用 @ 字符在命令行前,那么,这个命令将不被make显示出来。

    @echo 正在编译XXX模块......
    
    • 如果没有echo

      echo 正在编译XXX模块......
      正在编译XXX模块......
      
  • 带入make参数==-n--just-print== ,那么其只是显示命令,但不会执行命令.这个功能很有利于我们调试我们的Makefile.

  • make参数 -s--silent--quiet 则是全面禁止命令的显示

命令执行

  • 如果你要让上一条命令的结果应用在下一条命令时,你应该使用分号分隔这两条命令。比如你的第一条命令是cd命令,你希望第二条命令得在cd之后的基础上运行,那么你就**不能把这两条命令写在两行上**,而应该把这两条命令写在一行上,用分号分隔

    exec:
        cd /home/hchen
        pwd
    

    cd没有作用,pwd会打印出当前的makefile目录

    exec:
        cd /home/hchen; pwd
    

    cd就起作用了,pwd会打印出“/home/hchen”。

命令出错

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

  • 命令的出错并不表示就是错误的。为了忽略命令的出错,我们可以在Makefile的命令行前加一个减号 - (在Tab键之后),标记为不管命令出不出错都认为是成功的。

    clean:
        -rm -f *.o
    
  • 给make加上 -i 或是 --ignore-errors 参数,那么,Makefile中所有命令都会忽略错误

  • 如果一个规则是以==.IGNORE 作为目标的==,那么这个规则中的所有命令将会忽略错误。

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

嵌套执行make

  • 把我们不同模块或是不同功能的源文件放在不同的目录中,我们可以在每个目录中都书写一个该目录的Makefile,这有利于让我们的Makefile变得更加地简洁,而不至于把所有的东西全部写在一个Makefile中,这样会很难维护我们的Makefile,这个技术对于我们模块编译和分段编译有着非常大的好处。

  • 我们有一个子目录叫subdir,这个目录下有个Makefile文件,来指明了这个目录下文件的编译规则。那么我们总控的Makefile可以这样书写:

    subsystem:
        cd subdir && $(MAKE)
    

    或:

    subsystem:
        $(MAKE) -C subdir
    
    • 定义**$(MAKE)宏变量的意思是,也许我们的make需要一些参数**,所以定义成一个变量比较利于维护。
    • 先进入“subdir”目录,然后执行make命令。
  • 如果你要传递变量到下级Makefile中,那么你可以使用这样的声明:

    export <variable ...>;
    
    export variable = value
    
    variable = value
    export variable
    
    export variable := value   
    
    export variable += value	#+= 是添加等号后面的值
    
  • 如果你不想让某些变量传递到下级Makefile中,那么你可以这样声明:

    unexport <variable ...>;
    
  • 有两个变量,这两个变量不管你是否export,其总是要传递到下层 Makefile中,

    • 一个是 SHELL
    • 一个是 MAKEFLAGS ,其中包含了make的参数信息,如果我们执行“总控Makefile”时有make参数或是在上层 Makefile中定义了这个变量,那么 MAKEFLAGS 变量将会是这些参数,并会传递到下层Makefile中,这是一个系统级的环境变量。

定义命令包

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

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

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

使用变量

变量的基础

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

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

  • ```makefile
    foo = c
    prog.o : prog.$(foo)
    ( f o o ) (foo) (foo)(foo) - ( f o o ) p r o g . (foo) prog. (foo)prog.(foo)

    
    展开后
    
    ```makefile
    prog.o : prog.c
        cc -c prog.c
    

变量中的变量

  • 这个功能有好的地方,也有不好的地方,好的地方是,我们可以把变量的真实值推到后面来定义。

    CFLAGS = $(include_dirs) -O
    include_dirs = -Ifoo -Ibar
    
  • 这种形式也有不好的地方,那就是递归定义。

    CFLAGS = $(CFLAGS) -O
    

    这种方式会让我们的make运行时非常慢,他会使用得两个make的函数“wildcard”和“shell”发生不可预知的错误。

:=

  • 为了避免上面的这种方法,我们可以使用make中的另一种用变量来定义变量的方法。这种方法使用的是 := 操作符,这种方法**,前面的变量不能使用后面的变量**,只能使用前面已定义好了的变量

    x := foo
    y := $(x) bar	#y := foo bar
    x := later		#x := later
    
    • 错误示范

      y := $(x) bar
      x := foo		#y = bar
      
  • 如果我们要==定义一个变量,其值是一个空格,==那么我们可以这样来:

    nullstring :=
    space := $(nullstring) # end of the line
    

    nullstring是一个Empty变量,其中什么也没有,而我们的space的值是一个空格。因为在操作符的右边是很难描述一个空格的,这里采用的技术很管用,先用一个Empty变量来标明变量的值开始了,而后面采用**“#”注释符来表示变量定义的终止**,这样,我们可以定义出其值是一个空格的变量。

    • 错误示范:

      dir := /foo/bar    # directory to put the frobs in
      

      dir这个变量的值是“/foo/bar”,后面还跟了4个空格,如果我们这样使用这样变量来指定别的目录——“$(dir)/file”那么就完蛋了。

?=

FOO ?= bar

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

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

变量高级用法

变量值的替换

替换变量中的共有的部分,其==格式是 $(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)

把变量的值再当成变量

x = y
y = z
a := $($(x))  #$(x)的值是“y”,所以$($(x))就是$(y),于是$(a)的值就是“z”。

这里的 $($(x)) 被替换成了 $($(y)) ,因为 $(y) 值是“z”,所以,最终结果是: a:=$(z) ,也就是“Hello”。

x = variable1
variable2 := Hello
y = $(subst 1,2,$(x))
z = y
a := $($($(z)))

$($($(z))) 扩展为 $($(y)) ,而其再次被扩展为$($(subst 1,2,$(x)))$(x) 的值是“variable1”,subst函数把“variable1”中的所有“1”字串替换成“2”字串,于是,“variable1”变成 “variable2”,再取其值,所以,最终, $(a) 的值就是 $(variable2) 的值——“Hello”。(喔,好不容易)

  • $(subst ;,;, 😉

    名称:字符串替换函数——subst。

    功能:把字串 ;中的;字符串替换成;。

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

追加变量值

  • += 操作符给变量追加值,

    objects = main.o foo.o bar.o utils.o
    objects += another.o
    
  • 如果变量之前没有定义过,那么, += 会自动变成 =,如果前面有变量定义,那么 += 会继承于前次操作的赋值符。

  • 如果前一次的是 := ,那么 +=会以 := 作为其赋值符,

    variable := value
    variable += more
    
  • 由于前次的赋值符是 = ,所以 += 也会以 = 来做为赋值,

    variable = value
    variable += more
    

override提示符

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

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

环境变量

  • make运行时的系统环境变量可以在make开始运行时被载入到Makefile文件中,但是如果Makefile中已定义了这个变量,或是这个变量由make命令行带入,那么系统的环境变量的值将被覆盖。(如果make指定了“-e”参数,那么,系统环境变量将覆盖Makefile中定义的变量)

  • make嵌套调用时(参见前面的“嵌套调用”章节),上层Makefile中定义的变量会以系统环境变量的方式传递到下层的Makefile 中。当然,默认情况下,只有通过命令行设置的变量会被传递。而定义在文件中的变量,如果要向下层Makefile传递,则需要使用exprot关键字来声明。

使用条件判断

  • ifeqelseendif

    • ifeq 的意思表示条件语句的开始,并指定一个条件表达式,表达式包含两个参数,以逗号分隔,表达式以圆括号括起

    • else 表示条件表达式为假的情况。

    • endif 表示一个条件语句的结束,任何一个条件表达式都应该以 endif 结束

      ifeq (<arg1>, <arg2>)
      ifeq '<arg1>' '<arg2>'
      ifeq "<arg1>" "<arg2>"
      ifeq "<arg1>" '<arg2>'
      ifeq '<arg1>' "<arg2>"
      
  • ifneq :比较参数 arg1arg2 的值是否相同,不同则为真。

    ifneq (<arg1>, <arg2>)
    ifneq '<arg1>' '<arg2>'
    ifneq "<arg1>" "<arg2>"
    ifneq "<arg1>" '<arg2>'
    ifneq '<arg1>' "<arg2>"
    
  • ifdefifdef 只是测试一个变量是否有值,其并不会把变量扩展到当前位置。

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

使用函数

函数的调用语法

  • 函数调用,很像变量的使用,也是以 $ 来标识的,

    $(<function> <arguments>)
    ${<function> <arguments>}
    
    • <function> 就是函数名,make支持的函数不多。
    • <arguments> 为函数的参数,参数间以逗号 , 分隔,而函数名和参数之间以==“空格”==分隔。函数调用以 $ 开头,以圆括号或花括号把函数名和参数括起。

字符串处理函数

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

  • $(var:=😉

strip

$(strip <string>)
  • 名称:去空格函数。
  • 功能:去掉 <string> 字串中 ,开头和结尾的空字符,并将中间的多个连续空字符(如果有的话)合并为一个空字符。空字符包括 空格,tab等不可显示的字符
  • 返回:返回被去掉空格的字符串值。

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 函数会去掉 <list> 中相同的单词。

  • `$(sort foo bar lose)`
    

    返回 bar foo lose

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> 中的单词数。

  • 备注:如果我们要取 <text> 中最后的一个单词,我们可以这样: $(word $(words <text>),<text>)

  • $(words, foo bar baz)
    

    返回值是 3

firstword

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

文件名操作函数

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

foreach 函数

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

  • 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函数当中。

if函数

$(if <condition>,<then-part>)
$(if <condition>,<then-part>,<else-part>)
  • <condition> 参数是if的表达式,如果其返回的为非空字符串,那么这个表达式就相当于返回真,于是, <then-part> 会被计算,否则 <else-part> 会被计算。

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

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

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

origin函数

$(origin <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> 是一个命令运行中的自动化变量

shell函数

contents := $(shell cat foo)
files := $(shell echo *.c)
  • 这个函数会新生成一个Shell程序来执行命令,所以你要注意其运行性能,如果你的Makefile中有一些比较复杂的规则,并大量使用了这个函数,那么对于你的系统性能是有害的。

控制make函数

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

make的运行

make命令会找当前目录的makefile来执行,一切都是自动的。

不同的时候使用不同的编译规则,

make的退出码

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

  • 0:表示成功执行。
  • 1:如果make运行时出现任何错误,其返回1。
  • 2:如果你使用了make的“-q”选项,并且make使得一些目标不需要更新,那么返回2。

指定make

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

  • 我们也可以给make命令指定一个特殊名字的Makefile。要使用make的 -f 或是 --file 参数( --makefile 参数也行)

    make –f hchen.mk
    

指定目标

  • 一般来说,make的最终目标是makefile中的第一个目标,而其它目标一般是由这个目标连带出来的。这是make的默认行为。

  • 可以指示make,让其完成你所指定的目标。任何在makefile中的目标都可以被指定成终极目标,但是除了以 - 打头,或是包含了 = 的目标,因为有这些字符的目标,会被解析成命令行参数或是变量。

  • 有一个make的环境变量叫 MAKECMDGOALS ,这个变量中会存放你所指定的终极目标的列表,如果在命令行上,你没有指定目标,那么,这个变量是空值。

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

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

检查规则

不想让我们的makefile中的规则执行起来,我们只想检查一下我们的命令,或是执行的序列。

  • -n, --just-print, --dry-run, --recon
  不执行参数,这些参数只是打印命令,不管目标是否更新,把规则和连带规则下的命令打印出来,但不执行,这些参数对于我们调试makefile很有用处。
  • -t, --touch
  这个参数的意思就是把目标文件的时间更新,但不更改目标文件。也就是说,make假装编译目标,但不是真正的编译目标,只是把目标变成已编译过的状态。
  • -q, --question
  这个参数的行为是找目标的意思,也就是说,如果目标存在,那么其什么也不会输出,当然也不会执行编译,如果目标不存在,其会打印出一条出错信息。
  • -W <file>, --what-if=<file>, --assume-new=<file>, --new-file=<file>
这个参数需要指定一个文件。一般是是源文件(或依赖文件),Make会根据规则推导来运行依赖于这个文件的命令,一般来说,可以和“-n”参数一同使用,来查看这个依赖文件所发生的规则命令。

隐含规则

自动化变量

下面是所有的自动化变量及其说明:

  • $@ : 表示规则中的目标文件集。在模式规则中,如果有多个目标,那么, $@ 就是匹配于目标中模式定义的集合。
  • $% : 仅当目标是函数库文件中,表示规则中的目标成员名。例如,如果一个目标是 foo.a(bar.o) ,那么, $% 就是 bar.o$@ 就是 foo.a 。如果目标不是函数库文件(Unix下是 .a ,Windows下是 .lib ),那么,其值为空。
  • $< : 依赖目标中的第一个目标名字。如果依赖目标是以模式(即 % )定义的,那么 $< 将是符合模式的一系列的文件集。注意,其是一个一个取出来的。
  • $? : 所有比目标新的依赖目标的集合。以空格分隔。
  • $^ : 所有的依赖目标的集合。以空格分隔。如果在依赖目标中有多个重复的,那么这个变量会去除重复的依赖目标,只保留一份。
  • $+ : 这个变量很像 $^ ,也是所有依赖目标的集合。只是它不去除重复的依赖目标。
  • $* : 这个变量表示目标模式中 % 及其之前的部分。如果目标是 dir/a.foo.b ,并且目标的模式是 a.%.b ,那么, $* 的值就是 dir/a.foo 。这个变量对于构造有关联的文件名是比较有较。如果目标中没有模式的定义,那么 $* 也就不能被推导出,但是,如果目标文件的后缀是make所识别的,那么 $* 就是除了后缀的那一部分。例如:如果目标是 foo.c ,因为 .c 是make所能识别的后缀名,所以, $* 的值就是 foo 。这个特性是GNU make的,很有可能不兼容于其它版本的make,所以,你应该尽量避免使用 $* ,除非是在隐含规则或是静态模式中。如果目标中的后缀是make所不能识别的,那么 $* 就是空值。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值