构建发布 Blade - 腾讯开源的构建系统

liuxue.gu@hotmail.com · 2012年08月23日 · 75 次阅读

[i=s] 本帖最后由 scmroad 于 2012-8-23 10:57 编辑

typhoon-blade

Blade is an advanced building system developed with python, majorly for C/C++

Blade 是一个现代构建系统,期望的目标是强大而好用,把程序员从构建的繁琐中解放出来。

Blade 主要定位于 linux 下的大型 C++ 项目,密切配合研发流程,比如单元测试,持续集成,覆盖率统计等。但像 unix 下的文本过滤程序一样,保持相对的独立性,可以单独运行。目前重点支持 i386/x86_64 Linux,未来可以考虑支持其他的类 unix 系统。

在腾讯公司 “台风” 云计算平台开发过程中,为了解决 GNU Make,Autotools 的难用和繁琐的问题,我们开发了这个全新的构建系统,整个系统基于多个声明式的构建脚本,在构建脚本里,只需要声明要构建什么目标,目标的源代码,以及其直接依赖的其他目标,不需要说明如何构建。大大降低了使用难度,提高了开发效率。

首先,Blade 解决了依赖问题。 当你在构建某些目标时,头文件有变化,会自动重新构建。 最方便的是,Blade 也能追踪库文件的依赖关系。比如 库 foo 依赖库 common,那么在库 foo 的 BUILD 文件中列入依赖:[code] cc_library( name = 'foo', srcs = ... deps = ':common' ) [/code] 那么对于使用 foo 的程序,如果没有直接用到 common,那么久只需要列出 foo,并不需要列出 common。[code] cc_binary( name = 'my_app', srcs = ... deps = ':foo' ) [/code] 这样当你的库实现发生变化,增加或者减少库时,并不需要通知库的用户一起改动,Blade 自动维护这层间接的依赖关系。当构建 my_app 时,也会自动检查 foo 和 common 是否也需要更新。

说道易用性,除了依赖关系的自动维护,Blade 还可以做到,只要一行命令,就能把整个目录树的编译连接单元测试就可以全部搞定。例如:

递归构建和测试 common 目录下所有的目标 [code]$ blade test common...[/code] 以 32 位模式构建和测试 [code]$ blade test -m32 common...[/code] 以调试模式构建和测试 [code]$ blade test -pdebug common...[/code] 显然,你可以组合这些标志 [code]$ blade test -m32 -pdebug common...[/code] 特点

自动分析头文件依赖关系,构建受影响的代码。 增量编译和链接,只构建因变更受影响而需要构建的。 自动计算库的间接依赖,库的作者只需要写出直接依赖,构建时自动检查所依赖的库是否需要重新构建。 在任意代码树的任意子目录下都能构建。 支持一次递归构建多个目录下的所有目标,也支持只构建任意的特定的目标。 无论构建什么目标,这些目标所依赖的目标也会被自动连坐更新。 内置 debug/release 两种构建类型。 彩色高亮构建过程中的错误信息。 支持 ccache 支持 distcc 支持基于构建多平台目标 支持构建时选择编译器(不同版本的 gcc,clang 等) 支持编译 protobuf,lex, yacc, swig 支持自定义规则 支持测试,在命令行跑多个测试 支持并行测试(多个测试进程并发运行) 支持增量测试(无需重新运行的测试程序自动跳过) 集成 gperftools,自动检测测试程序的内存泄露 构建脚本 vim 语法高亮 svn 式的子命令命令行接口。 支持 bash 命令行补全 用 Python 编写,无需编译,直接安装使用。

彻底避免以下问题:

[list] 头文件更新,受影响的模块没有重新构建。 被依赖的库需要更新,而构建时没有被更新,比如某子目录依赖遥远的某外部目录的代码,我在这个目录构建,外部目录的代码会被自动检查是否也需要重新构建。 [/list] 致谢 Blade 是受 Google 官方博客发表的这篇文章的思想的启发而开发的: 云构建:构建系统是如何工作的

现阶段 Blade 生成 SCons 脚本进行构建,因此 Blade 的运行还需要依赖 SCons。

Python 是一种简单易用而又强大的语言,我们喜欢 Python。

Google 开放的一些库强大而好用,我们很喜欢,我们把对这些库的支持集成进了 Blade 中,既方便了库的使用,又增强了 Blade,这些库包括 protobuf,gtest, gproftools。

更多文档请参考 Wiki。

欢迎使用以及帮助我们改进 Blade,我们期待你的贡献。

GAE 地址:http://code.google.com/p/typhoon-blade/ GitHub 地址:https://github.com/chen3feng/typhoon-blade

文中提到的《云构建:构建系统是如何工作的》本站有链接 Build in the Cloud - Google [url]http://bbs.scmroad.com/thread-24637-1-1.html/url][

SCons 的介绍,本站也有,在这里 [url]http://bbs.scmroad.com/forum-57-1.html/url][

Blade FAQ 运行环境 为什么 blade 不能在我的平台运行? 描述: 运行 blade , 报 syntax error。

解决过程:

blade 运行需要 python 2.6 以上. 请使用 python -V 查看 python 版本。 装了 python 2.7 还是报错,确认 ptyhon -V 看到的是新版本,必要时配置 PATH 环境变量或者重新登录。 使用 env python, which python 等命令查看 python 命令到底用的是哪个。 vim 编辑 BUILD 文件时没有语法高亮 首先确认是否是以 install 的方式安装的 然后检查 ~/.vim/syntax/blade.vim 是否存在,是否指向正确的文件 然后检查 ~/.vimrc 里是否有 autocmd! BufRead,BufNewFile BUILD set filetype=blade 这条命令。

如果问题还没解决,请联系我们。 为什么 alt 用不了? 描述: alt 用不了

解决过程:

重新执行 install 把 ~/bin 加入到用户 profile,重新登录 构建问题 为什么 deps 里有写的依赖 target 顺序不同,编译结果不同? 描述: //common/config/ini:ini 在某个库的 deps 里放置的顺序不同,放前面没有通过,放到后面通过了。

解决过程:

查看编译错误输出,中间有个库 su.1.0 是 prebuilt 库。 //common/config/ini:ini 放在这个 target 前后编译结果不一样。 经查看,su.1.0 依赖//common/config/ini:ini,但是没有编译进静态库。所以 //common/config/ini:ini 放到它后面时,gcc 按顺序查找能查找到 symbols, 但放在 su.1.0 前就查找不到了,所以输出 undefined reference. 结论:

建议尽量源代码编译项目。 减少 prebuilt 项目,prebuilt 库尽量补全依赖的 target。 ccache 缓存了错误信息,是不是 ccache 出问题了? 描述: 编译提示有错误,在源文件里修改后重新编译还是有错误,是不是 ccache 缓存了告警或者错误信息,没有更新出问题了 ?

解决过程:

查看 ccache manual, ccache 在 direct mode 可能会有 internal error。 告知 XFS 同事如果再次遇到这个问题,立刻修改配置查看是否是 cache 自身问题。 同时查看预处理 cpp 文件后的结果,发现头文件修改没有反映在预处理后的文件里。 应该是包含路径错误,经过查找,build64_release 下存在相同的头文件,而且 build64_release 默认是加到 -I 里, 编译时默认加入 -Ibuild64_realease -I. 在 build64_realease 首先查找头文件, 因此找到这个同名头文件,XFS 同事放了一个文件在这个输出目录里,但是修改的却是 自己的工程文件。 结论:

检查 include path。 为什么不要从 windows 里拉下 svn 代码再放在 linux 开发机编译? 因为代码库里有些文件是符号链接,Windows 上的 SVN 客户端不支持符号链接。 windows 上 checkout 出来的代码,放到 linux 下,符号链接不正确,blade 编译不了。

