make和makefile,多文件项目管理

GNU Make简介

大型项目的开发过程当中,每每会划分出若干个功能模块,这样能够保证软件的易维护性。mysql

做为项目的组成部分,各个模块不可避免的存在各类联系,若是其中某个模块发生改动,那么其余的模块须要相应的更新。若是经过手动去完成这个工做的话,对于小型的项目可能还行,可是对于比较大型的项目就几乎是不可能的。ios

所以Linux 系统提供了一个自动维护和生成目标程序的工具 make,它能够根据各个模块的更改状况去从新编译链接目标代码。sql

Make 工具的做用就是实现编译链接过程的自动化。它定义了一种语言,用来描述源文件、目标文件以及可执行文件之间的关系,经过检查文件的时间戳来决定程序中哪些文件须要更新编译,并发送相应的命令。并发

咱们在开发项目的时候,将程序划分为多个模块,分解到不一样的文件中以后。当其中的某一部分发生改变以后,由于其余文件的目标源文件已经存在,因此编译器其实不须要编译所有代码来生成新的可执行文件,而只须要编译被改动的源文件,而后链接全部的目标文件就能够了,这在大型的项目开发中是很是重要的,由于这可能将编译时间从几小时缩小到几分钟。这就是Make所能作的。函数

Makefile文件书写规范

Makefile 文件描述了整个程序的编译、链接规则,主要包括:程序中哪些源文件须要编译以及如何编译,须要建立哪些库文件以及如何建立这些文件,如何产生最终的可执行文件等。工具

基本规则

# 开始的行是注释行。ui

若是一行太长能够用 反斜线 `` 来另起一行,至关于就是一行。spa

Makefile文件的做用是告诉 make工具作什么,多数状况下是如何编译链接一个程序:命令行

目标 : 依赖
<tab键>命令

目标,每每是程序的中间或者最终生成的文件名,好比目标文件、可执行文件……code

依赖,是指用来产生目标文件的输入文件名,一个目标每每依赖于一个或多个文件。

命令,是指任何一个文件发生改动以后,须要从新生成目标文件须要执行的命令,这里能够有多条命令,可是每一个命令必须单独占一行,且须要注意的是,每一个命令的前面必须有一个<tab键>,由于make是用过<tab>来识别命令行的,进而完成相应的动做。

例子1,首先是一个简单的 hello.c的C语言源程序:

#include<stdio.h>
int main()
{
    printf("hello world\n");
    return 0;
}

它的Makefile文件能够是:

/**
目标 : 依赖
<tab键>命令
**/
hello:hello.c
    gcc -o hello hello.c

而后在Makefile和hello.c 所在的目录下执行 make 命令,就能够编译hello.c 生成 hello可执行文件。

例子2,上面那个例子太简单,只有一个文件,下面的这个例子有三个文件和两个头文件。

大体的过程如图:
图片描述

它的对应的Makefile 文件是:

example:sort.o compute.o main.o
    gcc sort.o compute.o main.o -o example
sort.o:sort.c lib1.h
    gcc -c sort.c -o sort.o
compute.o:compute.c
    gcc -c compute.c -o compute.o
main.o:main.c lib2.h
    gcc -c main.c -o mian.o

注意最后须要生成的文件,须要在Makefile 里面写在最前面,它的大体的执行的逻辑是这样的:

1) 在命令行输入 make 命令以后,make命令会首先读取当前目录下的 Makefile文件

2) make 会处理 Makefile里面的第一条规则,也就是上面的 链接生成 example可执行文件

3) 完成第一条规则以后,须要首先处理的是 example所依赖的目标文件,也就是 sort.o、compute.o、main.o,目标文件根据其依赖的源文件或者头文件是否比如今的目标文件更新,或者是目标文件是否存在来决定是否须要从新编译。目标文件处理完成以后,make才会决定是否须要从新链接生成可执行文件,存在三种状况:

  1. 若是全部的文件都没有被编译,则编译全部的源文件,并链接生成可执行文件
  2. 从新编译上次执行 make 命令以后修改过的源文件,生成新的目标文件,而后和已经存在的,但此次没有编译的文件从新链接生成可执行文件
  3. 若是某个头文件在上次执行 make 命令以后被修改,则从新编译全部包含这个头文件的源文件,生成新的目标文件,而后和已经存在的,但此次没有编译的文件从新链接生成可执行文件

变量的定义和使用

Makefile 里面能够定义一个变量来代替一个字符串,这些字符串能够是目标、依赖或者是命令,以及Makefile 的其余部分,引用变量的值的时候,只须要使用 $ 就好,例子:

objects=sort.o compute.o main.o
CC=gcc
CFLAGS=-Wall -g
example:$(objects)
    $(CC) &(objects) -o example
