1. 程式人生 > >Linux下CMake簡明教程

Linux下CMake簡明教程

CMake是開源、跨平臺的構建工具,可以讓我們通過編寫簡單的配置檔案去生成本地的Makefile,這個配置檔案是獨立於執行平臺和編譯器的,這樣就不用親自去編寫Makefile了,而且配置檔案可以直接拿到其它平臺上使用,無需修改,非常方便。
這裡寫圖片描述
本文主要講述在Linux下如何使用CMake來編譯我們的程式。

一 安裝CMake

本文使用ubuntu18.04,安裝cmake使用如下命令,
sudo apt install cmake
安裝完成後,在終端下輸入cmake -version檢視cmake版本,
這裡寫圖片描述
這樣cmake就安裝好了。

二 簡單樣例

首先讓我們從最簡單的程式碼入手,先來體驗下cmake是如何操作的。編寫main.c,如下,

#include <stdio.h>

int main(void)
{
    printf("Hello World\n");

    return 0;
}

然後在main.c相同目錄下編寫CMakeLists.txt,內容如下,

cmake_minimum_required (VERSION 2.8)

project (demo)

add_executable(main main.c)

第一行意思是表示cmake的最低版本要求是2.8,我們安裝的是3.10.2;第二行是表示本工程資訊,也就是工程名叫demo;第三行比較關鍵,表示最終要生成的elf檔案的名字叫main,使用的原始檔是main.c
在終端下切到main.c所在的目錄下,然後輸入以下命令執行cmake,
cmake .


會輸出如下資訊,
這裡寫圖片描述
再來看看目錄下的檔案,
這裡寫圖片描述
可以看到成功生成了Makefile,還有一些cmake執行時自動生成的檔案。
然後在終端下輸入make並回車,
這裡寫圖片描述
可以看到執行cmake生成的Makefile可以顯示進度,並帶顏色。再看下目錄下的檔案,
這裡寫圖片描述
可以看到我們需要的elf檔案main也成功生成了,然後執行main,
這裡寫圖片描述
執行成功!

三 同一目錄下多個原始檔

接下來進入稍微複雜的例子:在同一個目錄下有多個原始檔。
在之前的目錄下新增2個檔案,testFunc.c和testFunc.h。新增完後整體檔案結構如下,
這裡寫圖片描述
testFunc.c內容如下,

/*
** testFunc.c
*/
#include <stdio.h> #include "testFunc.h" void func(int data) { printf("data is %d\n", data); }

testFunc.h內容如下,

/*
** testFunc.h
*/

#ifndef _TEST_FUNC_H_
#define _TEST_FUNC_H_

void func(int data);

#endif

修改main.c,呼叫testFunc.h裡宣告的函式func(),

#include <stdio.h>

#include "testFunc.h"

int main(void)
{
    func(100);

    return 0;
}

修改CMakeLists.txt,在add_executable的引數裡把testFunc.c加進來

cmake_minimum_required (VERSION 2.8)

project (demo)

add_executable(main main.c testFunc.c)

然後重新執行cmake生成Makefile並執行make,
這裡寫圖片描述
然後執行重新生成的elf檔案main,
這裡寫圖片描述
執行成功!

可以類推,如果在同一目錄下有多個原始檔,那麼只要在add_executable裡把所有原始檔都新增進去就可以了。但是如果有一百個原始檔,再這樣做就有點坑了,無法體現cmake的優越性,cmake提供了一個命令可以把指定目錄下所有的原始檔儲存在一個變數中,這個命令就是 aux_source_directory(dir var)
第一個引數dir是指定目錄,第二個引數var是用於存放原始檔列表的變數。

我們在main.c所在目錄下再新增2個檔案,testFunc1.c和testFunc1.h。新增完後整體檔案結構如下,
這裡寫圖片描述
testFunc1.c如下,

/*
** testFunc1.c
*/

#include <stdio.h>
#include "testFunc1.h"

void func1(int data)
{
    printf("data is %d\n", data);
}

testFunc1.h如下,

/*
** testFunc1.h
*/

#ifndef _TEST_FUNC1_H_
#define _TEST_FUNC1_H_

void func1(int data);

#endif

再修改main.c,呼叫testFunc1.h裡宣告的函式func1(),

#include <stdio.h>

#include "testFunc.h"
#include "testFunc1.h"

int main(void)
{
    func(100);
    func1(200);

    return 0;
}

修改CMakeLists.txt,

cmake_minimum_required (VERSION 2.8)

project (demo)

aux_source_directory(. SRC_LIST)

add_executable(main ${SRC_LIST})

