注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

notes

@-@

 
 
 

日志

 
 
 
 

学习GNU Make   

2009-05-27 15:17:12|  分类: 程序 |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |



在 GNU/Linux环境下, 使用make编译程序是个不错的选择. 较之IDE, make更灵活, 更健壮, 尤其对平台适应力强.  实际上, make在上实际七十年代就出现了, 它的年纪可比你我大许多. 许多大型项目(最著名的恐怕就是linux内核)都使用make进行管理.

在 众多的make版本中, GNU make是最突出的, 它已成了当前的"标准"make. 所有的GNU/Linux平台上都有提供, 一些BSD的变种, Mac OS X甚至cygwin也都使用GNU Make. 所以选择GNU make学习, 编写makefile是不二选择.

关于GNU Make, 可以看看这篇文章. 它摘录自 o'reilly的《GNU Make项目管理》

现在步入正题, 这里假设你使用C语言进行编程.


预备知识

首先我们必须具有一些预备知识: 你知道什么叫编译, 什么叫连接吗? 目标文件和可执行文件有什么区别? 一个C源程序是如何转化为可执行程序的? 以往的IDE往往对C初学者隐藏了这些基础概念. 要使用Make, 这些概念是要清楚的.

另外, 你需要对GNU/Linux上的编译器具有一定的了解——当然是gcc啦! 因为Makefile中的命令实际上是针对编译器的命令.
几个要说明的词汇
目标(target): 在makefile指定要生成的目标,  它可以是可执行文件,  目标文件,  也可以只是一个标签.

先决条件(prerequisite): makefile中指定的针对要生成某个target所依赖的文件或标号.

命令(command): Make针对target利用makefile中指定的规则所调用的命令. 它可以是你在shell中能够指定的所有命令.

GNU Make的工作原理

Make 通过 makefile文件被告知它的工作: 要生成怎样的目标文件?  由哪些代码编译生成?所以学习GNU Make实际上就是学习Makefile的写法. 下面是一个简单的makefile:

hello: hello.c
    gcc hello.c -o hello

我们熟悉的hello程序,
$ make
gcc hello.c -Wall -o hello

这样我们的hello可执行程序就出来了!

make的规则:
target: prereq1 prereq2
    commands

target就是目标文件, 它可能是,o文件, 可执行文件或标签中的一种.

关于target,有这样的约定:
如果某target被包含在执行make的命令行参数中, 则更新该target; 如果make没带参数, 则将第一个target作为默认目标进行更新.

prerequisites是生成target所需要的文件.

command是make调用gcc执行的指令. 注意, command以Tab键打头, 不能是空格!

到了这里, 就可以理解Make的原理了:

make的基本思想: 对于一个target (由命令行所指定或默认第一个target), 如果它不存在, 或者它的prerequisites中有比它更新的, 那么执行command所指定的命令.
所谓"更新", 是指比别的文件更晚被修改, make通过timestamp来记录谁更新.


makefile文件名

  1. 默认情况下, make命令会在当前目录下按顺序找寻文件名为“GNUmakefile”、“makefile”、“Makefile”的文件.

  2. 最好不要用“GNUmakefile”, 这个文件只被GNU make识别. 虽然有些Make只对全小写的“makefile”文件名敏感, 但大多数的make都支持“makefile”和“Makefile”这两种默认文件名.

  3. 可以使用别的文件名来书写makefile, 比如: "Make.Linux", “Make.Solaris”, “Make.AIX”等. 如果要指定特定的Makefile,你可以使用make的“-f”或者“--file”参数,如:make -f Make.Linux 或 make --file Make.AIX.

推荐以 Makefile 作为文件名!


命令行中调用Make命令


(1) $ make target-name
(2) $ make
(3) $ make non-exist-target
(4) $ make -n   or  $ make --just-print

