[心得] BJam 快速上手

看板C_and_CPP (C/C++)作者 (我要加入劍道社!)時間16年前 (2009/05/06 15:15), 編輯推噓10(1006)
留言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
其實我還是比較喜歡用 CMake。
05/06 19:20, 2F

05/06 21:00, , 3F
讚美主,之前就一直找不到 bjam 的 tutorial QQ
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
這篇可以m了啦
05/07 15:39, 8F

06/23 17:36, , 9F
bjam似乎是用..tools\build\v2\tools\裡的msvc.jam當作toolset
06/23 17:36, 9F

06/23 17:37, , 10F
的設定是msvc時的設定檔,但裡面看起來蠻複雜,想找到可以指定
06/23 17:37, 10F

06/23 17:38, , 11F
cl.exe 參數的地方,但不確定要放哪,有人知道嗎?
06/23 17:38, 11F

06/23 18:18, , 12F
我悟了,多虧了這篇的指導。才想到要用 bjam -o123.txt 輸出
06/23 18:18, 12F

06/23 18:19, , 13F
打開後終於看到實際的指令 cl /Zm800 -nologo 然後到 msvc.jam
06/23 18:19, 13F

06/23 18:20, , 14F
搜尋/Zm800只找到一行而已,在那後面加自己想要cl參數即可。
06/23 18:20, 14F

06/23 18:29, , 15F
多打了一個點,msvc.jam 路徑在.tools\build\v2\tools\ 裡才對
06/23 18:29, 15F

06/23 18:29, , 16F
.\tools\build\v2\tools\
06/23 18:29, 16F
文章代碼(AID): #1A0JaCMY (C_and_CPP)
討論串 (同標題文章)
文章代碼(AID): #1A0JaCMY (C_and_CPP)