随着工程规模的扩大,需要一个工具管理你的工程,但是很多时候,大家又不知道如何高效地管理工程文件。这里写一个关于工程管理与编译系统的博客,介绍如何快速、高效地管理工程。我就选择我最常见的makefile
和cmake
两个工具。makefile
适合管理规模不大的小工程,能够很方便的直接撰写编译规则。cmake
会自动生成makefile
规则,适合管理大规模的工程。
1. Makefile
MakeFile差不多是现在最流行的工程管理工具了,对于编译大型工程非常高效。这里,我们介绍makefile的大部分使用方法
1.1 工程视图
为了说明Makefile的效率,我们还是按照管理一个小型的工程来描述吧,构建一个这样的工程,各个模块如下:
./ # 根目录
├── a.cpp # class A的实现
├── a.h # class A的申明
├── b.cpp # class A的实现
└── b.h # class B的申明
├── main.cpp # main 函数
├── Makefile # 主模块的makefile
├── submodule # 子模块1,封装class D 的libmytest.so
│ ├── d.cpp # class D的实现
│ ├── d.h # class D的申明
│ └── Makefile # 子模块1的makefile
│ └── submodule2 # 子模块2,封装class E 的libclasselib.so
│ ├── e.cpp # class E的实现
│ ├── e.h # class E的申明
│ └── Makefile # 子模块2的makefile
├── together.h # 头文件,include所有的class
1.2 实现各个class以及makefile
1 主目录下的Makfile
TARGET:= Hello#我们的目标文件
CC:= g++ #编译工具
SUB_MODULE_DIR=submodule#子模块目录
EXTRA_LIB:=libmytest.so #使用到的动态连接库
OBJECTS:= a.o b.o main.o #依赖的目标文件
HEAD:= a.h b.h together.h #依赖的头文件
HEAD+= d.h #添加依赖的头文件
#HIDE?=@ 这里,添加@后,就不会打印所有的编译日志信息了
#一个例子解释说明一下使用的技巧
# target: relay1 relay2 relay3 ... relay*
#$< --> the first replay, i.e., relay1
#$^ --> all the relays, i.e., relay1 relay2 relay3 ... relay*
#$@ --> the output object, i.e., target
DEBUGINFO:=-Wall --g -O0 #调试选项
CXXFLAGS+= -Os $(DEBUGINFO) -I$(SUB_MODULE_DIR) #编译选项
LINKFLAGS:=-L./ -lmytest -Wl,-rpath,$(PWD) #链接选项
all:$(TARGET) #默认的make构建目标
@echo "Make $(TARGET) Done"
%.o:%.cpp $(HEAD) #编译生成中间目标文件的依赖和规则,这里使用的是隐式推导规则
$(HIDE)$(CC) -c $(CXXFLAGS) $< -o $@
$(TARGET): $(OBJECTS) #链接生成中间目标文件的依赖和规则
make $(EXTRA_LIB)
$(HIDE)$(CC) $^ -o $@ $(LINKFLAGS)
$(EXTRA_LIB): #构建动态链接库的规则
@echo "In making $(EXTRA_LIB)"
@make -C $(SUB_MODULE_DIR) #编译子模块,使用-C指定,还可以通过-f指定为makefile文件名,默认搜索Makefile
cp $(SUB_MODULE_DIR)/*.so ./
run:$(TARGET) #测试用例的规则
./$(TARGET)
.PHONY:clean
clean: #清理规则
rm -f $(TARGET) $(OBJECTS) *.so
make clean -C $(SUB_MODULE_DIR)
@echo "Clean done"
2编写主函数程序main.cpp
#include "together.h"
#include <iostream>
int main(int argc, char const *argv[])
{
std::cout << "Hello, world!" << std::endl;
A a;
B b;
D d;
a.test();
b.test();
d.test();
return 0;
}
3 编写主头文件together.h
#ifndef __TOGETHER_H__
#define __TOGETHER_H__
#include "a.h"
#include "b.h"
#include "d.h"
#endif
4 编写class A
的实现文件a.cpp
#include "a.h"
#include <iostream>
A::A()
{
std::cout << "Constructing the object of class A" << std::endl;
}
A::~A()
{
std::cout << "Destroying the object of class A" << std::endl;
}
void A::test(void)
{
std::cout << "Testing the object of class A" << std::endl;
}
5 编写class A
的声明文件a.h
#ifndef __A_H__
#define __A_H__
class A
{
public:
A();
~A();
public:
void test(void);
};
#endif
6 编写class B
的实现文件b.cpp
#include "b.h"
#include <iostream>
B::B()
{
std::cout << "Constructing the object of class B" << std::endl;
}
B::~B()
{
std::cout << "Destroying the object of class B" << std::endl;
}
void B::test(void)
{
std::cout << "Testing the object of class B" << std::endl;
}
7 编写class B
的声明文件b.h
#ifndef __B_H__
#define __B_H__
class B
{
public:
B();
~B();
public:
void test(void);
};
#endif
8 submodule
的构建文件 Makefile
EXTRA_LIB:=libmytest.so
CC:=g++
OBJECTS:= d.o
HEAD:= d.h
#HIDE?=@
# target: relay1 relay2 relay3 ... relay*
#$< --> the first replay, i.e., relay1
#$^ --> all the relays, i.e., relay1 relay2 relay3 ... relay*
#$@ --> the output object, i.e., target
SUB_MODULE2:=submodule2
DEPEND_LIB:=libclasselib.so
DEBUGINFO:=-Wall
CXXFLAGS:= -Os $(DEBUGINFO) -fPIC -I$(SUB_MODULE2)
LINKFLAGS:=-shared -L./ -lclasselib -Wl,-rpath,$(PWD)
all:$(EXTRA_LIB)
@echo "Make $(EXTRA_LIB) Done"
%.o:%.cpp $(HEAD)
$(HIDE)$(CC) -c $(CXXFLAGS) $< -o $@
$(EXTRA_LIB): $(OBJECTS)
make $(DEPEND_LIB)
$(HIDE)$(CC) $^ -o $@ $(LINKFLAGS)
$(DEPEND_LIB):
@echo "In making $(SUB_MODULE2)"
@make -C $(SUB_MODULE2)
cp $(SUB_MODULE2)/$(DEPEND_LIB) ./
.PHONY:clean
clean:
rm -f $(OBJECTS) $(EXTRA_LIB) $(DEPEND_LIB)
make clean -C $(SUB_MODULE2)
@echo "Clean done"
9 编写class D
的实现文件d.cpp
#include "d.h"
#include <iostream>
#include "e.h"
D::D()
{
std::cout << "Constructing the object of class D" << std::endl;
}
D::~D()
{
std::cout << "Destroying the object of class D" << std::endl;
}
void D::test(void)
{
E e;
e.test();
std::cout << "Testing the object of class D" << std::endl;
}
10 编写class D
的声明文件d.h
#ifndef __D_H__
#define __D_H__
class D
{
public:
D();
~D();
public:
void test(void);
};
#endif
11 编写submodule2
的Makefile
EXTRA_LIB:=libclasselib.so
CC:=g++
OBJECTS:= e.o
HEAD:= e.h
#HIDE?=@
# target: relay1 relay2 relay3 ... relay*
#$< --> the first replay, i.e., relay1
#$^ --> all the relays, i.e., relay1 relay2 relay3 ... relay*
#$@ --> the output object, i.e., target
DEBUGINFO:=-Wall
CXXFLAGS:= -Os $(DEBUGINFO) -fPIC
LINKFLAGS:=-shared
all:$(EXTRA_LIB)
@echo "Make $(EXTRA_LIB) Done"
%.o:%.cpp $(HEAD)
$(HIDE)$(CC) -c $(CXXFLAGS) $< -o $@
$(EXTRA_LIB): $(OBJECTS)
$(HIDE)$(CC) $^ -o $@ $(LINKFLAGS)
.PHONY:clean
clean:
rm -f $(OBJECTS) $(EXTRA_LIB)
@echo "Clean done"
13 编写class E
的声明文件e.h
#ifndef __E_H__
#define __E_H__
class E
{
public:
E();
~E();
public:
void test(void);
};
#endif
13 编写class E
的实现文件e.cpp
#include "e.h"
#include <iostream>
E::E()
{
std::cout << "Constructing the object of class E" << std::endl;
}
E::~E()
{
std::cout << "Destroying the object of class E" << std::endl;
}
void E::test(void)
{
std::cout << "Testing the object of class E" << std::endl;
}
好了,现在我们就可以测试了,
执行make run
,就可以看到基本的运行结果了。
newplan@g1-XXX:~/project/makefile$ make run
g++ -Os -Wall -Isubmodule/ -c -o a.o a.cpp
g++ -Os -Wall -Isubmodule/ -c -o b.o b.cpp
g++ -Os -Wall -Isubmodule/ -c -o main.o main.cpp
In making libmytest.so
make[1]: Entering directory '/home/newplan/project/makefile/submodule'
cc -c -Os -Wall -fPIC d.cpp -o d.o
cc d.o -o libmytest.so -shared
Make libmytest.so Done
make[1]: Leaving directory '/home/newplan/project/makefile/submodule'
cp submodule/libmytest.so ./
g++ a.o b.o main.o -o Hello -L. -lmytest -Wl,-rpath,/home/newplan/project/makefile
./Hello
Hello, world!
Constructing the object of class A
Constructing the object of class B
Constructing the object of class D
Testing the object of class A
Testing the object of class B
Constructing the object of class E
Testing the object of class E
Testing the object of class D
Destroying the object of class E
Destroying the object of class D
Destroying the object of class B
Destroying the object of class A
1.3 一些总结和说明
1.3.1 编译路径问题
细心的同学可能已经发现了,我的class D
申明所在的文件d.h
虽然在submodule目录下,但是我在together.h
include的时候,还是直接使用了#include "d.h"
这在正常情况下,肯定会报错,找不到d.h
文件,如下:
In file included from main.cpp:1:0:
together.h:6:15: fatal error: d.h: No such file or directory
#include "d.h"
^
compilation terminated.
但是由于我们使用了-I
指定了include查找的路径(见CXXFLAGS:= -Os $(DEBUGINFO) -I$(SUB_MODULE_DIR)
),因此就避免了这个错误问题。
1.3.1 动态链接库问题
很多时候,链接自己编写的动态链接库是一个特别头疼的事情,总是会报错,说找不到链接库,明明就是在当前路径下呀:
g++ a.o b.o main.o libmytest.so -o Hello -lmytest
/usr/bin/ld: cannot find -lmytest
collect2: error: ld returned 1 exit status
Makefile:27: recipe for target 'Hello' failed
make: *** [Hello] Error 1
1:针对链接时候找不到库问题,推荐使用-L
指定链接路径。注意-L和路径之间不能有空格,后面跟上你的链接库(这里需要有空格)。如当前目录下就使用-L. -lmytest
。
2:经过上述方法,能够帮助你链接成功,但是现在还是没有解决问题:在运行的时候,你还会遇到找不到找不到动态连接库的问题。心态都快要爆炸了。
make run
./Hello
./Hello: error while loading shared libraries: libmytest.so: cannot open shared object file: No such file or directory
Makefile:35: recipe for target 'run' failed
make: *** [run] Error 127
这里面有一些复杂的原因,主要是因为相对路径和查找的关系,一种办法是可以把动态链接库直接状态/usr/lib里面去,我们这里使用另外一种稍微优雅的方法,来解决这个问题:通过rpath
指定链接的绝对路径。这样,我们的链接选项就是这样子了:
LINKFLAGS:=-L. -lmytest -Wl,-rpath,$(PWD)
通过以上两个步骤,你的带有动态连接库的程序就可以运行了,注意,这里由于使用了绝对路径,你还是不能够移动你的动态连接库。为了一劳永逸地解决这个问题,推荐使用cmake
工具。
2. cmake
2.1 工程视图
我们在上个例子中进一步扩展,添加各个模块的cmake
编译规则,整体工程的目录如下:
├── a.cpp
├── a.h
├── b.cpp
├── b.h
├── CMakeLists.txt # 新增加的主CMakeLists.txt文件
├── main.cpp
├── submodule # 动态链接库mytest.so
│ ├── CMakeLists.txt # submodule的CMakeLists.txt文件
│ ├── d.cpp # 增加class D对于class E的调用
│ ├── d.h
│ └── submodule2 # 新增加的模块classelib.{so, a},封装class E
│ ├── CMakeLists.txt # submodule2的CMakeLists.txt文件
│ ├── e.cpp # class E的实现
│ └── e.h # class E的申明
└── together.h
2.2 编写cmake相关文件
1: 主目录下的CMakeLists.txt
CMAKE_MINIMUM_REQUIRED(VERSION 3.0) #cmake版本要求
project(Hello)
message("Building make for Hello") #打印消息
message("add -DDYNAMIC_E=1 when build dynamic class E, the default is static")
set(CMAKE_BUILD_TYPE "Debug")
set(SUB_MODULE_PATH "./submodule")
set(CMAKE_CXX_STANDARD 11) # 设置std=c++11
# 设置debug或者release的编译参数
set(CMAKE_CXX_FLAGS_DEBUG "$ENV{CXXFLAGS} -O0 -Wall -g -ggdb -fPIC")
set(CMAKE_CXX_FLAGS_RELEASE "$ENV{CXXFLAGS} -O3 -Wall -fPIC")
# collect all message to "SRC_LIST" <-- ./*.cpp
aux_source_directory(. SRC_LIST)
#分别指定编译include和链接link的搜索目录
include_directories(${SUB_MODULE_PATH})
link_directories(${SUB_MODULE_PATH})
#添加一个子模块,编译到lib目录下去
add_subdirectory(${SUB_MODULE_PATH} lib) # add a submodule, and compile it to the "build/lib"
#设置编译目标
add_executable(Hello ${SRC_LIST})
#添加编译目标依赖
target_link_libraries(Hello mytest)
2:submodule
目录下的CMakeLists.txt
### for sub module
add_subdirectory(submodule2 mo2_lib) # add new mod2, compile it to the path: build/lib/mod2_lib
link_directories(mod2_lib) # adding link search path: build/lib/mod2_lib
include_directories(submodule2) # add including search path
aux_source_directory(. DIR_LIB_SRCS)
MESSAGE("building mytest with ${DIR_LIB_SRCS}")
add_library(mytest SHARED ${DIR_LIB_SRCS})
target_link_libraries(mytest classelib) # add libmod2.so to--> libmod1.so
3: submodule2
目录下的CMakeLists.txt
aux_source_directory(. DIR_LIB_SRCS)
MESSAGE("building classelib with ${DIR_LIB_SRCS}")
add_library(classelib SHARED ${DIR_LIB_SRCS})
2.3 编译运行
1:执行mkdir build && cd build
创建独立的编译环境,这样所有编译的内容就只会在build文件夹里面。
2:cmake -DDYNAMIC_E=1 ..
生成makefile文件 DYNAMIC_E=1
是我指定的宏,通过指定这个宏,我可以把submodule2,也就是classelib
编译成动态连接库classelib.so
,默认编译成静态链接库classelib.a
。
3:执行编译:make VERBOSE=1 -j8
,其中VERBOSE=1
打印详细的编译log信息,方便调试用;-j8
指定编译器启用多线程(8个)加速编译。
4:使用ldd
查看运行程序的依赖关系:
newplan@g1-nasp:~/project/makefile/build$ ldd Hello
linux-vdso.so.1 => (0x00007ffd0a584000)
libmytest.so => /home/newplan/project/makefile/build/lib/libmytest.so (0x00007fdfab66d000)
libstdc++.so.6 => /usr/lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007fdfab2c5000)
libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007fdfab0ae000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fdfaace4000)
libclasselib.so => /home/newplan/project/makefile/build/lib/mo2_lib/libclasselib.so (0x00007fdfaaae1000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007fdfaa7d8000)
/lib64/ld-linux-x86-64.so.2 (0x0000563a37051000)
5: 一些有用的选项
可以使用-DCMAKE_EXPORT_COMPILE_COMMANDS=on
把cmake的compile给dump下来。
2.4 参考资料:
3. 其他编译选项
其他一些编译工具如:autogen/bazel/waf/python setup等不太熟悉,就不赘述了。