CMake的簡單學習筆記

# CMake的學習筆記
Cmake適用於C/C++、Java的編程,用於自動生成Makefile的語言,避免Makefile的複雜的撰寫。如果是一般的小型的項目,適用手寫Makefile即可,本筆記爲練習CMake PracticeCMake快速入門教程:實戰所寫,主要參考資料也是這篇文檔。如果僅僅使用QT編程,使用qmake即可。

本練習僅用於簡單的項目構造,詳細中文入門參考爲:Cmake入門

基本的語法

下面是一個簡單的CMakeLists.txt

 
PROJECT (HELLO)
SET(SRC_LIST main.c)
MESSAGE(STATUS "This is BINARY dir " ${HELLO_BINARY_DIR})
MESSAGE(STATUS "This is SOURCE dir "${HELLO_SOURCE_DIR})
ADD_EXECUTABLE(hello ${SRC_LIST})

PROJECT指令的語法是:
PROJECT(projectname [CXX] [C] [Java]) .
你可以用這個指令定義工程名稱,並可指定工程支持的語言,支持的語言列表是可以忽略的,默認情況表示支持所有語言。這個指令隱式的定義了兩個cmake變量:_BINARY_DIR以及_SOURCE_DIR,這裏就是HELLO_BINARY_DIR和HELLO_SOURCE_DIR(所以CMakeLists.txt中兩個MESSAGE指令可以直接使用了這兩個變量),因爲採用的是內部編譯,兩個變量目前指的都是工程所在路徑/backup/cmake/t1,後面我們會講到外部編譯,兩者所指代的內容會有所不同。

SET指令的語法是:
SET(VAR [VALUE] [CACHE TYPE DOCSTRING [FORCE]])
現階段,你只需要瞭解SET指令可以用來顯式的定義變量即可。如果有多個源文件,也可以定義成:SET(SRC_LIST main.c t1.c t2.c)

CMake執行過程中可以使用MESSAGE來輸出提示燈信息。MESSAGE指令的語法是:
MESSAGE([SEND_ERROR | STATUS | FATAL_ERROR] "message to display"
...)

這個指令用於向終端輸出用戶定義的信息,包含了三種類型:SEND_ERROR,產生錯誤,生成過程被跳過。SATUS,輸出前綴爲—的信息。FATAL_ERROR,立即終止所有cmake過程.

ADD_EXECUTABLE(hello ${SRC_LIST})
定義了這個工程會生成一個文件名爲hello的可執行文件,相關的源文件是SRC_LIST中定義的源文件列表。

aux_source_directory查找在某個路徑下的所有源文件。aux_source_directory(<dir> <variable>).蒐集所有在指定路徑下的源文件的文件名,將輸出結果列表儲存在指定的變量中。該命令主要用在那些使用顯式模板實例化的工程上。模板實例化文件可以存儲在Templates子目錄下,然後可以使用這條命令自動收集起來;這樣可以避免手工羅列所有的實例 。 如:aux_source_directory(. DIR_LIB_SRCS

綜上,CMake的基本語法如下:
* 變量使用${}方式取值,但是在IF控制語句中是直接使用變量名
* 指令(參數1 參數2…) 參數使用括弧括起,參數之間使用空格或分號分開。
* 指令是大小寫無關的,參數和變量是大小寫相關的。但,推薦你全部使用大寫指令。

上面的MESSAGE指令我們已經用到了這條規則:
MESSAGE(STATUS “This is BINARY dir” ${HELLO_BINARY_DIR})
也可以寫成:
MESSAGE(STATUS “This is BINARY dir ${HELLO_BINARY_DIR}”)

同時,還有另外一個非常重要的提示,就是:我們剛纔進行的是內部構建(in-source-build),而cmake強烈推薦的是外部構建(out-of-source build)。通過外部編譯進行工程構建,HELLO_SOURCE_DIR仍然指代工程路徑,即/backup/cmake/t1;而HELLO_BINARY_DIR則指代編譯路徑,即/backup/cmake/t1/build

外部構建(out-of-source build)

從本小節開始,後面所有的構建我們都將採用out-of-source外部構建,約定的構建目錄是工程目錄下的build自錄。
本小節的任務是讓前面的Hello World更像一個工程,我們需要作的是:
1,爲工程添加一個子目錄src,用來放置工程源代碼;
2,添加一個子目錄doc,用來放置這個工程的文檔hello.txt
3,在工程目錄添加文本文件COPYRIGHT, README;
4,在工程目錄添加一個runhello.sh腳本,用來調用hello二進制
5,將構建後的目標文件放入構建目錄的bin子目錄;
6,最終安裝這些文件:將hello二進制與runhello.sh安裝至/usr/bin,將doc目錄
的內容以及COPYRIGHT/README安裝到/usr/share/doc/cmake/t2。
目錄結構:

1.目錄結構


t2
├──src/(源碼)
│   ├main.c
│   ├─CMakeLists.txt
├─doc/(說明文檔)
│   ├─hello.txt
├─bin/(目標文件)
│   ├─
├─build/(外部編譯文檔))(編譯完成也在在這個目錄)
│   ├─
├─COPYRIGHT
├─CMakeLists.txt
t2主目錄中CMakeLists.txt中內容:
PROJECT(HELLO)
ADD_SUBDIRECTORY(src bin)
子目錄src,編寫CMakeLists.txt如下:
ADD_EXECUTABLE(hello main.c)

