工程管理與編譯工具-makefile

隨着工程規模的擴大,需要一個工具管理你的工程,但是很多時候,大家又不知道如何高效地管理工程文件。這裏寫一個關於工程管理與編譯系統的博客,介紹如何快速、高效地管理工程。我就選擇我最常見的makefilecmake兩個工具。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 編寫submodule2Makefile

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.hinclude的時候,還是直接使用了#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等不太熟悉,就不贅述了。

發佈了108 篇原創文章 · 獲贊 95 · 訪問量 125萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章