标签 博客 下的文章

一个C++项目的Makefile编写-Tony与Alex的对话系列

Tony : Hey Alex, How are you doing?
Alex : 不怎么样。(显得很消沉的样子)
Tony : Oh , Really ? What is the matter?
Alex : 事情是这样的。最近有一个Unix下的C++项目要求我独自完成,以前都是跟着别人做,现在让自己独立完成,还真是不知道该怎么办,就连一个最简单的项目的Makefile都搞不定。昨晚看了一晚上资料也没有什么头绪。唉!!
Tony : 别急,我曾经有一段时间研究过一些关于Makefile的东西,也许能帮得上忙,来,我们一起来设计这个项目的Makefile。
Alex : So it is a deal。(一言为定)
Tony : 我们现在就开始吧,给我拿把椅子过来。

(Tony坐在Alex电脑的旁边)
Tony : 把你的项目情况大概给我讲讲吧。
Alex : No Problem ! 这是一个“半成品”项目,也就是说我将提供一个开发框架供应用开发人员使用,一个类似MFC的东西。
Tony : 继续。
Alex : 我现在头脑中的项目目录结构是这样的:

APL (Alex's Programming Library)
 -Make.properties
 -Makefile(1)
 -include  //存放头文件
  -Module1_1.h
  -Module1_2.h
  -Module2_1.h
  -Module2_2.h
 -src   //存放源文件
  -Makefile(2)
  -module1
   -Module1_1.cpp
   -Module1_2.cpp
   -Makefile(3)
  -module2
   -Module2_1.cpp
   -Module2_2.cpp
   -Makefile(3)
  -…
  
 -lib  //存放该Project依赖的库文件,型如libxxx.a
 -dist  //存放该Project编译连接后的库文件libapl.a
 -examples  //存放使用该“半成品”搭建的例子应用的源程序
  Makefile(4)
  -appdemo1
   -Makefile(5)
   -src  //存放应用源代码
   -include
   -bin  //存放应用可执行程序
  -appdemo2
   -Makefile(5)
   -src  //存放应用源代码
   -include
   -bin  //存放应用可执行程序
  -…

Tony : I got it!
Alex : 下面我们该如何做呢?
Tony : 我们来分析一下各个Makefile的作用。你来分析一下各个级别目录下的Makefile的作用是什么呢?
Alex : (思考了一会儿)我想应该是这样的吧。
 Makefile(3)负责将其module下的.cpp源文件编译为同名.o文件,同时其phony target "clean"负责删除该目录下的所有.o文件;
 Makefile(2)负责调用src目录下所有module的Makefile文件。
 Makefile(1)负责先调用src中的Makefile生成静态库文件,然后调用examples中的Makefile构建基于该框架的应用。
 至于Make.properties,定义通用的目录信息变量、编译器参数变量和通用的依赖关系。

Tony : 说得很好。我们一点一点来,先从src中每个module下的Makefile着手,就如你所说在每个module下的Makefile负责将该module下的.cpp文件编译为同名的.o文件。
Alex : 好的,我来写吧,这个我还是能搞定的。看下面:
module1下的Makefile如下:

#
# Makefile for module1
#
all : Module1_1.o Module1_2.o

Module1_1.o : Module1_1.cpp
 g++ -c $^ -I ../../include
Module1_2.o : Module1_2.cpp
 g++ -c $^ -I ../../include

clean :
 rm -f *.o

module2下的Makefile如下:

#
# Makefile for module2
#
all : Module2_1.o Module2_2.o

Module2_1.o : Module2_1.cpp
 g++ -c $^ -I ../../include
Module2_2.o : Module2_2.cpp
 g++ -c $^ -I ../../include

clean :
 rm -f *.o

make一下,顺利产生相应的.o文件。

/*=============================================================
Note: 关于$^、$<和$@的用法说明:
$@ — “$@”表示目标的集合,就像一个数组,“$@”依次取出目标,并执于命令。
$^ — 所有的依赖目标的集合。以空格分隔。如果在依赖目标中有多个重复的,那个这个变量会去除重复的依赖目标,只保留一份。
$< — 依赖目标中的第一个目标名字

举例: Module1_1.o Module1_2.o : Module1_1.cpp Module1_2.cpp
则$@ — Module1_1.o Module1_2.o
$^ — Module1_1.cpp Module1_2.cpp
$< — Module1_1.cpp

==============================================================*/

Tony : Well done! 不过发现什么问题了么?
Alex : 什么问题?
Tony : 存在重复的东西。在重构中我们知道如果两个子类中都定义相同的接口函数,我们会将其pull up到基类中。同样我们可以重构我们的Makefile,把一些重复的东西拿到外层去。
Alex : (似乎略微明白了一些)我想有三处重复:a)查找头文件的路径是重复的; b)g++这个字符串可以用一个变量定义代替 c)编译器的编译参数可以也定义到一个变量中。我知道Make工具支持include一个文件,我们就建立一个公用的文件来存放一些通用的东西吧。
Tony : 没错,Just do it.
Alex : 就按我原先的想法,把这些公共的部分放到Make.properties中吧。

