2009-05-02
构建Linux下的函数库编译方案
就快离开学校了,最近打算把大学这几年积累下来的代码重构一下,写成类似于ACE那种形式的C++代码库,方便调用。也算是留给学弟学妹们的礼物。
在整理过程中遇到许多问题,感觉都颇有启发性。尤其是构建编译方案的过程,几乎让我重新学习和认识了make工具,收益匪浅。下面就把这个过程和盘托出,权当笔记,也希望对大家有用。
一:初始编译方案:
目录树:
|-- Makefile |-- README |-- doc | |-- CHANGES | |-- COPYING | |-- CREDITS | |-- INSTALL | `-- TODO |-- inc | |-- Exception.h | |-- HashTable.h | |-- MessageQueue.h | |-- Mutex.h | `-- Semaphore.h |-- lib |-- mks | `-- linux.mk |-- obj |-- sample `-- src |-- Exception.cpp |-- HashTable.tpl |-- MessageQueue.cpp |-- Mutex.cpp `-- Semaphore.cpp
主Makefile:
include mks/linux.mk TARGET = libisrc.a SOURCE_FILES = $(wildcard $(SRC_DIR)/*.$(SRC_EXT)) OBJS = $(patsubst $(SRC_DIR)/%.$(SRC_EXT), $(OBJ_DIR)/%.$(OBJ_EXT), $(SOURCE_FILES)) $(LIB_DIR)/$(TARGET): $(OBJS) ar -rv $@ $(OBJS) $(OBJ_DIR)/%.$(OBJ_EXT) : $(SRC_DIR)/%.$(SRC_EXT) $(INC_DIR)/%.h $(CC) -c $< $(CFLAGS) -o $@ $(INCLUDE) $(OBJ_DIR)/%.$(OBJ_EXT) : $(SRC_DIR)/%.$(SRC_EXT) $(CC) -c $< $(CFLAGS) -o $@ $(INCLUDE) .PHONY:clean clean: $(RM) $(OBJS) $(RM) $(LIB_DIR)/$(TARGET) linux.mk: CC = g++ ORACLE_HOME = /opt/oracle/instantclient # Compile options CFLAGS = -Wall -O -g LFLAGS = INCLUDE = -I./include \ -I$(ORACLE_HOME)/sdk/include LIB = -L$(ORACLE_HOME) # shell related SHELL = /bin/bash RM = rm -f CP = cp -f # dirctories INC_DIR = include SRC_DIR = src OBJ_DIR = obj LIB_DIR = lib # file extension SRC_EXT = cpp OBJ_EXT = o
说明:
doc — 各种文档
inc — 头文件
lib — 最终生成的目标库文件
mks — 共享makefile文件
obj — 存放编译生成的.o文件
sample — 范例程序
src — 代码文件
Makefile — 主Makefile
linux.mk — Makefile通用变量
然而随着添加到库中的源文件越来越多,开始发现一些组织不合理的地方:
1、没有在目录结构上显示出分类
所有头文件都挤在一个目录下(源文件也一样),显得很杂乱。
2、编译选项庞杂
不同分类的源文件需要不同的编译选项(主要是-I和-L),现在的模式要求把所有的编译选项都写在一起,导致编译选项很庞杂,最终影响编译速度。
3、不能快捷地对分类进行配置
倘若在某个应用中,不需要数据库相关类库,即不需编译数据库相关的文件。在当前的设置下,只能删除相关的源代码文件(编译时的目标列表是用wildcard函数自动生成的),而不能在编译选项中灵活配置。
考虑到这样的问题应该是共性的,很多开源项目都应该遇到过,于是下载了ACE的源代码,想研究一下它是怎么组织的,却发现也都挤在一个文件夹下。既然没地参考,只能自己想了。
二:改进
最后决定新的编译方案如下:
1、建立分类子文件夹
inc下和src下都建立分类子文件夹。把相关类别的代码放到子文件夹中,有些公用性质的源文件则仍保留在inc下。
例如,ipc相关的文件Mutex.h Semaphore.h都放到inc/ipc下,Mutex.cpp,Semaphore.cpp放到src/ipc下,log.h和log.cpp则仍旧放在inc和src下。
2、分离不同分类的Makefile
src根目录下放置一个Makefile,负责编译src目录下的文件,src中的每个子文件夹中包含一个Makefile,负责编译当前目录中的文件。这么做就实现了不同的代码分类由单独的Makefile文件负责,编译选项也可随不同类别灵活配置。
3、编译输出的.o统一存放至/obj目录下,方便ar读取,生成.a静态库文件。
4、主Makefile中设置SUBDIR变量,通过这个变量遍历各个分类目录,调用子Makefile进行编译
倘若不想包含某个模块,在主Makefile中的SUBDIR变量中删掉相关目录就可以了。
目录树:
|-- Makefile |-- README |-- doc | |-- CHANGES | |-- COPYING | |-- CREDITS | |-- INSTALL | `-- TODO |-- inc | |-- Exception.h | |-- HashTable.h | |-- IPC | | |-- MessageQueue.h | | |-- Mutex.h | | `-- Semaphore.h | |-- MDB | | |-- MDB.h | | |-- MDBColumn.h | | |-- MDBDriver.h | | |-- MDBDriver_Mysql.h | | |-- MDBDriver_Oracle.h | | |-- MDBField.h | | |-- MDBResult.h | | |-- MDBResult_Mysql.h | | |-- MDBResult_Oracle.h | | |-- MDBRow.h | | |-- MDBStatement.h | | `-- MDBStatement_Oracle.h | |-- Net | | |-- Socket.h | | |-- TcpClient.h | | `-- TcpListener.h | `-- Print.h |-- lib |-- mks | `-- linux.mk |-- obj |-- sample | `-- MDB | |-- Main.cpp | |-- Main.o | |-- Makefile | `-- mdb `-- src |-- Exception.cpp |-- HashTable.tpl |-- IPC | |-- Makefile | |-- MessageQueue.cpp | |-- Mutex.cpp | `-- Semaphore.cpp |-- MDB | |-- MDB.cpp | |-- MDBColumn.cpp | |-- MDBDriver.cpp | |-- MDBDriver_Mysql.cpp | |-- MDBDriver_Oracle.cpp | |-- MDBField.cpp | |-- MDBResult.cpp | |-- MDBResult_Mysql.cpp | |-- MDBResult_Oracle.cpp | |-- MDBRow.cpp | |-- MDBStatement.cpp | |-- MDBStatement_Oracle.cpp | `-- Makefile |-- Makefile |-- Net | |-- Makefile | |-- Socket.cpp | |-- TcpClient.cpp | `-- TcpListener.cpp `-- Print.cpp
主Makefile:
include mks/linux.mk TARGET = libisrc.a SUBDIRS = . Net IPC MDB SRC_FILES_WITH_DIR = $(foreach subdir, $(SUBDIRS), $(wildcard $(SRC_DIR)/$(subdir)/*.cpp)) SRC_FILES = $(notdir $(SRC_FILES_WITH_DIR)) OBJS = $(patsubst %.$(SRC_EXT), $(OBJ_DIR)/%.$(OBJ_EXT), $(SRC_FILES)) all: subdirs $(LIB_DIR)/$(TARGET) subdirs: @ for subdir in $(SUBDIRS); do \ (cd src/$$subdir && $(MAKE)); \ done $(LIB_DIR)/$(TARGET): $(OBJS) ar -rv $@ $(OBJS) .PHONY: clean clean: @ for subdir in $(SUBDIRS); do \ (cd $(SRC_DIR)/$$subdir && $(MAKE) clean); \ done $(RM) $(LIB_DIR)/$(TARGET)
linux.mk:
#compliler CC = g++ # shell related SHELL = /bin/bash RM = rm -f CP = cp -f # dirctories INC_DIR = inc SRC_DIR = src OBJ_DIR = obj LIB_DIR = lib # file extension SRC_EXT = cpp OBJ_EXT = o INC_EXT = h
子Makefile:
HOME = .. include $(HOME)/mks/linux.mk CFLAGS = -Wall -O LFLAGS = INCLUDE = -I$(HOME)/$(INC_DIR) LIBRARY = SOUCE_FILES = $(wildcard *.$(SRC_EXT)) OBJS = $(patsubst %.$(SRC_EXT), $(HOME)/$(OBJ_DIR)/%.$(OBJ_EXT), $(SOUCE_FILES)) $(HOME)/obj/%.$(OBJ_EXT) : %.$(SRC_EXT) %.$(INC_EXT) $(CC) -c $(CFLAGS) $< -o $@ $(INCLUDE) $(HOME)/obj/%.$(OBJ_EXT) : %.$(SRC_EXT) $(CC) -c $(CFLAGS) $< -o $@ $(INCLUDE) all: $(OBJS) .PHONY:clean clean: $(RM) $(OBJS)
这么做目录结构干净很多,分完类的代码整齐划一,还可以分类调试。呵呵,先沾沾自喜一下。
正规的开源项目一般会用AutoMake之类的工具为自己生成编译方案。但是自动化方案意味着屏蔽了许多底层设计上的细节,不利于学习。工具有时使人变笨(例如Google),就是这个道理。所以,初级阶段还是尝试自己构建编译方案比较好。一来能够更深入的理解Make的特性,编写出更好的Makefile;二来也能提高自己的设计能力,何乐而不为?
–EOF–