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–