#
# Properties for demo's Makefile
#
MAKEFILE  = Makefile

BASEDIR = $(HOME)/proj/demo

####################
# Directory layout #
####################
SRCDIR = $(BASEDIR)/src
INCLUDEDIR = $(BASEDIR)/include
LIBDIR = $(BASEDIRE)/lib
DISTDIR = $(BASEDIR)/dist

####################
# Compiler options #
#    F_ — FLAG    #
####################
CC = g++

# Compiler search options
F_INCLUDE = -I$(INCLUDEDIR)
F_LIB = -L $(LIBDIR)

CFLAGS =
CPPFLAGS = $(CFLAGS) $(F_INCLUDE)

然后修改一下,各个module中的Makefile文件,以module1为例,修改后如下:
#
# Makefile for module1
#
include ../../Make.properties

all : Module1_1.o Module1_2.o

Module1_1.o : Module1_1.cpp
 $(CC) -c $^ $(CPPFLAGS)
Module1_2.o : Module1_2.cpp
 $(CC) -c $^ $(CPPFLAGS)

clean :
 rm -f *.o

Tony : 其实这两个Makefile中还有一个隐含的重复的地方
Alex : 你是指依赖规则么?
Tony : 嗯,这个依赖规则在src中的各个module中都会用得到的。
Alex : 没错,我也是这么想的,我现在就把这个规则抽取出来,然后你来评审一下。我想利用make工具的传统的“后缀规则”来定义通用依赖规则,我在Make.properties加入下面的变量定义:

####################
# Common depends   #
####################
DEPS = .cpp.o

然后还是以module1为例,修改module1的Makefile后如下:
#
# Makefile for module1
#
include ../../Make.properties

all : Module1_1.o Module1_2.o

$(DEPS):
 $(CC) -c $^ $(CPPFLAGS)

clean :
 rm -f *.o

Tony : 基本满足需求。我们可以进行上一个层次的Makefile的设计了。我们来设计Makefile(2)。Alex,你来回顾一下Makefile(2)的作用。
/*=============================================================
Note: 关于后缀规则的说明
后缀规则中所定义的后缀应该是make 所认识的,如果一个后缀是make 所认识的,那么这个规则就是单后缀规则,而如果两个
连在一起的后缀都被make 所认识,那就是双后缀规则。例如:".c"和".o"都是make 所知道。因而,如果你定义了一个规则是
".c.o"那么其就是双后缀规则,意义就是".c"是源文件的后缀,".o"是目标文件的后缀, ".c.o"意为利用.c文件构造同名.o文件。
==============================================================*/

Alex : No Problem! 正如前面说过的Makefile(2)负责调用src目录下所有module子目录下的Makefile文件,并负责将各个module下的.o文件打包为libdemo.a文件放到dist目录中。所以存在简单的依赖关系就是libdemo.a依赖各个module子目录下的.o文件,而前面的Makefile(3)已经帮我们解决了.o文件的生成问题了,即我们只需要逐个在各module子目录下make即可。我的Makefile(2)文件设计如下:
#
# Makefile for src directory
#

include ../Make.properties

TARGET = libdemo.a

####################
#  Subdirs define  #
####################
MODULE1_PATH = module1
MODULE2_PATH = module2
SUBDIRS = $(MODULE1_PATH) $(MODULE2_PATH) 