我只有一个没有源代码的库,如何使用 请参考 [#cc_library] 中,关于 prebuilt 的部分。

prebuilt 库只有.so 文件,我也只需要编译.so 库? 描述: prebuilt 库只有.so 文件,我也只需要编译.so 库

解决过程:

cc_library 如果需要编译为动态库,那么只需要提供动态库。 cc_plugin 需要静态库。 结论:

所以 prebuilt 库最好提供静态库和动态库。 升级到最新 blade。 手头只有无源代码的静态库,但是我们需要编译动态库 描述: 只提供了静态库,但是我们需要编译动态库 ?

解决过程:

.a files are just archives of .o object files, so all you need to do is unpack the archive and repackage them as a shared object (.so)。 ar -x mylib.a gcc -shared .o -o mylib.so

我们提供了脚本自动转, atoso so 不能转为 .a 库。 结论:无源代码时,最好同时得到动态和静态库。

blade 支持环境变量里指定的 gcc 去编译项目吗? 描述: 想使用特定版本的 gcc 编译项目。

解决过程:[code] CC=/usr/bin/gcc CXX=/usr/bin/g++ CPP=/usr/bin/cpp LD=/usr/bin/g++ blade targets [/code] 结论:

升级到最新 blade 且注意环境变量的配置要一致,即使用版本一致的编译器和 linker。 我的代码已经修改了,blade 编译还有问题? 描述: 在 CI 机器上,blade 编译有 error, 修复错误后从新从 svn 拉取,但是还是提示相同的错误。

解决过程:

检查文件是否是修改后的 copy. 该文件由于在 CI 机器上是 root 权限,而该同事登录机器的用户名不是 root, 覆盖不了原来的文件。 提示错误的文件是老文件。 结论:

权限切换时需要注意文件的所属者。 编译出来的 SO 库带有路径信息? 描述: 使用 Blade 编译出来的 so 库带有路径信息,使用起来麻烦,可以配置更改一下吗 ?

在一个大的项目中,不同的子项目,库完全可能重名,如果人工去协调这个问题,显然是划不来的。 因此,Blade 使用库时,总是带有路径信息的,从根本上避免了这个问题。用的时候也带上路径即可。

为什么 Blade 新加的 error flag 不起作用? 描述: 使用更新后的 Blade 编译本地项目发现 error flag 没有起作用 ?

解决过程:

检查 Blade 是否是最新的。 检查 cpp 程序是否把 error flag 过滤了,如果不支持这个 error flag, Blade 不会使用,否则编译报错。 检查后发现 gcc 版本过低。 结论:

升级 gcc。 blade -c 清除不了项目生成的文件 描述: blade -c 清除不了项目生成的文件

解决过程:

请先检查命令是否配对使用 blade -prelease with blade -prelease -c , blade -pdebug with blade -pdebug -c。 结论:

检查命令。 如何显示构建的命令行 我想看到构建过程中中执行的完整命令。 构建时加上 --verbose 参数,就能显示完整的命令行。

我修改了源文件,为何还是失败,报错位置也匹配不上 (或者没有重新编译)? 首先 alt 到 build 目录下,看看是不是把源代码(或者头文件)放在这里了,由于 Blade 分离了源码和构建结果目录,Blade 也会到构建结果目录找源码优先,而且由于 scons 的限制,还会更优先,因此产生这样的错误,目前还没有好的解决办法。 如果是源文件误放到这里,构建时会显示 Compiling build64_release/...,根据这一点能更容易定位这个问题。

如何发布预编译的库? 有些机密的代码,希望以库的方式发布,但同时又依赖了非机密的库(比如 common),如何发布呢? 比如这样的库:[code] cc_library( name = 'secrity', srcs = 'secrity.cpp', deps = [ '//common/base/string:string', '//thirdparty/glog:glog', ] ) [/code] 这样发布: 修改 BUILD 文件,去掉 srcs[code] cc_library( name = 'secrity', prebuilt = True, # srcs 改为这个 deps = [ '//common/base/string:string', '//thirdparty/glog:glog', ] )

[/code] 同时对外的头文件保持不变,按照 cc_library 介绍中,prebuild 要求的方式组织库即可。 尤其需要注意的是,deps 必须保持不变,且不要把虽然被你一来但却不属于你的项目的库作为预编译库发布出去。

unrecognized options 是什么意思? 比如 unrecognized options {'link_all_symbols': 1}。 不同的目标有不同的选项参数,如果传了目标所不支持的参数,就会报告这个错误。可能的原因是误用了其他目标的参数,或者拼写错误,对于后一种情况,BLADE 的 vim 语法高亮功能可以帮你更容易看到错误。

Source file xxx.cc belongs to both xxx and yyy 是什么意思? 比如 Source file cp_test_config.cc belongs to both cc_test xcube/cp/jobcontrol:job_controller_test and cc_test xcube/cp/jobcontrol:job_context_test?

为了避免不必要的重复编译和可能的编译参数不同导致违反 C++ 的一次定义规则,通常每个源文件应该只属于一个目标,如果一个源文件被多个目标使用,应该写成单独的 cc_library,并在 deps 中依赖这个库。

程序故障 需要用到一个 prebuild 库,用 Blade 编译后程序 hang 了,是不是 blade 编译问题 描述: 需要使用到一个 c 库,先用这个库提供的 Makefile 编译生成 prebuild 的库。 然后程序里使用这个 prebuild 库, 但是一旦使用到相关代码,程序运行时就 hang 住, 不能进入 main 函数继续执行。

解决过程:

gdb debug 发现 tcmalloc 用到的一个初始化函数 InitModule 使用了两次,造成 spinlock 被递归持有,造成死锁。 查找为什么会被调用两次,发现 c 标准库里的一个函数符号被这个 prebuild 库的一个函数符号覆盖。 覆盖后的函数有调用 malloc 行为,但是 tcmalloc 还没有被初始化完,所有又初始化一次,造成锁被递归持有, 从而陷入等待状态。 结论:

用户把这个 prebuild 库改为使用 blade 源代码编译方式。

Blade 是什么 软件项目用各种工具来构建代码,最常用的恐怕是 GNU Make。但是 GNU Make 虽然本身功能比较强,但是要直接使用的话,也是比较难的。 很多人还在手工编写 Makefile,又没有去写正确的依赖,导致每次不得不先 make clean 才放心,Make 的意义大打折扣。这在几个文件的小项目下是没什么问题的,对于大的项目就很不方便了。

Autotools 号称 auto,但是还是需要人工写很多东西,运行一系列命令,用起来还是比较复杂,开发人员的学习和使用的门槛很高。

Blade 就是针对这些问题,为腾讯公司基础架构部的 “台风” 云计算平台项目而开发的新一代构建工具,希望能成为开发者手中的 “瑞士军刀”。我们现在把它开源出来,希望能让更多的人得到方便。

Blade 解决的问题 源文件更新导致需要重新构建。这个 gnu make 都能解决得很好。 头文件更新,所以以来这个头文件的源文件都需要重新构建。这个 gnu make 不直接支持,需要搭配 gcc 来生成和更新依赖。 库文件更新,所依赖的库文件更新后,程序应该重新连接,GNU Make 可以做到。 即使我只构建自己的目标,如果库的源代码变了,库应该重新生成,GNU Make 用递归 Make 无法做到。 库文件之间的依赖自动传递,一个库依赖另一个库,库的最终用户不需要关心。 构建过程中的警告和错误应该醒目地显示出来。 能自动支持台风系统大量使用的 proto buffer,以及方便扩充以支持外来可能引入的新工具。 应该能集成自动测试,代码检查等开发常用的功能。 Blade 运行条件 Blade 运行时需要以下条件:

SCons v2.0 or later (required) Python v2.6 or later (required) ccache v3.1 or later (optional) Blade 编译项目时可能需要到:

swig v2.0 or later (required for swig_library) flex v2.5 or later (required for lex_yacc) bison v2.1 or later (required for lex_yacc) 源代码树的组织 项目都要求源代码有明确的根目录,C++ 中的 #include 的路径也需要从这个目录开始写起,这样能有效地避免头文件重名造成的问题。 Blade 并不从某个配置文件或者环境变量读取这个信息,因为开发人员往往需要同时有多个目录树并存。Blade 获取源代码根的方法是,无论当前从哪一级子目录运行,都从当前目录开始向上查找 BLADE_ROOT 文件,有这个文件的目录即为源代码树的根。

目前源代码目录需要自己拉取,将来我们会集成到 Blade 中。BLADE_ROOT 文件也需要用户自己创建。方法:

$ touch BLADE_ROOT 一个源代码树的根目录看起来的样子如下:

BLADE_ROOT common thirdparty xfs xcube torca your_project ... BUILD 文件 Blade 通过一系列的名字为 "BUILD" 的文件(文件名全大写),这些文件需要开发者去编写。每个 BUILD 文件通过一组目标描述函数描述了一个目标的源文件,所依赖的其他目标,以及其他一些属性。

BUILD 文件的示例 构建脚本很简单:

范例:common/base/string/BUILD

cc_library( name = 'string', srcs = [ 'algorithm.cpp', 'string_number.cpp', 'string_piece.cpp', 'format.cpp', 'concat.cpp' ], deps = ['//common/base:int'] ) 也是说明式的,只需要列出目标名,源文件名和依赖名(可以没有)即可。

风格建议 四空格缩进,不要用 tab 字符 总是用单引号 目标名用小写 src 里的文件名按字母顺序排列 deps 里先写本目录内的依赖(:target),后写其他目录内的(//dir:name),分别按字母顺序排列。 不同目标之间空一行,前面可以加注释 注释的 # 后面空一格,比如 # This is a comment 描述目标 Blade 用一组 target 函数来定义目标,这些 target 的通用属性有:

name: 字符串,和路径一起成为 target 的唯一标识,也决定了构建的输出命名 srcs: 列表或字符串,构建该对象需要的源文件,一般在当前目录,或相对于当前目录的子目录中 deps: 列表或字符串,该对象所依赖的其它 targets deps 的允许的格式:

"//path/to/dir/:name" 其他目录下的 target,path 为从 BUILD_ROOT 出发的路径,name 为被依赖的目标名。看见就知道在哪里。 ":name" 当前目录下的 target, path 可以省略。 "#pthread" 系统库。直接写 # 跟名字即可。 cc* 目标 包括 cc_test, cc_binary, cc_library,CC 目标均支持的参数为:

srcs 源文件列表 deps 依赖列表 incs 头文件路径列表 defs 宏定义列表 warning 警告设置 optimize 优化设置

[attach] 1944[/attach]

cc_library 用于描述 C++ 库目标。 cc_library 同时用于构建静态和动态库,默认只构建静态库,只有被 dynamic_link=1 的 cc_binary 依赖时或者命令行指定 --generate-dynamic 才生成动态链接库。

举例:

cc_library( name='lowercase', srcs=['./src/lower/plowercase.cpp'],
deps=['#pthread'], link_all_symbols=False ) link_all_symbols=True 库在被静态链接时,确保库里所有的符号都被链接,以保证依赖全局对象构造函数,比如自动注册器的代码能够正常工作。 需要全部链接的部分最好单独拆分出来做成全部链接的库,而不是整个库全都全部链接,否则会无端增大可执行文件的大小。

相当于原来的 force_link,不过是在用在生成而不是使用库时。需要注意的是,link_all_symbols 是库自身的属性,不是使用库时的属性。

always_optimize True: 不论 debug 版本还是 release 版本总是被优化。 False: debug 版本不作优化。 默认为 False。目前只对 cc_library 有效。

prebuilt=True 主要应用在 thirdparty 中从 rpm 包解来的库,使用这个参数表示不从源码构建。对应的二进制文件必须存在 lib{32,64}{release,debug} 这样的子目录中。不区分 debug/release 时可以只有两个实际的目录。

在老的 BUILD 文件中你可能看到 pre_build 这个词,这是 prebuilt 的错误拼写,已经修正为现在的名字,并淘汰了旧的名字。

cc_binary 定义 C++ 可执行文件目标

cc_binary( name='prstr', srcs=['./src/mystr_main/mystring.cpp'], deps=['#pthread',':lowercase',':uppercase','#dl'], ) dynamic_link=True 目前我们的 binary 默认为全静态编译以适应云计算平台使用。 如果有应用需要动态编译方式,可以使用此参数指定,此时被此 target 依赖的所有库都会自动生成对应的动态库供链接。 需要注意的是,dynamic_link 只适用于可执行文件,不适用于库。

export_dynamic=True Pass the flag ‘-export-dynamic’ to the ELF linker, on targets that support it. This instructs the linker to add all symbols, not only used ones, to the dynamic symbol table. This option is needed for some uses of dlopen or to allow obtaining backtraces from within a program.

cc_test 相当于 cc_binary,再加上自动链接 gtest 和 gtest_main 还支持 testdata 参数, 列表或字符串,文件会被链接到输出所在目录 name.runfiles 子目录下,比如:testdata/a.txt =>name.runfiles/testdata/a.txt 用 blade -runtests(简写-t) 命令,会在成功构建后到 name.runfiles 目录下自动运行,并输出总结信息。

testdata= 在 name.runfiles 里建立 symbolic link 指向工程目录的文件,目前支持 以下几种形式

'file' 在测试程序中使用这个名字本身的形式来访问 '//your_proj/path/file' 在测试程序中用"your_proj/path/file"来访问。 ('//your_proj/path/file', "new_name") 在测试程序中用"new_name"来访问 可以根据需要自行选择,这些路径都也可以是目录。

cc_test( name = 'textfile_test', srcs = 'textfile_test.cpp', deps = ':io', testdata = [ 'test_dos.txt', '//your_proj/path/file', ('//your_proj/path/file', 'new_name') ] ) export_dynamic=True Pass the flag ‘-export-dynamic’ to the ELF linker, on targets that support it. This instructs the linker to add all symbols, not only used ones, to the dynamic symbol table. This option is needed for some uses of dlopen or to allow obtaining backtraces from within a program.

proto_library 用于定义 protobuf 目标 deps 为 import 所涉及的其他 proto_library 自动依赖 protobuf,使用者不需要再显式指定。 构建时自动调用 protoc 生成 cc 和 h,并且编译成对应的 cc_library

proto_library( name = 'rpc_meta_info_proto', srcs = 'rpc_meta_info.proto', deps = ':rpc_option_proto', ) Blade 支持 proto_library,使得在项目中使用 protobuf 十分方便。

要引用某 proto 文件生成的头文件,需要从 BLADE_ROOT 的目录开始,只是把 proto 扩展名改为 pb.h 扩展名。 比如 //common/base/string_test.proto 生成的头文件,路径为 "common/base/string_test.pb.h"。

lex_yacc_library srcs 必须为二元列表,后缀分别为 ll 和 yy 构建时自动调用 flex 和 bison, 并且编译成对应的 cc_library

lex_yacc_library( name = 'parser', srcs = [ 'line_parser.ll', 'line_parser.yy' ], deps = [ ":xcubetools", ], recursive=1 ) recursive=True 生成可重入的 C scanner. flex has the ability to generate a reentrant C scanner. This is accomplished by specifying %option reentrant (`-R'). 例如:

可以同时扫描两个文件并且在 token level 比较而不是单个字符比较

/* Example of maintaining more than one active scanner. */

do { int tok1, tok2;

tok1 = yylex( scanner_1 ); tok2 = yylex( scanner_2 );

if( tok1 != tok2 ) printf("Files are different.");

} while ( tok1 && tok2 ); gen_rule 用于定制自己的目标 outs = ,表示输出的文件列表,需要填写这个域 gen_rule 才会被执行 cmd, 字符串,表示被调用的命令行 cmd 中可含有如下变量,运行时会被替换成 srcs 和 outs 中的对应值 $SRCS $OUTS $FIRST_SRC $FIRST_OUT $BUILD_DIR -- 可被替换为 build[64,32][release,debug] 输出目录

gen_rule( name='test_gen_target', cmd='echo what_a_nice_day;touch test2.c', deps=[':test_gen'], # 可以有 deps , 也可以被别的 target 依赖 outs=['test2.c'] ) 很多用户使用 gen_rule 动态生成代码文件然后和某个 cc_library 或者 cc_binary 一起编译, 需要注意应该尽量在输出目录生成代码文件,如 build64_debug 下,并且文件的路径名要写对, 如 outs = ['websearch2/project_example/module_1/file_2.cc'], 这样使用 gen_rule 生成的文件和库一起编译时就不会发生找不到动态生成的代码文件问题了。

swig_library 根据.i 文件生成相应的 python, java 和 php cxx 模块代码,并且生成对应语言的代码。

swig_library( name = 'poppy_client', srcs = [ 'poppy_client.i' ], deps = [ ':poppy_swig_wrap' ], warning='yes', java_package='com.soso.poppy.swig', # 生成的 java 文件的所在 package 名称 java_lib_packed=1, # 表示把生成的 libpoppy_client_java.so 打包到依赖者的 jar 包里,如 java_jar 依赖这个 swig_library optimize=['O3'] # 编译优化选项 ) warning 这里的 warning 仅仅指 swig 编译参数 cpperraswarn 是否被指定了,swig_library 默认使用非标准编译告警级别(没有那么严格)。

cc_plugin 支持生成 target 所依赖的库都是静态库.a 的 so 库,即 plugin。

cc_plugin( name='mystring', srcs=['./src/mystr/mystring.cpp'], deps=['#pthread',':lowercase',':uppercase','#dl'], warning='no', defs=['_MT'], optimize=['O3'] ) cc_plugin 是为 JNI,python 扩展等需要动态库的场合设计的,不应该用于其他目的。

resource_library 编译静态资源。

大家都遇到过部署一个可执行程序,还要附带一堆辅助文件才能运行起来的情况吧? blade 通过 resource_library,支持把程序运行所需要的数据文件也打包到可执行文件里, 比如 poppy 下的 BUILD 文件里用的静态资源:

resource_library( name = 'static_resource', srcs = [ 'static/favicon.ico', 'static/forms.html', 'static/forms.js', 'static/jquery-1.4.2.min.js', 'static/jquery.json-2.2.min.js', 'static/methods.html', 'static/poppy.html' ] ) 生成 和 libstatic_resource.a 或者 libstatic_resource.so。 就像一样 protobuf 那样,编译后后生成一个库 libstatic_resource.a,和一个相应的头文件 static_resource.h,带路径包含进来即可使用。

在程序中需要包含 static_resource.h(带上相对于 BLADE_ROOT 的路径)和"common/base/static_resource.hpp", 用 STATIC_RESOURCE 宏来引用数据:

StringPiece data = STATIC_RESOURCE(poppy_static_favicon_ico); STATIC_RESOURCE 的参数是从 BLADE_ROOT 目录开始的数据文件的文件名,把所有非字母数字和下划线的字符都替换为_。

得到的 data 在程序运行期间一直存在,只可读取,不可写入。

用 static resource 在某些情况下也有一点不方便:就是不能在运行期间更新,因此是否使用,需要根据具体场景自己权衡。

java_jar 编译 java 源代码。

java_jar( name = 'poppy_java_client', srcs = [ 'src/com/soso/poppy' # 这里只需要指定 java 文件所在目录,不要写上具体 java 文件列表 ], deps = [ '//poppy:rpc_meta_info_proto', # 可以依赖 proto_library 生成的 java 文件一起编译打包 '//poppy:rpc_option_proto', '//poppy:rpc_message_proto', '//poppy:poppy_client', # 可以依赖 swig_library 生成的 java 文件一起编译打包 './lib:protobuf-java', # 可以依赖别的 jar 包 './lib:junit', ] ) prebuilt=True 主要应用在已经编译打包好的 java jar 包。

Blade 的输出 构建过程是彩色高亮的 出错信息是彩色的,方便定位错误。

默认生成 native arch 的可执行文件,指定生成 32/64 位结果也很简单,加上 -m32/64 即可。 默认生成 release 版本的结果,如果生成 debug 版的,加上 -p debug 即可。 默认构建当前目录,如果当前目录依赖的外面的模块需要重新构建,也会被连带构建起来(Make 很难做到)。如果要从当前目录构建所有子目录的目标,也很简单:blade ... 即可。

不同构建选项的结果放在不同的目录下,生成的文件一律按层次也放在这个目录里,不会污染源代码目录。

要清除构建结果(一般不需要),blade clean 即可。

Blade Cache blade 支持 cache,可以大幅度加快构建速度。 blade 支持两种 cache

ccache , cache 配置使用 ccache 的配置, 如通过配置 CCACHE_DIR 环境变量指定 ccache 目录。 ccache 没有安装,则使用 scons cache, 配置细节如下 scons cache 需要一个目录,依次按以下顺序检测:

命令行参数--cache-dir 环境变量 BLADE_CACHE_DIR 如果均未配置,则不启用 cache。 空的 BLADE_CACHE_DIR 变量或者不带参数值的--cache-dir=, 则会禁止 cache。 --cache-size 如不指定,则默认为 2G,如指定,则使用用户指定的以 Gigabyte 为单位的大小的 cache。 如 --cache-dir='~/user_cache' --cache-size=16 (16 G) 大小 cache。 用户可以根据需要配置大小,超出大小 blade 会执行清理工作,限制 cache 大小在用户指定的 cache 大小, 请谨慎设置这个大小,因为涉及到构建速度和机器磁盘空间的占用。

测试支持 Blade test 支持增量测试 ,可以加快 tests 的执行。 已经 Pass 的 tests 在下一次构建和测试时不需要再跑,除非:

tests 的任何依赖变化导致其重新生成。 tests 依赖的测试数据改变,这种依赖为显式依赖,用户需要使用 BUILD 文件指定,如 testdata。 tests 所在环境变量发生改变。 test arguments 改变。 Fail 的 test cases ,每次都重跑。 如果需要使用全量测试,使用--full-test option, 如 blade test common/... --full-test , 全部测试都需要跑。 另外,cc_test 支持了 always_run 属性,用于在增量测试时,不管上次的执行结果,每次总是要跑。 cc_test( name = 'zookeeper_test', srcs = 'zookeeper_test.cc', always_run = True ) Blade test 支持并行测试,并行测试把这一次构建后需要跑的 test cases 并发地 run。 blade test targets --test-jobs N -t, --test-jobs N 设置并发测试的并发数,Blade 会让 N 个测试进程并行执行 对于某些因为可能相互干扰而不能并行跑的测试,可以加上 exclusive 属性

cc_test( name = 'zookeeper_test', srcs = 'zookeeper_test.cc', exclusive = True ) 命令行参考 blade [action] [options] [targets]

action 是一个动作,目前有

build 表示构建项目 test 表示构建并且跑单元测试 clean 表示清除目标的构建结果 query 查询目标的依赖项与被依赖项 run 构建并 run 一个单一目标 targets 是一个列表,支持的格式:

path:name 表示 path 中的某个 target path 表示 path 中所有 targets path/... 表示 path 中所有 targets,并递归包括所有子目录 :name 表示当前目录下的某个 target 默认表示当前目录 参数列表:

-m32,-m64 指定构建目标位数,默认为自动检测 -p PROFILE 指定 debug/release,默认 release -k, --keep-going 构建过程中遇到错误继续执行(如果是致命错误不能继续) -j N,--jobs=N N 路并行编译,多 CPU 机器上适用 -t N,--test-jobs=N N 路并行测试,多 CPU 机器上适用 --cache-dir=DIR 指定一个 cache 目录 --cache-size=SZ 指定 cache 大小,以 G 为单位 --verbose 完整输出所运行的每条命令行 –h, --help 显示帮助 --color=yes/no/auto 是否开启彩色 --generate-dynamic 强制生成动态库 --generate-java 为 proto_library 和 swig_library 生成 java 文件 --generate-php 为 proto_library 和 swig_library 生成 php 文件 --gprof 支持 GNU gprof --gcov 支持 GNU gcov 做覆盖率测试 配置 Blade 支持三个配置文件

blade.zip 同一个目录下的 blade.conf,这是全局配置。 ~/.bladerc 用户 HOME 目录下的 .bladerc 文件,这是用户级的配置。 BLADE_ROOT 其实是个配置文件,写在这里的是项目级配置。 cc_config 所有 c/c++ 目标的公共配置

cc_config( extra_incs = ['thirdparty'] # 额外的 -I,比如 thirdparty ) cc_test_config 构建和运行测试所需的配置

cc_test_config( dynamic_link=True, # 测试程序是否默认动态链接,可以减少磁盘开销,默认为 False heap_check='strict', # 开启 gperftools 的 HEAPCHECK,具体取值请参考 gperftools 的文档 gperftools_libs='thirdparty/perftools:tcmalloc', # tcmclloc 库,blade deps 格式 gperftools_debug_lib='thirdparty/perftools:tcmalloc_debug', # tcmalloc_debug 库,blade deps 格式 gtest_lib='thirdparty/gtest:gtest', # gtest 的库,blade deps 格式 gtest_main_lib='thirdparty/gtest:gtest_main' # gtest_main 的库路径,blade deps 格式 ) proto_library_config 编译 protobuf 需要的配置

proto_library_config( protoc='protoc', # protoc 编译器的路径 protobuf_libs='thirdparty/protobuf:protobuf', # protobuf 库的路径,Blade deps 格式 protobuf_path='thirdparty', # import 时的 proto 搜索路径,相对于 BLADE_ROOT protobuf_include_path = 'thirdparty', # 编译 pb.cc 时额外的 -I 路径 ) 所有这些配置项都有默认值,如果不需要覆盖就无需列入相应的参数。默认值都是假设安装到系统目录下,如果你的项目中把这些库放进进了自己的代码中(比如我们内部),请修改相应的配置。

环境变量 Blade 还支持以下环境变量:

TOOLCHAIN_DIR,默认为空 CPP,默认为 cpp CXX,默认为 c++ CC,默认为 gcc LD,默认为 c++ TOOLCHAIN_DIR 和 CPP 等组合起来,构成调用工具的完整路径,例如: 调用/usr/bin 下的 gcc(开发机上的原版 gcc) TOOLCHAIN_DIR=/usr/bin blade 使用 clang

CPP='clang -E' CC=clang CXX=clang++ ld=clang++ blade 如同所有的环境变量设置规则,放在命令行前的环境变量,只对这一次调用起作用,如果要后续起作用,用 export,要持久生效,放入 ~/.profile 中。

环境变量的支持将来考虑淘汰,改为配置编译器版本的方式,因此建议暂时不要使用。

辅助命令 install blade 命令的符号链接会被安装下面的命令到~/bin 下。

lsrc 列出当前目录下指定的源文件,以 blade 的 srcs 列表格式输出。

genlibbuild 自动生成以目录名为库名的 cc_library,以测试文件的名为名的 cc_test,proto 的 BUILD 文件,并假设这些测试都依赖这个库

vim 集成 我们编写了 vim 的 blade 语法文件,高亮显示 blade 关键字,install 后就会自动生效。

我们编写了 Blade 命令,使得可以在 vim 中直接执行 blade,并快速跳转到出错行(得益于 vim 的 quickfix 特性)。

function! Blade(...) let l:old_makeprg = &makeprg setlocal makeprg=blade execute "make " . join(a:000) let &makeprg=old_makeprg endfunction

command! -complete=dir -nargs=* Blade call Blade('') 使用时直接在 vim 的 : 模式输入(可带参数):Blade 即可构建。

这个命令的源代码在 common/tools/.vimrc 中。

alt 在源代码目录和构建目标目录之间跳转

安装 执行 install 脚本即可安装到~/bin 下,目前因还在开发阶段,变化还比较快,以软链方式安装,install 后不能删除 checkout 出来的原始目录。 目前 blade 生成 scons 脚本,因此还需要安装 scons 2.0 以上版本。 Blade 需要至少 Python 2.6,我们建议使用 python 2.7.x (x 越高越好),但不用 python3

install 使得可以在任何目录下直接执行

$ blade 命令。 如果不行,确保~/bin 在你的 PATH 环境变量里,否则修改 ~/.profile,加入 export PATH=~/bin:$PATH 重新登录即可。 我们的理念:解放程序员,提高生产力。用工具来解决非创造性的技术问题。

有谁在使用 blade 支持项目开发的吗?

目前是不是仅仅支持 linux 平台,对 Unix 的 FreeBSD 支持吗?

浅谈 blade 中 C++Build 的设计与实现

blade 的背景我也就不详细介绍了,总体来说, 它是腾讯开源的一个构建工具,基于 scons 而非 make,采用 python 编写, 目前的版本定位于 linux 下的 C++ 程序。 它的设计思路来源于 google 的官方博客上的一篇文章, Build in the Cloud: How the Build System works, 好吧,刚刚发现上面的网址挂了~~,大家将就一下,看这里吧, 有兴趣的同学可以去看一下。事实上这是一个系列, 里面讲述了 google 对大规模协作开发的理解,推荐大家看看。 blade 是一个基于 scons 的构建系统,它做的主要工作是分析代码之间的依赖, 然后生成相应的 scons 的配置文件 SConstruct,由于这一系列生成的中间文件的路径, 都是有 blade 根据规则自动创建,可以保证每个文件只会被编译一次, 而且中间结果可以被复用,这样就大大提高了大规模构建时的效率。 这篇文章仅仅对 blade 中的 build 过程进行分析,不会对 run 以及 test 进行描述, 事实上 blade 对 test 的支持相当的好,也许有时间研究了我会再写一篇文章来描述, 这是后话,暂且不提。 为了读懂 blade,势必要了解什么是 scons,scons 是一个用 python 写的构建工具, 这里是它的官网,这篇文章的重点不在 scons 上面, 所以不会对它的好处和特点进行详细的说明,大家可以参考这里。 scons 是通过读取项目根目录下的 SConstruct 文件来开始执行构建的,事实上, SConstruct 就是一个 python 脚本,scons 为构建提供了一系列函数, 开发者在写 SConstruct 时,其实就是在写一个 python 的脚本。 这里介绍一下 scons 的简单用法: scons : 执行 SConstruct 中的脚本 scons -c : 相当于 make clean,会根据默认规则和 SConstruct 定义的规则做 Clean scons -Q : 只显示编译信息,去除多余的打印信息 scons 为它的配置脚本提供了一系列的函数用来构建,这里列举一些常用的: Program : 生成可执行文件,如 Program('foo','foo.cc') 将会生成一个名为 foo 的可执行文件 Object : 故名思议,就是用来生成目标文件的函数,如 Object('foo.cc'), 在 linux 下将会生成 foo.o Library : 生成静态库和动态库文件, 其中 SharedLibrary 将会仅仅生生成.so,而 StaticLibrary 将会生成.a SourceSignatures : 判断源文件是否更改,可以通过时间戳或者 MD5 或者二者来判断 TargetSignatures : 判断目标文件是否更改,可以根据编译结果或者是内容来判断 Ignore : 用于忽略某个依赖关系,如 Ignore(foo, 'foo.h'), 则当 foo.h 改变时,不会重新编译 foo Depends : 用于显示的指定某个依赖关系,如 Depends(foo, joke), 会将 joke 作为 foo 的依赖,joke 的变更将会导致 foo 的重新编译 Command : 用于执行命令行命令,这个命令比较复杂,这里不详细展开, 给出官方的 UserGuide Enviroment : 用于设定环境变量 Builder : 用于构建自己的 Builder,如 foobld = Builder(action = 'foobuild <$SOURCE> <$TARGET>'), 这个用法也相对复杂,这里不详细展开, 可以参考官方的 UserGuide 以上用法的详细信息都可以在 scons 的官方文档上找到。 介绍完了 scons,我们可以正式开始研究 blade 了。 首先我们来看一下 blade 的 BUILD 文件,在 BUILD 文件中,常用的函数有 cc_library, cc_test,cc_binary,proto_library 等等,这些函数,做了两件事情, 首先创建一个 target,然后将这个 target 注册给 blade。 这里就不得不解释一下什么是 target,在 blade 中,定义了一个抽象的父类叫 Target, 它用于描述一个抽象的 scons 的 target,子类如 CcLibrary,CcBinary 都会继承于它, 它定义了 get_rules 和 scons_rules 两个公共的抽象方法, 用于获取自身相关的 scons rule。target 中定义了一系列属性,用于描述该 target, 我们主要需要关心的有下面这些: name:target 的 name,就是在 BUILD 文件中定义的 name 属性,用来唯一标识一个 target path:这个属性通过 blade 来产生,用来标志 target 的路径 srcs:这个属性通过 BUILD 文件中的 srcs 来定义,存储了 target 相应的源码文件, 也有可能是一个目录 deps:这个属性通过 BUILD 文件中得 deps 来定义,存储了 target 的依赖 expand_deps:这个属性通过 blade 生成,用来表示 target 中 deps 的 deps data:这个属性用来存储各种额外的信息,比如 warning,incs 等等 上面曾经说到,BUILD 文件中你调用的函数,将会把 target 注册给 blade, 于是 blade 就可以调用该 target 的 scons_rules 来生成 SConstruct 中的相应部分。

http://tsgsz.github.io/blog/2013/11/01/thinking-in-design-of-blade-cpp-build/

下面我以一个来举一个例子,来描述 blade 的 build 的流程。 首先我们创建一个创建了 Fool 和 Me 的代码如下: Fool (fool.h)[code] // Copyright 2013, Kingslanding Inc. // Author: Pine cdtsgsz@gmail.com // // Description: a simple test code #ifndef TEST_FOOL_H_ #define TEST_FOOL_H_

namespace kingslanding { namespace test { class Fool { public: void Say(); }; } // namespace test } // namespace kingslanding

#endif // TEST_FOOL_H_[/code] Fool (fool.cc)[code] // Copyright 2013, Kingslanding Inc. // Author: Pine cdtsgsz@gmail.com // // Description: fool implement file

#include "test/fool.h"

#include

namespace kingslanding { namespace test { void Fool::Say() { printf("haha, I'm a fool!!!"); } } // namespace test } // namespace kingslanding [/code] Me (me.h)[code] // Copyright 2013, Kingslanding Inc. // Author: Pine cdtsgsz@gmail.com // // Description: simple test code

#ifndef TEST_ME_H_ #define TEST_ME_H_

#include "test/fool.h"

namespace kingslanding { namespace test { class Me { public: void IntroduceSelf(); private: Fool m_fool; }; } // namespace test } // namespace kingslanding #endif // TEST_ME_H_ [/code] Me (me.cc)[code] // Copyright 2013, Kingslanding Inc. // Author: Pine cdtsgsz@gmail.com // // Description: implement me

#include "test/me.h"

namespace kingslanding { namespace test { void Me::IntroduceSelf() { m_fool.Say(); } } // namespace test } // namespace kingslanding[/code] 接着我们建立 BUILD 文件如下:

通过 blade build 命令, blade 将会在 blade-bin/test 目录下 build 出一个 libfool.a 和 libme.a。 当在命令行输入了 blade build 命令,blade 做了什么呢? 我们可以从 blade_main.py 开始看起: 首先自然是要需要解析出 build 这个动作,将它和相应的函数映射起来, 可以看到 blade_main 中调用了 CmdArguments() 这个方法, 这个方法由 command_args 模块提供,这个方法将会从命令行中 parse 出 command 和 target、 options,返回的是 blade 中真正的处理函数名。 接着,blade 将找到 BLADE_ROOT 所在的目录,将当前目录切换到 BLADE_ROOT 所在的目录, 然后通过 configparse 这个模块,会解析出 blade 的 config。 blade 不支持多进程编译,也就是说, 一台机器上只允许有一个 blade 在同一个 BLADE_ROOT 下工作, blade 采用了文件锁确保了这个机制。 之后,blade_main 初始化了一个 blade 的实例,调用它的 blade.generate() 的方法, 在这一步骤中,blade 的源代码中给这一句的注释是 # Build the targets, 我个人觉得这个注释不太对,事实上 blade 本身不会真的去 build 一些 target, 它只是去生成一个 scons 的 SConstruct,而真正的 build 是在_build 方法中, 在这个方法中,blade 会开启一个新的进程,在这个进程中打开 scons, 这时候,scons 才会真正的去 build 源代码。 最后,blade_main 会删除生成的 SConstruct。

那么接下来,我们就进入 blade 的 generate 方法,来看一看它干了什么? blade 的 generate 方法如下: blade generate[code] def generate(self): """Generate the build script. """ self.load_targets() self.analyze_targets() self.generate_build_rules()[/code] 可以看到,blade 首先会加载所有的目标,这里就包括了去重,依赖展开等等, 然后,blade 会去分析目标,得到一个层层依赖的顺序, 最后生成相应的 build rule,写入 SConstruct 中。 到现在为止,关键路径已经出来了,我们来逐层深入, 在 blade 的 load_target() 函数中做了一大堆的判断,我们暂时不需要关心, 最后它调用了 load_build_files 模块的 load_targets() 函数,我们来看看它的实现: load_build_files load_targets[code] def load_targets(target_ids, working_dir, blade_root_dir, blade): """load_targets.

Parse and load targets, including those specified in command line and their direct and indirect dependencies, by loading related BUILD files. Returns a map which contains all these targets.

""" target_database = blade.get_target_database()

# targets specified in command line cited_targets = set() # cited_targets and all its dependencies related_targets = {} # source dirs mentioned in command line source_dirs = [] # to prevent duplicated loading of BUILD files processed_source_dirs = set()

direct_targets = [] all_command_targets = [] # Parse command line target_ids. For those in the form of :, # record (,) in cited_targets; for the rest (with # but without ), record into paths. for target_id in target_ids: if target_id.find(':') == -1: source_dir, target_name = target_id, '*' else: source_dir, target_name = target_id.rsplit(':', 1)

source_dir = relative_path(os.path.join(working_dir, source_dir), blade_root_dir)

if target_name != '*' and target_name != '': cited_targets.add((source_dir, target_name)) elif source_dir.endswith('...'): source_dir = source_dir[:-3] if not source_dir: source_dir = './' source_dirs.append((source_dir, WARN_IF_FAIL)) for root, dirs, files in os.walk(source_dir): # Skip over subdirs starting with '.', e.g., .svn. dirs[:] = [d for d in dirs if not d.startswith('.')] for d in dirs: source_dirs.append((os.path.join(root, d), IGNORE_IF_FAIL)) else: source_dirs.append((source_dir, ABORT_IF_FAIL))

direct_targets = list(cited_targets)

# Load BUILD files in paths, and add all loaded targets into # cited_targets. Together with above step, we can ensure that all # targets mentioned in the command line are now in cited_targets. for source_dir, action_if_fail in source_dirs: _load_build_file(source_dir, action_if_fail, processed_source_dirs, blade)

for key in target_database: cited_targets.add(key) all_command_targets = list(cited_targets)

# Starting from targets specified in command line, breath-first # propagate to load BUILD files containing directly and indirectly # dependent targets. All these targets form related_targets, # which is a subset of target_databased created by loading BUILD files. while cited_targets: source_dir, target_name = cited_targets.pop() target_id = (source_dir, target_name) if target_id in related_targets: continue

_load_build_file(source_dir, ABORT_IF_FAIL, processed_source_dirs, blade)

if target_id not in target_database: console.error_exit('%s: target //%s:%s does not exists' % ( _find_depender(target_id, blade), source_dir, target_name))

related_targets[target_id] = target_database[target_id] for key in related_targets[target_id].expanded_deps: if key not in related_targets: cited_targets.add(key)

# Iterating to get svn root dirs for path, name in related_targets: root_dir = path.split('/')[0].strip() if root_dir not in blade.svn_root_dirs and '#' not in root_dir: blade.svn_root_dirs.append(root_dir) [/code] return direct_targets, all_command_targets, related_targets 这个函数很长,里面的变量名也晦涩难懂,我贴出来也没指望有人有人会把它真正的读完, 但是,不需要完全的去分析每个细节,我们基本上可以看出这个函数做了两件事情: 找到所有的 target load 相应的 BUILD file 事实上仔细看这个函数的实现, 它无非是将 target 和它 deps 中所有 target 所在的路径解析出来, 然后调用_load_build_file 这个函数,去加载相应的配置文件, 这个递归过程在这里采用了循环的方式,使得代码读起来有点不容易, 不过循环的方式可以减少栈的深度,有助于效率的提高。 现在一切矛头都指向了_load_build_file 这个函数,我们来看看它的实现:

load_build_files _load_build_file[code] def _load_build_file(source_dir, action_if_fail, processed_source_dirs, blade): """_load_build_file to load the BUILD and place the targets into database.

Invoked by _load_targets. Load and execute the BUILD file, which is a Python script, in source_dir. Statements in BUILD depends on global variable current_source_dir, and will register build target/rules into global variables target_database. If path/BUILD does NOT exsit, take action corresponding to action_if_fail. The parameters processed_source_dirs refers to a set defined in the caller and used to avoid duplicated execution of BUILD files.

"""

# Initialize the build_target at first time, to be used for BUILD file # loaded by execfile global build_target if build_target is None: build_target = TargetAttributes(blade.get_options()) build_rules.register_variable('build_target', build_target)

source_dir = os.path.normpath(source_dir) # TODO(yiwang): the character '#' is a magic value. if source_dir in processed_source_dirs or source_dir == '#': return processed_source_dirs.add(source_dir)

if not os.path.exists(source_dir): _report_not_exist(source_dir, source_dir, blade)

old_current_source_path = blade.get_current_source_path() blade.set_current_source_path(source_dir) build_file = os.path.join(source_dir, 'BUILD') if os.path.exists(build_file): try: # The magic here is that a BUILD file is a Python script, # which can be loaded and executed by execfile(). execfile(build_file, build_rules.get_all(), None) except SystemExit: console.error_exit('%s: fatal error, exit...' % build_file) except: console.error_exit('Parse error in %s, exit...\n%s' % ( build_file, traceback.format_exc())) else: if action_if_fail == ABORT_IF_FAIL: _report_not_exist(source_dir, build_file, blade)

blade.set_current_source_path(old_current_source_path)[/code] 又是一大段的代码,对路径的各种操作,但是还好这些都是细节,如果不去实现, 不需要太过关心,事实上这一段内容的核心是一句代码两行注释:

# The magic here is that a BUILD file is a Python script, # which can be loaded and executed by execfile(). execfile(build_file, build_rules.get_all(), None) 到这里,是不是有种茅塞顿开的感觉? 在这篇文章的早些时候我曾经花过一下篇幅介绍 BUILD,现在我们来回头看一下, 以上面我举的那个 BUILD 文件为例,execfile 函数将会执行 BUILD 文件中的 cc_library 函数。 我们来看一下 cc_library 函数的实现,这个函数在 cc_targets 模块中,我们可以看一下: cc_targets cc_library[code] def cc_library(name, srcs=[], deps=[], warning='yes', defs=[], incs=[], export_incs=[], optimize=[], always_optimize=False, pre_build=False, prebuilt=False, link_all_symbols=False, deprecated=False, extra_cppflags=[], extra_linkflags=[], **kwargs): """cc_library target. """ target = CcLibrary(name, srcs, deps, warning, defs, incs, export_incs, optimize, always_optimize, prebuilt or pre_build, link_all_symbols, deprecated, extra_cppflags, extra_linkflags, blade.blade, kwargs) if pre_build: console.warning("//%s:%s: 'pre_build' has been deprecated, " "please use 'prebuilt'" % (target.path, target.name)) blade.blade.register_target(target)[/code] 现在巧妙之处就真正出来了,这个函数其实就是创建一个 target 对象, 并且将它注册进 blade,供后面使用,所以其实遍历一次 BUILD 文件, 所有的 target 都自动加载了,在读这段代码的时候,有一种感觉, 就是本来加载就如同收麦子一样,非常辛苦,需要 blade 一个个去收集, 而现在是走过每片田地,麦子自己就跳进来了。当然这个比喻并不是很准确, 但这个设计的确是相当的巧妙,让人觉得享受。 事实上到这里,后面的代码阅读起来已经没有难度了,现在所有的 target 已经加载, 接下来就是 target 的分析了,我们回到 blade 的主干,analyze_targets 的实现比较简单, 我这里就不贴上来逐步分析了, 它最后追溯到 dependency_analyzer 模块的 analyze_deps 函数中, 我们来看一下这个函数的实现: dependency_analyzer analyze_deps[code] def analyze_deps(related_targets): """analyze the dependency relationship between targets.

Input: related targets after loading targets from BUILD files. {(target_path, target_name) : (target_data), ...}

Output:the targets that are expanded and the keys sorted [all the targets keys] - sorted {(target_path, target_name) : (target_data with deps expanded), ...}

""" _expand_deps(related_targets) keys_list_sorted = _topological_sort(related_targets)

return keys_list_sorted[/code] 可以看到,其实这个地方就是根据依赖关系,将所有的 target 做了一个拓扑排序, 这个拓扑排序的算法其实很有意思,当然不是我这篇文章的重点, 不过有兴趣的同学可以去看一下。 现在内存中的 target 都是完整而且有序的了,我们可以安心地生成 SConstruct 了, 在这一步中,之前 target 的设计优势就体现了, 每个 target 根据自己的属性生成自己的 scons rule,最后汇总起来,加上文件头, 就是一个完整的 SConstruct 文件了。我们可以看一下我例子中用 blade 生成的 SConstruct:[code] import sys sys.path.insert(0, '/Users/Pine/Workspace/python/typhoon-blade/src/blade')

import os import subprocess import signal import time import socket import glob

import blade_util import console import scons_helper

from build_environment import ScacheManager from console import colors from scons_helper import MakeAction from scons_helper import create_fast_link_builders from scons_helper import echospawn from scons_helper import error_colorize from scons_helper import generate_python_binary from scons_helper import generate_resource_file from scons_helper import generate_resource_header

if not os.path.exists('build64_release'): os.mkdir('build64_release') os.environ["LC_ALL"] = "C" top_env = Environment(ENV=os.environ) top_env.Decider("MD5-timestamp") console.color_enabled=True top_env["SPAWN"] = echospawn

compile_proto_cc_message = '%sCompiling %s$SOURCE%s to cc source%s' % (colors('cyan'), colors('purple'), colors('cyan'), colors('end'))

compile_proto_java_message = '%sCompiling %s$SOURCE%s to java source%s' % (colors('cyan'), colors('purple'), colors('cyan'), colors('end'))

compile_proto_php_message = '%sCompiling %s$SOURCE%s to php source%s' % (colors('cyan'), colors('purple'), colors('cyan'), colors('end'))

compile_proto_python_message = '%sCompiling %s$SOURCE%s to python source%s' % (colors('cyan'), colors('purple'), colors('cyan'), colors('end'))

compile_thrift_cc_message = '%sCompiling %s$SOURCE%s to cc source%s' % (colors('cyan'), colors('purple'), colors('cyan'), colors('end'))

compile_thrift_java_message = '%sCompiling %s$SOURCE%s to java source%s' % (colors('cyan'), colors('purple'), colors('cyan'), colors('end'))

compile_thrift_python_message = '%sCompiling %s$SOURCE%s to python source%s' % (colors('cyan'), colors('purple'), colors('cyan'), colors('end'))

compile_resource_header_message = '%sGenerating resource header %s$TARGET%s%s' % (colors('cyan'), colors('purple'), colors('cyan'), colors('end'))

compile_resource_message = '%sCompiling %s$SOURCE%s as resource file%s' % (colors('cyan'), colors('purple'), colors('cyan'), colors('end'))

compile_source_message = '%sCompiling %s$SOURCE%s%s' % (colors('cyan'), colors('purple'), colors('cyan'), colors('end'))

link_program_message = '%sLinking Program %s$TARGET%s%s' % (colors('green'), colors('purple'), colors('green'), colors('end'))

link_library_message = '%sCreating Static Library %s$TARGET%s%s' % (colors('green'), colors('purple'), colors('green'), colors('end'))

ranlib_library_message = '%sRanlib Library %s$TARGET%s%s' % (colors('green'), colors('purple'), colors('green'), colors('end')) link_shared_library_message = '%sLinking Shared Library %s$TARGET%s%s' % (colors('green'), colors('purple'), colors('green'), colors('end'))

compile_java_jar_message = '%sGenerating java jar %s$TARGET%s%s' % (colors('cyan'), colors('purple'), colors('cyan'), colors('end'))

compile_python_binary_message = '%sGenerating python binary %s$TARGET%s%s' % (colors('cyan'), colors('purple'), colors('cyan'), colors('end'))

compile_yacc_message = '%sYacc %s$SOURCE%s to $TARGET%s' % (colors('cyan'), colors('purple'), colors('cyan'), colors('end'))

compile_swig_python_message = '%sCompiling %s$SOURCE%s to python source%s' % (colors('cyan'), colors('purple'), colors('cyan'), colors('end'))

compile_swig_java_message = '%sCompiling %s$SOURCE%s to java source%s' % (colors('cyan'), colors('purple'), colors('cyan'), colors('end'))

compile_swig_php_message = '%sCompiling %s$SOURCE%s to php source%s' % (colors('cyan'), colors('purple'), colors('cyan'), colors('end'))

top_env.Append( CXXCOMSTR = compile_source_message, CCCOMSTR = compile_source_message, SHCCCOMSTR = compile_source_message, SHCXXCOMSTR = compile_source_message, ARCOMSTR = link_library_message, RANLIBCOMSTR = ranlib_library_message, SHLINKCOMSTR = link_shared_library_message, LINKCOMSTR = link_program_message, JAVACCOMSTR = compile_source_message ) VariantDir("build64_release", ".", duplicate=0)

env_version = Environment(ENV = os.environ) env_version.Append(SHCXXCOMSTR = '%sUpdating version information%s' % (colors('cyan'), colors('end'))) env_version.Append(CPPFLAGS = '-m64') version_obj = env_version.SharedObject('build64_release/version.cpp')

time_value = Value("Thu Nov 7 02:44:31 2013") proto_bld = Builder(action = MakeAction("thirdparty/protobuf/bin/protoc --proto_path=. -I. -I thirdparty -I=dirname $SOURCE --cpp_out=build64_release $SOURCE", compile_proto_cc_message)) proto_java_bld = Builder(action = MakeAction("thirdparty/protobuf/bin/protoc --proto_path=. --proto_path=thirdparty --java_out=build64_release/dirname $SOURCE $SOURCE", compile_proto_java_message)) proto_php_bld = Builder(action = MakeAction("thirdparty/protobuf/bin/protoc --proto_path=. --plugin=protoc-gen-php=thirdparty/Protobuf-PHP/protoc-gen-php.php -I. -I thirdparty -Ithirdparty/Protobuf-PHP/library -I=dirname $SOURCE --php_out=build64_release/dirname $SOURCE $SOURCE", compile_proto_php_message)) proto_python_bld = Builder(action = MakeAction("thirdparty/protobuf/bin/protoc --proto_path=. -I. -I thirdparty -I=dirname $SOURCE --python_out=build64_release $SOURCE", compile_proto_python_message)) thrift_bld = Builder(action = MakeAction("/usr/local/bin/thrift --gen cpp:include_prefix -I . -I dirname $SOURCE -out build64_release/dirname $SOURCE $SOURCE", compile_thrift_cc_message)) thrift_java_bld = Builder(action = MakeAction("/usr/local/bin/thrift --gen java -I . -I dirname $SOURCE -out build64_release/dirname $SOURCE $SOURCE", compile_thrift_java_message)) thrift_python_bld = Builder(action = MakeAction("/usr/local/bin/thrift --gen py -I . -I dirname $SOURCE -out build64_release/dirname $SOURCE $SOURCE", compile_thrift_python_message))

blade_jar_bld = Builder(action = MakeAction('jar cf $TARGET -C dirname $SOURCE .', compile_java_jar_message))

yacc_bld = Builder(action = MakeAction('bison $YACCFLAGS -d -o $TARGET $SOURCE', compile_yacc_message))

resource_header_bld = Builder(action = MakeAction(generate_resource_header, compile_resource_header_message))

resource_file_bld = Builder(action = MakeAction(generate_resource_file, compile_resource_message))

python_binary_bld = Builder(action = MakeAction(generate_python_binary, compile_python_binary_message))

top_env.Append(BUILDERS = {"Proto" : proto_bld}) top_env.Append(BUILDERS = {"ProtoJava" : proto_java_bld}) top_env.Append(BUILDERS = {"ProtoPhp" : proto_php_bld}) top_env.Append(BUILDERS = {"ProtoPython" : proto_python_bld}) top_env.Append(BUILDERS = {"Thrift" : thrift_bld}) top_env.Append(BUILDERS = {"ThriftJava" : thrift_java_bld}) top_env.Append(BUILDERS = {"ThriftPython" : thrift_python_bld}) top_env.Append(BUILDERS = {"BladeJar" : blade_jar_bld}) top_env.Append(BUILDERS = {"Yacc" : yacc_bld}) top_env.Append(BUILDERS = {"ResourceHeader" : resource_header_bld}) top_env.Append(BUILDERS = {"ResourceFile" : resource_file_bld}) top_env.Append(BUILDERS = {"PythonBinary" : python_binary_bld}) top_env.Replace(CC="gcc", CXX="g++", CPPPATH=["thirdparty", "build64_release", "/System/Library/Frameworks/Python.framework/Versions/2.7/include/python2.7"], CPPFLAGS=['-m64', '-mcx16', '-pipe', '-g', '-DNDEBUG', '-D_FILE_OFFSET_BITS=64', '-D_STDC_FORMAT_MACROS', '-D_STDC_LIMIT_MACROS'], CFLAGS=[], CXXFLAGS=[], LINK="g++", LINKFLAGS=['-m64']) env_with_error = top_env.Clone() env_no_warning = top_env.Clone() env_with_error.Append(CPPFLAGS=['-Wall', '-Wextra', '-Wno-unused-but-set-variable', '-Wno-unused-parameter', '-Wno-unused-local-typedefs', '-Wno-missing-field-initializers', '-Wendif-labels', '-Wfloat-equal', '-Wformat=2', '-Wframe-larger-than=69632', '-Wmissing-include-dirs', '-Wpointer-arith', '-Wwrite-strings', '-Werror=char-subscripts', '-Werror=comments', '-Werror=conversion-null', '-Werror=empty-body', '-Werror=endif-labels', '-Werror=format', '-Werror=format-nonliteral', '-Werror=missing-include-dirs', '-Werror=overflow', '-Werror=parentheses', '-Werror=reorder', '-Werror=return-type', '-Werror=sequence-point', '-Werror=sign-compare', '-Werror=switch', '-Werror=type-limits', '-Werror=uninitialized', '-Werror=unused-label', '-Werror=unused-result', '-Werror=unused-value', '-Werror=unused-variable', '-Werror=write-strings'], CFLAGS=['-Werror-implicit-function-declaration'], CXXFLAGS=['-Wno-invalid-offsetof', '-Wnon-virtual-dtor', '-Woverloaded-virtual', '-Wvla', '-Werror=non-virtual-dtor', '-Werror=non-virtual-dtor', '-Werror=overloaded-virtual', '-Werror=vla']) env_v_test_mAgIc_fool = env_with_error.Clone() env_v_test_mAgIc_fool.Append(CPPFLAGS=['-O2', '-fno-omit-frame-pointer']) v_test_mAgIc_fool_cc_fool_object = env_v_test_mAgIc_fool.SharedObject(target = "build64_release/test/fool.objs/fool.cc" + top_env["OBJSUFFIX"], source = "build64_release/test/fool.cc") env_v_test_mAgIc_fool.Depends(v_test_mAgIc_fool_cc_fool_object, "build64_release/test/fool.cc") objs_v_test_mAgIc_fool = [v_test_mAgIc_fool_cc_fool_object] v_test_mAgIc_fool = env_v_test_mAgIc_fool.Library("build64_release/test/fool", objs_v_test_mAgIc_fool) env_v_test_mAgIc_fool.Depends(v_test_mAgIc_fool, objs_v_test_mAgIc_fool) env_v_test_mAgIc_me = env_with_error.Clone() env_v_test_mAgIc_me.Append(CPPFLAGS=['-O2', '-fno-omit-frame-pointer']) v_test_mAgIc_me_cc_me_object = env_v_test_mAgIc_me.SharedObject(target = "build64_release/test/me.objs/me.cc" + top_env["OBJSUFFIX"], source = "build64_release/test/me.cc") env_v_test_mAgIc_me.Depends(v_test_mAgIc_me_cc_me_object, "build64_release/test/me.cc") objs_v_test_mAgIc_me = [v_test_mAgIc_me_cc_me_object] v_test_mAgIc_me = env_v_test_mAgIc_me.Library("build64_release/test/me", objs_v_test_mAgIc_me) env_v_test_mAgIc_me.Depends(v_test_mAgIc_me, objs_v_test_mAgIc_me) [/code] 可以看到,上面有很多复杂的东西,这篇文章都没有提到, 事实上为了保证编译环境的干净,以及目录结构的清晰,blade 还做了环境变量的 Clone、 路径的变换等许多工作,希望了解细节的同学可以自己去读一下源代码。 最后,来提一下 blade 设计上的现有不足: 首先,大家可以发现 blade 这个模块全局贯穿各大模块, 而事实上很多模块是作为工具模块存在的,不需要了解 blade 的所有信息, 直接将 blade 当做参数传递没有做到信息的隐蔽性。 其次,现在的 rule_generator 在生成 SConstruct 文件头的时候,是固定生成一些 Builder, 这样就导致如果新增添一种 target 的类型,会需要修改现有代码, 可以考虑将 Builder 的生成分散在各个 target 中,每个新的 target 只需要去注册就 ok 了。 blade 中对 Clean 的考虑比较少,会有很多中间结果删不掉, 可以通过在 target 中定义 Clean 操作来实现。 总体来说,blade 是一款非常优秀的开源软件,它的理念很多都是超前的, 这种源码分发,统一环境的理念,事实上对一个公司是一个颠覆式的东西, “万丈高楼起于基石”,一切创新都将变得事半功倍。

锋利的 blade 到底锋利在哪里

刀是什么样的刀? 诸位看到标题,千万不要以为我是模仿《锋利的 JQuery》,或者什么书籍,而是因为,介绍 Blade 的文章,标题不得不这样。 Blade 由腾讯台风云计算平台出品,大约在 2012 年下半年开源,它是一把专用于构建软件的宝刀。Blade 的字面意义应该是"刀锋",意思是使用该软件构建软件更加强大,更加便捷。该系列宝刀,最早应该是由 Google 这位顶级刀匠打造而成,当年事迹见诸互联网记载: http://google-engtools.blogspot.hk/2011/08/build-in-cloud-how-build-system-works.html http://mike-bland.com/2012/10/01/tools.html#tools-blaze-forge-srcfs-objfs

听说 Google 内部打磨的宝刀,其名为"火焰刀",英文名为"Blaze",一样是锋芒毕露,炙热灼物,其面世后,以其熊熊烈焰,统一了 google 内部的软件编译方式。腾讯出品的 blade, 英文写法,只有一字之差,不过"火焰""刀锋"各得风流,虽说取名有点像偶像致敬的意思,但是也是锋芒不让。

提起 google 的宝刀,其实到 google 洗练过几年武学的 IT 牛人们,在远走他方后,也都各自打磨了一把。我的前东家,就成绩打磨过一把类似的宝剑,取名"Ymake",名字是虽然朴拙了一些,但是假如你见过他们亮出过宝刀,也会被其锋芒所吸引。在云壤的江湖朋友们,都称道其"活儿好"。

就互联网上能搜索到的铸刀秘诀而言,我能搜索到的,应该是我的前任授业恩师放出去的,其实他应该还没有加入云壤。项目地址为: https://code.google.com/p/qa52/ 其 BUILD 语法,虽然还没有简洁到极致,但是功能上似乎看起来也已经有模有样。

其示例的 BUILD 文件如下所示:

假如你仔细瞅瞅,会发现语法已经基本接近 google 开放出来的 BUILD 文件示例。

好吧,花了九牛二虎之力,追溯了 blade 的历史掌故,却还没点到题上。言归正传,blade 其实是一个多语言的构建工具,之所以使用"构建",而不是"编译"两字,实在是因为软件构建并不仅仅是软件编译,而非我喜欢故弄玄虚。Blade 除了编译软件以外,还奉上了很多其它的福利,比如集成了单元测试,性能测试等。这正如一把好刀,不仅应该能杀人,还且最好能力最短的时间内杀死对方,刀光一起,人已倒地,但是刀不流血,刀已回鞘。

那么,Blade 藏身何处,不急,以下就是它的所在: https://code.google.com/p/typhoon-blade/ https://github.com/chen3feng/typhoon-blade

目前该项目与陈峰维护,其微博名为"陈三丰",虽然我猜测可能是由于"陈峰"这个名字已经被他人抢先使用,继而只好采用拆字法,将"峰"一分为二,是为"三丰",不过从名字看,倒也和大侠张三丰很有渊源,就此而言,blade(刀锋) 由他维护倒是最妙不过了。

刀锋的锋芒在哪里?

既然号称刀锋,那么其锋芒何在?为什么有了 make 这把天下闻名的好刀,还要试试 blade 的锋芒?

那么下面就在历数一下 blade 的不同凡响处。

天下武功,唯快不破

江湖上最知名的刀,无不以快而闻名,blade 的一个好处,也是其编译速度快,快得益于几个方面: 1,可以进行并行编译和并行测试, 2,使用了 ccache 等库,可以将编译中间结果进行缓存。 3,如果使用了 distcc 模式,则可以享有更多的用于编译的硬件资源。听说 google 内部的 blaze 有三个模式,一个是 Local,一个是 distcc,一个是 Forge,按理说,forge 模式由于运行在大规模的集群中,编译的速度应该最为快。 刀虽为刀,却不仅仅是刀

在仙剑奇侠传四中,宝剑望舒,被男主角云天河各种虐待,从猎野猪、烤肉、劈柴到射箭、御剑,无所不用其极。而一把好刀,其功能自然不应该局限与杀人见血。 Blade 作为一个构建工具,其作用不仅仅是替代 make 和 makefile,且听我细细道来: 1,Blade 是软件工程的利器,有助于模块化,模块化的力度控制自如,对于提高系统的可维护性,复杂性的隔离,代码复用的最大化,都有很大的好处。 2,Blade 提供了单元测试的最佳使用方式。 假若单元测试和代码是分离的,编译代码和运行测试各自为政,那么往往测试只是测试,很难对开发有什么帮助,因为很多时候,虽然有测试代码,但是测试却很少被运行,或者时过境迁后,测试多有失效。大家假如编译过一些开源项目的话,可能就会有类似的经验,编译完静态库,拿到静态库文件就完了,很少有人去运行所有的单元测试,确保所有的测试都能运行通过。 有了 Blade,通过 cc_test 将 gtest_main.a 默认链接,使得单元测试的代码简化到极致。同时,主要运行 blade test target,就能在编译完目标程序后,运行(而且可并发运行)相应的单元测试,以随时跟进代码是否已经偏离了该有的轨道,开发是否早已脱缰狂奔。 只要发布部署时,使用上 Blade test target, 那么就可以保证,上线的模块,能够通过所有单元测试用例的验证把关。Blade 集成单元测试,提供 cc_test,看似功能简单,但是正是这个简单的功能,使得测试驱动开发,开发者测试等理念,能够在 google 全功能范围内得到认同并贯彻实现。 3,blade 实现了递归编译。 Blade build …即可对当前目录及其子目录下的所有编译对象进行编译。该功能虽然小,但是却很实用。在一个十几人的团队中,百万行级别的代码规模,只有递归编译功能,可以很方便地实现一个循环编译工具,当某个模块编译失败,或者某个单元测试失败后,自动发出邮件通知相关的开发人员,以保证频繁的开发提交代码成为开发。当然,相关的功能,可以通过类似 hudson 之类的软件完成。不过 blade 递归编译,对中小规模的代码而言,基本已经游刃有余。 4,blade 实现了依赖查询。 Blade query 可以查询依赖到某个模块的所有模块,这个对于代码重构而言,是个不错的功能。假如你在重构 code base,或者是修改接口,或者是 fix bug,或者是改进性能, 都可能使得使用到该库的相关模块,集体罢工,单元测试无法通过。这时候,假如使用上 blade query,你就可能编译相关模块,使得你提交的代码,不会给其它模块带来麻烦。 5,blade 绑定了静态代码审查功能。 对整个项目而言,严把质量关非常重要,因为一旦一些烂代码进去了,清扫牛粪的工作将会非常痛苦而且艰巨。这里的异己分子,可能包括 bug,风格差异迥异等。 Blade 会在进行编译之前,会所有被变更的编码,使用指定的静态代码审查功能进行代码审查。C++ 代码审查脚本,google cpplint 较为知名,地址为: http://google-styleguide.googlecode.com/svn/trunk/cpplint/cpplint.py 当然,每个公司都可以定制其自己的检查脚本,以符合其自己的代码风格等。 Blade 绑定该功能,可以避免出现一些常见的 Bug,统一团队的代码风格,对公司工程文化的塑造有润物细无声之效。 6,blade 是编译的统一入口,并且是开放的 这点是最重要的,也是 blade 的锋芒所在。因为 Blade 是统一的编译如果,因此修改统一的编译选项,设置统一的编译警告级别,实现 cc_test 等全部成为可能。 只要愿意,你不仅可以在编译前实现代码审查,也可以在编译时设置不同的参数,选择指定的编译器,更可以在编译后,运行单元测试,统计单元测试覆盖率,当可以定制各种操作。 因为统一,所以要升级编译器,使用统一的警告级别等牵一发而动全身的动作成为可能。因为开发,实现 cc_test, cc_benchmark, proto_library, thrift_library 成为可能。

好使的刀,方为好刀 假如刀虽然,但是太笨重,使用起来仿佛破解机关,这只有寂寞的高手才能使用了,普通人得知,如同废铁。因此,好使的刀,才是好刀。 Blade 之所以好使,在于其采用了足够简洁的语法,比如下面便是编译一个静态库 libencoding.a 的语法描述: cc_library( name = 'encoding', srcs = [ 'ascii.cpp', 'base64.cpp', 'hex.cpp', 'percent.cpp', 'shell.cpp', ], deps = [ '//toft/base/string:string', '//thirdparty/stringencoders:stringencoders' ] ) 你需要的只有三件事,要打造的刀叫什么刀,它需要使用什么特别的原料,它需要依赖哪些现成的原料。也就是 name, srcs,deps, 换言之,就是编译对象名,编译需要的源文件,需要依赖的其它静态库。你需要描述的有且仅有他们。怎么编译,编译选项,在哪里编译,中间产物是什么,中间产物放在哪里,你通通不需要关注。刀一出鞘,东西已在那,躺在了你希望它在的所在,热乎乎的,等你去品尝。

这种简单到极致的语法,好处却有很多。比如: 1, 容易学习,容易记忆,你需要记忆的只有 name, srcs,deps 等掰着十指都能数的过来的关键字和规则,剩下的就是,告诉它用到哪些代码文件,用到了哪些外部的库。 相比 Makefile 繁琐的语法,BUILD 文件可谓清爽宜人。 2,简化到了极致的好处是,各种 cc_library 可以在力度中间轻松转换,因此使用了 BUILD 文件后,模块划分会更加合理,哪怕有一个文件,其它模块可能可以使用上,你也可以方便地将它打包到一个静态库中,这样其它地方只要依赖上这个 cc_library,就可以坐享其成。 我曾经见识过各种 thrift 文件,protobuf 文件散落在代码各个目录甚至分支中,如果使用上 thrift_library, proto_library,将 thrift 文件定义出细粒度的 library,则可以避免因 Makefile 中频繁描述 thrift/protobuf 源文件生成与编译的痛苦。 羞于告诉他人的是,我至今都对 Makefile 不太熟悉,不过所幸,Blade 已经开源,我从此不必也不像和 Makefile 再打交道,我也没有必要告诉别人说,我认识 Makefile 这个哥们。

宝刀待屠龙 江湖中向来有"武林至尊,宝刀屠龙,号令天下,莫敢不从,倚天不出,谁与争锋."的说法,blade 既然是一把绝世好刀,在倚天剑面世之前,各位江湖好友,不妨试试其刀锋如何!

转自:http://blog.sina.com.cn/s/blog_4af176450101bg69.html

需要 登录 后方可回复。