CMake 入門/建置與連結程式庫

建置程式庫 編輯

在前面的例子裡我們直接將所有的 source 編譯、連結成執行檔,但有時候我們需要建立程式庫,之後再和執行檔連結。要在 CMakeLists 當中加入一個程式庫 target,用的是 add_library() 指令,和前面介紹的 add_executable() 指令很像,但有變化更為複雜,後續的篇幅會陸續介紹,這裡就先看最直接的用法。

指令 target_link_libraries() 用來指定某個 target 要和哪些程式庫連結,當連結的程式庫是另一個 target 時,CMake 會自動建立相依關係以確保建置順序正確。

下面這個例子 CMake 會替 MyProject 產生 myexec 和 mylib 兩個 target,並且在建立 myexec 時和 mylib 連結。

project(MyProject)

add_library(mylib ${mylib_sources})

add_executable(myexec ${myexec_sources})
target_link_libraries(myexec mylib)


建置程式庫的選擇 編輯

指令 add_library() 完整規格如下:

add_library(<name> [STATIC | SHARED | MODULE]
            [EXCLUDE_FROM_ALL]
            source1 source2 ... sourceN)

參數 name 為欲生成的程式庫名稱,cmake 會依照編譯器慣例自動加上前綴和副檔名,例如名稱為 calc 的程式庫最後生成的檔案可能是 libcalc.a 或 libcalc.lib,因此不必在 add_library() 的名稱中加上 lib* 前綴。

後面的 [STATIC | SHARED | MODULE] 為欲建置的程式庫類型:

  • STATIC:靜態程式庫。
  • SHARED:共享程式庫,例如 Unix like 系統上的 so 檔或 Windows 上的 dll 檔。在建置 dll 時也會連帶生成連結用的 .a 檔,例如建置 libcalc.dll 時也會生成 libcalc.dll.a。
  • MODULE:動態連結程式庫,不在編譯期進行連結,而是等到執行期才透過 dlopen() 或 LoadLibrary() 方式調用。一般情況下 MODULE 輸出的檔案類型也是 so 或 dll,但建立 dll 時不會順便產生 .a 檔。

如果沒有指定 target 之 STATIC、SHARED、MODULE 類型,則由全域變數 BUILD_SHARED_LIBS 來決定要建構何者,BUILD_SHARED_LIBS 預設為 OFF,亦即建立靜態程式庫。雖然可以直接在 CMakeLists 當中用 set() 指令指派 BUILD_SHARED_LIBS 的值,不過等到執行 CMake 再用 -D 指定通常會比較有彈性。

$ cmake -DBUILD_SHARED_LIBS=ON

這裡的 EXCLUDE_FROM_ALL 功能和 add_executable 一樣,都是使 make all 時不要主動建置此 target,除非手動指定或者是其他相依的 target 提出需求。

連結程式庫的選擇 編輯

指令 target_link_libraries()用於指定 target 所需要連結的項目,並且可以依不同組態選擇不同的連結項目。

target_link_libraries(<target> [item1 [item2 [...]]]
                      [[debug|optimized|general] <item>] ...)

連結項目可以是另一個 target,或者是編譯器能識別的程式庫表示法,例如 gcc 會依照 -l 所指定的名稱,尋找預設路徑下由 lib 開頭、附檔名為.a的檔案作為連結項目。

target_link_libraries(myapp
    debug -labc
    optimized -lxyz
    )

myapp 在 debug build 時會連結 libabc.a,在 release build 時會連結 libxyz.a,這裡假設上述兩者都放在編譯器預設的查找路徑之下。

稍微複雜一點的例子 編輯

接下來看一個稍微複雜的例子,我們分別建立兩個子專案,分別是名為 app 的執行檔和名為 calc 的程式庫,原始碼分散在各自的資料夾。

  • lib1/
    • src/
      • app/
        • CMakeLists.txt
        • main.c
      • calc/
        • CMakeLists.txt
        • calc.c
        • calc.h
      • CMakeLists.txt

專案 app 編輯

lib1/src/app/main.c

#include <stdio.h>
#include <calc/calc.h>
 
int main()
{
    printf("Square of 2 is %d \n",   Square(2));
    printf("Square of 5 is %d \n\n", Square(5));
 
    printf("Cube of 2 is %d \n",    Cube(2));
    printf("Cube of 5 is %d \n\n",  Cube(5));
    return 0;
}


lib1/src/app/CMakeLists.txt

cmake_minimum_required(VERSION 2.6)

include_directories(${CMAKE_SOURCE_DIR})

project(app)

add_executable(app main.c)
target_link_libraries(app calc)


注意這裡使用 include_directories() 指令添加引入檔的查找目錄,CMAKE_SOURCE_DIR 即 CMake 進入 source tree 的起始目錄,也就是本例的 lib1/src

以 GCC 編譯的話,執行 make VERBOSE=1 應該會看到編譯時加入類似 -I"lib1/src" 的項目。

專案 calc 編輯

lib1/src/calc/calc.h

#ifndef CALC_H_
#define CALC_H_
 
int Cube(int x);
 
int Square(int x);
 
#endif


lib1/src/calc/calc.c

int Cube(int x)
{
    return x * x * x;
}
 
int Square(int x)
{
    return x * x;
}


lib1/src/calc/CMakeLists.txt

cmake_minimum_required(VERSION 2.6)

project(calc)
add_library(calc calc.c)


最上層的 CMakeLists 編輯

lib1/src/CMakeLists.txt

cmake_minimum_required(VERSION 2.6)

add_subdirectory(calc)
add_subdirectory(app)


上面的 app 和 calc 都是完整的 CMake 專案,可以獨立建置。這裡在最上層加了一個 CMakeLists,內容只有兩條 add_subdirectory() 指令,這個指令的功能在於告訴 CMake 到子目錄下執行子目錄的 CMakeLists。


建置此範例 編輯

設當前工作目錄在 lib1/,執行下列命令:

$ mkdir -p build
$ cd build
$ cmake ../src
$ make

結果會在 build 目錄下比照 src 架構生成了 app 和 calc 兩個目錄,存放 app 和 libcalc.a 兩個目的檔。

  • lib1/
    • build/
      • app/
        • app (執行檔)
        • ... 中間檔 ...
      • calc/
        • libcalc.a
        • ... 中間檔 ...
    • src/
      • app/
      • calc/