####################
#  Objects define  #
####################
MODULE1_OBJS = $(MODULE1_PATH)/Module1_1.o $(MODULE1_PATH)/Module1_2.o
MODULE2_OBJS = $(MODULE2_PATH)/Module2_1.o $(MODULE2_PATH)/Module2_2.o
DEMO_OBJS = $(MODULE1_OBJS) $(MODULE2_OBJS)

all : subdirs $(TARGET)
 cp $(TARGET) $(DISTDIR)

subdirs:
 @for i in $(SUBDIRS); do \
  echo    "===>$$i"; \
  (cd $$i &&$(MAKE) -f $(MAKEFILE)) || exit 1; \
  echo    "<===$$i"; \
 done

$(TARGET) : $(DEMO_OBJS)
 ar -r $@ $^

clean:
 @for i in $(SUBDIRS); do \
  echo    "===>$$i"; \
  (cd $$i &&$(MAKE) clean -f $(MAKEFILE)) || exit 1; \
  echo    "<===$$i"; \
 done
 rm -f $(DISTDIR)/$(TARGET)

Tony : Alex你的进步真的是很大,分析问题的能力提高的很快,方法也不错。这个设计的缺点在于一旦新增了一个module子目录,这个Makefile文件就需要改动,不过改起来倒不是很难。有机会可以再想想,使这个Makefile更加通用。

Alex : 我记住了。我们继续么?
Tony : 歇一回吧^_^。

/*=============================================================
Alex and Tony are having a short break.
==============================================================*/

Tony : 你的咖啡味道真不错。
Alex : 这可是朋友从巴西带回来的极品咖啡豆,经过我精心研磨而成的。
Tony : 想不到你在这方面还有研究。
Alex : 呵呵。
Tony : Let's go on 。有了Makefile(2),后面的工作就轻松多了。
Alex : 现在我的信心也很足,我来设计Makefile(1),它负责先调用src中的Makefile生成静态库文件,然后调用examples中的Makefile构建基于该框架的应用。我还是按照Makefile(2)的思路走,看我的Makefile(1):

#
# Makefile for whole project
#

include Make.properties

SRC_PATH = src
EXAMPLES_PATH = examples

SUBDIRS = $(SRC_PATH) $(EXAMPLES_PATH) 

all : subdirs
 
subdirs:
 @for i in $(SUBDIRS); do \
  echo    "===>$$i"; \
  (cd $$i && $(MAKE) -f $(MAKEFILE)) || exit 1; \
  echo    "<===$$i"; \
 done

clean:
 @for i in $(SUBDIRS); do \
  echo    "===>$$i"; \
  (cd $$i && $(MAKE) clean -f $(MAKEFILE)) || exit 1; \
  echo    "<===$$i"; \
 done

运行一下,由于examples目录下的Makefile还是空的,所以没有成功。

Tony : 有了前面的经验,相信完成examples目录下的两个Makefile对你来说不成问题。
Alex : I could not agree with you any more(Alex脸上满是笑容),我来完成它。
每个appdemoX下的Makefile(5)我设计成这样:
#
# Makefile for appdemoX
#
include ../../Make.properties

TARGET = appdemoX
SRC = ./src/appdemoX.cpp

all :
 $(CC) -o $(TARGET) $(SRC) $(CPPFLAGS) -L $(DISTDIR) -ldemo
 mv $(TARGET).exe ./bin

