Boost.Jam(BJam)是包含在知名的 Boost C++ library 中,類似 make 的軟體建構工具。雖然 BJam 似乎只是拿來編譯 Boost 的附屬工具,但其實它的功能非常強大,同時改善了許多 make 的缺點,相當適合用來建構 C++ 軟體。
BJam 的前身是 FTJam,而 FTJam 又是從 Perforce Jam 發展出來的軟體建構工具。Jam 為了方便控制軟體建構流程,本身就是一個程式語言,除了文字處理外還有條件分支、迴圈等功能。這篇文章的重點在於簡單介紹使用 BJam 建構軟體的其本使用方法,因此不會講得太深入。如果你想進一步了解這套工具,請參考底下的 參考資料。
如果你是 make 的慣用者,可能會質疑 BJam 究竟有何過人之處,畢竟 make 是最流行的軟體建構工具,而且使用上非常簡單。然而 make 雖然行之有年,卻也有不少缺點:
- 在 Makefile 中,相依性的描述和編譯指令是寫在一起的,因此當我們想使用另一款編譯器時,由於下參數的方式不同,我們往往需要修改 Makefile。
- Makefile 無法針對多種不同的編譯選項建構出所有的組合。比如在編譯函式庫時,我們可以選擇要編出靜態或動態函式庫,也可以選擇是否加入除錯資訊以利除錯。若我們希望一次建構出這四種組合,就只能在 Makefile 中辛苦地列出四個 make target,然後使用不同的指令去編譯。然而 BJam 卻可以輕易處理這類需求。1
- make 僅有簡單的條件分支和字串處理功能,對於功能擴充上顯得相當麻煩。
安裝方法
完整的 BJam 有兩部份:一個是讀入 Jamfile(相當於 Makefile)並依照相依性進行編譯連結的主程式 bjam,以及定義各種編譯器如何運作的 boost-build 工具集。兩者都可以在 SourceForge.net 上下載。
安裝 bjam
主程式 bjam 只是一個單一執行檔,你可以直接下載編譯好的版本(Boost 已提供 Windows、OSX、Linux、FreeBSD 四種常見平台的執行檔)。想要自行編譯 bjam 也非常簡單,打開命令列模式,進入原始碼目錄後直接執行build.bat(若你用的是 UN*X 系統,請執行 build.sh),它會自動找出系統上現有的編譯器並編出 bjam 執行檔。
安裝 boost-build
boost-build 是與平台無關的工具定義文件。下載後解開到任何地方都可以,但你需要設定 BOOST_BUILD_PATH 這個環境變數指向你安裝的位置,這樣 bjam 才知道要去哪裡讀取它。
安裝到 C:\Program Files\boost-build 的例子
另外你需要去設定你所使用的編譯器。請編輯 boost-build 底下的 user-config.jam 這個檔案,並拿掉相對應的註解。比如說你想使用 GCC,找到 # using gcc ; 那一行並把井號註解拿掉即可:
using gcc ;
用 Visual C++ 的情況就是把 # using msvc ; 的註解拿掉,只要你安裝這些編譯器時選擇裝在預設的路徑下,BJam 就會自己去找到這些 compiler 來用,這樣夠簡單了吧。
有個需要特別注意的地方,因為 BJam 使用空白來區分符號(包含分號在內),所以不管是 user-config.jam 或是等下我們要寫的 Jamfile,請記得分號前面要留空白,否則會產生語法錯誤,這是剛開始使用 BJam 時常發生的問題。
using gcc; # 錯誤,分號前要留空白
開始使用 BJam
hello world
我們先從最簡單的 hello world 開始吧。假設你的程式非常簡單,只有一個 hello.cpp 原始碼。要使用 BJam 來建構你的程式時,你需要在相同目錄下放兩個檔案:Jamfile 及 Jamroot。Jamfile 扮演的角色就像傳統的 Makefile一樣,是用來描述相依性的,而 Jamroot 則是用來指出目錄樹中的根目錄位置。目錄中檔案的相關位置如下:
hello/
├─ hello.cpp
├─ Jamfile
└─ Jamroot
在這個簡單的例子中, Jamroot 唯一的作用就是指出根目錄的位置,所以我們不需要編輯它,讓它的內容保持空白即可(但這個空白檔案仍有必要存在目錄中)。馬上就來看看我們該如何寫 Jamfile:
exe hello : hello.cpp ; # 冒號和分號都要記得留空白唷
是的,只有一行。而且這行字的意思非常單純: hello 這個執行檔是由 hello.cpp 編譯出來的結果。和 Makefile 不一樣的是你不用去一一寫明編譯連結的指令,那些 BJam 都會幫你處理得好好的。剩下要做的,就是去執行 bjam 把執行檔編出來而已。在同一個目錄下執行 bjam,熱騰騰的 hello.exe 馬上出爐:
D:\tmp\hello>bjam
...found 9 targets...
...updating 5 targets...
MkDir1 bin
MkDir1 bin\gcc-mingw-3.4.5
MkDir1 bin\gcc-mingw-3.4.5\debug
gcc.compile.c++ bin\gcc-mingw-3.4.5\debug\hello.o
gcc.link bin\gcc-mingw-3.4.5\debug\hello.exe
...updated 5 targets...
D:\tmp\hello>
如你所見,BJam 會把編譯好的結果依照使用工具及參數的不同,放到對應的目錄底下。這麼做有個好處:你可以同時使用不同的工具及參數進行編譯,比如說你想分別用 GCC 以及 Visual C++ 來編你的 hello.exe:
bjam toolset=gcc toolset=msvc
你可以用逗號來結合選項。更進一步,因為 BJam 知道 gcc 和 msvc 都是指編譯器的意思,所以你可以省略前面的「 toolset=」。以下兩條指令和上面的指令效果是相同的:
bjam toolset=gcc,msvc
bjam gcc msvc
另外,你可以用 variant=debug 或 variant=release,來選擇你要編譯 debug 版或是 release 版。預設會編譯出 debug 版本,你可以用如下的指令編譯 release 版(相當於打開最佳化並關閉除錯資訊):
bjam variant=release
當然你也可以一次編兩份出來,而「 variant=」也可以省略:
bjam debug release
當然也可以同時選用不同的編譯器:
bjam debug release gcc msvc
上述的指令會分別用 GCC 與 Visual C++ 編出 debug 版與 release 版,共四個組合的執行檔。
BJam 目標宣告格式
我們先稍微解釋一下 BJam 中通用的目標宣告方式:
TYPE TARGET : SOURCE-LIST : REQUIREMENTS : DEFAULT-BUILD : USAGE-REQUIREMENTS
各個欄位代表的意思如下:
-
TYPE
- 目標的種類 2,比如說 exe 表示執行檔, lib 表示函式庫。 TARGET
- 目標的名稱。若沒有特別指定,產生出來的執行檔或函式庫檔名會和這個建構目標的名稱相同。 SOURCE-LIST
- 建構目標所需的原始檔,可以是檔案,也可以是其它目標(函式庫)。 REQUIREMENTS
- 建構該目標的必要選項。 DEFAULT-BUILD
- 建構該目標的預設選項,和 REQUIREMENTS 的不同處在於列於此處的選項可以在命令列中改寫。 USAGE-REQUIREMENTS
- 這邊所列的選項會傳遞給此目標的依賴者。比如說我們建出了一套函式庫並把表頭檔放在特定位置,那麼我們會希望在編譯所有用到這套函式庫的其它目標時,都會把這個特定的表頭檔位置加入編譯選項中。稍後我們會看到更具體的例子。
這邊的 REQUIREMENTS、DEFAULT-BUILD 和 USAGE-REQUIREMENTS 都是控制編譯與連結時要加入的參數,然而它們分別具有不同的意涵。以下我們會慢慢說明。
編譯參數設定
最常見的情況是把編譯選項放在 REQUIREMENTS 欄位中,以下是個例子:
exe hello : hello.cpp : <define>WIN32 <include>"C:/include" ;
如此一來,BJam 在編譯 hello.cpp 時,就會幫你加入 WIN32 這個前置處理器的定義,並且把 C:\include 加入表頭檔的搜尋路徑中。注意到我們會把路徑中的 backslash(\)改成 slash(/),因為就如大多數的程式語言一般,backslash 在 Jamfile 中也是跳脫字元,因此用 slash 來當作目錄分隔字元會比較方便。為了避免混亂,這篇文章中我會統一用 slash(/)來作為路徑的分隔字元。
常用的參數如下:
-
<include>
- 把指定的目錄加入 include path 中。 <warnings>
- 設定編譯器是否顯示警告。比如說 <warnings>off 就是把警告功能關掉。可接受的值有 on(預設的警告層級)、 off(不顯示任何警告)、 all(顯示所有警告)。 <debug-symbols>
- 設定是否加入除錯資訊。可接受的值為 on 或 off。 <define>
- 定義前置處理器的巨集。除了前面的例子,你也可以用 <define>A=B 的方式定義常數。 <optimization>
- 最佳化選項。 off 表示關閉, speed 表示對速度最佳化, space 表示對執行檔大小最佳化。 <cflags>
- 這個選項可以直接指定編譯器的命令列參數。 <cxxflags>
- 同上,但只有在編譯 C++ 原始碼時會生效。 <linkflags>
- 不用多說了吧?這個選項可以指定 linker 的命令列參數。
下面是另一個例子:
exe hack : hack.cpp
: <define>USE_DIRTY_METHOD
<define>MAGIC_NUMBER=0xFF
<warnings>off
<optimization>speed
;
這個例子中定義了 USE_DIRTY_METHOD 以及 MAGIC_NUMBER 這兩個前置處理器符號,並關掉警告、打開最佳化。在 Jamfile 中斷行或 tab 會被視為空白字元,因此你可以把 Jamfile 的內容排版成如上例般適合閱讀的型式。
除了在 Jamfile 中指定編譯選項,你也可以在命令列中指定:
bjam define=USE_DIRTY_METHOD define="MAGIC_NUMBER=0xff" warnings=off
需要注意的是,因為我們把這些選項寫在 REQUIREMENTS 欄位中,因此這些選項會成為編譯時的「必要條件」,即使你在命令列中指定了不同的選項,BJam 還是會忠實地遵守 Jamfile 中的設定。以下面的例子來看:
exe test1 : test.cpp ;
exe test2 : test.cpp : <optimization>speed ;
若你在命令列中指定 optimization=off,你會發現編譯 test1 時不會打開最佳化,但編譯 test2 時仍會打開最佳化。若想把「打開最佳化」當作是預設設定,但可接受使用者在命令列中修改,可以把它放在 DEFAULT-BUILD 欄位中:
exe test : test.cpp : : <optimization>speed ;
注意到 <optimization>speed 選項放在由冒號分隔的第三個欄位中。
以下是另一個例子:
exe hello : main.cpp func1.cpp func2.cpp : <debug-symbols>on : <optimization>speed ;
在這個例子中, <debug-symbol>on 被放在 REQUIREMENTS 內,所以不管如何下指令,編出來的執行檔一定會包含除錯資訊,而 <optimization>speed 被放在 DEFAULT-BUILD 中,所以只是預設打開最佳化,但不強制打開。
bjam release REM 編出 release 版的 hello.exe,但仍會加入除錯資訊
bjam debug REM 編出 debug 版的 hello.exe,不會打開最佳化
建構函式庫
當然,除了執行檔外,BJam 也能幫你建出函式庫,語法和編譯執行檔幾乎是一樣的:
lib my_toolkit : func1.cpp func2.cpp ;
預設情況下會編譯出動態連結函式庫(在 Windows 上會產生 my_toolkit.lib 及 my_toolkit.dll 兩個檔案,UN*X 上則通常叫 libmy_toolkit.so)。當然,你也可以指定連結的方式,比如說想產生靜態函式庫的情況:
lib my_toolkit : func1.cpp func2.cpp : <link>static ;
同樣的,你也可以在命令列指定連結的方式,而分別使用不同的連結方式產生動態及靜態函式庫也易如反掌。以下的指令會分別編出八種版本:使用 GCC 及 VC、debug 及 release、動態連結及靜態連結。
bjam gcc msvc debug release link=shared,static
連結執行檔與函式庫
若你的 hello.exe 用到了 my_toolkit 提供的功能,在編譯時只要把 my_toolkit 加到 hello 的原始檔中即可:
exe hello : hello.cpp my_toolkit ;
lib my_toolkit : func1.cpp func2.cpp ;
通常你的執行檔還會連結到系統上已編譯好的函式庫。這類函式庫也可以用 lib 來宣告:
exe hello : hello.cpp jpeg ;
lib jpeg : : <name>jpeg <search>"C:/Lib" ;
這邊因為 jpeg 是已經編好的函式庫,因此我們不須要指定它的原始檔,只需用 <name> 來指定它的名稱,並用 <search> 來指定函式庫的搜尋路徑。你不需要完整寫出函式庫的檔案名稱(比如說在 Windows 上可能叫 jpeg.lib,而在 UN*X 上可能是 libjpeg.a 或 libjpeg.so),BJam 會自動跟據你的平台及編譯器去處理名稱的問題。
另外,當你在使用某套函式庫時,通常也必須額外指定表頭檔的搜尋路徑:
exe prog1 : prog1.cpp png : <include>"C:/Include/png" ;
exe prog2 : prog2.cpp png : <include>"C:/Include/png" ;
lib png : : <name>png <search>"C:/Lib" ;
所有使用到 png 的執行檔在編譯時,都要加入特定的表頭檔搜尋路徑,但這樣一一指定不但容易出錯,也缺乏彈性。因此我們可以改成這樣:
exe prog1 : prog1.cpp png ;
exe prog2 : prog2.cpp png ;
lib png : : <name>png <search>"C:/Lib" : : <include>"C:/Include/png" ;
注意我們把 <include>"C:/Include/png" 寫在 USAGE-REQUIREMENTS 欄位中,因此所有使用到 png 的目標( prog1 以及 prog2)在編譯時都會自動加入這個選項。
使用多個 Jamfile
當你的程式愈長愈大,往往會將不同的模組放在不同的目錄中,編譯出多個函式庫後,再讓主程式去連結它們。在 Jamfile 中,我們也可以去參照其它 Jamfile 中的目標。
以下是我們的目錄內容:
project/
├─ Jamroot
├─ include/
│ ├─ common.h
│ └─ ...
├─ lib/
│ ├─ Jamfile
│ ├─ func.h
│ ├─ func.cpp
│ └─ ...
└─ src/
├─ Jamfile
├─ main.cpp
└─ ui.cpp
為了達成模組化及程式碼再利用,我們把一些核心功能放在 lib 目錄下並讓它成為一套函式庫,而主程式則放在 src 底下。
這兩個目錄中都會有一份 Jamfile 來說明檔案的相依性。在 lib/Jamfile 中是這麼寫的:
lib func : func.cpp ... # source list
: # requirements
: # default build
: <include>"." # usage requirements
;
因為編譯 src 底下的主程式時,會需要用到 lib 內的表頭檔,所以我們會把 <include>"." 放在 USAGE-REQUIREMENTS 欄位內。BJam 會很聰明地去了解目錄間的開係,所以當你把工作目錄切換到 src 底下並執行 bjam時, <include>"." 會代換成 <include>"../lib"。
而在 src/Jamfile 則是這樣:
exe main : main.cpp ui.cpp ../lib//func ;
注意其中的 ../lib//func,它意指「位於 ../lib 目錄下的 Jamfile 中,名叫 func 的目標」。因此在建構 main 這支主程式之前, bjam 會先把 lib 底下的 func 這套函式庫建構出來,然後讓 main 去連結它。
此外,放在 include 目錄下的表頭檔,是 lib 以及 src 底下的程式碼都可能會用到的。我們可以在最上層的 Jamroot 中加入這項指定:
project : requirements <include>"./include" ;
build-project src//main ;
這麼一來,在編譯 lib 及 src 底下的檔案時,BJam 會自動加入 include 作為表頭檔的路徑。第二行的 build-project 則是指定我們在根目錄下直接執行 bjam 時,預設建構的目標。
bjam 的命令列參數
最後,我們稍微介紹一下命令列下的 bjam 指令格式:
bjam [-option [value]] [target | feature=value ...]
target 就是列在 Jamfile 中的目標名稱,若不指定 target,則 bjam 會建構出 Jamfile 中所有能建出來的目標。當然我們也能用 feature=value 的方式來指定編譯選項,正如我們之前的範例那樣。
除了指明目標及編譯選項,bjam 還有一些其它選項可讓我們控制它的行為:
-
-a
- 不管原始檔是否有更新,都重新建構目標。 -j n
- 同時執行 n 個指令,在多處理器或多核心的電腦上可加速建構。 -n
- 顯示出所有的建構指令,但不實際執行。這個選項可以讓使用者快速了解 BJam 到底做了什麼事。 -o file
- 同上,但把指令輸出到檔案中,這樣就能以批次檔的方式執行建構工作。 -q
- 在編譯出錯時立刻停止。預設情況下, bjam 遇到編譯錯誤時,仍會把其它不相依於該目標的其它工作完成。
結語
除了以上提到的功能外,BJam 也提供了單元測試(unit testing)及檔案安裝等功能,而藉由 Jam 提供的程式功能,讓 BJam 支援其它的程式語言或編譯器也不是太困難的事。更深入的議題請參考 Boost.Build V2 的說明文件。
BJam 的功能非常強大,同時具有跨平台、跨編譯器的特性,很適合作為大型 C++ 專案的建構工具。然而 Jam 本身的語法頗為獨特,形成一道門檻,加上文件明顯不足,使得它難以流行。這份文件說明了 BJam 最基本的功能,希望能讓眾多程式設計師們事半功倍。
參考資料
-
Boost.Build V2
- 這些文件主要介紹 Boost.Build 工具集。除了一般使用方法外,也有如何擴充功能的說明。 Boost.Jam
- 這一頁介紹 bjam 指令,同時也介紹 Jam 這個語言以及 Boost 所加入的新功能。