文章目錄
C語言工程構建
爲什麼需要編譯腳本?
當C語言工程很大,源碼非常多時,如果還去使用GCC命令編譯程序,幾乎是不現實的。這時候,可以通過編寫shell腳本去執行編譯命令,當然這並不是一種好的方式。在Linux上我們可以寫shell腳本,在Windows上則可以編寫bat腳本
本篇以如下源碼作爲示例工程,需要編譯一個main.exe
程序出來
add.c
int add(int a, int b){
return a+b;
}
sub.c
int sub(int a, int b){
return a-b;
}
mul.c
int mul(int a, int b){
return a*b;
}
div.c
int div(int a, int b){
return a*b;
}
calc.h
int add(int a, int b);
int sub(int a, int b);
int mul(int a, int b);
int div(int a, int b);
main.c
#include <stdio.h>
#include "calc.h"
int main(){
printf("1+2=%d\n",add(1,2));
printf("18-9=%d\n",sub(18,9));
return 0;
}
shell腳本(bat腳本)
由於在Windows平臺,使用MinGW環境,這裏編寫的是bat腳本,創建一個名爲build
的文件(文件名任意),修改其擴展名爲build.bat
,使用文本編輯器編輯該文件(Linux平臺上,則保存擴展名build.sh
)
gcc add.c sub.c mul.c div.c main.c -o main.exe
可以看到,只需要執行build.bat
就能編譯生成main.exe
,這比每次手敲命令方便太多了。如果有多個源碼文件,只需要寫入腳本中,通過執行腳本完成編譯。
Makefile 腳本
Makefile 腳本文件是GNU make
工具的輸入文件,它也包含一套自己的語法規則,它也能幫助C語言實現編譯和鏈接。既然可以通過命令行腳本(shell)完成編譯工作,爲什麼還需要Makefile腳本文件呢?
雖然命令行腳本也能幫助編譯鏈接,但是它的能力還太弱,它每次都會將所有文件重新編譯,例如有幾百個源文件,我僅僅只修改了其中一個源文件,那麼重新編譯時,這幾百個源文件也都會重新編譯,這樣每次編譯一下都會耗費大量時間。而make 工具會自動根據修改情況完成源文件的對應.o
文件的更新、庫文件的更新以及最終的可執行程序的更新,它實際上是通過比較對應文件的最後修改時間,來決定哪些文件需要更新、那些文件不需要更新。
現在將命令行腳本改寫爲Makefile腳本,在源碼目錄下創建一個名爲Makefile
的文件(亦可以寫作makefile
),注意,它沒有拓展名,編輯如下內容:
# 編譯一個main.exe 程序
main.exe: main.o add.o sub.o mul.o div.o
gcc main.o add.o sub.o mul.o -o main.exe
main.o: main.c calc.h
gcc -c main.c
add.o: add.c
gcc -c add.c
sub.o: sub.c
gcc -c sub.c
mul.o: mul.c
gcc -c mul.c
div.o: div.c
gcc -c div.c
# 僞目標,刪除所有.o文件
clean:
rm *.o
cd
到當前目錄,執行輸入make
命令,即可快速編譯生成main.exe
程序,當我們需要清理整個工程時,即全部重新編譯時,可以輸入make clean
命令,即可刪除當前目錄下的所有.o
文件。
基本語法規則
注意,#
號開頭的行表示註釋
語法結構如下
target1 target2 target3...: prerequisite1 prerequisite2 prerequisite3...
command1
command2
command3
-
target
表示目標。通常有三種情況:可以是一個目標文件(.o
文件);可以是一個可執行文件;可以是一個標籤,標籤被稱爲僞目標 -
prerequisite
表示條件。實際上表達的是一種依賴關係,即要生成前面的target,所需要依賴的文件或是另一個目標 -
command
表示需要執行的命令。即要生成這個目標,對應執行的命令
需要注意,在冒號的左邊,可以是一個或多個目標,而在冒號的右邊,則可以是零個或多個依賴條件。目標頂格寫,而command前面則必須有一個製表符(即
Tab
鍵)
要想寫Makefile
文件,必須對C語言的編譯鏈接階段有基本的瞭解,總的來說,就是將.c
源碼文件編譯爲.o
目標文件,然後將.o
文件鏈接爲可執行程序,而Makefile
腳本正是將這個依賴關係反過來描述,即一個可執行程序需要依賴哪些.o
文件,每一個.o
文件又依賴於哪些.c
、.h
文件。
簡化版本
除了上面那種標準版本,我們還可以利用make工具的自動推導能力,省略對目標文件的條件依賴描述,包括編譯命令。
# 編譯一個main.exe 程序
main.exe: main.o add.o sub.o mul.o div.o
gcc main.o add.o sub.o mul.o -o main.exe
main.o: calc.h
add.o:
sub.o:
mul.o:
div.o:
# 僞目標,刪除所有.o文件和可執行文件
clean:
rm *.o main.exe
另一種風格
# 編譯一個main.exe 程序
main.exe: main.o add.o sub.o mul.o div.o
gcc main.o add.o sub.o mul.o -o main.exe
main.o: calc.h
# 另一種風格,寫在同一行
add.o sub.o mul.o div.o:
# 僞目標,刪除所有.o文件和可執行文件
clean:
rm *.o main.exe
在make工具中,它能夠自動完成對.c
文件的編譯並生成對應的.o
文件。它默認執行命令cc -c
來編譯.c
源文件,以main.o
爲例,它會默認執行cc -c main.c -o main.o
。但是要注意,我們如果在Windows上執行以上簡化版的make
,則會報錯,這是因爲在Linux系統中,cc
命令會默認的鏈接到gcc
命令上,執行cc
命令就是在執行gcc
命令,而我們Windows系統中是沒有cc
命令的。解決辦法非常簡單粗暴,就是進入gcc.exe
所在目錄,將gcc.exe
再複製一份,並更名爲cc.exe
即可。
僞目標
僞目標就是一個標籤,它本身既不是目標文件也不是可執行文件,例如上面例子中的clean
,我們可以通過僞目標定義一些命令,然後在make中去執行。
上面例子中的僞目標在定義上存在一些問題,假如源碼目錄下真的存在一個名爲clean
的文件,則會與當前的僞目標衝突。將一個目標聲明爲僞目標需要將它作爲特殊目標.PHONY
的依賴,這樣定義的僞目標就不會和源碼目錄下的文件名衝突。
正確的定義僞目標
.PHONY: clean
clean:
rm *.o main.exe
再看一個例子
# 定義一個僞目標print,它執行命令行的echo命令輸出hello,world
.PHONY: print
print:
echo "hello,world"
然後在命令行執行make print
,就會輸入出被執行的完整命令,以及命令執行的結果
我們可以根據自己的需要在Makefile
中定義自己的僞目標,通常會定義clean
、install
這些僞目標,install
一般定義拷貝命令,將生成的可執行程序拷貝到應用安裝目錄下。在Linux平臺下,通常是將C語言的源代碼和Makefile
腳本一同發佈出去,用戶只需要在源碼目錄下分別執行命令make
、make install
即完成了程序的編譯和安裝,可以看到,有了make
工具後,讓開源的C程序的編譯使用過程變得非常簡單。
補充說明
實際上完整的Makefile 語法體系是非常複雜靈活的,學習完整Makefile語法不亞於學習一門新的編程語言,而且許多語法功能並不是常用的,另一方面,在大型的複雜工程中,自己手寫Makefile
是極爲不明智的選擇。make
工具是一個比較古老的工具,已經有一些工具可以幫助我們自動生成Makefile
文件,例如Linux上的Autoconf
,當然,現在更好的工具是cmake
,它可以自動生成跨平臺編譯腳本,而且還能用於Android端的NDK開發,是最被推薦的構建工具。
CMake工具
它首先允許開發者編寫一種平臺無關的 CMakeLists.txt
文件來定製整個編譯流程,然後再根據目標用戶的平臺進一步生成所需的本地化 Makefile
或工程文件,如Linux
下的 Makefile
文件 或 Windows 的 Visual Studio 工程文件。
簡單說,以前我們編寫的C語言編譯腳本是不能跨平臺編譯的,例如上面示例中編寫的 Makefile
,它只能在GCC環境下編譯,通常是Linux系統上,而在Windows下的Visual Studio裏面就沒法用,得重新改造,如果是一個大型項目,那就是災難。現在我們用CMake工具編寫構建腳本,就與平臺無關了,它會自動生成對應平臺的構建方案,再也不用程序員去操心了。更準確的說,CMake工具真正厲害的地方並不只是跨平臺,而是跨編譯環境。
安裝
進入cmake官網下載頁 下載zip包或安裝器,安裝後,將cmake的bin
目錄加入PATH環境變量中,命令行輸入cmake --version
檢查環境是否配置成功
簡單示例
以上面的代碼爲例,在源碼目錄下創建 CMakeLists.txt
文件
# CMake最低版本號要求
cmake_minimum_required (VERSION 2.8)
# 配置項目名
project (ch1)
# 指定生成目標,main2爲生成的可執行程序名,後面是源碼列表
add_executable (main2 add.c sub.c mul.c div.c main.c)
當前面目錄下執行以下命令,注意.
不能掉
cmake .
在我們的目錄下自動生成了一個 Visual Studio 工程,因爲我本地安裝了Visual Studio開發環境。可以雙擊打開ch1.sln
文件或main2.vcxproj
文件,這裏會打開Visual Studio IDE,就能直接在IDE裏面編譯了。
這裏,如果我想生成MinGW開發環境的Makefile
,則只需要加一個-G
參數,來指定一個明確的編譯環境,從而生成對應的構建腳本。
cmake -G "MinGW Makefiles"
要注意,以上命令直接在CMD命令行執行可能會報錯,它需要一個sh
環境,這裏有兩種解決辦法
- 將
sh.exe
所在目錄加入到環境變量中,它位於MinGW
根目錄下的git\bin
下,修改環境變量後,打開新的命令行窗口然後再執行以上命令 - 第二種就是偷懶的做法,如果你本地安裝了
git
工具,則直接鼠標右鍵,選擇Git Bash Here
打開一個bash
來執行以上命令
命令執行完畢,本地目錄下就會自動生成一個Makefile
文件,然後執行make
命令即可編譯。我們如果打開這個Makefile
文件,會發現看不懂,裏面內容比較複雜。
到這裏我們已經學會了cmake
構建的簡單流程,接下來只需要學習一下 CMakeList.txt
文件的編寫規則
基礎規則
CMakeLists.txt
文件由命令、註釋和空格組成,其中命令是不區分大小寫的。#
開頭的行表示註釋。命令由命令名稱、小括號和參數組成,參數之間使用空格進行間隔。例如add_executable (main2 add.c sub.c mul.c div.c main.c)
外部構建
在上面的示例中,執行cmake
命令會在源碼工程的目錄下生成很多無法自動刪除的中間文件或臨時文件,這就弄亂了源碼工程的目錄,如果要發佈源碼,還得手動一個個去刪除這些文件,這顯然不是一種好的構建方式,這種方式被稱爲內部構建,相應的,我們需要使用外部構建的方式來解決問題。
在源碼工程的根目錄下創建一個build
文件夾,然後在命令行裏cd
到build
下,執行cmake ..
或
cmake -G "MinGW Makefiles" ..
命令,此時會將所有的中間文件生成到build
目錄中,包括Makefile
,然後執行make
編譯。當我們需要刪除臨時文件時,只需要刪除build
目錄即可,不會對源碼工程造成任何影響。
定義變量
源文件較多時,可以定義一個變量來保存,後續只需要引用該變量即可,如下,定義src_list
來保存源文件列表,引用是使用${}
包裹.
定義變量使用set
命令,取消命令可使用unset
命令
# 定義變量 src_list
set (src_list add.c sub.c mul.c div.c main.c)
# 打印日誌
message (STATUS "源文件列表:${src_list}")
# 引用變量
add_executable (main2 ${src_list})
message
命令是用來打印日誌的,它的第一個參數是mode,可省略,常用值如下
mode | 簡述 |
---|---|
(none) | 重要信息 |
STATUS | 附帶消息 |
WARNING | CMake警告,繼續處理 |
AUTHOR_WARNING | CMake警告(dev),繼續處理 |
SEND_ERROR | CMake錯誤,繼續處理,但會跳過生成 |
FATAL_ERROR | CMake錯誤,停止處理和生成 |
內置變量
在cmake
中已經內置了一些變量,我們可以直接使用,也可使用set
命令去修改
CMAKE_SOURCE_DIR
或PROJECT_SOURCE_DIR
表示工程的根目錄CMAKE_BINARY_DIR
或PROJECT_BINARY_DIR
表示編譯目錄。如果是內部構建,則編譯目錄與工程根目錄相同,如果是外部構建,則表示外部構建創建的編譯目錄,如上例中的build
目錄CMAKE_CURRENT_SOURCE_DIR
表示當前處理的CMakeLists.txt
所在文件夾的路徑CMAKE_CURRENT_LIST_FILE
當前CMakeLists.txt
文件的完整路徑CMAKE_C_COMPILER
和CMAKE_CXX_COMPILER
分別表示C和C++編譯器的路徑PROJECT_NAME
該變量可獲取project
命令配置的項目名
可以使用message
命令打印這些內置變量的值
cmake_minimum_required (VERSION 2.8)
project (ch1)
message (${CMAKE_SOURCE_DIR})
message (${PROJECT_SOURCE_DIR})
message (${CMAKE_BINARY_DIR})
message (${PROJECT_BINARY_DIR})
message (${CMAKE_CURRENT_SOURCE_DIR})
message (${CMAKE_CURRENT_LIST_FILE})
message (${CMAKE_C_COMPILER})
message (${CMAKE_CXX_COMPILER})
message (${PROJECT_NAME})
EXECUTABLE_OUTPUT_PATH
設置該變量可修改可執行程序的生成路徑LIBRARY_OUTPUT_PATH
設置該變量可修改庫文件生成路徑
# build/bin/
SET(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)
# build/lib/
SET(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}/lib)
BUILD_SHARED_LIBS
指定默認生成的庫的類型
命令
CMakeLists.txt
文件基本上就是由命令和參數組成的,例如之前的set
、message
這些,下面就瞭解一下常用的命令
add_executable
使用給定的源文件,生成一個可執行程序add_library
使用給定的源文件,生成一個庫(靜態庫或共享庫)add_subdirectory
添加一個子目錄,該子目錄也必須包含一個CMakeLists.txt文件include_directories
添加頭文件路徑add_definitions
添加編譯參數target_link_libraries
鏈接指定的庫find_library
查找指定的庫,並將庫文件路徑保存到一個變量set_target_properties
設置目標的一些屬性,從而改變構建方式link_directories
添加庫的搜索路徑aux_source_directory
查找指定路徑下的所有源文件
綜合實例
調整上面示例工程的結構,在工程跟目錄下創建四個文件夾,分別是build
、calc
、include
、src
,具體工程結構如下所示
ch1
|
+--- build/
|
+--- calc/
|
+--- add.c
|
+--- div.c
|
+--- mul.c
|
+--- sub.c
|
+--- CMakeLists.txt
|
+--- include/
|
+--- calc.h
|
+--- src/
|
+--- main.c
|
+--- CMakeLists.txt
calc
目錄作爲一個子項目,用於編譯一個libcalc.a
靜態庫,主工程源碼在src
下,且需鏈接靜態庫。
子項目calc
下需要一個CMakeLists.txt
文件,內容如下
cmake_minimum_required (VERSION 2.8)
# 創建靜態庫calc,其生成的文件名爲libcalc.a
add_library (calc STATIC add.c sub.c mul.c div.c)
工程根目錄下也需要CMakeLists.txt
文件,內容如下
cmake_minimum_required (VERSION 2.8)
# 配置項目名
project (ch1)
# 添加一個子文件夾 calc,這裏寫的相對路徑
add_subdirectory (calc)
# 定義變量 SRCS_DIR, 指向src目錄的絕對路徑
set (SRCS_DIR "${PROJECT_SOURCE_DIR}/src")
# 添加頭文件目錄,即添加工程根目錄下的include目錄
include_directories ("${PROJECT_SOURCE_DIR}/include")
# 添加庫的搜索路徑,即libcalc.a所在的目錄(build/calc/libcalc.a)
link_directories ("${PROJECT_BINARY_DIR}/calc")
# 用於生成可執行文件 main.exe
add_executable (main "${SRCS_DIR}/main.c")
# 爲main程序指定鏈接靜態庫calc
target_link_libraries(main calc)
首先執行cmake -G "MinGW Makefiles" ..
命令自動生成Makefile
文件,然後執行make
命令進行編譯,完成後build
目錄下即生成main.exe
當鏈接已經編譯好的庫時,推薦使用find_library
來查找庫,因爲link_directories
命令傳入相對路徑時,會直接將相對路徑傳給編譯器,導致出現找不到問題。
find_library
命令原型如下,第一個參數爲變量,第二個參數爲庫名稱,最後面可以填入多個路徑
find_library(<VAR> name1 [path1 path2 ...])
# 在指定的目錄下查找名爲calc的庫,
# 並將庫文件的絕對路徑保存到變量STATIC_LIB中
find_library(STATIC_LIB calc "${PROJECT_BINARY_DIR}/calc")
message (${STATIC_LIB})
# 爲main程序指定鏈接靜態庫calc
target_link_libraries(main ${STATIC_LIB})
靜態庫與動態庫
使用add_library
命令默認生成靜態庫,如add_library (calc add.c sub.c mul.c div.c)
,亦可加上參數STATIC
顯式指定,如需生成動態庫,則添加參數SHARED
,如add_library (calc SHARED add.c sub.c mul.c div.c)
,此外,還可以通過設置變量BUILD_SHARED_LIBS
來修改默認行爲,當該變量爲真時,默認會生成動態庫,如
# 使用option命令定義選項
option(BUILD_SHARED_LIBS "build shared or static libraries" ON)
自動獲取源碼列表
當我們工程的源碼非常多時,一個個去手寫源碼列表是非常麻煩的,以上述calc
目錄下的CMakeLists.txt
文件爲例,這時可以使用aux_source_directory
命令
cmake_minimum_required (VERSION 2.8)
# 獲取當前目錄下的源文件路徑列表,並保存到變量SRC_LIST中
aux_source_directory (. SRC_LIST)
# 打印
message (STATUS ${SRC_LIST})
add_library (calc STATIC ${SRC_LIST})
該命令原型如下,第一個參數爲搜索的路徑,第二個參數爲變量
aux_source_directory(<dir> <variable>)
這個命令只能識別源碼文件,不能識別其他文件,比如.h
文件就不能掃描出來,因此存在一定缺陷,想知道能識別哪些拓展名的源文件,可打印兩個內置變量獲取
message (STATUS ${CMAKE_C_SOURCE_FILE_EXTENSIONS})
message (STATUS ${CMAKE_CXX_SOURCE_FILE_EXTENSIONS})
遞歸獲取文件列表
aux_source_directory
命令只能獲取源碼文件列表,且無法遞歸獲取給定路徑下的嵌套子文件夾下的各種源文件,這時可以使用file
命令,結合GLOB_RECURSE
參數,對指定的文件拓展名進行遞歸獲取。
# 遞歸遍歷當前目錄下的所有.c .cpp後綴名的文件,並將結果列表保存到SRC_LIST變量中
FILE(GLOB_RECURSE SRC_LIST *.c *.cpp)
# 打印
message (STATUS ${SRC_LIST})
add_library (calc STATIC ${SRC_LIST})
原型如下
file(GLOB_RECURSE
variable
[RELATIVE path]
[FOLLOW_SYMLINKS]
[globbing expressions]...)
如不需遞歸,可將GLOB_RECURSE
改爲GLOB
指定庫的輸出名稱
add_library (calc STATIC ${SRC_LIST})
# 將生成 libcalculate.a
set_target_properties(calc PROPERTIES OUTPUT_NAME "calculate")
定義宏與條件編譯
可使用add_definitions
命令,傳入-D
加上宏名稱來定義宏,以下定義宏USER_PRO
# 定義宏 USER_PRO
add_definitions(-DUSER_PRO)
# 等價於 #define VER 1 、#define Foo 2
add_definitions(-DVER=1 -DFoo=2)
配合使用option
命令,實現條件編譯
project(test)
option(USER_PRO "option for user" OFF)
if (USER_PRO)
add_definitions(-DUSER_PRO)
endif()
option
命令原型:
option(<option_variable> "描述選項的幫助性文字" [initial value])
add_definitions
命令主要用來添加編譯參數,add_compile_options
命令也具有相同的功能,示例如下
add_compile_options(-std=c99 -Wall)
add_definitions(-std=c99 -Wall)
指定構建環境
前面已經學會了-G
參數指定構建環境,那麼到底可以指定哪些構建環境呢?這裏根據官方文檔,整理一下-G
後面可以跟哪些值。
生成 Makefile文件
以下是不同環境下的Makefile文件
Borland Makefiles
MSYS Makefiles
MinGW Makefiles
NMake Makefiles
NMake Makefiles JOM
Unix Makefiles
Watcom WMake
生成 Visual Studio工程
Visual Studio 6
Visual Studio 7
Visual Studio 7 .NET 2003
Visual Studio 8 2005
Visual Studio 9 2008
Visual Studio 10 2010
Visual Studio 11 2012
Visual Studio 12 2013
Visual Studio 14 2015
Visual Studio 15 2017
Visual Studio 16 2019
其他環境
Green Hills MULTI
Xcode
CodeBlocks
CodeLite
Eclipse CDT4
Kate
Sublime Text 2
補充
Ninja
這裏重點說一下Ninja
,當前的官方文檔中沒有寫Ninja
,實際上CMake從2.8.9版本開始可以支持Ninja
構建
Ninja
是一個注重速度的小型構建系統。它與其他構建系統在兩個主要方面不同:它被設計爲使其輸入文件由更高級別的構建系統生成,並且被設計爲儘可能快地運行構建。
簡單說,它被設計出來是爲了替代make
工具以及Makefile
文件的,它與make
工具的顯著區別是,Makefile
是設計出來給人手寫的,而Ninja
的build.ninja
設計出來是給其它程序生成的。Makefile
是一個DSL,Ninja
則只是一種配置文件。 Makefile
支持分支、循環等流程控制,而Ninja
僅支持一些固定形式的配置。
兩者的對應關係:
ninja
對應make
,build.ninja
文件對應於Makefile
文件
安裝
到下載鏈接 下載對應版本的ninja
工具,解壓後配置PATH環境變量,輸入ninja --version
檢查環境
生成 build.ninja
文件
cmake -G "Ninja" ..
編譯
ninja