隨着工程規模的擴大,需要一個工具管理你的工程,但是很多時候,大家又不知道如何高效地管理工程文件。這裏寫一個關於工程管理與編譯系統的博客,介紹如何快速、高效地管理工程。我就選擇我最常見的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)
2.4 參考資料:
3. 其他編譯選項
其他一些編譯工具如:autogen/bazel/waf/python setup等不太熟悉,就不贅述了。