sort.o:sort.c lib1.h
    $(CC) $(CFLAGS) -c sort.c -o sort.o
compute.o:compute.c
    $(CC) $(CFLAGS) -c compute.c -o compute.o
mian.o:mian.c lib2.h
    $(CC) $(FLAGS) -c mian.c -o mian.o

变量名是大小写敏感度的,通常命令相关的变量习惯用大写(CC),文件相关的变量习惯用小写(objects),参数相关的变量也习惯使用大写(CFLAGS)。

若是变量名是单字符,能够直接使用 $变量名来引用,$C;可是变量名为多于一个字符的字符串,在引用的时候,必须使用$(变量名) 的形式,好比 $(CFLAGS),不然make工具只会解析第一个字符,好比 $CFLAGS,将只会解析 $C 的变量,后面的FLAGS做为普通的字符串看待,等价于$(C)FLAGS,因此就可能出错。

根据变量的定义和展方式是不一样,能够将Makefile里面的变量分为:

1) 递归展开式变量

经过 = 来进行定义,引用的时候进行严格的文本替换,变量中对于其余的变量或者函数的引用在使用时候才进行展开。例子:

A=$(B)
B=$(C)
C=Hello

若是在这个Makefile 里面存在对变量 A的引用:$(A),那么在执行make 命令的时候,变量开始替换,首先将变量 A替换为变量 B,接下来替换为变量C, 最终替换为 Hello。

递归展开式的优势是,变量定义的时候能够引用后续定义的变量。

缺点是,有可能在变量展开时出现无穷的循环,这就很蛋疼了。

2) 直接展开式变量

为了不递归展开式变量存在的问题,因此可使用直接展开式变量,经过:= 进行定义。变量中对于其余的变量或者函数的引用在定义时候就进行展开。

例子1

A=Hello
B:=$(A)World
A:=HI

由于是在定义的时候就展开,因此,变量B 的值是HelloWorld,而不像递归展开式中会是HIWorld(递归展开式变量中,A会持续对B形成影响),由于A首先定义为 Hello,而后定义B,由于在定义时候就展开,因此B的值是HelloWorld,然后面再从新定义A 的话是不会对B在形成影响的。

例子2

B:=$(A)World
A=Hi

则最终 B的值是 World,由于在定义B 的时候A 尚未定义,因此make 会认为A 是空。

隐含规则

隐含规则是系统或用户预先定义好的一些特殊规则,主要是一些经常使用的依赖关系和更新命令。

通常规则使用文件的全名,而隐含规则中出现的目标文件和依赖文件都只使用文件的扩展名。若是Makefile 文件里面没有显式给出文件的依赖关系的时候,make 就会根据文件的扩展名找到相应的隐含规则,而后按照隐含规则来更新目标。

例子,隐含规则是:

.c:
    $(CC) $(CFLAGS) -o &@ $<
.c .o:
    $(CC) $(CFLAGS) -c $<

下面给出的Makefile就是使用上面的隐含规则:

objects=sort.o compute.o mian.o
CC=gcc
CFLAGS=-Wall -g
example:$(objects)
    $(CC) $^ -o $@
sort.o:lib1.h
mian.o:lib2.h

伪目标

Makefile 文件中的目标分为两类:实目标和伪目标。

实目标是真正要生成的以文件形式存放在磁盘上的目标,上面所讲解到的都属于实目标;而伪目标不要求生成实际的文件,它主要是用于完成一些辅助操做。例子:

clean
    rm example $(objects)

在Makefile 里面增长了上面的规则以后,在命令里面输入命令:make clean 就会执行命令:rm example sort.o compute.o mian.o

可是这种书写形式不是很严谨,由于可能在当前目录下面存在文件名为 clean 的文件,由于这时候: 后面没有依赖文件,因此make 就认为这个文件是最新的,因此就不会执行 rm example sort.o compute.o mian.o

因此为了不这种状况的发生,因此建议使用这种:

.PHONY:clean
clean:
    rm example $(objects)

这样,无论当前目录下是否存在文件名为 clean 的文件,rm example sort.o compute.o mian.o命令都会被执行。

函数

GNU make提供了不少的函数,能够在Makefile文件中调用这些函数来进行文件名、变量以及命令等的处理。

函数的调用方式与变量相似,使用 $ 符号。

1) patsubst 函数

主要用于对字符串经行运算和分析,格式是:

$(patsubst pattern,replacement,text)

例子1

$(patsubst %.c,%.o,sort.o compute.c main.c)

这个就是输出与源文件相对应的目标文件列表,输出为:

sort.o compute.o main.o

2) dir 函数

主要用于获取文件的路径,例子:

$(dir main.c)

若是main.c 在当前目录下,就会输出:

./  //使用相对路径的形式

3) notdir 函数

抽取文件名中除了路径以外的其余字符,例子:

$(notdir /home/perfect/Mywork/C/main.c ./Makefile

输出是:

main.c Makefile

4) suffix 函数

获取文件名的后缀:

$(suffix ./main.c)

输出结果是:

.c  //也就是 ./main.c上的后缀

通用Makefile文件

能够看出,编写一个Makefile仍是很复杂的。

下面给出一个通用的Makefile 文件,其做者是应该的Gorge Foot,之因此说它是通用的,主要是由于它不须要通过修改就能够应用于大部分的项目之中:

######################################
# Copyright (c) 1997 George Foot (george.foot@merton.ox.ac.uk)
# All rights reserved.
######################################
#目标(可执行文档)名称,库(譬如stdcx,iostr,mysql等),头文件路径
DESTINATION := test
LIBS := 
INCLUDES := .


RM := rm -f
#C,CC或CPP文件的后缀
PS=cpp
# GNU Make的隐含变量定义
CC=g++
CPPFLAGS = -g -Wall -O3 -march=i486
CPPFLAGS += $(addprefix -I,$(INCLUDES))
CPPFLAGS += -MMD

#如下部分无需修改
SOURCE := $(wildcard *.$(PS))
OBJS := $(patsubst %.$(PS),%.o,$(SOURCE))
DEPS := $(patsubst %.o,%.d,$(OBJS))
MISSING_DEPS := $(filter-out $(wildcard $(DEPS)),$(DEPS))
MISSING_DEPS_SOURCES := $(wildcard $(patsubst %.d,%.$(PS),$(MISSING_DEPS)))

.PHONY : all deps objs clean rebuild

all : $(DESTINATION)

deps : $(DEPS)
         $(CC) -MM -MMD $(SOURCE)

objs : $(OBJS)

clean :
         @$(RM) *.o
         @$(RM) *.d
         @$(RM) $(DESTINATION)

rebuild: clean all 

ifneq ($(MISSING_DEPS),)
$(MISSING_DEPS) :
         @$(RM) $(patsubst %.d,%.o,$@)
endif

-include $(DEPS)

$(DESTINATION) : $(OBJS)
         $(CC) -o $(DESTINATION) $(OBJS) $(addprefix -l,$(LIBS))
#结束

简介(经过研究这个Makefile 文件能够很好的理解Makefile的规则):

  • 原做者是Gorge Foot,写这个Makefile的时候仍是一个学生

  • ":="赋值,和"="不一样的是,":="在赋值的同时,会将赋值语句中全部的变量就地展开,也就是说,A:=$(B)后,B的值的改变再也不影响A

  • 隐含规则。GUN Make在不特别指定的状况下会使用诸如如下编译命令:$(CC) $(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c $< -o $@,这也是为何这个Makefile最后一个命令没有添加$(CPPFLAGS)的缘由,由于缺省是包含这个变量的

  • 函数和变量很类似:"$ (函数名,空格,一列由逗号分隔的参数)"

  • SOURCES = $(wildcard .cpp) 列出工做目录下文件名知足".cpp"条件的文件,以空格分隔,并将列表赋给SOURCE变量

  • patsubst函数:3个参数。功能是将第三个参数中的每一项(由空格分隔)符合第一个参数描述的部分替换成第二个参数制定的值

  • addprefix函数:2个参数。将源串(第2个参数,由空格分隔)中的每一项添加前缀(第1个参数)

  • filter-out函数:2个参数。从第二串中过滤掉包含在第一个串中的项

  • $(CC) -MM -MMD $(SOURCE) : 对每一个源文件生成依赖(dependence,Make经过依赖规则来判断是否须要从新编译某个文件),"D"生成".d"文件,-MM表示去掉 depends里面的系统的头文件(使用<>包含的头文件)(若使用-M则所有包含,事实上,系统头文件被修改的可能性极小,不须要执行依赖检查)

  • .PHONY,不检查后面制定各项是否存在同名文件

  • ifneg...else...endif,Makefile中的条件语句

  • -include $(DEPS) : 将DEPS中的文件包含进来,"-"表示忽略文件不存在的错误

  • @$(RM) *.o : 开头的"@"表示在Make的时候,不显示这条命令(GNU Make缺省是显示的)

  • all : 做为第一个出现的目标项目,Make会将它做为主要和缺省项目("make"就表示"make all")

  • deps : 只生成依赖文件(.d文件)

  • objs : 为每个源码程序生成或更新 '.d' 文件和'.o'文件

  • clean : 删除全部'.d','.o'和可执行文件

  • rebuild : clean而后重建

  • 内部变量$@, $< $^ : 分别表示目标名(:前面的部分,好比all),依靠列表(:后面的部分)中的第一个依靠文件,全部依靠文件

相关文章
相关标签/搜索