makefile学习

本文简单记录下对Makefile学习的一些笔记。

原文链接:https://blog.csdn.net/weixin_38391755/article/details/80380786

makefile简介和例子

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

该文章是基于GNU make的。

makefile介绍

makefile主要规则:

  1. 如果一个工程没有编译过,那么我们的所有源文件都要编译并链接;
  2. 如果一个工程的某几个源文件被修改过,那么我们只编译被修改的源文件,并链接目标程序;
  3. 如果头文件改变,那么需要编译引用了该头文件的源文件,并链接目标程序。

makefile规则

核心规则:

1
2
3
4
target ... : prerequisites ...
command
...
...

target是一个目标文件,可以是objectfile,也可以是执行文件,还可以是一个标签。

prerequisites是要生成target所需的文件或是目标。

command就make需要执行的命令,即任意的shell命令。

说白了,所有makefile的东西就是这个规则,但具体还有很多细节。

【注】在别人的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
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

反斜杠(/)是换行符的意思。

在定义好依赖关系后,后续的那一行定义了如何生成目标文件的操作系统命令,一定要以一个Tab键作为开头。记住,make并不管命令是怎么工作的,他只管执行所定义的命令。

clean不是一个文件,只不过是一个动作名字,有点像C语言中的label一样,其冒号后什么也没有,那么make就不会自动去找文件的依赖性,也不会自动执行其后定义的命令。要执行该命令,就要在make命令后显式指出该label的名字,比如make clean。

makefile使用变量

makefile中可以定义变量,变量是一个字符串,用$(name)即可使用。

所以改良版makefile如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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
    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 $(objects)

让make自动推导

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

只要make看到一个.o文件,它就会自动的把.c文件加入到依赖关系中,比如make找到一个a.o,那么a.c就会是a.o的依赖文件,并且cc -c a.c也会被推导出来,于是我们的makefile又可以更新如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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)

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

另类风格的makefile

如果觉得看到那堆[.o]和[.h]的依赖有点不爽,则可以把重复的.h收拢,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
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都应该写一个清空目标文件的规则,clean的规则不要放在开头,应该放在最后。

稳健的做法如下:

1
2
3
.PHONY : clean
clean :
-rm edit $(objects)

rm前加-表示也许某些文件会出问题,但不要管,继续做后面的事。

makefile总述

makefile里有什么

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

  1. 显式规则。显式规则说明了,如何生成一个或多个的目标文件,这是由makefile的书写者显式指出要生成的文件、文件的依赖文件以及生成的命令。
  2. 隐晦规则。由于我们的make有自动推导的功能,所以隐晦的规则可以让我们比较粗糙地简略地书写makefile。
  3. 变量定义。
  4. 文件指示。其包含了三个部分,一个是在一个makefile中引用另一个makefile,就像C中的include一样;另一个是指根据某些情况指定makefile中的有效部分,就像C中的预编译#if一样;还有就是定义一个多行的命令。
  5. 注释。只有行注释,用#字符。

makefile文件名

默认情况下,make命令会在当前目录下按顺序查找文件名为”GNUmakefile”、“makefile”和“Makefile”的文件,找到就解释该文件。最好不要用“GNUmakefile”。

如果要用其他文件名书写,则可以使用如下命令:

make -f yourmakemake --file yourmake

引用其他makefile

使用include关键字可以把其他makefile包含进来。

include<filename>

在include前可以有空字符,但不能是Tab键,多个文件之间可以用空格隔开。

include前加-表示无论include过程中出现什么错误,都不要报错,继续执行。

如果文件没有指定路径的话,make会先在当前目录下找,如果没有找到,那么make还会去以下目录中找:

  1. 若make执行时有参数-I或–include-dir,那么make会去参数指定目录下找
  2. 一般也会去/user/local/bin 或/usr/include中找,如果这些目录存在的话。

环境变量MAKEFILES

如果你的当前环境中定义了环境变量MAKEFILES,那么make会把这个变量中的值做一个类似于include的动作。

不建议设置,如果遇到问题可以去看看这个变量是否被定义了。

make的工作方式

make执行步骤如下:

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

makefile书写规则

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

规则的顺序是很重要的,因为makefile中只应该有一个最终目标。

规则语法之前已经讲过了。

规则中可以使用通配符,通配符也可以用在变量里。

比如objects=*.o,但要注意的是通配符并不会展开,objects的值就是“.o”,如要要让通配符在变量中展开,那么可以这样写:`objects=$(wildcard .o)`

文件搜寻

