`

Managing Projects with GNU Make 笔记二

 
阅读更多

Managing Projects with GNU Make 笔记二 规则

9/23/2009 10:56:28 PM

Managing Projects with GNU Make 笔记二 规则

GNU Make 笔记 2009-08-02 23:47 阅读27评论0
字号:

这是针对该书第二章所作的笔记。首先确认一下环境及测试文件。在某个目录下,执行如下命令
$mkdir makefile_2
$cd makefile_2
$mkdir src
$mkdir include
编辑8个文件保存在 makefile_2的当前目录下,以下通过cat,显示8个文件的内容,========一如既往的表示当前文件显示结束,但是该文件并不包含这一行。
$cat main.c
#include <stdio.h>
#include "func1.h"
#include "func2.h"
#include "func3.h"

int main(int argv ,char *argc[]){
printf("ok, this is a test for <Managine Project with GNU make>'s chapter 2 by luckystar!/n");
func_1();
func_2();
func_3();
return 1;
}
================================
$cat func1.c
#include <stdio.h>
#include "func1.h"

void func_1(void){
printf("the func 1 called in the func1.c file !/n");
}
================================
$cat func1.h
#ifndef _FUNC_1_H_
#define _FUNC_1_H_
void func_1(void);
#endif
================================
$cat func2.c
#include <stdio.h>
#include "func2.h"

void func_2(void){
printf("the func 2 called in the func2.c file !/n");
}
================================
$cat func2.h
#ifndef _FUNC_2_H_
#define _FUNC_2_H_
void func_2(void);
#endif
================================
$cat func3.c
#include <stdio.h>
#include "func3.h"

void func_3(void){
printf("the func 3 called in the func3.c file !/n");
}
================================
$cat func3.h
#ifndef _FUNC_3_H_
#define _FUNC_3_H_
void func_3(void);
#endif
================================
$cat makefile
chapter_2:main.o func3.o func2.o func1.o
gcc -o chapter_2 main.o func3.o func2.o func1.o
func3.o:func3.c func3.h
gcc -c func3.c
func2.o:func2.c func2.h
gcc -c func2.c
func1.o:func1.c func1.h
gcc -c func1.c
main.o:main.c func1.h func2.h func3.h
gcc -c main.c
clean:
rm -r *.o
rm chapter_2
================================
上述文件整理完毕后,执行 make
$make
$./chapter_2
则会显示一段打印文字如下
ok, this is a test for <Managine Project with GNU make>'s chapter 2 by luckystar!
the func 1 called in the func1.c file !
the func 2 called in the func2.c file !
the func 3 called in the func3.c file !

从第一章内容中就可以看到。makefile文件中,最基本的三个内容是,目标,依赖,执行。而这三种内容需要通过规则来描述,以体现相互关系。
这本书在第二章描述了以下几个规则
explicit rules 显式规则(我自己的粗糙的翻译,希望别使得有人误解)
implicit rules 隐式规则
pattern rules 模式规则

explicit rules
显式规则,非常简单。回到例子,
修改 func1.c func2.c func1.h func2.h如下
$cat func1.h
#ifndef _FUNC_1_H_
#define _FUNC_1_H_
void func_1(void);
void func_12(void);
#endif
================================

$cat func1.c
#include <stdio.h>
#include "func1.h"
#include "func2.h"
void func_1(void){
printf("the func 1 called in the func1.c file !/n");
func_21();
}
void func_12(void){
printf("the func 12 called by func_2 !/n");
}
void func_13(void){
printf("the func 13 called by func_3 !/n");
}
================================
$cat func2.h
#ifndef _FUNC_2_H_
#define _FUNC_2_H_
void func_2(void);
void func_21(void);
#endif
================================
$cat func2.c
#include <stdio.h>
#include "func1.h"
#include "func2.h"
void func_2(void){
printf("the func 2 called in the func2.c file !/n");
func_12();
}
void func_22(void){
printf("the func 21 called by func_1 !/n");
}
================================
此时我们重新调整 makefile如下
$cat makefile
chapter_2:main.o func3.o func2.o func1.o
gcc -o chapter_2 main.o func3.o func2.o func1.o
func3.o:func3.c func3.h
gcc -c func3.c
func2.o func1.o main.o: func2.h func1.h
func2.o : func2.c
gcc -c func2.c
func1.o:func1.c
gcc -c func1.c
main.o:main.c func3.h
gcc -c main.c
clean:
rm -r *.o
rm chapter_2
================================
依次执行
$make clean
$make
$./chapter_2
此时,输出结果为
ok, this is a test for <Managine Project with GNU make>'s chapter 2 by luckystar!
the func 1 called in the func1.c file !
the func 21 called by func_1 !
the func 2 called in the func2.c file !
the func 12 called by func_2 !
the func 3 called in the func3.c file !
这里告所我们两个事实
1、目标需要多个依赖时,可以分开写,例如 target : a b c 可以写成如下格式
target : a
target : b
target : c
2、多个目标需要相同依赖时,可以写在一个目标依赖关系中。如例子中的func2.o func1.o main.o: func2.h func1.h
当然你得确保,如果在多个目标具备相同依赖时,make的 最终目标还是需要写在第一个。而上述例子中 func2.o func1.o的顺序则不会影响make。反之,如果加入了chapter_2 这个目标,而采用 func2.o chapter_2 : func2.h,则make的工作则会完全改变。
需要注意的是,上述func2.o func1.o main.o: func2.h func1.h完全等价于
func2.o : func2.h func1.h
func1.o : func2.h func1.h
main.o : func2.h func1.h
而我们可以继续修改func3.c 和makefile如下
$cat func3.c
#include <stdio.h>
#include "func1.h"
#include "func3.h"
void func_3(void){
printf("the func 3 called in the func3.c file !/n");
func_13();
}

================================
$cat makefile
chapter_2:main.o func3.o func2.o func1.o
gcc -o chapter_2 main.o func3.o func2.o func1.o
*.o:func1.h
func3.o:func3.c func3.h
gcc -c func3.c
func2.o func1.o main.o: func2.h
func2.o : func2.c
gcc -c func2.c
func1.o:func1.c
gcc -c func1.c
main.o:main.c func3.h
gcc -c main.c
clean:
rm -r *.o
rm chapter_2
===============================
执行如下命令
$make clean
$make
$./chapter_2
则打印如下
ok, this is a test for <Managine Project with GNU make>'s chapter 2 by luckystar!
the func 1 called in the func1.c file !
the func 21 called by func_1 !
the func 2 called in the func2.c file !
the func 12 called by func_2 !
the func 3 called in the func3.c file !
the func 13 called by func_3 !
查看makefile 此时存在
*.o:func1.h
这个规则,很显然,没有任何执行部分,只是描述一种依赖关系。而此处使用了通配符 *。至于有多少通配符make可以接受,此处就无法一一列举了。当然 *这个通配符并不是随时随地可以写的。有时会引发一些混乱。后面如有例子,可以再作讨论。

Phony target
伪目标
查看 makefile文件,存在以下段
clean:
rm -r *.o
rm chapter_2
当执行 make clean时,会自动调用起执行命令,两个删除操作。修改makefile如下
$cat makefile
chapter_2:main.o func3.o func2.o func1.o
gcc -o chapter_2 main.o func3.o func2.o func1.o
*.o:func1.h
func3.o:func3.c func3.h
gcc -c func3.c
func2.o func1.o main.o: func2.h
func2.o : func2.c
gcc -c func2.c
func1.o:func1.c
gcc -c func1.c
main.o:main.c func3.h
gcc -c main.c
hehe:
rm -r *.o
rm chapter_2
===============================
按如下顺序执行命令
$make
$make clean
$make hehe
无论make的提示符的内容,此时 make clean的提示如下
make: *** 没有规则可以创建目标“clean”。 停止。
而 make hehe 提示如下
rm -r *.o
rm chapter_2
很显然,make clean 或make hehe 中的 clean ,hehe 只是 makefile里的一个目标的名称。特别是针对 clean 并不是make 的一个参数。而大家为了一个相互的默契,把涉及到删除make生成文件的工作目标,设定为clean。在 chapter 2 page 15列出了,标准伪目标的几个内容,例如 all,install,clean,distchlean 等等。
即你可以把一段涉及删除make生成文件的工作目标定义为任意名称,如此处的hehe。但这样做有两个问题。
1、如果没有一些默契,别人使用此makefile,或使用别人的 makefile。则每次需要查看,一些通用的工作目标被定义成什么特殊及富有个性的名称
2、如果目录下存在hehe这个文件呢?
创建一个新文件在当前目录下,如下
$cat hehe


=======================
可以看出,hehe 只是个空文件,再次执行
$make
$make hehe
此时提示
make: “hehe”是最新的。
即,此时make 把 hehe 当作了一个目标文件。恢复makefile 如下
$cat makefile
chapter_2:main.o func3.o func2.o func1.o
gcc -o chapter_2 main.o func3.o func2.o func1.o
*.o:func1.h
func3.o:func3.c func3.h
gcc -c func3.c
func2.o func1.o main.o: func2.h
func2.o : func2.c
gcc -c func2.c
func1.o:func1.c
gcc -c func1.c
main.o:main.c func3.h
gcc -c main.c
clean:
rm -r *.o
rm chapter_2
===============================
再创立 clean 文件在当前目录下,该文件一样不需要存在任何内容,按如下方式执行
$make
$make clean
此时提示,和 hehe 一样。这里说明,通过make 加目标参数,来处理某个特定的操作,这个目标参数不能和当前目录下的文件同名,因为make 会优先考虑当前目录下的文件。
现在,我们可以保证当前目录下,不保留有clean。但以下两种情况会出现麻烦,虽然不一定是问题
1、工程大到,无法一一判断文件名和目标名的冲突,至少检测这个冲突,想当然的是件非常痛苦的事情
2、别人用此工程,如何保证他的环境下没有诸如 hehe ,clean的文件。
为了解决这个问题,make 存在 伪目标实现方式,修改makefile如下
$cat makefile
chapter_2:main.o func3.o func2.o func1.o
gcc -o chapter_2 main.o func3.o func2.o func1.o
*.o:func1.h
func3.o:func3.c func3.h
gcc -c func3.c
func2.o func1.o main.o: func2.h
func2.o : func2.c
gcc -c func2.c
func1.o:func1.c
gcc -c func1.c
main.o:main.c func3.h
gcc -c main.c
.PHONY: clean
clean:
rm -r *.o
rm chapter_2
===============================
再次运行
$make
$make clean
OK,此时,删除工作重新奏效。
.PHONY实际上告诉make此目标不是一个文件目标或目标文件(因此我在所有的笔记里,都把targets 翻译为目标而不是目标文件,因为这样更准确,当然方法很简单,我拉大了描述对象的集合)。make 本身的工作内容,如前面所说 ,是 目标,依赖,实现方式。但并不是说目标一定要是文件,可以是个描述实现方式的命令,而通过执行这个目标,实际是执行其对应的命令。那么 .PHONY这个规则,则可以确保make区分 target究竟是个文件,或纯粹的执行。为了再次强调,此处再次变态的做个以下改变
$cat makefile
chapter_2:main.o func3.o func2.o func1.o
gcc -o chapter_2 main.o func3.o func2.o func1.o
*.o:func1.h
func3.o:func3.c func3.h
gcc -c func3.c
func2.o func1.o main.o: func2.h
func2.o : func2.c
gcc -c func2.c
func1.o:func1.c
gcc -c func1.c
main.o:main.c func3.h
gcc -c main.c
.PHONY: clean.txt
clean.txt:
rm -r *.o
rm chapter_2
===============================
同时,创建一个文本文件叫做 clean.txt,执行如下命令
$make
$make clean.txt
此时你会发现,make 仍然执行到 clean.txt段,而没有理睬clean.txt文件。这就是 .PHONY,伪目标规则定义的有效性。

Empty targets
空目标
空目标。目前,暂时没有比较好的实例来描述这个empty targets。从empty targets所最常被使用的情况来看,如时间戳文件,似乎和empty targets的定义没有太多关联。就我个人理解。所谓空目标,和伪目标一样,我们并不关心目标对象的输出。而和伪目标不一样的是,其可存在输出。这其实引起了混乱。例如书上的例子,
prog: size prog.o
$(CC) $(LDFLAGS) -o $@ $^
size: prog.o
size $^
touch size
prog.o的文件更新,引发 size段的执行,size $^我们可以理解。打印出文件信息。但是使用touch size创建一个size文件,如果此叫作 空目标,如果文件本身我们并不关心,由何必创建它呢?当然可以按照书本的解释,我们需要一个空文件(毫无意义的文件)的创建时间记录当前整体make工作的时间。但这个时间隶属于文件的性质,难道就可以说这个文件没有意义吗?我养羊毫无意义,因为我是要羊毛而已,所以这是个空羊,呵呵,这个逻辑难道没有问题吗?
显然,可以肯定,此处,是我在抬杠,但抬杠的目的,是对 empty targets的无法理解。参考
http://www.linuxsir.org/main/doc/gnumake/GNUmake_v3.80-zh_CN_html/make-04.html#_gnu_make_4.8
下的一段文字(上述地址的内容,算是makefile 中文手册中比较权威的解释)
:空目标文件是伪目标的一个变种;此目标所在规则执行的目的和伪目标相同——通过make命令行指定将其作为终极目标来执行此规则所定义的命令。和伪目标不同的是:这个目标可以是一个存在的文件,但文件的具体内容我们并不关心,通常此文件是一个空文件。

空目标文件只是用来记录上一次执行此规则命令的时间。在这样的规则中,命令部分都会使用“touch”在完成所有命令之后来更新目标文件的时间戳,记录此规则命令的最后执行时间。make时通过命令行将此目标作为终极目标,当前目录下如果不存在这个文件,“touch”会在第一次执行时创建一个空的文件(命名为空目标文件名)。
=====================
以上为摘抄部分,可以看出,Empty targets (空目标)如果需要输出文件时,此和我们平时的目标文件没有区别。我个人的认为,唯一区别是目标文件内部的内容没有意义而已。之所以再次抬杠,只是想指出,与其拿empty targets于 phony targets (伪目标)作联系,进行解释或分类,倒不如将其归入传统目标文件中。其和传统目标最大的差异,并不是在文件是否用户关系,因为make 肯定不考虑这点,而在于其可以类似像phony(伪目标)那样,没有输出。就是没有任何新文件产生。

Variables
变量
变量可以说毫无用处,也可以说非常有用。毫无用处,是针对 make本生。对C了解的人,可以把 变量看作类似C里的一种宏,#define。当然只能说是类似,其并不完全等待。而make 在处理makefile文本时,会根据变量,进行展开。这里重复一下书本中对于变量的一些规则约束。
标记一个单词或称连续字符串是变量的方式是 前面 加上 $ ,而如果变量名称超过一个字符,则需要用 ()在$后包含住变量。当然需要注意,这和我写的笔记中,黑体的诸如$cat 中的$不是一个东西。后者只是表示说这是在终端提示符下的一个命令而已。
例如,假设存在变量
a
ab
abc
则可以用 $a $(a) $(ab) $(abc)来描述,但不能 用 $abc,此时$只能涵盖 a这个字符,而不能把bc作为变量符号的一部分。
修改makefile 如下
$cat makefile
a = main.o
ab = func3.o
abc = func2.o
chapter_2:$a $(ab) $(abc) func1.o
gcc -o ~/makefile_2/chapter_2 main.o func3.o func2.o func1.o
*.o:func1.h
$(ab):func3.c func3.h
gcc -c func3.c
$(abc) func1.o main.o: func2.h
$(abc) : func2.c
gcc -c func2.c
func1.o:func1.c
gcc -c func1.c
$a:main.c func3.h
gcc -c main.c
.PHONY: clean
clean:
rm -r *.o
rm chapter_2
===============================
执行
$make clean
$make
此时可以发现,一切正常,和往常一样正常。所以说 变量毫无用处。只是一个替代。此处我们再修改makefile如下
$cat makefile
a = main.o
f = func
ab = $f3.o
abc = $f2.o
chapter_2:$a $(ab) $(abc) $f1.o
gcc -o ~/makefile_2/chapter_2 main.o $f3.o $f2.o $f1.o
*.o:$f1.h
$(ab):$f3.c $f3.h
gcc -c $f3.c
$(abc) $f1.o main.o: $f2.h
$(abc) : $f2.c
gcc -c $f2.c
$f1.o:$f1.c
gcc -c $f1.c
$a:main.c $f3.h
gcc -c main.c
.PHONY: clean
clean:
rm -r *.o
rm chapter_2
=========================================
继续执行
$make clean
$make
$./chapter_2
一切正常,哦。此处makefile里找不到funcX.h funcX.c funcX.o的文字,此时变量的作用很明显,让makefile所包含的字符更少了。所有func 被 f替代,而引用时只需要$f。但显然,这并不是变量最有用的地方。
假设一个工程中,如 func1.c那样,在 makefile多处出现。而当func这个文件名前缀不再喜欢时,你将func1.c修改为 funny1.c。如果不采用变量的方式,你需要依次修改所有的地方。而现在只需要修改f = func 变为 f = funny。(当然这个用途并不是最终常用用途,但能说明出现变量的意义(仅谈make 和make file,此处的变量和编程语言中的变量没有联系))

Automatic Variables

我不想和大牛Robert Mecklenburg争论英文单词用语,但此处我仍然想把 Automatic翻译为“默认”或“隐式规则”或“内部”而不是如
http://www.linuxsir.org/main/doc/gnumake/GNUmake_v3.80-zh_CN_html/make-06.html#_gnu_make_6.2
中翻译的“自动化"

先列出一些内部变量(按我的翻译)或自动化变量(按字面直译,如很多其他文章所说的)如下
$@ 表示当前目标名称
$% 如果目标是个归档库 archive,则此表示这个库中的一个成员。比如 a.a 存在 b.o ,则 $%表示的是b.o,而不是a.a
$< 代表规则中,第一个依赖
$? 代表依赖中,所有比目标更新的依赖,其名称展开时用空格分割
$^ 代表所有依赖,但出现重复时,则之表示一次
$+ 代表所有依赖,如果出现重复,则全部表示
$* 表示目标名称中,去除了后缀的部分。例如 main.o 是目标的话,则此处之表示 main,而将 .o 去除。
看一下上面的翻译,或许有误,当然可以参考网路上绝大多数的翻译。没有办法说明,上述变量和自动化有任何联系。而上述变量的含义也是无法改变的。这和自动化有什么联系,呵呵,天晓得。你可以尝试挑战这个“自动化”,将比如 @ 等于某个你想描述的内容,你会发现,除了你改写 make 的源代码本身以外,似乎找不到其他方法可以实现。$@始终指向目标。因此,我仍然抬杠的认为 Automatic应该翻译为 "内部“表示你不可修改 ”默认“表示已经包含一定含义,简单的说也可以叫做“隐式规则”

回到正题,简单的通过上述 “内部默认”变量,来调整一下makefile
$cat makefile
chapter_2:main.o func2.o func3.o func1.o
gcc -o $@ $^
*.o:func1.h
func3.o:func3.c func3.h
gcc -c $<
func2.o func1.o main.o: func2.h
func2.o : func2.c
gcc -c $<
func1.o:func1.c
gcc -c $<
main.o:main.c func3.h
gcc -c $<
.PHONY: clean
clean:
rm -r *.o
rm chapter_2
===========================================
执行
$make clean
$make
一切正常,照旧。但你会发现,
chapter_2:main.o func2.o func3.o func1.o
gcc -o $@ $^
这样的书写,简洁,明了,修改方便。这就是变量,及"内部默认“变量的基本价值。

VPATH
路径变量
DOS年代,就有路径变量,当然更早的,更复杂的其他操作系统虽然没有用过,但我相信也有类似的路径变量。与其说是变量,不过说是个容器。
很多简单的编程或文件处理的例子都是针对当前目录下的。如上面个的gcc 所操作的内容。但复杂的工程,往往存在不同的目录保存了不同性质的文件。这需要告知系统,默认情况下,可能需要在非当前目录下查找文件。针对上述实验环境,执行以下命令
$mkdir inc
$mkdir src
$mv *.h inc/.
$mv *.c src/.
$make clean
此时当前目录下,应该只存在 makefile文件。当然可能会有一些上述例子所产生的残渣,比如clean,clean.txt,那么全部删除,只保留makefile文件。执行
$make
则提示如下
make: *** 没有规则可以创建“main.o”需要的目标“main.c”。 停止。
问题出在,makefile在当前目录下,main.c文件在 ./src目录下。此时由于makefile没有定义VPATH,则不会自动在 src目录下进行搜索。因此,makefile文件第一行增加如下内容
VPATH =src
再执行
$make
此时提示是
make: *** 没有规则可以创建“main.o”需要的目标“func3.h”。 停止。
很简单。make 首先分析到最总目标为chapter_2,而其第一个依赖是main.o,则转去执行main.o的规则。而main.o需要main.c和func3.h两个文件。很显然,此时无法找到func3.h。因为其在 inc子目录。那么可以如下修改 VPATH
VPATH = src inc
此处可以看到,VPATH,这个路径变量后面可以定义多个路径。因此其更象个容器,可以包含多个路径,而make根据这些路径依次查找文件。
此时执行
$make
提示为
gcc -c src/main.c
src/main.c:2:19: 错误: func1.h:没有该文件或目录
src/main.c:3:19: 错误: func2.h:没有该文件或目录
src/main.c:4:19: 错误: func3.h:没有该文件或目录
make: *** [main.o] 错误 1
呵呵。make 是没错了。但gcc 报错了。gcc找不到指定文件。此处虽然在谈论 VPATH但需要搞清楚,VPATH是make的,而不是 gcc的。也不是那些其他makefile 里面列出的执行命令的操作路径。换言之,VPATH只对make本身有用,对make本身,作依赖性检测时发挥作用,并将查找到的文件,包含路径传递给gcc。因此此处补充一个 新的默认变量 CPPFLAGS,此不是强制的,就是说,通过别的方式也可以替代,例如
CPPFLAGS = XXX,也可以用 OTHER_TEST = XXX来实现。但用CPPFLAGS 如同 make clean一样,符合大家的默契。
在makefile 第二段,新增一行
CPPFLAGS = -I inc
-I 是 GCC的参数。记得和make本身没有关系。gcc是 C的编译器,而make不是。重复,make只是,在编译,连接,安装工程时,就是说从源代码处理成可以用的软件时,你需要执行多个操作,为了保证多个操作的正确性,可以将多个操作,及其相互依赖关系,通过makefile来描述,并用make执行。而make本身完成依赖关系的检测,和批处理makefile里的各种执行命令。但这些执行命令本身和make 没有毛关系。自然,GCC和make 也是没有毛关系。你可以尝试如下gcc 命令
$gcc -c src/main.c inc/func1.h inc/func2.h inc/func3.h
一样出错。错误在哪?在于main.c里存在#include "funcx.h",而这需要通过gcc -I XXX来告知,make 里的路径只能提供上述诸如 src/main.c这样的信息。并不能和gcc的参数符合联动。
此时makefile 如下
$cat makefile
VPATH =src inc
#CPPFLAGS = -I inc
chapter_2:main.o func2.o func3.o func1.o
gcc -o $@ $(CPPFLAGS) $^
*.o:func1.h
func3.o:func3.c func3.h
gcc -c $(CPPFLAGS) $<
func2.o func1.o main.o: func2.h
func2.o : func2.c
gcc -c $(CPPFLAGS) $<
func1.o:func1.c
gcc -c $(CPPFLAGS) $<

main.o:main.c func3.h
gcc -c $(CPPFLAGS) $<
.PHONY: clean
clean:
rm -r *.o
rm chapter_2
=======================================
执行
$make
OK,此时一切正常。输出照旧。上述例子告诉我们几个信息,重复一边
1、可以通过VPATH,来指定一个目录。而某些文件存在于此
2、可以通过依次列出多个目录,通过空格或":"来分隔,来指定多个目录
3、VPATH所引用的路径目录,只对make有效,所以我们需要对CPPFLAGS这个默认规则的变量来进行针对 gcc这个命令的头文件路径进行引用。引用的方法是通过一个变量,这个变量名叫CPPFLAGS。而这个变量在 make里存在一个默契的约束,是用来提供给gcc的。

VPATH比较粗糙。所谓粗糙,就是不够精细。此时可以通过vpath,注意大小写来实现类似工作。照抄原文
The vpath directive is a more precise way to achieve our goals. The syntax of this directive is :
vpath pattern directory-list
也就是说,可以通过vpath来细分不同的路径。如下,
$cat makefile
vpath func%.c src
vpath main.c src
vpath %.h inc
CPPFLAGS = -I inc
chapter_2:main.o func2.o func3.o func1.o
gcc -o $@ $(CPPFLAGS) $^
*.o:func1.h
func3.o:func3.c func3.h
gcc -c $(CPPFLAGS) $<
func2.o func1.o main.o: func2.h
func2.o : func2.c
gcc -c $(CPPFLAGS) $<
func1.o:func1.c
gcc -c $(CPPFLAGS) $<

main.o:main.c func3.h
gcc -c $(CPPFLAGS) $<
.PHONY: clean
clean:
rm -r *.o
rm chapter_2
===============================
此处特意没有使用书上的例子,书上的描述如下
vpath %.c src
vpath %.h inc
,这样的描述存在一定歧异,在我第一次看到这样的描述时,我的直觉以为如下(是错的)
1、模式必须是一类文件
2、模式对应的路径不能重复定义。
3、%是对文件名的统称。
而实际上呢 ?
vpath的定义描述,翻译过来,因该是
vpath 依赖文件的描述(可以用通配符实现) 路径
依赖文件的描述,可以是一个独立的文件名(完整的),也可以是代通配符的一类文件,或者是一种文件类型的所有文件。
此处,仍然要强调的是,vpath只是针对make本身。用于make在解析makefile文件的工作中使用。和 makefile里的某个 目标的执行操作内部所需要的路径没有关系。如果学习,甚至设计过C编译器本身的人,当然附带处理过C的预处理工作的人,可以理解,make实际可以分两次扫描来完成。而第一次扫描,是展开makefile里的书写规则,第二次扫描开始执行规则中的执行命令(类似DOS下的BAT,批处理)。这让我想到个笑话,曾经有个软驱-COPY->软驱的软件,号称如果存在4M内存,则不需要硬盘。但是在那个年代,386的年代,有4M内存的机器没有硬盘,似乎很难想象。所以处理过C编译器的人,还需要学习makefile,恐怕也少有。此处也不在通过这个方式来类比了。
回来,继续做个我喜爱的事情,就是变态的事情。
在当前目录下,执行这样的命令
$mkdir src1
$cp src/func1.c src1/func1.c
编辑 src/func1.c 和src1/func1.c如下
$cat src1/func1.c
#include <stdio.h>
#include "func1.h"
#include "func2.h"
void func_1(void){

printf("the func 1 called in the /src1/func1.c file !/n");
func_21();
}
void func_12(void){
printf("the func 12 called by func_2 !/n");
}
void func_13(void){
printf("the func 13 called by func_3 !/n");
}
=================================
$cat src/func1.c
#include <stdio.h>
#include "func1.h"
#include "func2.h"
void func_1(void){

printf("the func 1 called in the /src/func1.c file !/n");
func_21();
}
void func_12(void){
printf("the func 12 called by func_2 !/n");
}
void func_13(void){
printf("the func 13 called by func_3 !/n");
}
=================================
继续执行
$make clean
$make
$./chapter_2
此时显示为
ok, this is a test for <Managine Project with GNU make>'s chapter 2 by luckystar!
the func 1 called in the /src/func1.c file !
the func 21 called by func_1 !
the func 2 called in the func2.c file !
the func 12 called by func_2 !
the func 3 called in the func3.c file !
the func 13 called by func_3 !
注意,此处调用的是 /src/func1.c内的函数。
那么我们修改makefile ,如下
$cat makefile
vpath func1.c src1
vpath func%.c src
vpath main.c src
vpath %.h inc
CPPFLAGS = -I inc
chapter_2:main.o func2.o func3.o func1.o
gcc -o $@ $(CPPFLAGS) $^
*.o:func1.h
func3.o:func3.c func3.h
gcc -c $(CPPFLAGS) $<
func2.o func1.o main.o: func2.h
func2.o : func2.c
gcc -c $(CPPFLAGS) $<
func1.o:func1.c
gcc -c $(CPPFLAGS) $<

main.o:main.c func3.h
gcc -c $(CPPFLAGS) $<
.PHONY: clean
clean:
rm -r *.o
rm chapter_2
==============================================
继续执行
$make clean
$make
$./chapter_2
显示如下
ok, this is a test for <Managine Project with GNU make>'s chapter 2 by luckystar!
the func 1 called in the /src1/func1.c file !
the func 21 called by func_1 !
the func 2 called in the func2.c file !
the func 12 called by func_2 !
the func 3 called in the func3.c file !
the func 13 called by func_3 !
呵呵,有点意思吧。可以尝试,将makefile中
vpath func1.c src1
vpath func%.c src
两行的顺序对调,看一下效果。由此我们可以确认一个 vpath的事实
1、vpath可以针对不同的依赖文件指定不同的路径,但当两个vpath出现子集冲突时,则以先描述的为准。也就是说,vpath在make识别中,是采用一个链表方式存放不同的vpath的路径规则,但扫描时,是从第一个规则开始扫描,发现符合后,立刻执行。

因此,先描述vpath func1.c src1则,make在识别到func1.c的依赖文件时,会采用此规则,而忽略后面的 vpath func%.c src的方式。这就解决了,不同路径下,文件重名问题。引出一个话题。

从工程管理的角度来看,只要不在当前目录下的文件。无论你认为肯定某个文件的名称,是一个别人想也想不到,你也不再会想到,而不会导致别的目录出现相同名称的文件名。那你也需要对个这文件路径进行vpath约束。这是个好习惯。特别是针对条件 make时。

到目前为止,我所举的变量的例子,除了Automatic 变量,基本上都是针对依赖文件的。当然不仅仅只能用于依赖文件。看一个描述
.c.o:
# 对隐含规则的搜索尚未完成。
# 从不检查修改时间。
# 文件尚未被更新。
# 要执行的命令 (内置):
$(COMPILE.c) $(OUTPUT_OPTION) $<
这个信息可以从
$make -p
中得到。
这里称述了一个隐形规则即 implicit rules。 其规则描述存在于 implicit rules database中。即,已经帮我们列举好了。这个规则可以看作如下描述
%.o:%.c
而 %.o 和 %.c就是变量。当然,用书本中的模式,来表示更确切些。现不看 .c.o,先修改一下makefile 如下
$cat makefile
vpath func1.c src1
vpath func%.c src
vpath main.c src
vpath %.h inc
CPPFLAGS = -I inc
%.o:%.c
gcc -c $(CPPFLAGS) $<
chapter_2:main.o func2.o func3.o func1.o
gcc -o $@ $(CPPFLAGS) $^
.PHONY: clean
clean:
rm -r *.o
rm chapter_2
=====================================
此处,执行
$make clean
$make
$./chapter_2
呵呵,一切照旧,依然是调用的src1里的func1.c。而且上述描述,比以前的描述简单了许多。上面的例子表达了两个信息
1、%.o:%.c是一个模式规则的描述,即我们可以在规则中的目标和依赖文件,都采用变量符,通配符来实现。来表示,一类目标文件和依赖文件的统一实现目标。
2、但这个规则中的目标是个虚目标,并不会成为make的最终目标,因此你放在最上方,也没有关系。所谓虚目标是指,只有在实际需要该规则时,才会根据已有信息,将该规则实际展开。例如,当chapter_2:main.o 时,需要寻找main.o 的规则,则此时才会将 %.o:%.c套用到 main.o中,此时为 main.o:main.c。并使用gcc -c $(CPPFLAGS) $<来实现。
但需要注意一个问题
1、%.o:%.c描述了扩展名(suffix name),并没有约束文件名(stem name),所以其实其就约束了,这个规则要求,目标和依赖的文件名必须相同。
为什么不尝试一下如下写法,我一向很变态
srcfile = %.c
%.o:srcfile
你会发现,变量在此还是适用的。甚至你可以把 %.o:srcfile段放在第一行,而 srcfile = %.c放在最后一行。你会发现,srcfile = %.c仍然在起作用,而其他vpath 也在其作用。如下
$cat makefile
%.o:srcfile
gcc -c $(CPPFLAGS) $<
vpath func1.c src1
vpath func%.c src
vpath main.c src
vpath %.h inc
CPPFLAGS = -I inc
chapter_2:main.o func2.o func3.o func1.o
gcc -o $@ $(CPPFLAGS) $^

.PHONY: clean
clean:
rm -r *.o
rm chapter_2

srcfile = %.c
============================================
原因很简单,就是我上面说的二次扫描的问题。而回到.c.o:上。
.c.o:在 chapter 2 page 24上明确说明,此等于 %.o:%.c。这就是后缀规则。而其实这个后缀规则,由于被默认包含了
$(COMPILE.c) $(OUTPUT_OPTION) $<
因此,即便你不需要显示的写出这个规则,在implicit rule database里,也存在,如果在当前目录下,makefile甚至可以只有chpater_2段的规则,而对于func1.o func2.o等等可以忽略。make会自动调用 inplicit rule隐式规则,来执行。
因此,完全可以删除掉 makefile 里
%.o:srcfile 段的内容。不过执行make 后的显示为
cc -I inc -c -o main.o src/main.c
cc -I inc -c -o func2.o src/func2.c
cc -I inc -c -o func3.o src/func3.c
cc -I inc -c -o func1.o src1/func1.c
gcc -o chapter_2 -I inc main.o func2.o func3.o func1.o
此处表示,隐式规则里,$(COMPILE.c)是用的 cc ,而不是gcc。当然 $(COMPILE.c)展开后包含很多东西。
这里再次体现了默认规则或默契的重要性。 在.c.o:的描述中,并没有 $(CPPFLAGS)出现。但实际上已经包括了。可以尝试删除掉 makefile里的 CPPFLAGS的定义以及使用,如下
$cat makefile
#%.o:srcfile
# gcc -c $(CPPFLAGS) $<
vpath func1.c src1
vpath func%.c src
vpath main.c src
vpath %.h inc
MYCPPFLAGS = -I inc
chapter_2:main.o func2.o func3.o func1.o
gcc -o $@ $(MYCPPFLAGS) $^

.PHONY: clean
clean:
rm -r *.o
rm chapter_2

#srcfile = %.c
============================================
执行
$make clean
$make
此时出错了。第一行信息就是
cc -c -o main.o src/main.c
这里缺少了 -I inc 这段文字。为什么呢?因为CPPFLAGS,作为一个默认的变量,其有默认的含义,是一种默契,和make的内部库有默契,也和其他使用你的 makefile的人有默契。
因此,现在你还不知道默认变量,默认操作方式不要轻易改变的重要性,我就无语了。类似我前面作的,不采用 make clean,而采用 make hehe来删除文件的操作,可以用来玩一玩。但不能用这样的脾性做事情。

关于规则,到这里,基本上讨论完。当然还存在一个 staic pattern rules,静态模式规则。如下
$(OBJECTS): %.o:%.c
这里描述的含义表示,其操作执行,只针对 OBJECTS变量有效。修改makefile 如下
$cat makefile
vpath func1.c src1
vpath func%.c src
vpath main.c src
vpath %.h inc
CPPFLAGS = -I inc
OBJECTS = main.o
chapter_2:main.o func2.o func3.o func1.o
gcc -o $@ $(CPPFLAGS) $^
$(OBJECTS): %.o:%.c
gcc -c $(CPPFLAGS) $<
.PHONY: clean
clean:
rm -r *.o
rm chapter_2
=============================
$make clean
$make
此时打印的内容如下
gcc -c -I inc src/main.c
cc -I inc -c -o func2.o src/func2.c
cc -I inc -c -o func3.o src/func3.c
cc -I inc -c -o func1.o src1/func1.c
gcc -o chapter_2 -I inc main.o func2.o func3.o func1.o
有点意思吧。第一行,使用了makefile里静态模式规则的定义,而funcX使用了隐式模式的定义。当然不代表你需要使用默认的OBJECTS这个变量。你用其他变量一样可以。
笔记二的后语,我尽量想用例子来说明第二章中的问题,而不是简单的翻译原文。其实参考网络的makefile的中文手册,以及本书的中文翻译版(听说翻译的很烂,我没有看到过因此不想评价,当然大概率事件,应该翻译的比我好,虽然听说其很烂)。原因很简单。包括编程,和其他一些涉及计算机方面的书籍,只是看看,或者简单的把英文翻译成中文,往往并不能理解其含义。就我个人对该书的理解,也是出于以下几个方面
1、文字的直译,通过我浅薄的英语。
2、大体猜测make源代码设计的思路
3、例子。
通过一些极端和变态的例子,基本上可以搞清楚,文字直译过来的描述的基本含义。当然一些变态的例子,是根据我对make 本身的实现方式的猜测。如果有一本介绍make本身设计的工程及源代码说明的书,如同linux内核的分析那样,我会很高兴看,当然要确保有足够的时间,但目前为止,我只能通过猜测其实现方法来理解文中的含义。
那么对于学习计算机方面的新手而言,我个人的建议就是,针对一些书上基本的内容,进行各种例子的实践和理解。这样才能抓住重点,和区分差异,比如C的a++,和 ++a只有在一些变态的例子中,才能体现差异。在后期大工程管理或性能优化等高等级工作中,才能拥有足够的能力,去处理面对的问题。这也是为什么我不上来讨论automake autoconf的原因。搞清楚 make本身,再去看 automake这样更简单,甚至可以自己写一套精简的,符合自己要求的,甚至有些变态但很实用的,自己的automake.
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics