用makefile来管理工程 (转)

转自: http://www.360doc.com/content/10/0225/12/883685_16767758.shtml

 

* 用makefile来管理工程

author:sarrow

date:2009-04-13

======================================================================

好多人估计都不了解make以及makefile,更可能听都没有听过。我想说的是,make是一种可
以比IDE所提供的工程管理更为强大的工具。这个工具虽然已经很老了,但是依然有用;并
且有些功能是IDE模仿不来的。

** IDE缺点

IDE是啥?继承编译环境的英文缩写而已。就msvc而言,就是一个可视化编辑器(文本+视图
),套上若干编译工具链;bcb如此。市面上大多数的IDE都如此。

由于看起来很舒服,并且有些工具确实比较吸引人,因此,用者很多。

不过,它还是有不少的缺点。

*** 工具链比较自闭

IDE设置麻烦;不能很好地处理多工程,而且和其他工具不怎么好配合——比如如果你想在
msvc中使用bison或lex的话,你会发现呵呵。

*** 附属文件导致打开耗时

还有就是附属文件的问题。比如msvc,你建立一个工程,就要生成若干的附属文件,比如工
程设定,工作空间,智能补全数据库等等。其中尤以智能补全数据库占用的空间最多。对于
大型工程来说,该文件上百兆不是问题——而一般的实用工具的源代码加起来也不过数M。

可以想象,过多、过大的附属文件将会导致你打开工程的时候越来越慢,系统资源越占越多


对于经常做源码编辑的人来说,这绝对是一个不小的问题。因此,对于经常性的源代码的小
修小补,以及没法定时的灵光一现来说,很多人宁肯用小一点、功能不怎么全的编译器,而
不愿意点开IDE。

*** 编译指令不清晰

虽然msvc的工程看起来很透明,好像一眼就知道最终要编译出来一个啥东西,不过对于多目
标工程,以及控制具体的编译参数的话,就比较不方便了——这其实是所有的gui式程序的
共有缺点,你得连续点击多个地方然后才能找到需要具体修改的地方。而且这些操作没有办
法保存下来,到下一次遇到类似需求的时候,你还得再点击一遍。

======================================================================

** make以及makefile

make,就本文而言,是指GNU GCC工具链中的实用工具——make。可以把它看做是一个解释
器,它所解释的语言叫做makefile。

PS:其实,msvc工具链中也有其对应体,就是nmake——估计用到的人不多。

make工具可没有上面IDE的那些常有的限制。

*** makefile语言是工具的粘合剂

可以任意添加第三方工具——只要需要该工具支持非交互式命令行,那么就能联合使用了。

*** makefile的入口点(Entry point)在哪里?

IDE的编译目标比较含混,makefile脚本也有类似的问题。并且如果不是自己写的makefile
文件,第三者往往不能一眼看出需要编译出什么——因为makefile不像C、C++程序,有一个
入口程序;它的结构是非常松散的,语句几乎可以随便乱放。不过,对于熟悉者而言,这还
是比较清晰的;并且编译参数修改起来,也比较方便,不需要点击多个地方,往往修改一处
即可。

至于操作的保存,makefile本来就是一个脚本文件,当然能够保存你的修改啰!不同的工程
之间,copy-paste大法也不会失效。

----------------------------------------------------------------------

当然,IDE 也有很多优点;比如:

1. 工具集成度较高。

2. 插件比较“友好”。

makefile没有流行,当然也有其缺点,比如:

1. 文本方式,没有花花绿绿的按钮可以看、可以点。

2. 学习成本不可忽略。

======================================================================

** 戏肉来了

本文可不是用来论战的,该进入主题了。——虽然很可能应者寥寥。

有一本关于makefile的英文读物,其英文名和本文章的名字大意一样,不过,我这里不可能
像书本一样面面俱到,我这里只说几个要点。

*** makefile文件的构成

makefile文件是由一些松散的依赖关系和目标生成指令构成的。

什么是目标?什么是依赖关系?

makefile是编写来管理工程的。对于一个最终目标是可执行程序的工程来说,该可执行程序
就是目标,而生成该程序所需要的一切源代码以及资源文件则构成了该目标的依赖。