在大工程中,有大量源文件,通常会把这许多源文件分类存放到不同目录中,所以当make需要去寻找文件的依赖关系时,可以在文件前加上路径,但最好的方法是把路径告诉make,让它自动去找。

makefile中的特殊变量VPATH就是用来完成该功能的,如果没有声明该变量,那make只会在当前的目录中去找,如果声明了,那当前目录找不到就会去VPATH所指定的目录中找了,多个目录由冒号分开。

VPATH = src:../headers

另一个方法是使用make的vpath(全小写)关键字,它和VPATH类似,但更灵活,使用方法有三种:

  1. vpath <pattern> <dirs>为符合pattern的文件指定目录
  2. vpath <patter>清空pattern模式的文件搜索目录
  3. vpath清空所有已经设置好的文件搜索目录

中需要包含%通配符,表示匹配零个或若干个字符。

伪目标

在最早的例子里,有一个clean目标,这就是一个伪目标。

1
2
clean :
rm *.o temp

伪目标的取名不能和文件名重名,为了避免这种情况,可以使用特殊标记.PHONY来指明一个目标时伪目标。

.PHONY : clean

makefile的第一个目标会被作为其默认目标。

伪目标一般没有依赖的文件。但是,我们也可以为伪目标指定所依赖的文件。伪目标同样可以作为“默认目标”,只要将其放在第一个。一个示例就是,如果你的Makefile需要一口气生成若干个可执行文件,但你只想简单地敲一个make完事,并且,所有的目标文件都写在一个Makefile中,那么你可以使用“伪目标”这个特性。

多目标

makefile的规则中的目标可以不止一个,其支持多目标。

1
2
3
4
5
6
7
bigoutput littleoutput : text.g
generate text.g -$(subst output,,$@) > $@
# 等价于如下makefile
bigoutput : text.g
generate text.g -big > bigoutput
littleoutput : text.g
generate text.g -little > littleoutput

-$(subst output,,$@)中的$表示执行一个makefile的函数,函数名为subst,后面是参数。

$@表示目标的集合,就像一个数组,依次取出目标执行命令。

静态模式

静态模式可以更加容易地定义多目标的规则,先看语法:

1
2
3
<targets ...>: <target-pattern>: <prereq-patterns ...>
<commands>
...

targets定义了一系列的目标文件,可以有通配符。是目标的一个集合。

target-parrtern是指明了targets的模式,也就是的目标集模式。

prereq-parrterns是目标的依赖模式,它对target-parrtern形成的模式再进行一次依赖目标的定义。

自动生成依赖性

在makefile中,我们的依赖关系可能会需要包含一系列的头文件,但如果是一个大工程,你必须清楚哪些C文件包含了哪些头文件,并且在加入或删除头文件时也需要小心地修改makefile,这就很麻烦。

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

如果使用的是GNU的C++编译器,则使用-MM参数,否则会把标准库头文件也输出。

有点复杂,没看太明白。

makefile书写命令

每条规则中的命令和操作系统Shell的命令行是一致的。make会一按顺序一条一条的执行命令,每条命令的开头必须以Tab键开头,除非命令是紧跟在依赖规则后面的分号后的。在命令行之间中的空格或是空行会被忽略,但是如果该空格或空行是以Tab键开头的,那么make会认为其是一个空命令。

显示命令

通常,make会把其要执行的命令行在命令执行前输出到屏幕上。

当我们用“@”字符在命令行前,那么,这个命令将不被make显示出来。

如果make执行时,带入make参数“-n”或“–just-print”,那么其只是显示命令,但不会执行命令,这个功能很有利于我们调试我们的Makefile。

而make参数“-s”或“–slient”则是全面禁止命令的显示。

命令执行

当依赖目标新于目标时,也就是当规则的目标需要被更新时,make会一条一条的执行其后的命令。需要注意的是,如果你要让上一条命令的结果应用在下一条命令时,你应该使用分号分隔这两条命令。

命令出错

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

有些时候,命令的出错并不表示就是错误的。例如mkdir命令,我们一定需要建立一个目录,如果目录不存在,那么mkdir就成功执行,万事大吉,如果目录存在,那么就出错了。我们之所以使用mkdir的意思就是一定要有这样的一个目录,于是我们就不希望mkdir出错而终止规则的运行。

为了做到这一点,忽略命令的出错,我们可以在Makefile的命令行前加一个减号“-”(在Tab键之后),标记为不管命令出不出错都认为是成功的。如:

clean:
-rm -f *.o

还有一个全局的办法是,给make加上“-i”或是“–ignore-errors”参数,那么,Makefile中所有命令都会忽略错误。而如果一个规则是以“.IGNORE”作为目标的,那么这个规则中的所有命令将会忽略错误。这些是不同级别的防止命令出错的方法,你可以根据你的不同喜欢设置。

还有一个要提一下的make的参数的是“-k”或是“–keep-going”,这个参数的意思是,如果某规则中的命令出错了,那么就终目该规则的执行,但继续执行其它规则。

嵌套执行make

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

如果要传递参数到下级makefile,则使用这样的声明export <variable ...>

如果你不想让某些变量传递到下级Makefile中,那么你可以这样声明:unexport <variable ...>

需要注意的是,有两个变量,一个是SHELL,一个是MAKEFLAGS,这两个变量不管你是否export,其总是要传递到下层Makefile中,特别是MAKEFILES变量,其中包含了make的参数信息,如果我们执行“总控Makefile”时有make参数或是在上层Makefile中定义了这个变量,那么MAKEFILES变量将会是这些参数,并会传递到下层Makefile中,这是一个系统级的环境变量。

还有很多参数,具体看原文吧。

定义命令包

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

1
2
3
4
5
6
7
define run-yacc

yacc $(firstword $^)

mv y.tab.c $@

endef

run-yacc就是命令包的名字,像下面这样使用:

1
2
3
foo.c : foo.y

$(run-yacc)

使用变量

在Makefile中的定义的变量,就像是C/C++语言中的宏一样,他代表了一个文本字串,在Makefile中执行的时候其会自动原模原样地展开在所使用的地方。但和宏不同的是,你可以在makefile中改变其值。

变量的命名字可以包含字符、数字,下划线(可以是数字开头),但不应该含有“:”、“#”、“=”或是空字符(空格、回车等),且大小写敏感。

变量的基础

变量在声明时需要给予初值,使用时加上$符号,如果你要使用真实的“$”字符,那么你需要用“$$”来表示。

变量中的变量

在定义变量的值时,我们可以使用其它变量来构造变量的值,在Makefile中有两种方式来在用变量定义变量的值。

第一种就是用等号。

第二种用“:=”,推荐用这种,但用这种方式下,前面定义的变量不能用后面定义的变量,等号可以.

下面再介绍两个定义变量时我们需要知道的,请先看一个例子,如果我们要定义一个变量,其值是一个空格,那么我们可以这样来:

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)

这依赖于被替换字串中的有相同的模式,模式中必须包含一个“%”字符,这个例子同样让$(bar)变量的值为“a.c b.c c.c”。

第二种高级用法是把变量的值再变成变量。

1
2
3
x = y
y = z
a := $($(x))

追加变量值

可以使用+=给变量追加值。

override指示符

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

1
2
3
override <variable> = <value>

override <variable> := <value>

多行变量

还有一种设置变量值的方法是使用define关键字。使用define关键字设置变量的值可以有换行,这有利于定义一系列的命令。

define指示符后面跟的是变量的名字,而重起一行定义变量的值,定义是以endef关键字结束。

环境变量

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

目标变量

前面我们所讲的在Makefile中定义的变量都是“全局变量”,在整个文件,我们都可以访问这些变量。当然,“自动化变量”除外,如“$<”等这种类量的自动化变量就属于“规则型变量”,这种变量的值依赖于规则的目标和依赖目标的定义。

当然,我样同样可以为某个目标设置局部变量,这种变量被称为“Target-specific Variable”,它可以和“全局变量”同名,因为它的作用范围只在这条规则以及连带规则中,所以其值也只在作用范围内有效。而不会影响规则链以外的全局变量的值。

其语法是:

<target ...> : <variable-assignment>

<target ...> : overide <variable-assignment>

可以是前面讲过的各种赋值表达式,如“=”、“:=”、“+=”或是“?=”。第二个语法是针对于make命令行带入的变量,或是系统环境变量。

模式变量

在GNU的make中,还支持模式变量(Pattern-specific Variable),通过上面的目标变量中,我们知道,变量可以定义在某个目标上。模式变量的好处就是,我们可以给定一种“模式”,可以把变量定义在符合这种模式的所有目标上。

%.o : CFLAGS = -O

使用条件判断

使用条件判断,可以让make根据运行时的不同情况选择不同的执行分支。条件表达式可以是比较变量的值,或是比较变量和常量的值。