(1) 如果在命令行中指定了target, 则make针对该target进行工作
(2) 如果未指定target, 则make对默认target进行操作.(Makefile中出现的第一个target)
(3) 如果指定的target不存在, 则make报错:

 make: *** No rule to make target `non-exist-target'.  Stop.

(4) 若加上 -n 或者 --just-print选项, make只是列出将要执行的命令, 并不执行它们. 这非常有用!


Makefile 的基本语法


Makefile 采用自上而下 (top-down)的组织, 最开始的target即"终极目标", 也是默认target. 观察make输出的命令可以发现, 它们的顺序与出现在Makefile中的命令刚好相反. 可以把Makefile中针对target的规则想象成C函数. 最开始的是main(), 然后依次调用相关的函数(即针对prereq调用相关的规则).

看看一个比较简单的Makefile规则:

target1 target2 target3 : prereq1 prereq2
    command1
    command2
    command3

  • 第一行列出了targets和prereqs, 注意目标与它们依赖的文件之间以" : "分隔. 目标或prereq之间以空格分隔.

  • 在冒号左边可以有1个或多个targets, 在冒号右边可以有0个或者多个prerequisites. 如果prerequisite的数目为0, 则只有当对应的target不存在时才执行命令.

  • 如果command不和目标, 先决条件在一行,那么它之前必须以Tab开头; 如果command与目标, 先决条件在一行, 那么它要以";"开头. Make将command传递给子shell去执行.

 注意: 不要忘了在命令以Tab开头, 也别在不是命令开头的地方输入Tab, 否则Make会将它之后的字符当作命令.
  • Make中的注释符为 "#". #之前可以有空格, 从#开始到行尾的所有字符都会被忽略. 注意: 如果你把#写在了命令中, 包括#在内的行都会传递给shell, 会有什么样的结果取决于你所用的shell.

  • 可以在Makefile中使用转义字符 \, 我们都知道, 在命令行中, 可以使用 \ 在下一行输入命令. 同样的, 在Makefile的所有行中, 都可以使用 \ 来分割太长的语句.

规则

Makefile中指定了针对某个target的规则(rule). 注意Makefile中可以有多条规则: 即针对Makefile中的一条targets, prerequisites列表, 就有一条对应的规则.

规则指定了target: 要更新的目标, 及目标所依赖的先决条件, 并指定了更新目标所采取的行为: command.

如 果prerequisites比target更新, 那么Make就利用Makefile中所指定的命令脚本来更新目标. 同样地, prerequisites也可能是另一条规则中的目标, Make会查看属于它的先决条件是否比它更新以执行它的规则中的命令脚本. 这类似于C中的函数调用, 在Make中, 这些依赖关系建立了"依赖图"(dependency graph).

关于target和prerequisite的依赖性, 我们可以理解它有两方面的含义:
(1) 要生成targets, 先要保证它依赖的prerequisites是最新的
(2) 保证prerequisites在在targets之前被更新.


规则包含两个部分, 一个是依赖关系, 一个是生成目标的方法. GNU Make支持五种规则: Explicit rules, Pattern rules, Implicit rules, Static pattern rules, Suffix rules. 后面会介绍它们. 在了解这些规则之前, 我们先弄清makefile中通配符, 伪目标, 变量, VPATH, vpath的用法.

通配符


在Makefile中可以使用通配符, GNU Make能够识别Bash的通配符(参考《学习Bash》, p22):
~       : 当前用户的home目录
~zp    : 用户zp的home目录
*.o      : 所有的目标文件
?         : 单个字符
[abc]   : a, 或b, 或c
[a-zA-Z]: 所有大小写字母
[!0-9]  : 所有的非数字

最好不要使用*通配符, 它可能使你无意引入一些不相关的文件.

出现在targets, prerequisites中的通配符由Make展开, 而出现在commands中的通配符由shell展开.

若想使用通配符所代表字符的原意, 在它之前加上转义字符\: 比如\?表示问号, 而非任意单个字符.

伪目标 (phony targets)

正如前面所说, 目标不仅可以是文件, 也可以标签. 用作标签的目标就是一个伪目标. 伪目标一般用来定义一个代表一条命令脚本的标签(也可以为伪目标指定prerequisites).

GNU Make是无法区别目标文件和伪文件的. 这样就存在一个问题, 如果你在Makefile里用到了一个标签, 如clean, 用以删除生成的中间目标文件:

clean:
   rm -f *.o

如果当前目录刚好有一个名为clean的文件, Make就会把这个你想用作伪目标的clean与clean文件关联起来, 当你运行 $ make clean 时, 由于clean不存在prerequisites, make 会给出这样的信息:
make: `hello' is up to date.