然後建立build目錄,進入build目錄進行外部編譯。
cmake ..
make
構建完成後,你會發現生成的目標文件hello位於build/bin目錄中。

2.語法解釋:
ADD_SUBDIRECTORY(source_dir [binary_dir] [EXCLUDE_FROM_ALL])
這個指令用於向當前工程添加存放源文件的子目錄,並可以指定中間二進制和目標二進制存放的位置。EXCLUDE_FROM_ALL參數的含義是將這個目錄從編譯過程中排除,比如,工程的example,可能就需要工程構建完成後,再進入example目錄單獨進行構建(當然,你也可以通過定義依賴來解決此類問題。
如果我們在上面的例子中將ADD_SUBDIRECTORY (src bin)修改爲SUBDIRS(src)。那麼在build目錄中將出現一個src目錄,生成的目標代碼hello將存放在src目錄中。

3.換個地方保存目標二進制

不論是SUBDIRS還是ADD_SUBDIRECTORY指令(不論是否指定編譯輸出目錄),我們都可以通過SET指令重新定義EXECUTABLE_OUTPUT_PATH和LIBRARY_OUTPUT_PATH變量來指定最終的目標二進制的位置(指最終生成的hello或者最終的共享庫,不包含編譯生成的中間文件).
SET(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)
SET(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}/lib)
在第一節我們提到了_BINARY_DIR和PROJECT_BINARY_DIR變量,他們指的編譯發生的當前目錄,如果是內部編譯,就相當於PROJECT_SOURCE_DIR也就是工程代碼所在目錄,如果是外部編譯,指的是外部編譯所在目錄,也就是本例中的build目錄。把握一個簡單的原則,在哪裏ADD_EXECUTABLE或ADD_LIBRARY,如果需要改變目標存放路徑,就在哪裏加入上述的定義

4.文件的安裝:將文件複製到制定的目錄。

這裏需要引入一個新的cmake 指令 INSTALL和一個非常有用的變量CMAKE_INSTALL_PREFIX。
CMAKE_INSTALL_PREFIX變量類似於configure腳本的 –prefix,常見的使用方法看起來是這個樣子:
cmake -DCMAKE_INSTALL_PREFIX=/usr .如CMake快速入門教程:實戰
目標文件的安裝:


INSTALL(TARGETS targets...  
        [[ARCHIVE|LIBRARY|RUNTIME]
            [DESTINATION  dir]  [PERMISSIONS    permissions...]
            [CONFIGURATIONS
         [Debug|Release|...]]
            [COMPONENT ]
            [OPTIONAL]
            ] [...])

參數中的TARGETS後面跟的就是我們通過ADD_EXECUTABLE或者ADD_LIBRARY定義的目標文件,可能是可執行二進制、動態庫、靜態庫。目標類型也就相對應的有三種,ARCHIVE特指靜態庫,LIBRARY特指動態庫,RUNTIME特指可執行目標二進制。DESTINATION定義了安裝的路徑,如果路徑以/開頭,那麼指的是絕對路徑,這時候CMAKE_INSTALL_PREFIX其實就無效了。如果你希望使用CMAKE_INSTALL_PREFIX來定義安裝路徑,就要寫成相對路徑,即不要以/開頭,那麼安裝後的路徑就是${CMAKE_INSTALL_PREFIX}/