ifeq、else和endif。ifeq的意思表示条件语句的开始,并指定一个条件表达式,表达式包含两个参数,以逗号分隔,表达式以圆括号括起。else表示条件表达式为假的情况。endif表示一个条件语句的结束,任何一个条件表达式都应该以endif结束。

语法

条件表达式的语法为:

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

以及:

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

其中表示条件关键字,如“ifeq”。这个关键字有四个:ifeq、ifneq、ifdef、ifndef。注意,ifdef只是测试一个变量是否有值,其并不会把变量扩展到当前位置。

这一行上,多余的空格是被允许的,但是不能以[Tab]键做为开始(不然就被认为是命令)。而注释符“#”同样也是安全的。“else”和“endif”也一样,只要不是以[Tab]键开始就行了。

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

而且,为了避免混乱,make不允许把整个条件语句分成两部分放在不同的文件中。

使用函数

在Makefile中可以使用函数来处理变量,从而让我们的命令或是规则更为的灵活和具有智能。make所支持的函数也不算很多,不过已经足够我们的操作了。函数调用后,函数的返回值可以当做变量来使用。

函数的调用语法

函数调用很像变量的使用,也是以$来标识 ,其语法如下:

$(<function> <arguments> )或是${<function> <arguments>}.

字符串处理函数

1

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

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

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

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

2

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

名称:模式字符串替换函数——patsubst。

功能:查找中的单词(单词以“空格”、“Tab”或“回车”“换行”分隔)是否符合模式,如果匹配的话,则以替换。这里,可以包括通配符“%”,表示任意长度的字串。如果中也包含“%”,那么,中的这个“%”将是中的那个“%”所代表的字串。(可以用“\”来转义,以“\%”来表示真实含义的“%”字符)返回:函数返回被替换过后的字符串。

$(var:<pattern>=<replacement> )相当于$(patsubst <pattern>,<replacement>,$(var))

3

$(strip <string> )

名称:去空格函数——strip。

功能:去掉字串中开头和结尾的空字符。

返回:返回被去掉空格的字符串值。

4

$(findstring <find>,<in> )

名称:查找字符串函数——findstring。

功能:在字串中查找字串。

返回:如果找到,那么返回,否则返回空字符串。

5

$(filter <pattern...>,<text> )

名称:过滤函数——filter。

功能:以模式过滤字符串中的单词,保留符合模式的单词。可以有多个模式。

返回:返回符合模式的字串。

6

$(filter-out <pattern...>,<text> )

名称:反过滤函数——filter-out。

功能:以模式过滤字符串中的单词,去除符合模式的单词。可
以有多个模式。

返回:返回不符合模式的字串。

7

$(sort <list> )

名称:排序函数——sort。

功能:给字符串中的单词排序(升序)。

返回:返回排序后的字符串。

备注:sort函数会去掉中相同的单词。

8

$(word <n>,<text> )

名称:取单词函数——word。

功能:取字符串中第个单词。(下标从一开始)

返回:返回字符串中第个单词。如果中的单词数要大,那么返回空
字符串。

9

$(wordlist <s>,<e>,<text> )

名称:取单词串函数——wordlist。

功能:从字符串中取从开始到的单词串。是一个数字。

返回:返回字符串中从的单词字串。如果中的单词数要大,那
么返回空字符串。如果大于的单词数,那么返回从开始,到结束的单
词串。

10

$(words <text> )

名称:单词个数统计函数——words。

功能:统计中字符串中的单词个数。

返回:返回中的单词数。

11

$(firstword <text> )

名称:首单词函数——firstword。

功能:取字符串中的第一个单词。

返回:返回字符串的第一个单词。

文件名操作函数

下面我们要介绍的函数主要是处理文件名的。每个函数的参数字符串都会被当做一个或是
一系列的文件名来对待。

1

$(dir <names...> )

名称:取目录函数——dir。

功能:从文件名序列中取出目录部分。目录部分是指最后一个反斜杠(“/”)之前的部分。如果没有反斜杠,那么返回“./”。

返回:返回文件名序列的目录部分。

2

$(notdir <names...> )

名称:取文件函数——notdir。

功能:从文件名序列中取出非目录部分。非目录部分是指最后一个反斜杠(“/”
)之后的部分。

返回:返回文件名序列的非目录部分。

3

$(suffix <names...> )

名称:取后缀函数——suffix。

功能:从文件名序列中取出各个文件名的后缀。

返回:返回文件名序列的后缀序列,如果文件没有后缀,则返回空字串。

4

$(basename <names...> )

名称:取前缀函数——basename。

功能:从文件名序列中取出各个文件名的前缀部分。