如何解决呢? 这里就引入了.PHONY. 以下面的格式来生命一个伪target:

.PHONY : clean
clean:
    rm -f *.o


.PHONY告诉了Make: 它后面的prerequisite, 也就是clean是一个伪目标.

.PHONY的作用:
(1)告知Make, 该伪目标并不是真正的文件, 不遵循由源代码生成目标文件的规则.
(2)告诉Make, 该伪目标始终需要被更新.

由 于伪目标始终需要被更新, 那么如果它作为某个目标文件的prerequisite, 那么在每次调用make时都会执行命令. 所以通常不将伪目标作为prerequisite. 但给伪目标指定prerequisite非常有用(实际上就是让这些prerequisites对应的命令在伪目标被指定为target的时候被执行).

我们来看看这个例子: 该makefile同时生成多个可执行文件, 而且只需输入"make"

all : prog1 prog2 prog3
.PHONY : all

prog1 : prog1.o utils.o
    gcc -o prog1 prog1.o utils.o
prog2 : prog2.o
    gcc -o prog2 prog2.o
prog3 : prog3.o sort.o utils.o
    gcc -o prog3 prog3.o sort.o utils.o

all 是第一个target, 即为默认的target.  由于指定了all是一个伪目标, 所以all总是要被更新. 于是, 它的三个prerequisites: prog1, prog2, prog3所对应的规则也需要被裁决, 看它们各自是否需要被更新. 由此, 一条简单的make语句就可以生成多个可执行target.

注意: all : prog1 prog2 prog3 和 .PHONY : all 出现的前后顺序在这里是无关, 但最好将.PHONY : all放在前面, 这貌似更符合习惯.

注意, 在上述情况下, 如果生成的prog1, prog2, prog3不需要更新, make反馈的信息为:
make: Nothing to be done for `all'. 它不同于(非伪)目标不需要更新的信息: `<target>' is up to date.

变量

变量以这样的形式被定义:

$(variable-name)

它表示我们希望将名为variable-name的变量在makefile中扩展.(可以把它理解成C语言中的宏)

变量能包含所有的字符, 符号. 比如:$( compile.c)

变量的格式: 除了单个字符可以直接跟在$之后外( $?), 其他的变量都必须用()或者{}引起来.

我们可以自己定义变量, 同时, Make也会自动建立一些变量(自动变量).

自动变量(automatic variables)
自动变量一般用来表征target或prerequisites, 这样你就不用显式地指定它们的名称. 一共有7种自动变量:

$@    : target
$%    : archive menmber specification
$<    : 第一个prerequisite
$?     : 所有比target新的prerequisites, 它们之间以空格分隔.
$^    : 所有的prerequisites(但不包括重复的prerequisites).
$+    : 与$?相同, 但包括重复的prerequisites.
$*     : 目标文件名的主干(既不包括后缀), 它一般只用于pattern rules.

针对上面的自动变量, 有的版本的Make还提供了变种: 在变量名之后加上D表示变量的目录部分, 加上F表示变量的文件部分. 如: $(<D). 在GNU Make中提供了更好的表示目录和文件的方法, 所以不提倡用这些变种.

由于自动变量是Make在建立target和prerequisites的对应关系后自动生成的, 所以只能在command中用到它们!

利用VPATH和vpath来查找文件

对于非常简单的项目, 我们可以把所有的源代码, 头文件和Makefile一起放在同一个目录. 但对于比较复杂的情况(有比较多的头文件, 源代码文件. 这往往是现实中经常存在的情况), 我们一般把代码分类, 放在不同的目录中. 一种比较简单的做法就是将源代码文件放在src(或source)目录, 将头文件放在include目录. 然后将Makefile放在它们的父目录.