#SET(CMAKE_INSTALL_PREFIX ${PROJECT_BINARY_DIR}) SET(CMAKE_INSTALL_PREFIX /usr/local) INSTALL(PROGRAMS crnode.sh DESTINATION bin) INSTALL(FILES COPYRIGHT README DESTINATION share/doc/crnode) INSTALL(DIRECTORY doc/ DESTINATION share/doc/crnode)

更多關於安裝的信息:如安裝普通文件、可執行程序(如腳本)等參見參考CMake Practice.

靜態庫和動態庫構建

本節實驗任務:1,建立一個靜態庫和動態庫,提供HelloFunc函數供其他程序編程使用,HelloFunc向終端輸出Hello World字符串。2,安裝頭文件與共享庫。

知識點:

  • 如何通過ADD_LIBRARY指令構建動態庫和靜態庫。
  • 如何通過SET_TARGET_PROPERTIES同時構建同名的動態庫和靜態庫。
  • 如何通過SET_TARGET_PROPERTIES控制動態庫版本

1.目錄結構


t3
├─doc(說明文檔)
│   ├─hello.txt
├──lib/(靜態庫源碼文檔)
│   ├─hello,c
│   ├─hello.h
│   ├─CMakeLists.txt
├─bin/(目標文件)
│   ├─
├─build/(外部編譯文檔)(編譯完成也在在這個目錄)
│   ├─
├─COPYRIGHT
├─CMakeLists.txt
在t3目錄下建立CMakeLists.txt,內容如下:
PROJECT(HELLOLIB)
ADD_SUBDIRECTORY(lib)
在lib目錄下建立CMakeLists.txt,內容如下:
SET(LIBHELLO_SRC hello.c)
ADD_LIBRARY(hello SHARED ${LIBHELLO_SRC})

編譯共享庫:仍然採用out-of-source編譯的方式,按照習慣,我們建立一個build目錄,在build目錄中
cmake ..
make
這時,你就可以在build目錄中lib目錄得到一個libhello.so,這就是我們期望的共享庫

2.同時生成共享庫和靜態庫

同樣使用上面的指令,我們在支持動態庫的基礎上再爲工程添加一個靜態庫,按照一般的習慣,靜態庫名字跟動態庫名字應該是一致的,只不過後綴是.a罷了。下面我們用這個指令再來添加靜態庫:ADD_LIBRARY(hello STATIC ${LIBHELLO_SRC})然後再在build目錄進行外部編譯,我們會發現,靜態庫根本沒有被構建,仍然只生成了一個動態庫。因爲hello作爲一個target是不能重名的,所以,靜態庫構建指令無效。如果我們把上面的hello修改爲hello_static:ADD_LIBRARY(hello_static STATIC ${LIBHELLO_SRC})就可以構建一個libhello_static.a的靜態庫了。

這種結果顯示不是我們想要的,我們需要的是名字相同的靜態庫和動態庫,因爲target名稱是唯一的,所以,我們肯定不能通過ADD_LIBRARY指令來實現了。這時候我們需要用到另外一個指令:
SET_TARGET_PROPERTIES,其基本語法是:

SET_TARGET_PROPERTIES(target1 target2 ...  
                    PROPERTIES prop1 value1  
                    prop2 value2 ...)

這條指令可以用來設置輸出的名稱,對於動態庫,還可以用來指定動態庫版本和API版本。

在本例中,我們需要作的是向lib/CMakeLists.txt中添加一條:SET_TARGET_PROPERTIES(hello_static PROPERTIES OUTPUT_NAME "hello")
這樣,我們就可以同時得到libhello.so/libhello.a兩個庫了讓我們來檢查一下最終的構建結果,我們發現,libhello.a已經構建完成,位於build/lib目錄中,但是libhello.so去消失了。這個問題的原因是:cmake在構建一個新的target時,會嘗試清理掉其他使用這個名字的庫,因爲,在構建libhello.a時,就會清理掉libhello.so.爲了迴避這個問題,比如再次使用SET_TARGET_PROPERTIES定義CLEAN_DIRECT_OUTPUT屬性。

向lib/CMakeLists.txt中添加:SET_TARGET_PROPERTIES(hello PROPERTIES CLEAN_DIRECT_OUTPUT 1)SET_TARGET_PROPERTIES(hello_static PROPERTIES CLEAN_DIRECT_OUTPUT 1).這時候,我們再次進行構建,會發現build/lib目錄中同時生成了libhello.so和libhello.a

3.動態庫版本號