使用aux_source_directory把當前目錄下的原始檔存列表存放到變數SRC_LIST裡,然後在add_executable裡呼叫SRC_LIST(注意呼叫變數時的寫法)。
再次執行cmake和make,並執行main,
這裡寫圖片描述
可以看到執行成功了。

四 不同目錄下多個原始檔

一般來說,當程式檔案比較多時,我們會進行分類管理,把程式碼根據功能放在不同的目錄下,這樣方便查詢。那麼這種情況下如何編寫CMakeLists.txt呢?
我們把之前的原始檔整理一下(新建2個目錄test_func和test_func1),整理好後整體檔案結構如下,
這裡寫圖片描述
把之前的testFunc.c和testFunc.h放到test_func目錄下,testFunc1.c和testFunc1.h則放到test_func1目錄下。整理完後文件結構如下,

其中,CMakeLists.txt和main.c在同一目錄下,內容修改成如下所示,

cmake_minimum_required (VERSION 2.8)

project (demo)

include_directories (test_func test_func1)

aux_source_directory (test_func SRC_LIST)
aux_source_directory (test_func1 SRC_LIST1)

add_executable (main main.c ${SRC_LIST} ${SRC_LIST1})

這裡出現了一個新的命令:include_directories。該命令是用來向工程新增多個指定標頭檔案的搜尋路徑,路徑之間用空格分隔。
因為main.c裡include了testFunc.h和testFunc1.h,如果沒有這個命令來指定標頭檔案所在位置,就會無法編譯。當然,也可以在main.c裡使用include來指定路徑,如下

#include "test_func/testFunc.h"
#include "test_func1/testFunc1.h"

只是這種寫法不好看。
另外,我們使用了2次aux_source_directory,因為原始檔分佈在2個目錄下,所以新增2次。

五 正規一點的組織結構

正規一點來說,一般會把原始檔放到src目錄下,把標頭檔案放入到include檔案下,生成的物件檔案放入到build目錄下,最終輸出的elf檔案會放到bin目錄下,這樣整個結構更加清晰。讓我們把前面的檔案再次重新組織下,
這裡寫圖片描述
我們在最外層目錄下新建一個CMakeLists.txt,內容如下,

cmake_minimum_required (VERSION 2.8)

project (demo)

add_subdirectory (src)

這裡出現一個新的命令add_subdirectory(),這個命令可以向當前工程新增存放原始檔的子目錄,並可以指定中間二進位制和目標二進位制的存放位置,具體用法可以百度。
這裡指定src目錄下存放了原始檔,當執行cmake時,就會進入src目錄下去找src目錄下的CMakeLists.txt,所以在src目錄下也建立一個CMakeLists.txt,內容如下,

aux_source_directory (. SRC_LIST)

include_directories (../include)

add_executable (main ${SRC_LIST})

set (EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin)

這裡又出現一個新的命令set,是用於定義變數的,EXECUTABLE_OUT_PATH和PROJECT_SOURCE_DIR是CMake自帶的預定義變數,其意義如下,

  • EXECUTABLE_OUTPUT_PATH :目標二進位制可執行檔案的存放位置
  • PROJECT_SOURCE_DIR:工程的根目錄

所以,這裡set的意思是把存放elf檔案的位置設定為工程根目錄下的bin目錄。(cmake有很多預定義變數,詳細的可以網上搜索一下)

新增好以上這2個CMakeLists.txt後,整體檔案結構如下,
這裡寫圖片描述
下面來執行cmake,不過這次先讓我們切到build目錄下,然後輸入以下命令,
cmake ..
Makefile會在build目錄下生成,然後在build目錄下執行make,
這裡寫圖片描述
執行ok,我們再切到bin目錄下,發現main已經生成,並執行測試,
這裡寫圖片描述
測試OK!

這裡解釋一下為什麼在build目錄下執行cmake?從前面幾個case中可以看到,如果不這樣做,cmake執行時生成的附帶檔案就會跟原始碼檔案混在一起,這樣會對程式的目錄結構造成汙染,而在build目錄下執行cmake,生成的附帶檔案就只會待在build目錄下,如果我們不想要這些檔案了就可以直接清空build目錄,非常方便。

另外一種寫法:
前面的工程使用了2個CMakeLists.txt,這種寫法是為了處理需要生成多個elf檔案的情況,最外層的CMakeLists.txt用於掌控全域性,使用add_subdirectory來新增要生成elf檔案的原始碼目錄。

如果只生成一個elf檔案,那麼上面的例子可以只使用一個CMakeLists.txt,可以把最外層的CMakeLists.txt內容改成如下,