如果你不在调用make时明确地作出规定, make只在当前目录中搜索targets和prerequisites. make将makefile所在的目录默认为搜索路径优先级最高的目录, 即先在当前目录查找文件, 若找不到, 再到VPATH, vpath指定的目录查找.

下面以2种情况来说明文件查找的问题, 给出了对应的3个Makefile和2个.c文件.

VAPTH

VPATH语法: VPATH = dir1 dir2 ...

在UNIX下, 目录用" " 或 ":"分隔, 在windows下, 目录用 " "或";"分隔. 推荐使用空格!

1, 在当前目录创建一个src目录, 里面放置hello.0.c, 一个Makefile.0. 它们的内容分别如下:

/* hello.0.c  */
#include <stdio.h>
int main(void)
{
    printf("hello, world!\n");
    return 0;
}

#Makefile.0
hello: hello.c
    gcc -Wall -o hello hello.c

在Makefile所在的目录运行make -f Makefile.0, 会得到这样的报错信息:

make: *** No rule to make target `hello.0.c', needed by `hello.0'.  Stop.

这正是由于Make默认地在当前目录搜索target和prerequisites. 将Makefile改写为如Makefile.1所示, 在其中引入了VPATH变量.

#Makefile.1
VPATH  = src

hello.0 : hello.0.c
    gcc -Wall -o hello.0 hello.0.c

运行 $ make -f Makefile.1, 我靠, 还是报错? 没办法, 把命令改为如下:
gcc -Wall -o $@ $<

这下就成功了, 是不是要使用VPATH, 命令行就必须用make的自动变量代替文件名呢?