返回:返回文件名序列的前缀序列,如果文件没有前缀,则返回空字串。

5

$(addsuffix <suffix>,<names...> )

名称:加后缀函数——addsuffix。

功能:把后缀加到中的每个单词后面。

返回:返回加过后缀的文件名序列。

6

$(addprefix <prefix>,<names...> )

名称:加前缀函数——addprefix。

功能:把前缀加到中的每个单词后面。

返回:返回加过前缀的文件名序列。

7

$(join <list1>,<list2> )

名称:连接函数——join。

功能:把中的单词对应地加到的单词后面。如果的单词个数要比<
list2>的多,那么,中的多出来的单词将保持原样。如果的单词个数要比

多,那么,多出来的单词将被复制到中。

返回:返回连接过后的字符串。

示例:$(join aaa bbb , 111 222 333)返回值是“aaa111 bbb222 333”。

foreach函数

foreach 函数和别的函数非常的不一样。因为这个函数是用来做循环用的,Makefile中的
foreach函数几乎是仿照于Unix标准Shell(/bin /sh)中的for语句,或是C-Shell(/bin
/csh)中的foreach语句而构建的。它的语法是:

$(foreach <var>,<list>,<text> )

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

所以,最好是一个变量名,可以是一个表达式,而中一般会使用

if函数

$(if <condition>,<then-part> )$(if <condition>,<then-part>,<else-part> )

可见,if函数可以包含“else”部分,或是不含。即if函数的参数可以是两个,也可以是三个。参数是if的表达式,如果其返回的为非空字符串,那么这个表达式就相当于返回真,于是,会被计算,否则 会被计算。

而if函数的返回值是,如果为真(非空字符串),那个会是整个函数的返回值,如果为假(空字符串),那么会是整个函数的返回值,此时如果没有被定义,那么,整个函数返回空字串。

所以,只会有一个被计算。

call函数

call函数是唯一一个可以用来创建新的参数化的函数。你可以写一个非常复杂的表达式,这个表达式中,你可以定义许多参数,然后你可以用call函数来向这个表达式传递参数。其语法是

$(call <expression>,<parm1>,<parm2>,<parm3>...)

当 make执行这个函数时,参数中的变量,如$(1),$(2),$(3)等,会被参数依次取代。而的返回值就是 call函数的返回值。

origin函数

origin函数不像其它的函数,他并不操作变量的值,他只是告诉你你的这个变量是哪里来的。

$(origin <variable> )

如果variable没有被定义过,返回undefined

如果是一个默认定义,就返回default

如果是环境变量,返回environment

如果是被定义在makefile中,返回file

如果是命令行定义的,返回command line

如果是被override指示符重新定义的,返回override

如果是一个命令运行中的自动化变量,返回automatic。

shell函数