爲了實現動態庫版本號,我們仍然需要使用SET_TARGET_PROPERTIES指令。
具體使用方法如下:SET_TARGET_PROPERTIES(hello PROPERTIES VERSION 1.2 SOVERSION 1)。VERSION指代動態庫版本,SOVERSION指代API版本。將上述指令加入lib/CMakeLists.txt中,重新構建看看結果。
在build/lib目錄會生成:
libhello.so.1.2
libhello.so.1->libhello.so.1.2
libhello.so ->libhello.so.1

參考Akagi201/learning-cmake 中learning-cmake/hello-world-lib/src/CMakeLists.txt的代碼:


在lib目錄下建立CMakeLists.txt,內容如下(包括安裝):
cmake_minimum_required(VERSION 2.8.4)

set(LIBHELLO_SRC hello.c)

add_library(hello_dynamic SHARED ${LIBHELLO_SRC})
add_library(hello_static STATIC ${LIBHELLO_SRC})

set_target_properties(hello_dynamic PROPERTIES OUTPUT_NAME "hello")
set_target_properties(hello_dynamic PROPERTIES VERSION 1.2 SOVERSION 1)
set_target_properties(hello_static PROPERTIES OUTPUT_NAME "hello")

SET_TARGET_PROPERTIES(hello_dynamic  PROPERTIES CLEAN_DIRECT_OUTPUT 1)
SET_TARGET_PROPERTIES(hello_static PROPERTIES CLEAN_DIRECT_OUTPUT 1)

set(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}/lib)

install(TARGETS hello_dynamic hello_static
        LIBRARY DESTINATION lib
        ARCHIVE DESTINATION lib)

install(FILES hello.h DESTINATION include/hello)

4.安裝共享庫和頭文件
參考Cmake practice

使用外部共享庫和頭文件

  • 如何通過INCLUDE_DIRECTORIES指令加入非標準的頭文件搜索路徑。
  • 如何通過LINK_DIRECTORIES指令加入非標準的庫文件搜索路徑。
  • 如果通過TARGET_LINK_LIBRARIES爲庫或可執行二進制加入庫鏈接。

並解釋瞭如果鏈接到靜態庫。
在工程內部添加鏈接庫target_link_libraries(target1 LIbName )
編寫一個程序使用我其他程序構建的共享庫。頭文件和共享庫安裝到系統目錄/usr/lib/usr/include/hello

1.目錄結構


t3
├─doc(說明文檔)
│   ├─hello.txt
├──src/(源碼)
│   ├main.c
│   ├─CMakeLists.txt
├─bin/(目標文件)
│   ├─
├─build/(外部編譯文檔)(編譯完成也在在這個目錄)
│   ├─
├─COPYRIGHT
├─CMakeLists.txt
編寫工程主文件CMakeLists.txt
PROJECT(NEWHELLO)
ADD_SUBDIRECTORY(src)
編寫src/CMakeLists.txt
ADD_EXECUTABLE(main main.c)

按照習慣,仍然建立build目錄,使用cmake ..方式構建。
過程:
cmake ..
make
構建失敗,如果需要查看細節,可以使用第一節提到的方法
make VERBOSE=1來構建
錯誤輸出爲是:
/backup/cmake/t4/src/main.c:1:19: error: hello.h: 沒有那個文件或目錄

2.引入頭文件搜索路徑。
hello.h位於/usr/include/hello目錄中,並沒有位於系統標準的頭文件路徑,
爲了讓我們的工程能夠找到hello.h頭文件,我們需要引入一個新的指令
INCLUDE_DIRECTORIES,其完整語法爲:
INCLUDE_DIRECTORIES([AFTER|BEFORE] [SYSTEM] dir1 dir2 ...)
這條指令可以用來向工程添加多個特定的頭文件搜索路徑,路徑之間用空格分割,如果路徑中包含了空格,可以使用雙引號將它括起來,默認的行爲是追加到當前的頭文件搜索路徑的後面,你可以通過兩種方式來進行控制搜索路徑添加的方式:
1,CMAKE_INCLUDE_DIRECTORIES_BEFORE,通過SET這個cmake變量爲on,可以將添加的頭文件搜索路徑放在已有路徑的前面。
2,通過AFTER或者BEFORE參數,也可以控制是追加還是置前。
現在我們在src/CMakeLists.txt中添加一個頭文件搜索路徑,方式很簡單,加入:INCLUDE_DIRECTORIES(/usr/include/hello)
進入build目錄,重新進行構建,這是找不到hello.h的錯誤已經消失,但是出現了一個
新的錯誤:main.c:(.text+0x12): undefined reference to 'HelloFunc'
因爲我們並沒有link到共享庫libhello上。