下面在Makefile所在的目录创建include目录, 在里面放个hello.h (内容很简单: #include <stdio.h>), 并将hello.c改写为:

/* hello.1.c */
#include "hello.h"
int main(void)
{
  printf("hello\n");
  return 0;
}

Makefile当然要改写:

#Makefile.2
VPATH  = src include
CFLAGS = -I include

hello.1 : hello.1.c hello.h
    gcc $(CFLAGS) -Wall -o $@ $<

有两点值得注意的地方:
(1) src, include都包括在VPATH中, 要告诉Make在哪里搜寻source, .c和.h文件的目录都要指定.
(2) VPATH只是告诉了Make在哪里寻找文件, 而gcc并不知道在哪里寻找hello.h (即VPATH只和Makefile中的target, prerequisites相关, 而不和command相关). 所以要声明一个CFLAGS变量. gcc $(CFLAGS) == gcc -I src/ .
还有一个值得注意的地方: 这里使用$<, 指代第hello.1.c. 如果用$^, 会把hello.h也包含进去, 而gcc的命令行中是不带.h文件的. 如果有多个.c文件, 使用变量表征它们的时候就要斟酌一下了.

使用VPATH有一个隐患: VPATH在每个目录中搜索所有的source文件. 如果一个相同的文件名出现在不同的目录中, VPATH将第一个搜索到的文件作为结果.

考 虑下面的情况: make搜索hello.1.c和hello.h文件, 它首先在当前目录找到了hello.1.c, 但找不到hello.h, 于是它到VPATH, vpath指定的src和include去查找, 在src目录它又发现了hello.1.c文件, 但它将之前在当前目录找到的的hello.1.c作为优先的prerequist, 随后它又在include目录找到了hello.h: 将原本位于src目录中的hello.1.c拷贝到Makefile.2所在的目录, 修改其printf()语句, 让其显示"from the wrong dir!", 再次执行make -f Makefile.2, 运行新生成的hello.1, 我们会发现是错误的hello.1.c被编译了.

vpath
使用vpath较之VPATH更灵活. 它的基本语法为:

vpath <pattern> <directory-list>

其中<pattern>和<directory-list>都是可选的, 由此有三种使用vpath的方法:
1, vpath <pattern> <directory-list>
指定符合<pattern>模式的文件的搜索路径为<directory-list>.
2, vpath <pattern>
清除符合<pattern>模式的文件的搜索路径.
3, vpath
清除所有通过vpath所设置的文件搜索路径.

<pattern>中需要包含"%"字符, 它类似于扩展符中的"*", 表示匹配0或若干字符. 如: %.c 表示所有以.c结尾的C源文件.

可以利用vpath将Makefile.2改为如下:


"vpath %.c src" 和"vpath %.h include" 告诉Make在src目录搜索.c文件, 在include目录搜索.h文件.
要注意的是, 如果hello.1.c仍位于当前目录, make还是会将它作为首选, 而非vpath所指定的位于src目录中的hello.1.c!

VPATH is good for finding sources, not for finding targets.

还可以利用vpath来指定目标文件的路径, 后面再讨论.

注意使用VPATH和vpath时使用自动变量!

VPATH和vpath的比较
1, VPATH是make的特殊变量, 而path是make的关键字.
2, make首先在当前目录搜索文件, 并将位于当卡那目录的文件作为首选的prerequisites, 若在当前目录找不到该文件, 再到由VPATH或vpath指定的目录去搜索.

(一) 显式规则(Explicit Rules)

在makefile中显式指定了targets, prerequisites. 这即使用了make的显式规则, 它是我们常用的规则.

(二) 模式规则(Pattern Rules)


如果使用的文件比较多(比如需要使用大量的.o), 再使用显式规则, 输入那些.c, .h文件名就有点让人抓狂了. 在这种情况下可利用Make的自动推导(模式规则), 这样就不用针对每个.o文件声明command了.

利用下面的Makefile可以生成前面的hello.1:

#Makefile.4
vpath %.c src
vpath %.h include
CFLAGS = -I include

%: %.c %.h
    $gcc -Wall $(CFLAGS) -o $@ $<
hello.1:

利用Pattern Rules, 可以让Make进行自动推导: Make看到一个finename目标, 就会将filename.c和filename.h加入到prerequisites.


Static Pattern Rules

它和前面的Pattern Rules只有一个区别: 限定了针对那些target使用你给定的命令. 可以将Makefile.4改写为:

#Makefile.5
vpath %.c src
vpath %.h include
CFLAGS = -I include

OBJECTS = hello1

%(OBJECTS): %.c %.h
    $gcc -Wall $(CFLAGS) -o $@ $<
hello.1:

make三个有用的变量


$@    目标文件
$^    所有的依赖文件
$<   第一个依赖文件

默认的Make文件
#
# 编译第一个GTK程序
# Auth : PeiZhengfeng
# Date : 2008-05-11  7:29:29
#
cc=gcc
INCLUDE=.
CFLAGS=`pkg-config --libs --cflags gtk+-2.0`

first: first.o
    $(CC) -o first first.o $(CFLAGS) -I$(INCLUDE)

first.o : first.c
    $(CC) -c first.c $(CFLAGS) -I$(INCLUDE)


利用变量后的Makefile文件
#
# Auth : PeiZhengfeng
# Date : 2008-05-10 12:39:14
# Purpose : 编译base.c
#           GTK+例子
cc=gcc
INCLUDE=.
CFLAGS=`pkg-config gtk+-2.0 --libs --cflags`

helloworld : helloworld.o
    $(CC) -o $@ $^ $(CFLAGS) -I$(INCLUDE)

helloworld.o : helloworld.c
    $(CC) -c $< $(CFLAGS) -I$(INCLUDE)

clean:
    rm -rf *.o

make的默认规则
..c.o:
gcc -c $<
这个规则表示所有的.o文件都是依赖与相应的.c文件的。

最终的Makefile
#
# Auth : PeiZhengfeng
# Date : 2008-05-10 12:39:14
# Purpose : 编译base.c
#           GTK+例子
cc=gcc
INCLUDE=.
CFLAGS=`pkg-config gtk+-2.0 --libs --cflags`

helloworld : helloworld.o
    $(CC) -o $@ $^ $(CFLAGS) -I$(INCLUDE)

..c.o:
    gcc -c $<

clean:
    rm -rf *.o


  评论这张
 
阅读(1192)| 评论(0)
推荐 转载

历史上的今天

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2018