作者littleshan (我要加入剑道社!)
看板C_and_CPP
标题[心得] BJam 快速上手
时间Wed May 6 15:15:20 2009
很久以前就想写一篇关於 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
1F:推 fuha:不得不推你的用心 05/06 18:45
2F:→ tinlans:其实我还是比较喜欢用 CMake。 05/06 19:20
3F:推 yoco315:赞美主,之前就一直找不到 bjam 的 tutorial QQ 05/06 21:00
4F:推 wangm4a1:很好 推! 05/06 21:42
5F:推 saxontai:推,感谢佛心分享 05/06 22:46
6F:推 sunneo: 太棒了! 05/06 22:54
7F:推 SHBK:很用心 05/06 23:06
8F:推 vizshala:这篇可以m了啦 05/07 15:39
9F:推 zlw:bjam似乎是用..tools\build\v2\tools\里的msvc.jam当作toolset 06/23 17:36
10F:→ zlw:的设定是msvc时的设定档,但里面看起来蛮复杂,想找到可以指定 06/23 17:37
11F:→ zlw:cl.exe 参数的地方,但不确定要放哪,有人知道吗? 06/23 17:38
12F:推 zlw:我悟了,多亏了这篇的指导。才想到要用 bjam -o123.txt 输出 06/23 18:18
13F:→ zlw:打开後终於看到实际的指令 cl /Zm800 -nologo 然後到 msvc.jam 06/23 18:19
14F:→ zlw:搜寻/Zm800只找到一行而已,在那後面加自己想要cl参数即可。 06/23 18:20
15F:推 zlw:多打了一个点,msvc.jam 路径在.tools\build\v2\tools\ 里才对 06/23 18:29
16F:→ zlw:.\tools\build\v2\tools\ 06/23 18:29