【轉】C++構建系統的選擇

C++少說也用了十年了,從簡單的Hello World到200萬行的遊戲項目,編譯和構建的工具也經歷了各種升級。最終的開發環境,選擇了Clang+GDB+CMake。當然不斷改進和升級開發工具的腳步尚未停止,只要能提高開發效率,怎樣折騰都是值得的。

期間經歷了:

  1. 直接調用編譯和鏈接命令
  2. 使用Makefile
  3. 使用CMake
  4. 不斷嘗試其他構建系統,如:b2、WAF、SCons

C++構建系統

對構建系統的要求

由於C/C++本身的特性,如:跨平臺、高性能等、編寫複雜等,對構建系統也是提出了一定的要求:

  • 支持並行編譯:構建系統能否支持並行編譯?對於編譯速度的要求,我給自己定的目標是<10min,超過10min要麼換機器,要麼想辦法優化代碼依賴。上百萬行的代碼,並行編譯時必須的,否則一不小心改一行代碼等個把小時,這樣開發時間白白浪費在編譯上太不值得了。

  • 自動生成依賴:構建系統是否僅僅編譯剛修改過的及其依賴的文件?代碼的依賴關係,要我們自己去手動寫腳本(一般gcc/clang的話,使用gcc -M xx.cpp)?

  • 跨平臺:構建系統能否僅寫一份構建腳本,支持多種平臺?有些項目需要進行交叉編譯,測試環境和運行環境是在不同的平臺環境下。

  • 支持自定義構建目標: 構建系統必須支持擴展,支持自定義Target等。如:protobuf文件可以根據依賴規則自動生成.h、.cpp;自定義一些用於打包或測試的命令(make packmake 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 爲我們廣大程序員提供了編寫構建規則和檢查安裝環境的功能。

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

CMake是一個跨平臺的安裝(編譯)工具,可以用簡單的語句來描述所有平臺的安裝(編譯過程)。他能夠輸出各種各樣的Makefile或者project文件,能檢查編譯器所支持的C++特性,類似UNIX下的automake。CMake 並不直接建構出最終的軟件,而是產生標準的建構腳本(如Unix 的 Makefile 或 Windows Visual C++ 的 projects/workspaces),然後再使用相應的工具進行編譯。

CMak的特點主要有:

  1. 開放源代碼, 使用類 BSD 許可發佈。 http://cmake.org/HTML/Copyright. html
  2. 跨平臺, 並可生成 native 編譯配置文件, 在 Linux/Unix 平臺, 生成 makefile, 在蘋果平臺, 可以生成 xcode, 在 Windows 平臺, 可以生成 MSVC 的工程文件。
  3. 能夠管理大型項目, KDE4 就是最好的證明。
  4. 簡化編譯構建過程和編譯過程。 CMake 的工具鏈非常簡單: cmake+make
  5. 可擴展, 可以爲 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工程文件。

CMake的過程

第二步:編譯。沒啥好說的,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架構

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

參考資料

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