clean :
 rm -f ./src/*.o ./bin/$(TARGET).exe

而examples目录下的Makefile(4)的样子如下:
#
# Makefile for examples directory
#

include ../Make.properties

EXAMPLE1_PATH = appdemo1
EXAMPLE2_PATH = appdemo2

SUBDIRS = $(EXAMPLE1_PATH) $(EXAMPLE2_PATH) 

all : subdirs
 
subdirs:
 @for i in $(SUBDIRS); do \
  echo    "===>$$i"; \
  (cd $$i &&$(MAKE) -f $(MAKEFILE)) || exit 1; \
  echo    "<===$$i"; \
 done

clean:
 @for i in $(SUBDIRS); do \
  echo    "===>$$i"; \
  (cd $$i &&$(MAKE) clean -f $(MAKEFILE)) || exit 1; \
  echo    "<===$$i"; \
 done

Tony : 可以,不知不觉间,我们的工作已经接近尾声,剩下的工作就是细节了,包括编译器参数的细化等。
Alex : 在Makefile(1)中加上install,tar等目标,使用户得到有更多的功能。十分感谢你的指导。
Tony : 那晚上去原味斋吧,想烤鸭了^_^。

/*=============================================================
Note : Makefile常识
a) "=" vs ":="
例子:
C_OPTIONS = $(C_EXTRA_OPTION) -O2
C_EXTRA_OPTION = -g
cfoo: foo.c exam.c
    gcc $(C_OPTIONS) -o $@ $^
=>gcc -g -O2 -o cfoo foo.c exam.c

C_OPTIONS := $(C_EXTRA_OPTION) -O2
C_EXTRA_OPTION = -g
cfoo: foo.c exam.c
    gcc $(C_OPTIONS) -o $@ $^
=>gcc -O2 -o cfoo foo.c exam.c

大家发现不同了,Why? 使用“=”赋值的变量在使用时才被展开,并且每使用一次就会展开一次,其值每次展开的时候有可能是不同的,就如第一个C_OPTION由于在使用时展开,所以C_EXTAR_OPTION定义的位置不影响C_OPTION的值。而使用“:=”进行赋值的变量,则在赋值的时候就被展开,并且仅仅展开一次,从此以后其值将不会发生任何变化,就第二个C_OPTION由于定义时展开所以由于定义时看不到C_EXTAR_OPTION所以值为-02,而不是-g -02。

b) wildcard 函数
在 GNU Make 里有一个叫 'wildcard' 的函数,它有一个参数,功能是展开成一列所有符合由其参数描述的文件名,文件间以空格间隔。你可以像下面所示使用这个命令:SOURCES = $(wildcard *.cpp)   SOURCES = xx.cpp yy.cpp … zz.cpp
==============================================================*/

一个Xml Parser的TDD开发过程-Tony与Alex的对话系列

Tony : Alex今天我们来做一个xml parser.我们使用的开发工具为Eclipse + JUnit
Alex : 好啊,喜欢接受挑战。
Tony : 先看看我们要解析的xml file的样子:

 
  
   
   
    
 

使用XmlSpy自动生成其DTD如下:

<!ATTLIST test 
 name CDATA #REQUIRED
>

<!ATTLIST suite 
 name CDATA #REQUIRED
 category CDATA #REQUIRED
>

<!ATTLIST classes 
 name CDATA #REQUIRED
>

我们就是要将该xml文件中的test , suite, class标签中存储的信息读出来,存储在数据结构中XmlTest中。所以我们的Parser的parse方法返回的就是个XmlTest的引用。我们通过上面的xml文件,我们可以确定一个类-XmlTest,带有一个成员name,创建XmlTest, 这个so easy

public class XmlTest {
    private String name;
   
    /**
     * @param name The name to set.
     */
    public void setName(String name) {
        this.name = name;
    }

    /**
     * @return Returns the name.
     */
    public String getName() {
        return name;
    }
   
}

接下来,tdd的经典流程就是先写出feature list,来我们琢磨一下吧,

Alex :首先我们要知道我们要解析的xml文件的存放处,如果用户提供一个错误的位置,我们应该抛出异常。—-(1)

Tony :Great idea! 那我们就动手把。首先我们创建一个测试类ParserTest,添加一个方法

testNotFoundXmlFile,并创建一个Parser实例,将错误的xml文件路径传给它。代码如下:
public class ParserTest extends TestCase {
    public void testNotFindXmlFile(){
        try{
            Parser parser = new Parser("D:\\TestDemo\\demo1.xml");
            XmlTest result = parser.parse();
            fail("The FileNotFileFoundException should be raised!");
        }catch(FileNotFoundException fnfe){
           
        }
    }
}

run the test –> red bar
由于Parser类暂不存在,当然测试通过不了了。添加Parser类,并添加parse方法
public class Parser {
    private String xmlFilePath;
    public Parser(String xmlFilePath){
        this.xmlFilePath = xmlFilePath;
    }
   
    public XmlTest parse() {
        return null;
    }
}

run the test–>red bar

