[心得] BJam 快速上手
看板C_and_CPP (C/C++)作者littleshan (我要加入劍道社!)時間16年前 (2009/05/06 15:15)推噓10(10推 0噓 6→)留言16則, 9人參與討論串1/2 (看更多)
很久以前就想寫一篇關於 BJam 的教學,這工具雖然冷門,但卻是無庸置疑地強
大。因為我自己也不算是非常了解 BJam 內部的實作,所以這篇文章可能有些謬
誤,也未能完整描述 BJam 的所有功能,若其它版友有更多心得,也歡迎提出討
論。
網頁版: http://electronic-blue.wikidot.com/doc:bjam-quickstart
* * * * * *
前言
Boost.Jam(BJam)是包含在知名的 Boost C++ library 中,類似 make 的軟體
建構工具。雖然 BJam 似乎只是拿來編譯 Boost 的附屬工具,但其實它的功能非
常強大,同時改善了許多 make 的缺點,相當適合用來建構 C++ 軟體。
BJam 的前身是 FTJam,而 FTJam 又是從 Perforce Jam 發展出來的軟體建構工
具。Jam 為了方便控制軟體建構流程,本身就是一個程式語言,除了文字處理外
還有條件分支、迴圈等功能。這篇文章的重點在於簡單介紹使用 BJam 建構軟體
的其本使用方法,因此不會講得太深入。如果你想進一步了解這套工具,請參考
底下的參考資料。
如果你是 make 的慣用者,可能會質疑 BJam 究竟有何過人之處,畢竟 make 是
最流行的軟體建構工具,而且使用上非常簡單。然而 make 雖然行之有年,卻也
有不少缺點:
1. 在 Makefile 中,相依性的描述和編譯指令是寫在一起的,因此當我們想使
用另一款編譯器時,由於下參數的方式不同,我們往往需要修改 Makefile。
2. Makefile 無法針對多種不同的編譯選項建構出所有的組合。比如在編譯函式
庫時,我們可以選擇要編出靜態或動態函式庫,也可以選擇是否加入除錯資
訊以利除錯。若我們希望一次建構出這四種組合,就只能在 Makefile 中辛
苦地列出四個 make target,然後使用不同的指令去編譯。然而 BJam 卻可
以輕易處理這類需求。(註1)
3. make 僅有簡單的條件分支和字串處理功能,對於功能擴充上顯得相當麻煩。
註1:事實上幾乎所有的 IDE 都無法達成這件事,以 VC 來說,你必需新增四個
不同的組態並個別指定其中的編譯選項。
安裝方法
完整的 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 才
知道要去哪裡讀取它。
另外你需要去設定你所使用的編譯器。請編輯 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 都是控制編譯與
連結時要加入的參數,然而它們分別具有不同的意涵。以下我們會慢慢說明。
註2:其實在 Jam 這個語言中,這邊的 TYPE 是一條 rule 的名稱。所謂的 rule
有點類似一般程式語言中的 function,而在 Jam 中的 rule 還會與檔案系
統中的檔案產生關聯,並可指定在更新該目標時應執行哪些指令。
編譯參數設定
最常見的情況是把編譯選項放在 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 表示對執行
檔大小最佳化。
下面是另一個例子:
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 http://www.boost.org/doc/tools/build/index.html
這些文件主要介紹 Boost.Build 工具集。除了一般使用方法外,也有如何擴
充功能的說明。
Boost.Jam http://www.boost.org/doc/tools/jam/index.html
這一頁介紹 bjam 指令,同時也介紹 Jam 這個語言以及 Boost 所加入的新功
能。
--
※ 發信站: 批踢踢實業坊(ptt.cc)
◆ From: 219.87.151.2
推
05/06 18:45, , 1F
05/06 18:45, 1F
→
05/06 19:20, , 2F
05/06 19:20, 2F
推
05/06 21:00, , 3F
05/06 21:00, 3F
推
05/06 21:42, , 4F
05/06 21:42, 4F
推
05/06 22:46, , 5F
05/06 22:46, 5F
推
05/06 22:54, , 6F
05/06 22:54, 6F
推
05/06 23:06, , 7F
05/06 23:06, 7F
推
05/07 15:39, , 8F
05/07 15:39, 8F
推
06/23 17:36, , 9F
06/23 17:36, 9F
→
06/23 17:37, , 10F
06/23 17:37, 10F
→
06/23 17:38, , 11F
06/23 17:38, 11F
推
06/23 18:18, , 12F
06/23 18:18, 12F
→
06/23 18:19, , 13F
06/23 18:19, 13F
→
06/23 18:20, , 14F
06/23 18:20, 14F
推
06/23 18:29, , 15F
06/23 18:29, 15F
→
06/23 18:29, , 16F
06/23 18:29, 16F
討論串 (同標題文章)
C_and_CPP 近期熱門文章
PTT數位生活區 即時熱門文章