为了实现如标题所述的将多个静态库合并为一个动态库,内置的 Bazel 规则是没有这个功能的,Bazel C/C++
相关的内置规则有:
C/C++
库,包括动态库、静态库.proto
文件生成 C++
代码C/C++
样例C++
工具链C++
工具链的集合而我们知道规则(Rule)定义了 Bazel 对输入执行的一系列操作,以生成一组输出。例如 cc_binary
规则可能:
.cpp
文件g++
从 Bazel 的角度来看,g++
和标准 C++
库也是这个规则的输入。作为规则编写人员,你不仅必须考虑用户提供的规则输入,还必须考虑执行操作(Actions)所需的所有工具和库。比如我们手动的将多个静态库(libA.a、libB.a、libC.a
)合并为一个动态库(libcombined.so
):
$ gcc -shared -fPIC -Wl,--whole-archive libA.a libB.a libC.a -Wl,--no-whole-archive -Wl,-soname -o libcombined.so
“注:
-Wl,option
后面接的选项最终会作为链接器 ld 的参数,即上面的命令最终还调用了 ld 命令。而-Wl,--whole-archive {xxx} -Wl,--no-whole-archive
所包围的库表示将{xxx}
库列表中所有.o
中的符号都链接进来,这样会导致链接不必要的代码进来,从而导致生成的库会相对很大。目前还没有找到相关办法是否可以做到只链接进上层模块库所调用到的函数。
在编写规则中我们就需要获取当前的编译器,我们不能直接使用固定的路径,比如 Linux 下 /usr/bin/gcc
,因为可能是交叉编译器,路径就不一样了。另外我们还需要传入 gcc
将多个静态库合并成一个动态库的相关参数、待合成的静态库列表、最后要生成的动态库名称和路径。这样就是一个比较完善的自定义规则了。
将多个静态库合并成一个动态库:
$ gcc -shared -fPIC -Wl,--whole-archive libA.a libB.a libC.a -Wl,--no-whole-archive -Wl,-soname -o libcombined.so
将多个静态库合并成一个静态库:
方式一:
$ cd temp
$ ar x libA.a
$ ar x libB.a
$ ar x libC.a
$ ar rc libcombined.a *.o
用这种方式无法指定库的输出目录。笨方法就是,将每个待合并的静态库都拷贝到目标目录里去,然后一一 ar -x
操作,然后再到目标目录里操作 ar rc
。这就涉及到了中间文件的产生,有一个很重要的点就是中间文件的产生只能在当前 Bazel 包中创建。中间文件的创建我们可以使用 File actions.declare_file(filename, *, sibling=None)
声明然后结合 Action 去真实创建。
方式二(需安装libtool):
# MacOS系统
$ libtool -static -o libcombined.a libA.a libB.a libC.a
在 Unix-like 系统上:
$ sudo apt-get install libtool-bin
# 生成的libcombined.a ar -x 解压出来是 libA.a libB.a libC.a ,而不是 *.o 文件。
$ libtool --mode=link gcc -o libcombined.a libA.a libB.a libC.a
# 这样可以指定生成路径,但是 *.o 的生成还是需要 ar -x 来生成
$ libtool --mode=link gcc -o libcombined.a *.o
另外我们需要规则具有参数输入功能,参数输入类型定义可以详见:https://docs.bazel.build/versions/3.4.0/skylark/lib/attr.html ,比如定义一个决定是否合成动态库或静态库的布尔参数(genstatic),以及带依赖项配置(deps):
my_cc_combine = rule(
implementation = _combine_impl,
attrs = {
"genstatic" : attr.bool(default = False),
"deps": attr.label_list(),
}
)
Action
描述了如何从一组输入生成一组输出,例如 “在 hello.c 上运行 gcc 并获取 hello.o”。创建操作(Action)时,Bazel 不会立即运行命令。它将其注册在依赖关系图中,因为一个 Action 可以依赖于另一个 Action 的输出(例如,在 C 语言中,必须在编译后调用链接器)。在执行阶段,Bazel 会决定必须以何种顺序运行哪些操作。所有创建 Action 的函数都定义在 ctx.actions
中:
ctx.actions.run
:运行一个可执行文件ctx.actions.run_shell
:运行一个脚本命令ctx.actions.write
:将一个字符串写入文件ctx.actions.expand_template
:从模板文件中创建一个文件因此我们可以通过创建一个运行脚本命令的 Action
来运行上面所述的打包命令,即使用 ctx.actions.run_shell
函数。
如前言中讲到的,如果是交叉编译器呢? 那我们还需要在规则中获取到当前编译器的信息,包括 gcc
、ld
、ar
工具。需要在规则中传入当前编译器信息:
my_cc_combine = rule(
implementation = _combine_impl,
attrs = {
"_cc_toolchain": attr.label(default = Label("@bazel_tools//tools/cpp:current_cc_toolchain")),
"genstatic" : attr.bool(default = False),
"deps": attr.label_list(),
}
)
然后在 _combine_impl
中通过 load("@bazel_tools//tools/cpp:toolchain_utils.bzl", "find_cpp_toolchain")
中的 find_cpp_toolchain(ctx)
获取当前编译器信息。
还有一个比较重要的问题就是,如果依赖还有依赖呢? 比如 libA.a
依赖了 libD.a
和 libE.a
,那我们还需要将 libD.a
和 libE.a
也合并到 libcombined.so
中。这种依赖也分为两种,一种是 libD.a
是外部已经编译好的静态库,而 libE.a
是有 cc_library
规则编译出来的静态库。那如何能够把这两种方式的库都最后合并到 libcombined.so
呢?
depset
是一种专门的数据结构,支持有效的合并操作,并定义了遍历顺序。通常用于从 rules 和 aspects 的传递依赖中积累数据。depset
的成员必须是可散列的(hashable),并且所有元素都是相同类型。具体的其他特性和用法这里就不展开了,我们只需要知道这种数据结构保存了 rules
里目标的依赖关系信息。Depsets
可能包含重复的值,但是使用 to_list()
成员函数可以获取一个没有重复项的元素列表,遍历所以成员。
我们在 _combine_impl
中可以用 ctx.attr.deps
获得当前目标的依赖列表,每个元素的组成为<target //libA:A, keys:[CcInfo, InstrumentedFilesInfo, OutputGroupInfo]>
,即包含一个目标和目标的三个信息体,目标里结构具体可以参考官方文档并获取相关信息,比如用 {Target}.files.to_list()
可以获取 Target 直接生成的一组文件列表,意思就是比如 A
目标,直接生成的就是 libA.a
。目标 A
的依赖目标 E
信息在 CcInfo
结构体内,这里先不展开如何获取了,这里只做个提示:
x = dep_target[CcInfo].linking_context.linker_inputs.to_list()
for linker_in in x:
# <LinkerInput(owner=//libA:A, libraries=[<LibraryToLink(pic_objects=[File:[[<execution_root>]bazel-out/k8-fastbuild/bin]libA/_objs/A/liba.pic.o], pic_static_library=File:[[<execution_root>]bazel-out/k8-fastbuild/bin]libA/libA.a, alwayslink=false)>, ], userLinkFlags=[], nonCodeInputs=[])>
for linker_in_lib in linker_in.libraries:
# <generated file libE/libA.a>
# <generated file libE/libE.a>
internal_link_lib = linker_in_lib.pic_static_library
# <source file 3rdparty/libs/libD.a>
external_link_lib = linker_in_lib.static_library
my_cc_combine.bzl:
load("@bazel_tools//tools/cpp:toolchain_utils.bzl", "find_cpp_toolchain")
def _combine_impl(ctx):
cc_toolchain = find_cpp_toolchain(ctx)
target_list = []
for dep_target in ctx.attr.deps:
# CcInfo, InstrumentedFilesInfo, OutputGroupInfo
cc_info_linker_inputs = dep_target[CcInfo].linking_context.linker_inputs
target_dirname_list = []
for linker_in in cc_info_linker_inputs.to_list():
for linker_in_lib in linker_in.libraries:
if linker_in_lib.pic_static_library != None:
target_list += [linker_in_lib.pic_static_library]
if linker_in_lib.static_library != None:
target_list += [linker_in_lib.static_library]
output = ctx.outputs.output
if ctx.attr.genstatic:
cp_command = ""
processed_list = []
processed_path_list = []
for dep in target_list:
cp_command += "cp -a " + dep.path + " " + output.dirname + "/ && "
processed = ctx.actions.declare_file(dep.basename)
processed_list += [processed]
processed_path_list += [dep.path]
cp_command += "echo 'starting to run shell'"
processed_path_list += [output.path]
ctx.actions.run_shell(
outputs = processed_list,
inputs = target_list,
command = cp_command,
)
command = "cd {} && ar -x {} {}".format(
output.dirname,
" && ar -x ".join([dep.basename for dep in target_list]),
" && ar -rc libauto.a *.o"
)
print("command = ", command)
ctx.actions.run_shell(
outputs = [output],
inputs = processed_list,
command = command,
)
else:
command = "export PATH=$PATH:{} && {} -shared -fPIC -Wl,--whole-archive {} -Wl,--no-whole-archive -Wl,-soname -o {}".format(
cc_toolchain.ld_executable,
cc_toolchain.compiler_executable,
" ".join([dep.path for dep in target_list]),
output.path)
print("command = ", command)
ctx.actions.run_shell(
outputs = [output],
inputs = target_list,
command = command,
)
my_cc_combine = rule(
implementation = _combine_impl,
attrs = {
"_cc_toolchain": attr.label(default = Label("@bazel_tools//tools/cpp:current_cc_toolchain")),
"genstatic" : attr.bool(default = False),
"deps": attr.label_list(),
"output": attr.output()
},
)
在 BUILD
文件中调用我们创建的规则示例:
load(":my_cc_combine.bzl", "my_cc_combine")
my_cc_combine(
name = "hello_combined",
# 这里将所有的静态库合并成一个静态库
genstatic = True,
output = "libcombined.a",
deps = ["//libA:A", "//libB:B", "//libC:C"]
)
至此自定义规则实现完成,中间遇到了一些麻烦,不过最终都解决了,因为 Bazel 的中文社区目前为止并不是很完善,可以说中文资料大都是概念性介绍和简单入门,很多内容都需要参考官方文档或者去 https://groups.google.com/forum/#!forum/bazel-discuss
提问题,有 Bazel bug 的话就只有去 https://github.com/bazelbuild/bazel/issues
提 issue 了。最后在实现自定义规则中将多个静态库合并为一个动态库示例中,这里有几个点我们需要注意下:
output
Action,那么中间文件也不会产生,这在我调试过程中带给了我一阵疑惑find_cpp_toolchain(ctx)
,而不是直接使用 /usr/bin/gcc
等工具链action.run_shell
。其他的比如还可以编写测试规则(类名需以_test结尾)、actions.write
(适合小文件生成)、actions.expand_template
(用模板生成文件)、用 aspect 从依赖中搜集信息等等规则的具体用法