Makefile之大型工程项目子目录Makefile的一种通用写法

原创作品,转载时请务必以超链接形式标明文章原始出处:http://blog.csdn.net/gqb666/article/details/8902133,作者:gqb666

   管理Linux环境下的C/C++大型项目,如果有一个智能的Build System会起到事半功倍的效果,本文描述Linux环境下大型工程项目子目录Makefile的一种通用写法,使用该方法,当该子目录内的文件有增删时无需对Makefile进行改动,可以说相当的智能。下面先贴代码(为减小篇幅,一些非关键的代码被去掉,本方法的局限是用于一个C文件生成一个可执行文件的场合):

[plain] view plain copy
  1. ROOTDIR = .  
  2.   
  3. EXE_DIR = ./bin  
  4. CFLAGS = -I$(INCLUDE_DIR) -I$(LIB_INC) -Wall  
  5. LFLAGS = -L$(LIB_DIR)  
  6.   
  7. objects := $(patsubst %.c,%.o,$(wildcard *.c))  
  8. executables := $(patsubst %.c,%,$(wildcard *.c))  
  9.   
  10. all : $(objects)  
  11. $(objects) :%.o : %.c  
  12.     @mkdir -p ./bin$  
  13.     $(CROSS_COMPILE)gcc -c $(CFLAGS) $< -o $@  
  14.     $(CROSS_COMPILE)gcc $(CFLAGS) $< -o $(subst .o, ,$(EXE_DIR)/$@) $(LFLAGS) $(LIBS)  
  15. clean:  
  16.     @rm -f *.o rm -f $(executables)  
  17.     @rm -rf ./bin   
  18. distclean: clean  
假如当前目录里面有a.c b.c 两个文件

       Makefile 里的函数跟它的变量很相似——使用的时候,你用一个$符号跟左圆括号,函数名,空格后跟一列由逗号分隔的参数,最后用右圆括号结束。例如,在 GNU Make里有一个叫'wildcard' 的函数,它有一个参数,功能是展开成一列所有符合由其参数描述的文件名,文件间以空格间隔。像这个命令: 

    objects= $(wildcard *.c)    

   会产生一个所有以'.c' 结尾的文件列表(本例结果为a.c b.c),然后存入变量 objects里。    

   另一个有用的函数是 patsubst ( patten substitude,匹配替换的缩写)函数。它需要3个参数——第一个是一个需要匹配的式样,第二个表示用什么来替换它,第三个是一个需要处理由空格分隔的序列。我们将两个函数合起来用:

objects := $(patsubst %.c,%.o,$(wildcard *.c))

会被处理为:

objects := a.o b.o

同理:

executables := $(patsubst %.c,%,$(wildcard *.c))

会被处理为:

executables := a b

%o:所有以“.o”结尾的目标也就是a.o b.o

依赖模式“%.c”:取模式“%.o”的%,也就是foo bar,并为其加上.c后缀,即a.c,b.c

$<:表示所有依赖目标集,也就是a.c b.c

$@:表示目标集,也就是a.o b.o

命令前加@,表示在终端中不打印,如@mkdir -p ./bin

$(objects) : %.o: %.c 
       $(CROSS_COMPILE)gcc -c $(CFLAGS) $< -o $@

即可翻译为:

a.o b.o : a.c b.c    $(CROSS_COMPILE)gcc -c $(CFLAGS) (a.c b.c) -o (a.o b.o)

明白了这些,这种Makefile的写法就可以完全掌握了

注:当前目录直接make的两种写法见博文Makefile之写demo时的通用Makefile写法




一个适用于层级目录结构的makefile模版

今天写了个层次化的Makefile模版,用来自动化编译项目,这个模版应当包含以下功能:

  • 适用于层次化结构,Makefile主要内容都放在顶层目录下的Makefile.env中,子层Makefile包含这个Makefile.env,只要增加一些变量就可以编译,特别方便添加新的功能模块
  • 自动解析头文件依赖

我的程序的目录结构是这样的:

1. 源文件目录src,模块xxx放在src/xxx下,主程序在src/main下面

2.公共头文件放在include目录下,模块xxx的头文件放在include/xxx目录下

3.模块输出的链接库放在lib目录下

4.可执行文件放在bin目录下

先来看一下Makefile.env,这个类似于c的头文件,包含了所有Makefile的公共部分,

复制代码
###########  MakeFile.env  ##########
# Top level pattern, include by Makefile of child directory
# in which variable like TOPDIR, TARGET or LIB may be needed

CC=gcc
MAKE=make

AR=ar cr
RM = -rm -rf

CFLAGS+=-Wall

dirs:=$(shell find . -maxdepth 1 -type d)
dirs:=$(basename $(patsubst ./%,%,$(dirs)))
dirs:=$(filter-out $(exclude_dirs),$(dirs))
SUBDIRS := $(dirs)