就好比一辆汽车由发动机、车架、驱动构件,轮胎等组成。车子出了毛病,就需要拿到修理
厂去修理,比如换个轮胎、刹车盘什么的。可以说,汽车就依赖于组成汽车的各个组件。这
里,汽车即目标,组件即依赖。组件出了问题,需要修理、更换,那么整辆车都要送到车厂
里。

车子到了修理厂,第一步就是根据操作规范,听取车主的描述,来检查车子的毛病。

对于厂里来说,其检查过程可以写成这样:

汽车:发动机、车架、驱动构件,轮胎
    rule:如果任意构件坏了,送修理厂

轮胎:
    rule:更换

冒号的两边,分别是目标和依赖,也就是检查规范。次一行带缩进的一句,就相当于操作规
范。

与之类似,makefile文件也就定义了类似目标与依赖之间的关系。

比如写道:

tool.exe: a.o b.o
    gcc -o tool.exe a.o b.o

a.o: a.cpp a.h
    gcc -o a.o -c a.cpp

b.o: b.cpp b.h
    gcc -o b.o -c b.cpp

就是说,最终目标是tool.exe,它依赖于两个文件,分别叫a.o,b.o ——若“依赖”文件
被修改了,发生了变化,就要执行一次下面的rule一次——这里,就是重新链接一次。

就是说,如果a.o或者b.o发生了变化,那么用对应的rule,“gcc -o tool.exe a.o b.o”
更新tool.exe。

后面两句的意思类似,如果a.cpp或a.h发生了变化,那么用对应的rule更新a.o;

b.o的情况也如此,略。

当然,如果每一个工程就像这样硬着编写依赖关系,都会把人给累死,也就更没有人会去写
makefile了。

makefile毕竟是一种脚本语言,而且又是向管理工程的方向特化,因此必然提供了一些便捷
的工具以方便makefile文件的编写。

一个是变量;一种是助记符;还有就是内建的函数了。这三种是makfile中最常用的武器。

由于makefile要与多种命令行工具交流,因此提供多种类型的变量是没有多大实际意义的,
所以makefile在内部只支持一种变量类型,就是字符串。

其定义式简单:

CXX = g++

就是定义了一个名叫CXX的字符串变量,其内部保存的值是g++。

字符串值中也可以使用空格。

FLAGs = -O2 -Wall

使用上也很简单。

要取出某变量的值,用“$(”和“)”套住变量名即可。比如,更新a.o的rule可以写作:

a.o: a.cpp a.h
     $(CXX) -o a.o -c a.cpp $(FLAGs)

对于某目标来说,其依赖可能很多,依赖的种类也可能多种多样——即,依赖的生成需要不
同的rule。为了方便编写rule,makefile提供了一些助记符。

a.o: a.cpp a.h
     $(CXX) -o $@ -c $< $(FLAGs)

其中,$@代表目标的名字;$<代表第一个依赖的名字。

类似有:
tool.exe: a.o b.o
    $(CXX) -o $@ $^

其中,$^代表所有的依赖列表。

对于.o文件的生成来说,其规则都基本相同,那么可以把这样的rule和在一起写吗?可以。
makefile可以把依赖关系和rule分开书写。即:

# 这三句还是说明的依赖关系如何
tool.exe: a.o b.o
a.o: a.cpp a.h
b.o: b.cpp b.h

# 以下4句则是说,目标(注意,目标是相对而言的!就一个依赖关系来说,:左边的是目标
# ,右边的则是该目标的依赖。因此,同一个目标,在不同的依赖关系中来看,可以既是目
# 标又是依赖。)名与“%.exe”相配的话(%号相当于cmd.dir中的*号,可以匹配任意长连
# 续字符;不过,在一个匹配模式中,只能出现一次),在适用于下一句的规则。
%.exe:
    $(CXX) -o $@ $^

%.o:
    $(CXX) -o $@ -c $< $(FLAGs)

其中,#号,后面的是注释。

这样的话,对于大多数类型的工程来说,makefile文件的编写就变成了依赖关系的提供了。

呵呵,依赖关系的生成,对于GCC编译工具来说,也是可以自动的。

比如:

!gcc -MM a.cpp b.cpp > tool.dep

这将调用gcc编译器,让它把编译a.cpp和b.cpp时所依赖的文件(通过include来的文件)列
表,按makefile的“依赖关系”格式,写到外部文件tool.dep内。
即:

a.o: a.cpp a.h
b.o: b.cpp b.h

如何利用这个外部文件呢?这时,到了需要介绍makefile的另一系列关键字的时候了。

先给用法:

include tool.dep

这句话和C/C++中的#include的含义以及行为都基本相同,都是把include 后面的字符串,
当作文件名,在makefile文件被分析的时候,给直接读进来。如果有多个文件,可把其文件
名,用空格分隔,一并写到include后。

说明,include即为makefile语言的关键字了。

类似这样的关键字还有:define,if,ifeq,endif等等。具体解释,后诉。

其实,像上面这样提供对象文件的依赖关系表很是不方便。

1. 仅仅输出一个关系表而已,就要启动一次编译器,这无疑浪费了时间。

2. 有多少个cpp文件,写多少条依赖,还写在一个文件里。这导致了加入新的编译单元的时
   候有点不方便。

为了更自由,起码需要:

1. 依赖关系和具体的编译动作同时执行——节省一次启动以及语法分析的时间。

2. dep文件和对象文件一一对应。

解决方案如下:

%.o:
    $(CXX) -Wp,-MM,-MF,$(subst .o,.dep,$@),-MT,$@ \
        -o $@ -c $< $(FLAG)

说明:-Wp是一种参数传递手段,另一个-Wl与之类似。至于-MM,-MF,-MT这几个参数的具体
含义,请查看gcc手册。

      $(subst x,y,z)则是makefile语言风格的函数调用了。

含义即文本替换。

用法为:

    $(subst <search-string>,<replace-string>,<text>)


makefile中的目标一般都是指实体的文件。所谓的根据依赖关系来执行对应的rule,
make工具其实是凭文件的修改时间来检查的。即如果,文件a依赖于文件b。若a不存在或者
b比a的修改时间更新,那么调用相应的rule。

如果,我调用makefile脚本,就是想在屏幕上输出一句“I'm happy!”怎么办?

车辆年检知道吧?年检可不管车主是否原意,它是强制性的。

makefile里也有类似的东西。

即“虚目标”。使用如下:

.PHONY:all release debug

这定义了分别叫all,release,debug的三个目标,它们是“虚”的,并不和某一个同名的
文件绑定。

比如,你的makefile中再写道:

all: tool.exe

并且,你的源码文件夹中恰好也有一个叫all的文件。那么,make工具在解析makefile文件
的时候就不会管这个叫all的文件是否存在,以及是否没有被修改什么的。

而会直接认为all所依赖的tool.exe比目标新,需要执行一次相应的rule。不过,这里,
rule是空的,无须执行。这时,make会接着检查tool.exe的依赖是否比tool.exe更新。直到
……

 

*** makefile解析方式

一个makefile文件被解析执行,可分为三个步骤。

1. 命令行调用

!make release

这句的意思是,以“release”为最终目标,调用make程序。此时,make程序将在当前文件
夹下搜索名字为makefile的文件,找到了就解析并执行这个脚本。反之报错。

(当然,makefile脚本文件不一定就取作这个没啥特色的名字,你看着不爽,可以另外取名
。不过这时,调用的时候,你就必须制定makefile脚本的文件名了。如下:

!maake -f I_like_this_name.mak release

含义同上,不再解释)

2. 生成目标依赖关系“图”——此“图”,是《数据结构》中的那个“图”

makefile文件在解析的时候,其实是在内部构建一个依赖(图)——可以联想《数据结构
》中的同名数据结构模型。并且,此后,该“图”不再可变。

其中,目标和依赖都相当于“图”中的“顶点”——记住,目标和依赖是相对的概念!rule
则是“边”。

不过这里不允许循环依赖——即,这个“图”不允许循环结构。

那么,这个解析、生成“图”的阶段,哪些makefile元素是起作用的呢?

上面一节提到的那些关键字都是在这个阶段起作用的。变量的定义也是如此。额外需要提一
下的就是MAKECMDGOALS,MAKE这两个内部变量。

MAKECMDGOALS的值为命令行的时候提供的目标参数。注意“-f I_like_this_name.mak”这
样的序列会被filter掉,因为这是提供给make工具的,而不是需要解析的makefile脚本的。

即无论是:

!make release

还是

!maake -f I_like_this_name.mak release

来启动make,解析中,MAKECMDGOALS的值均为release。

利用MAKECMDGOALS和ifeq .. endif,我们可以进行最简单的判断。

    ifeq "$(firstword $(MAKECMDGOALS))" "all"
    CFG = release
    endif
    ifeq "$(firstword $(MAKECMDGOALS))" ""
    CFG = release
    endif
    ifeq "$(firstword $(MAKECMDGOALS))" "release"
    CFG = release
    endif
    ifeq "$(firstword $(MAKECMDGOALS))" "debug"
    CFG = debug
    endif

即,目标是all,空,release时,我们让CFG变量都等于release。而只有目标为debug的时
候,CFG变量才等于debug。

前一节提到的“include 依赖文件”的方式,也是在此时起作用。

问题来了,我还未执行过本makefile脚本,依赖关系文件都没有生成起来,include不久不
能工作吗?C/C++编程的时候,#include一个不存在的头文件,将导致编译错误,这里也如
此!

再介绍makefile语言的有一个常用的工具——错误忽略!

用法很简单,在可能出错的“动作”(包括include这样的内建关键字)语句之前,加上一个
减号即可。假设Dependencies变量存储的是所有需要用到的依赖关系文件名的列表:

-include $(Dependencies)

有了这些工具,我们就可以根据命令行目标的不同而生成合适的“依赖关系图”了。

3. 根据提供的目标,和生成的依赖关系图,执行所需的rule序列

在第一节里,我用了车辆的修理来类比makefile的执行。

对于车主来说,车子是驾驶时有问题,才会去修理厂——然后让修理长进行检查,然后根据
检查结果进行适当的修理。即,这是一个自顶向下的搜索过程;而真正到找到车子毛病的原
因的时候,就反过来了,先修理出问题的地方,然后一步一步组装。这又是一个自底向上的
过程。

makefile文件在执行时也如此。在执行makefile脚本解析时,往往需要提供一个参数作为目
标名,然后根据这个目标名来检查依赖。检查的时候,就是根据第二节里提到的“关系图”
来分析具体的依赖“树”——对于某一个具体的目标而言,其依赖,以及依赖的依赖,都看
做节点的话,将组成一棵“树”(因为环结构是不允许的)。

如果这棵树的某叶子发生了改变,那么回溯其直系祖先结点,依次执行“边”所代表的“
rule”以更新结点。

 

*** 一个例子

# system tool setting
include ~/tool_setting.mak

# make configure file
MK_CFG    := make_cfg

# Set make-configure
ifeq "$(firstword $(MAKECMDGOALS))" "all"
CFG := release
endif
ifeq "$(firstword $(MAKECMDGOALS))" ""
# 如果 $(MK_CFG)文件存在,那么读取其第一个单词作为make的目标;否则以默认值
# release作为make目标
CFG := $(if $(wildcard $(MK_CFG)),$(firstword $(shell $(TYPE) $(MK_CFG))),release)
endif
ifeq "$(firstword $(MAKECMDGOALS))" "release"
CFG := release
endif
ifeq "$(firstword $(MAKECMDGOALS))" "debug"
CFG := debug
endif
ifeq "$(firstword $(MAKECMDGOALS))" "clean"
CFG := $(firstword $(shell $(TYPE) $(MK_CFG)))
endif

ifeq "$(CFG)" "release"
FLAG    := -O2 -Wall
endif

ifeq "$(CFG)" "debug"
FLAG    := -O0 -g
endif

# ==== user defined variables
OBJ_DIR    := $(CFG)
LIBS    =
TARGET    := @DIRNAME@
CPPS    := $(wildcard *.cpp)

# ==== media files ====
OBJS    := $(addprefix $(OBJ_DIR)/,$(patsubst %.cpp,%.o,$(CPPS)))
DEPS    := $(addprefix $(OBJ_DIR)/,$(patsubst %.cpp,%.dep,$(CPPS)))

# ==== media files ====
OBJS    := $(addprefix $(OBJ_DIR)/,$(patsubst %.cpp,%.o,$(CPPS)))
DEPS    := $(addprefix $(OBJ_DIR)/,$(patsubst %.cpp,%.dep,$(CPPS)))

# ==== mainly targets type
.PHONY: all debug release
all debug release : create_out_dir ./$(TARGET)
    @echo $(CFG) > ./$(MK_CFG)

.PHONY: create_out_dir
create_out_dir:
    $(call mkdir-if-not-exist,$(OBJ_DIR))

# ==== dependencies of target
$(TARGET): $(OBJS)

.PHONY: run
run: $(TARGET)
    ./$(TARGET)

.PHONY: clean
clean: clean-media
    $(call rm-file,$(TARGET))

.PHONY: clean-media
clean-media:
    $(call rm-file,$(OBJS))
    $(call rm-file,$(DEPS))
    $(call rm-file,$(MK_CFG))

# ==== media object file make rule :
#      create dependency file and object file at the same time
$(OBJ_DIR)/%.o: %.cpp
    $(CXX) -Wp,-MM,-MF,$(subst .o,.dep,$@),-MT,$@ \
        -o $@ -c $< $(FLAG)

# ==== final target rule ===
# make executable
%.exe:
    $(CXX) -o $@ $^ $(LIBS)

# make dynamic-linked-lib file
%.$(dynamic_lib_suffix):
    $(LD) $(LDFLAGS) -o $@ $(^) $(LIBS) \
        -Wl,--out-implib=$(TARGET).a \
        -Wl,--export-all-symbols \
        -Wl,--enable-auto-import

# make static archive file
%.a:
    $(AR) $(ARFLAGS) $@ $?
    ranlib $@

-include $(DEPS)

==========================================

其中,~/tool_setting.mak中的内容是系统相关的。你为不同的系统提供一个合适的工具设
定文件,将可以达到makfile文件的跨系统使用。

比如:

windows 下,你个人主目录可如下编写

# ~/tool_setting.mak
CXX        := g++
CC        := gcc
RM        := del /f
RC        := windres -O COFF

LD        = $(CXX)
LDFLAGS    := -shared

AR        := ar
ARFLAGS := -rus

# === system tool =================
MKDIR    = mkdir

TYPE    = type

# =================================
dynamic_lib_suffix = dll

define mkdir-if-not-exist
    $(if $(wildcard $1),@echo dir $1 already exist,@echo make dir `$1` && $(MKDIR) $1)
endef

define rm-file
    -$(RM) $(subst /,\,$1)
endef

Linux下,你的个人主目录:

# ~/tool_setting.mak
XX        := g++
CC        := gcc
RM        := rm -f
#RC        := windres -O COFF

LD        = $(CXX)
LDFLAGS    := -shared

AR        := ar
ARFLAGS := -rus

# === system tool =================
MKDIR    = mkdir -p

TYPE    = cat

# =================================
# dynamic_lib_suffix = so

define mkdir-if-not-exist
    $(if $(wildcard $1),@echo dir $1 already exist,@echo make dir `$1` && $(MKDIR) $1)
endef

define rm-file
    -$(RM) $1
endef

另外,你如果不同类型目标的rule的编写也可以省略的话,也可以另起一个makefile文件,
把不同编译器对应的rule写进去,这样就达到了跨编译器使用了。

说明:

     本makefile适用于单个目标的C++工程,可根据目标的后缀自动调用对应的rule生成目
     标(可执行、动态库、静态库),自动生成文件依赖,release版本和debug版本的对
     象文件分别防止,不会发生干扰。添加源文件的时候也不用修改、跨系统使用,也不
     须修改。

trouble shooting

make的时候,若提示makefile文件错误,可先make clean,再make一次即可。

** 延伸阅读 与 reference

    Robert Mecklenburg - Managing Projects with GNU Make

    Gnu Make manual

    跟我一起写 Makefile

 

posted on 2013-01-28 16:52  kkzone  阅读(450)  评论(0)    收藏  举报