Alex :我来修改一下parse方法
public XmlTest parse()throws FileNotFoundException{
        File file = new File(xmlFilePath);
        if(!file.exists()){
            throw new FileNotFoundException();
        }
        return null;
    }
再试试吧。

Tony : Ok,green bar ,so beautiful!!! 我们这么快就完成了第一个feature了。
Alex : 是呀,不过这只是个开头。最主要的功能还没有实现呢。Just go on!

Tony : ok ,继续。

Alex :从dtd可以看出,该xml的根元素是test标签。下一步我们要完成的就是读取根元素test标签的属性信息—-(2)

Tony :i could not agree with you any more !! 这样就涉及到一些xml解析的知识了,我们来学习一下吧。

a short break !
Tony and Alex are studying JAXP together!

Tony :怎么样,ok了么
Alex :差不多了,JAXP不难,我们继续把。
Tony :是呀,按照JAXP给的例子,我们可能很容易就达到目的。ok,让我们add a new test.

public void testReadTestNode(){
        try{
            Parser parser = new Parser("D:\\TestDemo\\demo.xml");
            XmlTest result = parser.parse();         
            assertEquals("my first test", result.getName());
        }catch(FileNotFoundException fnfe){
            fail("The program should not reach here");
        }
}

run the test, as we expect it is a red bar.

Alex :根据JAXP的例子,我们解析特殊的xml file需要自己定制一个DefaultHandler的子类。然后将之传给SAXParser的parse方法。

Tony :好吧,让我们创建一个MyHandler class吧!

Alex :根据SAX的解析流程,我们要override some methods

Tony :要让我们的测试用例通过,我们得override DefaultHandler的startElement method,通过MyHandler获取我们所要的内容。

public class MyHandler extends DefaultHandler {

    private XmlTest curXmlTest = null;
   
    public XmlTest getXmlTest(){
        return curXmlTest;
    }
    public void startElement(String uri, String localName, String qName,
            Attributes attributes) throws SAXException {
        if ("test".equals(qName)) {
            curXmlTest = new XmlTest();
            curXmlTest.setName(attributes.getValue("name"));
        }
    }   
}

Alex :同时我们还要修改一下我们的Parser类的parse方法,如下:
parser.parse(file, myHandler);
result = myHandler.getXmlTest();
这样我们通过MyHandler取得了我们需要的东西,相信这一回test一定会通过的

Tony : 如你所愿,通过了。

Alex : 看看我们的代码,有些乱是吧。我们来重构一下吧。

Tony :我们先来重构一下测试代码。利用Junit提供的setUp将testfixture部分封装起来。

Alex :我来做。

public class ParserTest extends TestCase {

    private Parser parser;
   
    protected void setUp() throws Exception {       
        super.setUp();
        parser = new Parser("D:\\TestDemo\\demo.xml");
    }
   
    //…

   public void testReadTestNode(){
        try{           
            XmlTest result = parser.parse();         
            assertEquals("my first test", result.getName());
        }catch(FileNotFoundException fnfe){
            fail("The program should not reach here");
        }
    }
}
运行一下测试,一切Ok.还是那条令人兴奋的green bar

Tony : 以应用代码作为基础的的测试代码重构做完了,这两部分互相保证还是让我们很是放心的。

Alex :是呀,要不继续。我们来想想下一个todo item。你觉得我们是不是该读取suite标签信息了亚?———-(3)

Tony :就按你所说我们继续添加测试用例testReadSuiteNode, 我们修改一下XmlTest使之存储Suite信息。

Alex :那我们再创建一个类XmlSuite,然后在XmlTest中包含XmlSuite的集合。

Tony : 那我们的testReadSuiteNode可以这样写
public void testReadSuiteNode() {
        try {
            XmlTest result = parser.parse();
            assertEquals(1, result.getXmlSuites().size());
            assertEquals("test1", result.getXmlSuites().get(0).getName());
            assertEquals("unit", result.getXmlSuites().get(0).getCategory());
        } catch (FileNotFoundException fnfe) {
            fail("The program should not reach here");
        }
}

Alex : 我们需要添加XmlSuite类,还要改写XmlTest

Tony :我们在XmlTest中用一个ArrayList来存储XmlSuite。并提供get和add suite方法

Alex :我完成了,我们运行一下吧。green bar ,ok, let us have a short break!

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats