一、Why?

​ 最近在学习oMnet++,经常需要用到一些工程,里面有很多Makefile文件,经常会出现报错,尤其是在安装软件的时候,经常遇到,平时遇到的可能都是利用工具自动生成的,问题比较少。

先放链接

w3school的教程

二、 程序的编译和链接

​ 一般来说,无论是C、C++、还是pas,首先要把源文件编译成中间代码文件,在Windows下也就是 .obj 文件,UNIX下是 .o 文件,即 Object File,这个动作叫做编译(compile)。然后再把大量的Object File合成执行文件,这个动作叫作链接(link)。

编译时,编译器需要的是语法的正确,函数与变量的声明的正确。对于后者,通常是你需要告诉编译器头文件的所在位置(头文件中应该只是声明,而定义应该放在C/C++文件中),只要所有的语法正确,编译器就可以编译出中间目标文件。一般来说,每个源文件都应该对应于一个中间目标文件(O文件或是OBJ文件)。
链接时,主要是链接函数和全局变量,所以,我们可以使用这些中间目标文件(O文件或是OBJ文件)来链接我们的应用程序。链接器并不管函数所在的源文件,只管函数的中间目标文件(Object File),在大多数时候,由于源文件太多,编译生成的中间目标文件太多,而在链接时需要明显地指出中间目标文件名,这对于编译很不方便,所以,我们要给中间目标文件打个包,在Windows下这种包叫“*库文件”(Library File)*,也就是 .lib 文件,在UNIX下,是Archive File,也就是 .a 文件。

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

Make

Make命令直接用了这个意思,就是要做出某个文件。make是根据makefile中的规则来构建文件的。

Makefile文件的格式

  1. 文件格式:

    <target> : <prerequisites> 
    [tab] <commands>

    上面第一行冒号前面的部分,叫做"目标"(target),冒号后面的部分叫做"前置条件"(prerequisites);第二行必须由一个tab键起首,后面跟着"命令"(commands)。

    "目标"是必需的,不可省略;"前置条件"和"命令"都是可选的,但是两者之中必须至少存在一个。

    每条规则就明确两件事:构建目标的前置条件是什么,以及如何构建。下面就详细讲解,每条规则的这三个组成部分。

  2. 目标(target)

    一个目标(target)就构成一条规则。目标通常是文件名,指明Make命令所要构建的对象。目标可以是一个文件名,也可以是多个文件名,之间用空格分隔。

    1. 伪目标

      除了文件名,目标还可以是某个操作的名字,这称为"伪目标"(phony target)。

      clean:
      rm *.o

      上面代码的目标是clean,它不是文件名,而是一个操作的名字,属于"伪目标 ",作用是删除对象文件。

      但是如果目录里存在了clean文件,那么make会认为文件已经存在,没有必要重新构建了,就不会执行指定的rm命令。

      解决办法,指明为目标:

      .PHONY: clean
      clean:
      rm *.o temp

    如果Make命令运行时没有指定目标,默认会执行Makefile文件的第一个目标

  3. 前置条件(prerequisites)

    前置条件通常是一组文件名,之间用空格分隔。它指定了"目标"是否重新构建的判断标准:只要有一个前置文件不存在,或者有过更新(前置文件的last-modification时间戳比目标的时间戳新),"目标"就需要重新构建。

  4. 命令(commands)

    命令(commands)表示如何更新目标文件,由一行或多行的Shell命令组成。它是构建"目标"的具体指令,它的运行结果通常就是生成目标文件。

    每行命令之前必须有一个tab键。如果想用其他键,可以用内置变量.RECIPEPREFIX声明

    .RECIPEPREFIX = >
    all:
    > echo Hello, world

    上面代码用.RECIPEPREFIX指定,大于号(>)替代tab键。所以,每一行命令的起首变成了大于号,而不是tab键。

    每行命令在一个单独的shell中执行。这些Shell之间没有继承关系。

    如果需要在一起使用:

    ​ 一个解决办法是将两行命令写在一行,中间用分号分隔。

    ​ 另一个解决办法是在换行符前加反斜杠转义。

    ​ 最后一个方法是文件头部加上.ONESHELL:命令。

三、 Makefile的语法

3.1注释

​ 井号(#)在Makefile中表示注释。可以单独一行,或者在行尾.

3.2 回声(echoing)

正常情况下,make会打印每条命令,然后再执行,这就叫做回声(echoing)。

在命令的前面加上@,就可以关闭回声.

由于在构建过程中,需要了解当前在执行哪条命令,所以通常只在注释和纯显示的echo命令前面加上@。

3.3 通配符

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

3.4 模式匹配

Make命令允许对文件名,进行类似正则运算的匹配,主要用到的匹配符是%。比如,假定当前目录下有 f1.c 和 f2.c 两个源码文件,需要将它们编译为对应的对象文件。

%.o: %.c

等同于下面的写法。

f1.o: f1.c
f2.o: f2.c

使用匹配符%,可以将大量同类型的文件,只用一条规则就完成构建。

3.5 变量和赋值符

Makefile 允许使用等号自定义变量。

调用时,变量需要放在 $( ) 之中

调用Shell变量,需要在美元符号前,再加一个美元符号,这是因为Make命令会对美元符号转义。

有时,变量的值可能指向另一个变量。

v1 = $(v2)

上面代码中,变量 v1 的值是另一个变量 v2。这时会产生一个问题,v1 的值到底在定义时扩展(静态扩展),还是在运行时扩展(动态扩展)?如果 v2 的值是动态的,这两种扩展方式的结果可能会差异很大。

为了解决类似问题,Makefile一共提供了四个赋值运算符 (=、:=、?=、+=)

VARIABLE = value
# 在执行时扩展,允许递归扩展。

VARIABLE := value
# 在定义时扩展。

VARIABLE ?= value
# 只有在该变量为空时才设置值。

VARIABLE += value
# 将值追加到变量的尾端。

3.6 内置变量(Implicit Variables)

Make命令提供一系列内置变量,比如,$(CC) 指向当前使用的编译器,

$(MAKE) 指向当前使用的Make工具。

3.7 自动变量(Automatic Variables)

Make命令还提供一些自动变量,它们的值与当前规则有关

  1. $@

    $@指代当前目标,就是Make命令当前构建的那个目标。比如,make foo

    $@ 就指代foo。

  2. $<

    指代指代第一个前置条件。比如,规则为 t: p1 p2,那么$< 就指代p1。

  3. $?

    指代比目标更新的所有前置条件,之间以空格分隔。比如,规则为 t: p1 p2,其中 p2 的时间戳比 t 新,$?就指代p2。

  4. $^

    $^ 指代所有前置条件,之间以空格分隔。比如,规则为 t: p1 p2,那么

    $^ 就指代 p1 p2 。

  5. $*

    $ *指代匹配符 % 匹配的部分, 比如% 匹配 f1.txt 中的f1 ,

    $* 就表示 f1。

  6. $(@D) 和

    ** $(@F)**

    $(@D) 和

    $(@F) 分别指向

    $@ 的目录名和文件名。

    比如,$@是 src/input.c,

    那么$(@D) 的值为 src ,

    $(@F) 的值为 input.c。

3.8 判断和循环

Makefile使用 Bash 语法,完成判断和循环。

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

上面代码判断当前编译器是否 gcc ,然后指定不同的库文件。

LIST = one two three
all:
for i in $(LIST); do \
echo $$i; \
done

# 等同于

all:
for i in one two three; do \
echo $i; \
done

上面代码的运行结果。

one
two
three

3.9 函数

Makefile 还可以使用函数,格式如下。

$(function arguments)
# 或者
${function arguments}

Makefile提供了许多内置函数,可供调用。下面是几个常用的内置函数。

(1)shell 函数

shell 函数用来执行 shell 命令

srcfiles := $(shell echo src/{00..99}.txt)

(2)wildcard 函数

wildcard 函数用来在 Makefile 中,替换 Bash 的通配符。

srcfiles := $(wildcard src/*.txt)

(3)subst 函数

subst 函数用来文本替换,格式如下。

$(subst from,to,text)

下面的例子将字符串"feet on the street"替换成"fEEt on the strEEt"。

$(subst ee,EE,feet on the street)

下面是一个稍微复杂的例子。

comma:= ,
empty:=
# space变量用两个空变量作为标识符,当中是一个空格
space:= $(empty) $(empty)
foo:= a b c
bar:= $(subst $(space),$(comma),$(foo))
# bar is now `a,b,c'.

(4)patsubst函数

patsubst 函数用于模式匹配的替换,格式如下。

$(patsubst pattern,replacement,text)

下面的例子将文件名"x.c.c bar.c",替换成"x.c.o bar.o"。

$(patsubst %.c,%.o,x.c.c bar.c)

(5)替换后缀名

替换后缀名函数的写法是:变量名 + 冒号 + 后缀名替换规则。它实际上patsubst函数的一种简写形式。

min: $(OUTPUT:.js=.min.js)

上面代码的意思是,将变量OUTPUT中的后缀名 .js 全部替换成 .min.js 。