3.爲target添加共享庫
我們現在需要完成的任務是將目標文件鏈接到libhello,這裏我們需要引入兩個新的指令
LINK_DIRECTORIES和TARGET_LINK_LIBRARIES
LINK_DIRECTORIES的全部語法是:
LINK_DIRECTORIES(directory1 directory2 …)
這個指令非常簡單,添加非標準的共享庫搜索路徑,比如,在工程內部同時存在共享庫和可執行二進制,在編譯時就需要指定一下這些共享庫的路徑。這個例子中我們沒有用到這個指令。

TARGET_LINK_LIBRARIES的全部語法是:
TARGET_LINK_LIBRARIES(target library1
《debug | ptimized》 library2
…)
這個指令可以用來爲target添加需要鏈接的共享庫,本例中是一個可執行文件,但是同樣可以用於爲自己編寫的共享庫添加共享庫鏈接。

爲了解決我們前面遇到的HelloFunc未定義錯誤,我們需要作的是向
src/CMakeLists.txt中添加如下指令:
TARGET_LINK_LIBRARIES(main hello)
也可以寫成
TARGET_LINK_LIBRARIES(main libhello.so)
這裏的hello指的是我們上一節構建的共享庫libhello.

4.那如何鏈接到靜態庫呢?
方法很簡單:
將TARGET_LINK_LIBRRARIES指令修改爲:
TARGET_LINK_LIBRARIES(main libhello.a)

5.特殊的環境變量CMAKE_INCLUDE_PATH和CMAKE_LIBRARY_PATH
務必注意,這兩個是環境變量而不是cmake變量。
使用方法是要在bash中用export或者在csh中使用set命令設置或者CMAKE_INCLUDE_PATH=/home/include cmake ..等方式。
這兩個變量主要是用來解決以前autotools工程中
–extra-include-dir等參數的支持的。
也就是,如果頭文件沒有存放在常規路徑(/usr/include, /usr/local/include等),則可以通過這些變量就行彌補。我們以本例中的hello.h爲例,它存放在/usr/include/hello目錄,所以直接查找肯定是找不到的。
前面我們直接使用了絕對路徑INCLUDE_DIRECTORIES(/usr/include/hello)告訴工程這個頭文件目錄。
爲了將程序更智能一點,我們可以使用CMAKE_INCLUDE_PATH來進行,使用bash的方法如下:export CMAKE_INCLUDE_PATH=/usr/include/hello
然後在頭文件中將INCLUDE_DIRECTORIES(/usr/include/hello)替換爲:


FIND_PATH(myHeader hello.h)
IF(myHeader)
INCLUDE_DIRECTORIES(${myHeader})
ENDIF(myHeader)

上述的一些指令我們在後面會介紹。
這裏簡單說明一下,FIND_PATH用來在指定路徑中搜索文件名,比如:FIND_PATH(myHeader NAMES hello.h PATHS /usr/include
/usr/include/hello)

這裏我們沒有指定路徑,但是,cmake仍然可以幫我們找到hello.h存放的路徑,就是因
爲我們設置了環境變量CMAKE_INCLUDE_PATH
如果你不使用FIND_PATH,CMAKE_INCLUDE_PATH變量的設置是沒有作用的,你不能指
望它會直接爲編譯器命令添加參數-I。
以此爲例,CMAKE_LIBRARY_PATH可以用在FIND_LIBRARY中。
同樣,因爲這些變量直接爲FIND_指令所使用,所以所有使用FIND_指令的cmake模塊都
會受益。

參考

[1]: CMake 入門
[2]:Cmake入門
[3]:Cmake手冊詳解
[4]:CMake中文手冊
[5]:CMake 入門實戰(什麼是 CMake、入門案例:單個源文件、多個源文件、自定義編譯選項、安裝和測試、支持 gdb、添加環境檢查、添加版本號、生成安裝包、將其他平臺的項目遷移到 CMake、相關鏈接、類似工具。)

注:Cmake practice電子書存在錯誤,第6頁CmakeLists.txt文檔最後一行應爲:ADD_EXCUTABLE(hello ${SRC_LIST})

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章