shell 函数也不像其它的函数。顾名思义,它的参数应该就是操作系统Shell的命令。它和反引号”`”是相同的功能。这就是说,shell函数把执行操作系统命令后的输出作为函数返回。

控制make的函数

make提供了一些函数来控制make的运行。通常,你需要检测一些运行Makefile时的运行时信息,并且根据这些信息来决定,你是让make继续执行,还是停止。

$(error <text ...> )

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

$(warning <text ...> )

这个函数很像error函数,只是它并不会让make退出,只是输出一段警告信息,而make继续执行。

make的运行

一般来说,最简单的就是直接在命令行下输入make命令,make命令会找当前目录的makefile来执行,一切都是自动的。但也有时你也许只想让 make重编译某些文件,而不是整个工程,而又有的时候你有几套编译规则,你想在不同的时候使用不同的编译规则,等等。

make的退出码

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

0——成功

1——make运行时出现任何错误,其返回1

2——如果使用了make的-q选项,并且make使得一些目标不需要更新,那么返回2。

指定makefile

除了按照默认规则寻找makefile,也可以指定特殊名字的makefile。

make -f filename或者make --file filename

如果在make的命令行时,你不只一次地使用了“-f”参数,那么,所有指定的makefile将会被连在一起传递给make执行。

指定目标

一般来说,make的最终目标是makefile中的第一个目标,而其它目标一般是由这个目标连带出来的。这是make的默认行为。当然,一般来说,你的 makefile中的第一个目标是由许多个目标组成,你可以指示make,让其完成你所指定的目标。要达到这一目的很简单,需在make命令后直接跟目标的名字就可以完成(如前面提到的“make clean”形式)任何在makefile中的目标都可以被指定成终极目标,但是除了以“- ”打头,或是包含了“=”的目标,因为有这些字符的目标,会被解析成命令行参数或是变量。

即然make可以指定所有makefile中的目标,那么也包括“伪目标”,于是我们可以根据这种性质来让我们的makefile根据指定的不同的目标来完成不同的事。在Unix世界中,软件发布时,特别是GNU这种开源软件的发布时,其 makefile都包含了编译、安装、打包等功能。我们可以参照这种规则来书写我们的makefile中的目标。

具体可以看原文,比如常见的有“all”、“clean”、“install”等等。

检查规则

有时候,我们不想让我们的makefile中的规则执行起来,我们只想检查一下我们的命令,或是执行的序列。于是我们可以使用make命令的下述参数:

“-n”

“–just-print”

“–dry-run”

“–recon”

不执行参数,这些参数只是打印命令,不管目标是否更新,把规则和连带规则下的命令打印出来,但不执行,这些参数对于我们调试makefile很有用处。

“-t”

“–touch”

这个参数的意思就是把目标文件的时间更新,但不更改目标文件。也就是说,make假装编译目标,但不是真正的编译目标,只是把目标变成已编译过的状态。

“-q”

“–question”

这个参数的行为是找目标的意思,也就是说,如果目标存在,那么其什么也不会输出,当然也不会执行编译,如果目标不存在,其会打印出一条出错信息。

“-W

“–what-if=

“–assume-new=

“–new-file=

这个参数需要指定一个文件。一般是是源文件(或依赖文件),Make会根据规则推导来运行依赖于这个文件的命令,一般来说,可以和“-n”参数一同使用,来查看这个依赖文件所发生的规则命令。

make的参数

“-b”

“-m”

这两个参数的作用是忽略和其它版本make的兼容性。

“-B”

“–always-make”

认为所有的目标都需要更新(重编译)。

“-C

“–directory=

指定读取makefile的目录。如果有多个“-C”参数,make的解释是后面的路径以前面的作为相对路径,并以最后的目录作为被指定目录。如:“make –C ~hchen/test –C prog”等价于“make –C ~hchen/test/prog”。

“–debug[=]”

输出make的调试信息。它有几种不同的级别可供选择,如果没有参数,那就是输出最简单的调试信息。

-i”

“–ignore-errors”

在执行时忽略所有的错误。

“-I

“–include-dir=

指定一个被包含makefile的搜索目标。可以使用多个“-I”参数来指定多个目录。

还有看原文吧,感觉不用的话看了也记不住,就不搬过来了。

隐含规则

在我们使用Makefile时,有一些我们会经常使用,而且使用频率非常高的东西,比如,我们编译C/C++的源程序为中间目标文件(Unix下是[.o] 文件,Windows下是[.obj]文件)。本章讲述的就是一些在Makefile中的“隐含的”,早先约定了的,不需要我们再写出来的规则。

“隐含规则”也就是一种惯例,make会按照这种“惯例”心照不喧地来运行,那怕我们的Makefile中没有书写这样的规则。例如,把[.c]文件编译成[.o]文件这一规则,你根本就不用写出来,make会自动推导出这种规则,并生成我们需要的[.o]文件。

使用隐含规则

如果要使用隐含规则生成你需要的目标,你所需要做的就是不要写出这个目标的规则。那么,make会试图去自动推导产生这个目标的规则和命令,如果make可以自动推导生成这个目标的规则和命令,那么这个行为就是隐含规则的自动推导。当然,隐含规则是make事先约定好的一些东西。

隐含规则一览

这里列一下隐含规则,当然,我们也可以使用make的参数“-r”或“–no-builtin-rules”选项来取消所有的预设置的隐含规则。

  1. 编译C的隐含规则。
  2. 编译C++的隐含规则。
  3. 编译Pascal的隐含规则。
  4. 编译Fortran/Ratfor程序的隐含规则。
  5. 预处理Fortran/Ratfor程序的隐含规则。
  6. 编译Modula-2程序的隐含规则。
  7. 汇编和汇编预处理的隐含规则。
  8. 链接Object文件的隐含规则。“” 目标依赖于“.o”。
  9. Yacc C程序时的隐含规则。
  10. Lex C程序时的隐含规则。
  11. Lex Ratfor程序时的隐含规则。
  12. 从C程序、Yacc文件或Lex文件创建Lint库的隐含规则。

隐含规则使用的变量

在隐含规则中的命令中,基本上都是使用了一些预先设置的变量。你可以在你的makefile中改变这些变量的值,或是在make的命令行中传入这些值,或是在你的环境变量中设置这些值,无论怎么样,只要设置了这些特定的变量,那么其就会对隐含规则起作用。当然,你也可以利用make的“-R”或“–no– builtin-variables”参数来取消你所定义的变量对隐含规则的作用。

具体看原文吧。

隐含规则链

有些时候,一个目标可能被一系列的隐含规则所作用。例如,一个[.o]的文件生成,可能会是先被Yacc的[.y]文件先成[.c],然后再被C的编译器生成。我们把这一系列的隐含规则叫做“隐含规则链”。

在默认情况下,对于中间目标,它和一般的目标有两个地方所不同:第一个不同是除非中间的目标不存在,才会引发中间规则。第二个不同的是,只要目标成功产生,那么,产生最终目标过程中,所产生的中间目标文件会被以“rm -f”删除。

在“隐含规则链”中,禁止同一个目标出现两次或两次以上,这样一来,就可防止在make自动推导时出现无限递归的情况。

定义模式规则

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

有一点需要注意的是,”%”的展开发生在变量和函数的展开之后,变量和函数的展开发生在make载入Makefile时,而模式规则中的”%”则发生在运行时。

模式规则介绍

模式规则中,至少在规则的目标定义中要包含”%”,否则,就是一般的规则。目标中的”%”定义表示对文件名的匹配,”%”表示长度任意的非空字符串。

模式规则示例

把所有的.c文件都编译成.o文件:

1
2
%.o : %.c
$(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@

自动化变量

在上述的模式规则中,目标和依赖文件都是一系例的文件,那么我们如何书写一个命令来完成从不同的依赖文件生成相应的目标?因为在每一次的对模式规则的解析时,都会是不同的目标和依赖文件。

自动化变量就是完成这个功能的。在前面,我们已经对自动化变量有所提涉,相信你看到这里已对它有一个感性认识了。所谓自动化变量,就是这种变量会把模式中所定义的一系列的文件自动地挨个取出,直至所有的符合模式的文件都取完了。这种自动化变量只应出现在规则的命令中。

$@:表示规则中的目标文件集。

$%:仅当目标是函数库文件中,表示规则中的目标成员名。例如,如果一个目标是”foo.a(bar.o)”,那么,”$%”就是”bar.o”,”$@”就是”foo.a”。如果目标不是函数库文件(Unix下是[.a],Windows下是[.lib]),那么,其值为空。

$<:依赖目标中的第一个目标名字。

$?:所有比目标新的依赖目标的集合。以空格分隔。

$^:所有的依赖目标的集合。以空格分隔。如果在依赖目标中有多个重复的,那个这个变量会去除重复的依赖目标,只保留一份。

$+:这个变量很像”$^”,也是所有依赖目标的集合。只是它不去除重复的依赖目标。

$*:这个变量表示目标模式中”%”及其之前的部分。如果目标是”dir/a.foo.b”,并且目标的模式是”a.%.b”,那么,”$*”的值就是”dir/a.foo”。

最后想提醒一下的是,对于”$<”,为了避免产生不必要的麻烦,我们最好给$后面的那个特定字符都加上圆括号,比如,”$(< )”就要比”$<”要好一些。

还得要注意的是,这些变量只使用在规则的命令中,而且一般都是”显式规则”和”静态模式规则”(参见前面”书写规则”一章)。其在隐含规则中并没有意义。

模式的匹配

一般来说,一个目标的模式有一个有前缀或是后缀的”%”,或是没有前后缀,直接就是一个”%”。因为”%”代表一个或多个字符,所以在定义好了的模式中,我们把”%”所匹配的内容叫做”茎”,例如”%.c”所匹配的文件”test.c”中”test”就是”茎”。因为在目标和依赖目标中同时有”%”时,依赖目标的”茎”会传给目标,当做目标中的”茎”。

当一个模式匹配包含有斜杠(实际也不经常包含)的文件时,那么在进行模式匹配时,目录部分会首先被移开,然后进行匹配,成功后,再把目录加回去。在进行”茎”的传递时,我们需要知道这个步骤。例如有一个模式”e%t”,文件”src/eat” 匹配于该模式,于是”src/a”就是其”茎”,如果这个模式定义在依赖目标中,而被依赖于这个模式的目标中又有个模式”c%r”,那么,目标就是”src/car”。(”茎”被传递)

重载内建隐含规则

你可以重载内建的隐含规则(或是定义一个全新的)。

老式风格的后缀规则

后缀规则是一个比较老式的定义隐含规则的方法。后缀规则会被模式规则逐步地取代。因为模式规则更强更清晰。为了和老版本的Makefile兼容,GNU make同样兼容于这些东西。后缀规则有两种方式:”双后缀”和”单后缀”。

双后缀规则定义了一对后缀:目标文件的后缀和依赖目标(源文件)的后缀。如”.c.o”相当于”%o : %c”。单后缀规则只定义一个后缀,也就是源文件的后缀。如”.c”相当于”% : %.c”。

后缀规则中所定义的后缀应该是make所认识的,如果一个后缀是make所认识的,那么这个规则就是单后缀规则,而如果两个连在一起的后缀都被make所认识,那就是双后缀规则。例如:”.c”和”.o”都是make所知道。因而,如果你定义了一个规则是”.c.o”那么其就是双后缀规则,意义就是”.c” 是源文件的后缀,”.o”是目标文件的后缀。

后缀规则中,如果没有命令,那是毫无意义的。因为他也不会移去内建的隐含规则。

而要让make知道一些特定的后缀,我们可以使用伪目标”.SUFFIXES”来定义或是删除。

隐含规则搜索算法

比如有一个目标T,下面列出搜索目标T的规则的算法:

  • 把T的目录部分分离出来。叫D,而剩余部分叫N。(如:如果T是”src/foo.o”,那么,D就是”src/“,N就是”foo.o”)

  • 创建所有匹配于T或是N的模式规则列表。

  • 如果在模式规则列表中有匹配所有文件的模式,如”%”,那么从列表中移除其它的模式。

  • 移除列表中没有命令的规则。

  • 对于第一个在列表中的模式规则:

    1. 推导其”茎”S,S应该是T或是N匹配于模式中”%”非空的部分。
    2. 计算依赖文件。把依赖文件中的”%”都替换成”茎”S。如果目标模式中没有包含斜框字符,而把D加在第一个依赖文件的开头。
    3. 测试是否所有的依赖文件都存在或是理当存在。(如果有一个文件被定义成另外一个规则的目标文件,或者是一个显式规则的依赖文件,那么这个文件就叫”理当存在”)
    4. 如果所有的依赖文件存在或是理当存在,或是就没有依赖文件。那么这条规则将被采用,退出该算法。
  • 如果经过第5步,没有模式规则被找到,那么就做更进一步的搜索。对于存在于列表中的第一个模式规则:
    1. 如果规则是终止规则,那就忽略它,继续下一条模式规则。
    2. 计算依赖文件。(同第5步)
    3. 测试所有的依赖文件是否存在或是理当存在。
    4. 对于不存在的依赖文件,递归调用这个算法查找他是否可以被隐含规则找到。
    5. 如果所有的依赖文件存在或是理当存在,或是就根本没有依赖文件。那么这条规则被采用,退出该算法。
  • 如果没有隐含规则可以使用,查看”.DEFAULT”规则,如果有,采用,把”.DEFAULT”的命令给T使用。

一旦规则被找到,就会执行其相当的命令,而此时,我们的自动化变量的值才会生成。

使用make更新函数库文件

函数库文件也就是对Object文件(程序编译的中间文件)的打包文件。在Unix下,一般是由命令”ar”来完成打包工作。

函数库文件的成员

一个函数库文件由多个文件组成。你可以以如下格式指定函数库文件及其组成:

archive(member)

这个不是一个命令,而一个目标和依赖的定义。一般来说,这种用法基本上就是为了”ar”命令来服务的。如:

1
2
foolib(hack.o) : hack.o
ar cr foolib hack.o

函数库成员的隐含规则

当 make搜索一个目标的隐含规则时,一个特殊的特性是,如果这个目标是”a(m)”形式的,其会把目标变成”(m)”。于是,如果我们的成员是”%.o” 的模式定义,并且如果我们使用”make foo.a(bar.o)”的形式调用Makefile时,隐含规则会去找”bar.o”的规则,如果没有定义bar.o的规则,那么内建隐含规则生效,make会去找bar.c文件来生成bar.o。

函数库文件的后缀规则

你可以使用”后缀规则”和”隐含规则”来生成函数库打包文件,如

1
2
3
4
.c.o:
$(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $*.o
$(AR) r $@ $*.o
$(RM) $*.o

注意事项

在进行函数库打包文件生成时,请小心使用make的并行机制(”-j”参数)。如果多个ar命令在同一时间运行在同一个函数库打包文件上,就很有可以损坏这个函数库文件。

【END】