C++少說也用了十年了,從簡單的Hello World到200萬行的游戲項目,編譯和構建的工具也經歷了各種升級。最終的開發環境,選擇了Clang+GDB+CMake。當然不斷改進和升級開發工具的腳步尚未停止,只要能提高開發效率,怎樣折騰都是值得的。
期間經歷了:
- 直接調用編譯和鏈接命令
- 使用Makefile
- 使用CMake
- 不斷嘗試其他構建系統,如:b2、WAF、SCons
對構建系統的要求
由于C/C++本身的特性,如:跨平臺、高性能等、編寫復雜等,對構建系統也是提出了一定的要求:
支持并行編譯:構建系統能否支持并行編譯?對于編譯速度的要求,我給自己定的目標是<10min,超過10min要么換機器,要么想辦法優化代碼依賴。上百萬行的代碼,并行編譯時必須的,否則一不小心改一行代碼等個把小時,這樣開發時間白白浪費在編譯上太不值得了。
自動生成依賴:構建系統是否僅僅編譯剛修改過的及其依賴的文件?代碼的依賴關系,要我們自己去手動寫腳本(一般gcc/clang的話,使用
gcc -M xx.cpp
)?跨平臺:構建系統能否僅寫一份構建腳本,支持多種平臺?有些項目需要進行交叉編譯,測試環境和運行環境是在不同的平臺環境下。
支持自定義構建目標: 構建系統必須支持擴展,支持自定義Target等。如:protobuf文件可以根據依賴規則自動生成.h、.cpp;自定義一些用于打包或測試的命令(
make pack
、make test
)。
本文下面大概介紹一下剛提到的構建系統,具體用法不贅述,官方網站是最好的開始地方。若有必要會另起文章詳細講解如何使用及其工作原理。
基于make的
GNU Make
對于玩Linux的人來說,這是太熟悉不過的東西了。小規模的項目或僅自己玩的項目,手寫Makefile完全就足夠了。
GNU Make 是一個控制源碼生成可執行文件或其他文件的工具。需要一個叫Makefile的文件來說明構建的目標和規則。
最簡單的規則大概是這樣的:
target: dependencies ...
commands
...
意思是:生成target
,依賴于dependencies
,如果dependencies
有修改或者target
不存在,就逐個執行下面的commands
去生成target
。
下面貼一個復雜的Makefile感受下:
CXX = g++
CXXFLAGS = -g -I../proto.client -I../common
LDFLAGS = -L../common -L../proto.client/ -lproto.client -L/usr/local/lib -lzmq -lprotobuf -ltinyworld
OBJS = main.o
SRCS = $(OBJS:%.o=%.cpp)
DEPS = $(OBJS:%.o=.%.d)
TARGET=gateserver
.PHONY: all clean
all : $(TARGET)
include $(DEPS)
$(DEPS): $(SRCS)
@$(CXX) -M $(CXXFLAGS) $< > $@.$$$$; \\
sed 's,\\($*\\)\\.o[ :]*,\\1.o $@ : ,g' < $@.$$$$ >$@; \\
rm -f $@.$$$$
$(OBJS): %.o: %.cpp
$(CXX) -c $(CXXFLAGS) $< -o $@
$(TARGET): $(OBJS) ../common/libtinyworld.a
$(CXX) $(OBJS) -o $@ $(CXXFLAGS) $(LDFLAGS)
clean:
@rm -rf $(TARGET)
Microsoft NMake
在Windows下面做開發,Visual Studio基本上完全勝任。微軟自己的IDE功能強大,對于項目構建的管理IDE幫著你搞定了。VS的構建的管理其實用的是微軟自己的Make,叫NMAKE。腳本還是IDE,各有千秋:IDE好處就是它什么都幫你干了,簡單方便;壞處就是對構建的方式和過程了解的比較淺,自由度沒那么大,遇到大型項目的特殊需求時要各種查資料。
MSDN上面的NMAKE腳本示例:
# Sample makefile
!include <win32.mak>
all: simple.exe challeng.exe
.c.obj:
$(cc) $(cdebug) $(cflags) $(cvars) $*.c
simple.exe: simple.obj
$(link) $(ldebug) $(conflags) -out:simple.exe simple.obj $(conlibs) lsapi32.lib
challeng.exe: challeng.obj md4c.obj
$(link) $(ldebug) $(conflags) -out:challeng.exe $** $(conlibs)
自動生成make腳本的
手動寫make腳本自由度大,為了自由度,它的設計比較簡單,有許多上述對構建系統的要求它沒法支持。如:GUN Make沒法自己知道代碼的依賴,需要借助編譯器來自己寫腳本;跨平臺就更不可能了。
還有一個重要的影響就是對于環境的自動檢測。如果你的代碼發布出去,任何一個人下載下來需要進行編譯,他的編譯器、操作系統環境、依賴的第三方庫的位置和版本都會有差異,如何進行編譯?難到要下載你代碼的人去手動修改你的Makefile嗎?當然不是,這個時候在編譯之前還需要一步:檢測當前編譯環境、操作系統環境、第三方庫的位置等,不滿足要求就直接報錯,檢測到所有依賴后再根據這些信息生成適合你當前系統的Makefile,然后才能進行編譯。
GNU Build System
認識GNU Build System可以從兩個角度入手:使用者和開發者。主要包含三大模塊:
- Autoconf
- Automake
- Libtool
站在使用者的角度,GNU Build System為我們提供了源碼包編譯安裝的方式:
tar -xvzf package-name.version.tar.gz # tar -xvjf package-name.version.tar.bz2
cd package-name.version
./configure --prefix=xxx
make
make install
其中的configure
就是檢測環境,生成Makefile的腳本。大概的過程如下:
站在開發者的角度,GNU Build System 為我們廣大程序員提供了編寫構建規則和檢查安裝環境的功能。
要發布自己的源碼,首先需要一個Autoconf的configure.ac
,最簡單的長這樣:
AC_INIT([hello], [1.0])
AC_CONFIG_SRCDIR([hello.c])
AC_CONFIG_HEADERS(config.h)
AC_PROG_CC
AC_CONFIG_FILES(Makefile)
AC_PROG_INSTALL
AC_OUTPUT
其次還需要一個Automake的Makefile.am
來描述構建規則,看起來這這樣的:
AC_INIT([hello], [1.0])
AC_CONFIG_SRCDIR([hello.c])
AC_CONFIG_HEADERS(config.h)
AM_INIT_AUTOMAKE
AC_PROG_CC
AC_CONFIG_FILES(Makefile)
AC_PROG_INSTALL
AC_OUTPUT
定義好檢查環境和配置的configure.ac
和描述構建規則的Makefile.am
,生成一個可以發布的源碼包大概過程如下:
aclocal
autoconf
autoheader
touch NEWS README AUTHORS ChangeLog
automake -a
./configure
make
make dist
CMake
CMake是一個跨平臺的安裝(編譯)工具,可以用簡單的語句來描述所有平臺的安裝(編譯過程)。他能夠輸出各種各樣的Makefile或者project文件,能檢查編譯器所支持的C++特性,類似UNIX下的automake。CMake 并不直接建構出最終的軟件,而是產生標準的建構腳本(如Unix 的 Makefile 或 Windows Visual C++ 的 projects/workspaces),然后再使用相應的工具進行編譯。
CMak的特點主要有:
- 開放源代碼, 使用類 BSD 許可發布。 http://cmake.org/HTML/Copyright. html
- 跨平臺, 并可生成 native 編譯配置文件, 在 Linux/Unix 平臺, 生成 makefile, 在蘋果平臺, 可以生成 xcode, 在 Windows 平臺, 可以生成 MSVC 的工程文件。
- 能夠管理大型項目, KDE4 就是最好的證明。
- 簡化編譯構建過程和編譯過程。 CMake 的工具鏈非常簡單:
cmake+make
。 - 可擴展, 可以為 cmake 編寫特定功能的模塊, 擴充 cmake 功能。
其實CMake工具包不僅僅提供了編譯,還有:支持單元測試的CTest,支持不同平臺打包的CPack,自動化測試及其展示的CDash。有興趣的訪問官方網站學習:https://cmake.org/
一般,在每個源碼目錄下都有一個 CMakeLists.txt,看起來是這樣的:
cmake_minimum_required (VERSION 2.6)
project (Tutorial)
# The version number.
set (Tutorial_VERSION_MAJOR 1)
set (Tutorial_VERSION_MINOR 0)
# configure a header file to pass some of the CMake settings
# to the source code
configure_file (
"${PROJECT_SOURCE_DIR}/TutorialConfig.h.in"
"${PROJECT_BINARY_DIR}/TutorialConfig.h"
)
# add the binary tree to the search path for include files
# so that we will find TutorialConfig.h
include_directories("${PROJECT_BINARY_DIR}")
# add the executable
add_executable(Tutorial tutorial.cxx)
使用的時候:
第一步:根據CMakeLists.txt生成Makefile,命令如下:
mkdir path-to-build
cd path-tob-build
cmake path-to-source
cmake的過程可以分為配置和生成過程。配置的時候優先從CMakeCache.txt中讀取設置,然后再掃一遍CMakeList.txt中的設置,該步驟會檢查第三方庫和構建過程的變量;生成步驟則根據當前的環境和平臺,生成不同的構建腳本,如Linux的Makefile,Windows的VC工程文件。
第二步:編譯。沒啥好說的,Linux下直接make -jxx
,其他的操作系統的IDE直接打開點一下build按鈕即可。
非基于make的
非基于make的構建系統五花八門,這里只大概介紹一下我所知的幾個。
SCons
SCons 是一個開放源代碼、以 Python 語言編寫的下一代的程序建造工具。作為下一代的軟件建造工具,SCons 的設計目標就是讓開發人員更容易、更可靠和更快速的建造軟件。與傳統的 make 工具比較,SCons 具有以下優點:
- 使用 Python 腳本做為配置文件。
- 對于 C,C++,Fortran,內建支持可靠自動依賴分析 。不用像 make 工具那樣需要 執行"make depends"和"make clean"就可以獲得所有的依賴關系。
- 內建支持 C, C++, D, Java, Fortran, Yacc, Lex, Qt,SWIG 以及 Tex/Latex。 用戶還可以根據自己的需要進行擴展以獲得對需要編程語言的支持。
- 支持 make -j 風格的并行建造。相比 make -j, SCons 可以同時運行 N 個工作,而 不用擔心代碼的層次結構。
- 使用 Autoconf 風格查找頭文件,函數庫,函數和類型定義。
- 良好的夸平臺性。SCons 可以運行在 Linux, AIX, BSD, HP/UX, IRIX, Solaris, Windows, Mac OS X 和 OS/2 上。
SCons架構:
SCons的腳本名為SConstruct, 內容看起來是這樣的:
Program('helloscons2', ['helloscons2.c', 'file1.c', 'file2.c'],
LIBS = 'm',
LIBPATH = ['/usr/lib', '/usr/local/lib'],
CCFLAGS = '-DHELLOSCONS')
其中,
- LIBS: 顯示的指明要在鏈接過程中使用的庫,如果有多個庫,應該把它們放在一個列表里面。這個例子里,我們使用一個稱為 m 的庫。
- LIBPATH: 鏈接庫的搜索路徑,多個搜索路徑放在一個列表中。這個例子里,庫的搜索路徑是 /usr/lib 和 /usr/local/lib。
- CCFLAGS: 編譯選項,可以指定需要的任意編譯選項,如果有多個選項,應該放在一個列表中。這個例子里,編譯選項是通過 -D 這個 gcc 的選項定義了一個宏 HELLOSCONS。
編譯命令:
$ scons -Q
gcc -o file1.o -c -DHELLOSCONS file1.c
gcc -o file2.o -c -DHELLOSCONS file2.c
gcc -o helloscons2.o -c -DHELLOSCONS helloscons2.c
gcc -o helloscons2 helloscons2.o file1.o file2.o -L/usr/lib -L/usr/local/lib -lm
Waf
SCons項目小的話還好,規模一大,依賴分析速度急速下降,而且自動配置功能很弱 (跨平臺構建能力不足),Waf嘗試去解決SCons所暴露的問題。Waf也是基于Python的配置、編譯、安裝程序。主要特性:
- 構建順序自動化:輸入輸出文件的構建順序自動化識別。
- 依賴自動分析:根據文件或命令自動進行依賴分析。
- 性能:任務都是并發執行的。
- 靈活性:可以方便地通過添加新的子類創建新的命令或任務,特定構建過程中的瓶頸可以動過方法的動態重載來消除。
- 可擴展性:默認支持多種編程語言和編譯器,有需求新加的也可以通過插件進行支持。
- IDE支持:Eclipse, Visual Studio and Xcode project generators (waflib/extras/)
- 文檔詳細:入門到深入可以閱讀:《Waf Book》
- Python兼容:cPython 2.5 to 3.4, Jython 2.5, IronPython, and Pypy
一個簡單的C++構建腳本wscript,先睹為快:
#! /usr/bin/env python
# encoding: utf-8
# Thomas Nagy, 2006-2010 (ita)
# the following two variables are used by the target "waf dist"
VERSION='0.0.1'
APPNAME='cxx_test'
# these variables are mandatory ('/' are converted automatically)
top = '.'
out = 'build'
def options(opt):
opt.load('compiler_cxx')
def configure(conf):
conf.load('compiler_cxx')
conf.check(header_name='stdio.h', features='cxx cxxprogram', mandatory=False)
def build(bld):
bld.shlib(source='a.cpp', target='mylib', vnum='9.8.7')
bld.shlib(source='a.cpp', target='mylib2', vnum='9.8.7', cnum='9.8')
bld.shlib(source='a.cpp', target='mylib3')
bld.program(source='main.cpp', target='app', use='mylib')
bld.stlib(target='foo', source='b.cpp')
# just a test to check if the .c is compiled as c++ when no c compiler is found
bld.program(features='cxx cxxprogram', source='main.c', target='app2')
if bld.cmd != 'clean':
from waflib import Logs
bld.logger = Logs.make_logger('test.log', 'build') # just to get a clean output
bld.check(header_name='sadlib.h', features='cxx cxxprogram', mandatory=False)
bld.logger = None
Boost.Build(b2)
在編譯Boost庫的時候,會用到b2
命令,其實就是Boost.Build
的縮寫。編譯C++/C代碼時,只需要指定要編譯那些可執行文件或庫,然后列出相關的源碼,Boost.Build幫你搞定其他事情,支持Windows、OSX、Linux和商業的Unix系統。
HelloWorld項目的jamroot.jam腳本(Jamfiles,一種不同于Makefile的構建腳本,有興趣自google):
exe hello : hello.cpp ;
Boost.Build是一個高級編譯系統,它能盡可能容易的管理C++項目集。其思想是在配置文件中指定編譯程序的要素。例如,它不需要告訴Boost.Build如何使用某個編譯器。Boost.Build支持多個編譯程序,并知道如何使用它們。如果你創建一個配置文件,你只需要告訴Boost.Build在何處尋找源文件,調用哪些可執行文件,Boost.Build使用哪個編譯器。然后,Boost.Build將嘗試查找編譯器并自動生成程序。
Boost.Build支持許多不包含任何編譯器特定選項的編譯器的配置文件。配置文件完全是編譯器獨立的。當然,可以設置選項是否應該優化代碼。這些選項都是boost.build語言寫的。一旦選擇編譯器去編譯程序, Boost.Build會將配置文件中的選項翻譯成相應編譯器的命令行選項。這樣就有可能寫一次配置文件,在不同的平臺上用不同的編譯器構建程序。
Boost.Build只支持C++和C項目。它是為在不同平臺上用不同編譯器編譯和安裝Boost C++庫而創造的。
小結
各種構建系統各有優缺點,需要深入研究和使用才能了解。沒有那個是最好的,只有最適合的。一般:
- 一兩個源文件的C++代碼,完全沒必要用構建系統,直接使用編譯器命令直接搞定;
- 自己用的小項目,直接手動寫Makefile即可
- 大型C++項目建議使用CMake,GNU Build System比較年齡大了,規則有些復雜,寫起來沒有CMake那么舒服,跨平臺的話就根本沒戲。
- 偶爾突破一下,想嘗試一下新鮮的構建系統,SCons、Waf、B2等等,等著你玩。
- 某天感覺構建系統也不過如此,閑的無聊你也可以嘗試寫一個,這不500行代碼搞定:http://www.aosabook.org/en/500L/contingent-a-fully-dynamic-build-system.html