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–