SRCS=$(wildcard *.c)
OBJS=$(SRCS:%.c=%.o)
DEPENDS=$(SRCS:%.c=%.d)


all:$(TARGET)  $(LIB) subdirs

$(LIB):$(OBJS) 
    $(AR)  $@  $^
    cp $@ $(LIBPATH) 

subdirs:$(SUBDIRS)
    for dir in $(SUBDIRS);\
    do $(MAKE) -C $$dir all||exit 1;\
    done

$(TARGET):$(OBJS)
    $(CC) -o $@ $^ $(LDFLAGS)
    cp $@ $(EXEPATH)


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


-include $(DEPENDS)

$(DEPENDS):%.d:%.c
    set -e; rm -f $@; \
    $(CC) -MM $(CFLAGS) $< > $@.$$$$; \
    sed 's,\($*\)\.o[:]*,\1.o $@:,g' < $@.$$$$ > $@; \
    rm $@.$$$$

clean:
    for dir in $(SUBDIRS);\
    do $(MAKE) -C $$dir clean||exit 1;\
    done
    $(RM) $(TARGET) $(LIB)  $(OBJS) $(DEPENDS)
复制代码

当前目录下的子目录是通过shell命令自动得到的,subdirs:$(SUBDIRS) 这块会进入每个子目录执行make,当然有些子目录并不需要编译,可以通过exclude_dirs指定,比如顶层目录的exclude_dirs=bin lib include。

$(DEPENDS):%.d:%.c 这块作用是自动生成头文件依赖,这部分包括5条命令,看起来很复杂,其实原理很简单,假设main.c,包含头文件depend.h, 解析过程如下:

1. @set –e 命令设置当前Shell进程状态为:如果执行的任何一条命令的退出状态非零则立刻终止当前进程。

2. rm -f $@ 删除原来的main.d文件

3. gcc的-MM参数能够生成文件的依赖关系main.o:main.c depend.h,写入文件main.d. $$$$,$$是进程号

4. sed命令作用是将main.o:main.c depend.h替换成main.o main.d:main.c depend.h, 并写入main.d文件

5. rm -f $@.$$$$删除临时文件

Include $(SRCS:.c=.d)将main.d包含进来后,Makefile增加了以下依赖

main.o main.d:main.c depend.h

不管是main.c还是depend.h的变化都会更新main.o 以及main.d,main.d的更新又反过来更新上面这条依赖关系。

这条依赖下面并没有对应的命令,为什么会更新目标文件呢?这跟Makefile的运行步骤有关系,引用下陈浩先生的《跟我一起写Makefile》

GNU的 make 工作时的执行步骤如下:

1、读入所有的 Makefile。

2、读入被 include 的其它 Makefile。

3、初始化文件中的变量。

4、推导隐晦规则,并分析所有规则。

5、为所有的目标文件创建依赖关系链。

6、根据依赖关系,决定哪些目标要重新生成。

7、执行生成命令。

所以1-5 步为第一个阶段,形成了所有的依赖关系链,6-7 为第二个阶段,决定了所有需要生成的目标文件后,执行对应的命令。上面的依赖关系虽然没有命令,但是确定了main.o要重新生成,就会找到以下编译模块生成目标文件

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

假设有一个模块first,源文件都放在src/first下,Makefile如下

复制代码
TOPDIR=./../..

LIB=libfirst.a

INCPATH=$(TOPDIR)/include/first
LIBPATH=$(TOPDIR)/lib
CFLAGS= -I$(INCPATH)

include $(TOPDIR)/Makefile.env
复制代码

TOPDIR是相对于顶层目录的相对路径,LIB是要生成的链接库,这样只要几行命令就可以完成当前模块的编译了,而且first下面还可以添加子模块。

假设main.c在src/main目录下,调用了first模块,Makefile如下

复制代码
TOPDIR=./../..

TARGET=main

LIBPATH=$(TOPDIR)/lib
EXEPATH=$(TOPDIR)/bin

CFLAGS= -I$(TOPDIR)/include/first 
LDFLAGS= -lfirst

include $(TOPDIR)/Makefile.env
复制代码

TARGET是生成的可执行文件名,在LIBPATH目录下寻找链接库,生成的可执行文件会被mv到EXEPATH目录下

src下没有源文件,只有目录,所以Makefile非常简单

TOPDIR=./..

include $(TOPDIR)/Makefile.env

顶层目录下的Makefile也很简单,相对增加了exclude_dirs,排除不需要编译的目录

TOPDIR=.

exclude_dirs= include  bin  lib

include $(TOPDIR)/Makefile.env

现在只需要在顶层目录下make一下,src下所有目录都会编译,生成的链接库放在lib下,可执行文件在bin目录中。如果要增加新的功能模块,只要在src/目录下新建目录,增加一个类似first下的Makefile即可,是不是很方便?


评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值