在linux上,GNU make原生支持makefile语法,很多c/c++的工程都是基于makefile来管理,来自动化编译。

如果需要跨平台,可以考虑使用cmake或gyp之类的配置来管理工程,在linux上会调用make,在mac上会基于xcode,在windows上会基于vs。

因此如果应用面向的只是linux系统,那么使用makefile来管理工程是方便直接的。

基本语法

target: prerequisites
	command

在定义好依赖关系后,后续的那一行定义了如何生成目标文件的操作系统命令,一定要以一个 Tab 键作为开头。

如果命令太长,可以使用反斜框(\)作为换行符,同时反斜框还可以用于转义,如:\#\*

注释使用#号

关于自动推导

make支持一些隐晦的规则,可以自动推到

比如对象 main.o 会自动寻找 main.c|.cc|.cpp

清空目标文件的规则

一般的风格都是:

clean:
	rm edit $(objects)

包含文件

在 Makefile 使用 include 关键字可以把别的 Makefile 包含进来:

include filename

一般被依赖的makefile文件后缀使用 .mk,并且支持通配符和变量

include *.mk $(bar)

make 的工作方式

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

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

1-5 步为第一个阶段,6-7 为第二个阶段。第一个阶段中,如果定义的变量被使用了,那么, make 会把其展开在使用的位置。但 make 并不会完全马上展开,make 使用的是拖延战术,如 果变量出现在依赖关系的规则中,那么仅当这条依赖被决定要使用了,变量才会在其内部展 开。

目标规则

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

目标命名规范

在Unix世界中,软件发布时,特别是GNU这种开源软件的发布时,其makefile都包含了编译、安装、打包等功能。我们可以参照这种规则来书写我们的makefile中的目标。

使用通配符

make支持三各通配符:“*”,“?”和“[…]”。这是和Unix的B-Shell是相同的。

字符 含义 实例
* 匹配 0 或多个字符 a*b a与b之间可以有任意长度的任意字符, 也可以一个也没有, 如aabcb, axyzb, a012b, ab。
? 匹配任意一个字符 a?b a与b之间必须也只能有一个字符, 可以是任意字符, 如aab, abb, acb, a0b。
[list] 匹配 list 中的任意单一字符 a[xyz]b a与b之间必须也只能有一个字符, 但只能是 x 或 y 或 z, 如: axb, ayb, azb。
[!list] 匹配 除list 中的任意单一字符 a[!0-9]b a与b之间必须也只能有一个字符, 但不能是阿拉伯数字, 如axb, aab, a-b。
[c1-c2] 匹配 c1-c2 中的任意单一字符 [0-9] [a-z] a[0-9]b 0与9之间必须也只能有一个字符 如a0b, a1b… a9b。

注意:

波浪号(“~”)字符

波浪号在文件名中也有比较特殊的用途。如果是“~/test”,这就表示当前用户的$HOME目录下的test目录。

关于变量中使用通配符

objects = *.o

上面这个例子,表示了,通符同样可以用在变量中。并不是说[.o]会展开,不!objects的值就是“.o”。Makefile中的变量其实就是C/C++中的宏。如果你要让通配符在变量中展开,也就是让objects的值是所有[.o]的文件名的集合,那么,你可以这样:

objects := $(wildcard *.o)

这种用法由关键字“wildcard”指出,关于Makefile的关键字

伪目标

“伪目标”并不是一个文件,只是一个标签,只有通过显示地指明这个“目标”才能让其生效。

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

比如清理操作:

.PHONY : clean

clean :
	-rm edit $(objects)

.PHONY 意思表示 clean 是一个“伪目标”,。而在 rm 命令前面加了一个小 减号的意思就是,也许某些文件出现问题,但不要管,继续做后面的事。当然,clean 的规 则不要放在文件的开头,不然,这就会变成 make 的默认目标,相信谁也不愿意这样。不成 文的规矩是——“clean 从来都是放在文件的最后”

伪目标一般没有依赖的文件。但是,我们也可以为伪目标指定所依赖的文件。伪目标同样可以作为“默认目标”,只要将其放在第一个。

一个示例就是,如果你的Makefile需要一口气生成若干个可执行文件

all : prog1 prog2 prog3 

.PHONY : all 

prog1 : prog1.o utils.o 
        cc -o prog1 prog1.o utils.o 