cmake_minimum_required (VERSION 2.8)

project (demo)

aux_source_directory (src SRC_LIST)

include_directories (include)

add_executable (main ${SRC_LIST})

set (EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin)

同時,還要把src目錄下的CMakeLists.txt刪除。

六 動態庫和靜態庫的編譯控制

有時我們只需要編譯出動態庫,靜態庫,然後等著讓其它程式去使用。讓我們看下這種情況該如何使用cmake。首先按照如下重新組織檔案,只留下testFunc.h和TestFunc.c,
這裡寫圖片描述
我們會在build目錄下執行cmake,並把生成的庫檔案存放到lib目錄下。
最外層的CMakeLists.txt內容如下,

cmake_minimum_required (VERSION 2.8)

project (demo)

add_subdirectory (lib_testFunc)

lib_testFunc目錄下的CMakeLists.txt如下,

aux_source_directory (. SRC_LIST)

add_library (testFunc_shared SHARED ${SRC_LIST})
add_library (testFunc_static STATIC ${SRC_LIST})

set_target_properties (testFunc_shared PROPERTIES OUTPUT_NAME "testFunc")
set_target_properties (testFunc_static PROPERTIES OUTPUT_NAME "testFunc")

set (LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/lib)

這裡又出現了新的命令和預定義變數,

  • add_library: 生成動態庫或靜態庫(第1個引數指定庫的名字;第2個引數決定是動態還是靜態,如果沒有就預設靜態;第3個引數指定生成庫的原始檔)
  • set_target_properties: 設定輸出的名稱,還有其它功能,如設定庫的版本號等等
  • LIBRARY_OUTPUT_PATH: 庫檔案的預設輸出路徑,這裡設定為工程目錄下的lib目錄

好了,讓我們進入build目錄下執行cmake ..,成功後再執行make,
這裡寫圖片描述
cd到lib目錄下進行檢視,發現已經成功生成了動態庫和靜態庫,
這裡寫圖片描述
ps:可以看出前面使用set_target_properties重新定義了庫的輸出名字,如果不用set_target_properties也可以,那麼庫的名字就是add_library裡定義的名字,只是我們連續2次使用add_library指定庫名字時,這個名字不能相同,而set_target_properties可以把名字設定為相同,只是最終生成的庫檔案字尾不同,這樣相對來說會好看點。

七 對庫進行連結

既然我們已經生成了庫,那麼就進行連結測試下。把build裡的檔案都刪除,然後在在工程目錄下新建src目錄和bin目錄,在src目錄下新增一個main.c和一個CMakeLists.txt,整體結構如下,
這裡寫圖片描述
main.c內容如下,

#include <stdio.h>

#include "testFunc.h"

int main(void)
{
    func(100);

    return 0;
}

修改工程目錄下的CMakeLists.txt,如下,

cmake_minimum_required (VERSION 2.8)

project (demo)

add_subdirectory (lib_testFunc)

add_subdirectory (src)

只是使用add_subdirectory把src目錄新增進來。
src目錄下的CMakeLists.txt如下,

aux_source_directory (. SRC_LIST)

# find testFunc.h
include_directories (../lib_testFunc)

link_directories (${PROJECT_SOURCE_DIR}/lib)

add_executable (main ${SRC_LIST})

target_link_libraries (main testFunc)

set (EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin)

這裡出現2個新的命令,

  • link_directories: 新增非標準的共享庫搜尋路徑
  • target_link_libraries: 把目標檔案與庫檔案進行連結

cd到build目錄下,然後執行cmake ..,成功後再執行make,
這裡寫圖片描述
make成功,進入到bin目錄下檢視,發現main已經生成,並執行,
這裡寫圖片描述
執行成功!

ps:在lib目錄下有testFunc的靜態庫和動態庫,target_link_libraries (main testFunc)預設是使用動態庫,如果lib目錄下只有靜態庫,那麼這種寫法就會去連結靜態庫。也可以直接指定使用動態庫還是靜態庫,寫法是:target_link_libraries (main libtestFunc.so)target_link_libraries (main libtestFunc.a)

ps: 檢視elf檔案使用了哪些庫,可以使用readelf -d ./xx來檢視

八 總結

以上是自己學習CMake的一點學習記錄,通過簡單的例子讓大家入門CMake,學習的同時也閱讀了很多網友的部落格。CMake的知識點還有很多,具體詳情可以在網上搜索。總之,CMake可以讓我們不用去編寫複雜的Makefile,並且跨平臺,是個非常強大並值得一學的工具。

如果有寫的不對的地方,希望能留言指正,謝謝閱讀。