prog2 : prog2.o 
        cc -o prog2 prog2.o 

prog3 : prog3.o sort.o utils.o 
        cc -o prog3 prog3.o sort.o utils.o 

静态模式

静态模式可以更加容易地定义多目标的规则,可以让我们的规则变得更加的有弹性和灵活。

我们还是先来看一下语法:

<targets ...> : <target-pattern> : <prereq-patterns ...>
		<commands>

看一个例子:

objects = foo.o bar.o 

all: $(objects) 

$(objects): %.o: %.c 
        $(CC) -c $(CFLAGS) $< -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 

静态模式的过滤规则

    files = foo.elc bar.o lose.o 

    $(filter %.o,$(files)): %.o: %.c 
            $(CC) -c $(CFLAGS) $< -o $@ 
    $(filter %.elc,$(files)): %.elc: %.el 
            emacs -f batch-byte-compile $< 

$(filter %.o,$(files))表示调用Makefile的filter函数,过滤“$filter”集,只要其中模式为“%.o”的内容。

自动生成依赖性

如果是一个比较大型的工程,你必需清楚哪些C/C++文件包含了哪些头文件,并且,你在加入或删除头文件时,也需要小心地修改Makefile,这是一个很没有维护性的工作。

为了避免这种繁重而又容易出错的事情,我们可以使用GNU的C/C++编译器的一个功能,用-MM参数,即自动找寻源文件中包含的头文件,并生成一个依赖关系。

命令执行

当依赖目标新于目标时,也就是当规则的目标需要被更新时,make会一条一条的执行其后的命令。

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

exec: 
	cd /home/suninf; pwd 

pwd会打印出“/home/suninf”

命令出错

如果一个规则中的某个命令出错了(命令退出码非零),那么make就会终止执行当前规则,这将有可能终止所有规则的执行。

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

clean: 
	-rm -f *.o 

变量

一些重复使用的内容,可以用变量来代替使用

objects = main.o kbd.o command.o display.o

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

变量的命名字可以包含字符、数字,下划线(可以是数字开头),变量是大小写敏感的,传统的Makefile的变量名是全大写的命名方式。

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

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

    foo = c 
    prog.o : prog.$(foo) 
            $(foo)$(foo) -$(foo) prog.$(foo) 

展开后得到:

    prog.o : prog.c 
            cc -c prog.c 

关于变量递归定义

简单的使用“=”号,在“=”左侧是变量,右侧是变量的值,右侧变量的值可以定义在文件的任何一处,也就是说,右侧中的变量不一定非要是已定义好的值,其也可以使用后面定义的值。

但是这会让make陷入无限的变量展开过程中去:

  A = $(B) 
  B = $(A) 

使用:=操作符

这种方法,前面的变量不能使用后面的变量

  # x := foo 
  y := $(x) bar 
  x := later 

由于x未先定义,则$(x)为空,y为bar

+=操作符给变量追加值

objects = main.o foo.o bar.o utils.o 
objects += another.o 

等价于:

objects = main.o foo.o bar.o utils.o 
objects := $(objects) another.o 

系统变量说明

函数与条件语句

使用条件判断

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

例如:

    libs_for_gcc = -lgnu 
    normal_libs = 

    ifeq ($(CC),gcc) 
      libs=$(libs_for_gcc) 
    else 
      libs=$(normal_libs) 
    endif 

    foo: $(objects) 
    	$(CC) -o foo $(objects) $(libs) 

ifeq,ifneq,ifdef,ifndef

使用函数

函数调用语法

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

$(<function> <arguments>)

字符串操作函数

文件名操作函数

foreach函数

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

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

names := a b c d 
files := $(foreach n, $(names), $(n).o) 

返回:”a.o b.o c.o d.o”

if 函数

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

call函数

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

其语法是: $(call <expression>, <parm1>, <parm2>, <parm3>...)

例如:

reverse =  $(1) $(2) 
foo = $(call reverse, a, b) 

foo的值就是“a b”

shell函数

它的参数应该就是操作系统Shell的命令。它和反引号“`”是相同的功能。这就是说,shell函数把执行操作系统命令后的输出作为函数返回。于是,我们可以用操作系统命令以及字符串处理命令awk,sed等等命令来生成一个变量,如:

contents := $(shell cat foo)

参考