当前位置: 首页 > 工具软件 > Tomcat Lite > 使用案例 >

构建最小 tomcat docker 镜像

阴宏爽
2023-12-01

所谓的“最小” tomcat 镜像是相对的,它的大小取决于如下几点:

  1. 基础镜像是否使用 glibc,也就是是否使用 alpine 作为基础镜像;
  2. 使用 jdk 还是只使用 jre 作为 tomcat 运行环境;
  3. 使用 openjdk 还是 oracle jdk。

上述的条件决定了 tomcat 镜像的大小。

总所周知,alpine 算是基础镜像中最小的了,它还自带包管理器,但是它的缺点也同样明显,就是它没有使用 glibc,因此会带来兼容性的问题。

本文会使用基于 glibc 的 distroless 作为基础镜像,然后使用 oracle jdk8 + tomcat8,最终镜像大小为 171M。这个大小是有一定浮动空间的,因为即使是使用 oracle jdk8,不同的小版本之间大小也会相差很大。

以下是和官方镜像的一个对比:

镜像名java大小镜像层glibc
tomcat:8openjdk jre463MB31glibc
tomcat:8-alpineopenjdk jre106MB25muslc
tomcat:8-slimopenjdk jre223MB29glibc
本文编译使用oracle jdk171M3glibc

可以看出 alpine 的优势非常大,它足够小,但是它使用的是 jre,且不是 glibc。如果你程序编译是针对 glibc,那么运行起来会有问题。我也不确定我公司的开发是否对 glibc 有强依赖,但是不敢冒险,而且 alpine 对我来说并没有什么优势。

你可以看到本文编译使用的 tomcat 的镜像层只有 3 层,你可能会吃惊于它的层数,等你看完你就会明白了,因为这个镜像并不是通过 dockerfile 构建出来的。

因为我已经将编译好的镜像上传到了 dockerhub,你可以直接使用 docker run maxadd/tomcat:8-jdk8-distroless 运行查看,或者使用 dive 查看其构成。

缘由

当你选择 tomcat 镜像时,其实要考虑很多东西:

  • 是否存在必须的命令;
  • java 是否能够满足需要;
  • 是否足够灵活定制;
  • 是否足够安全(命令和库文件足够少);
  • 是否足够小。

真当你需要用的时候,发现官方镜像使用起来或多或少都有些不顺手,总不是那么令人满意。对我而言,最重要的是官方没有 oracle jdk 的镜像提供,因为狗日的 oracle 要对 oracle jdk 收费。虽然也有人自己提供了基于 oracle jdk 的版本,但是镜像实在太大。

总之基于这样或那样的原因,我准备手动创建一个自己的 tomcat 镜像,这让我将目光移向了 distroless 镜像。因为 distroless 镜像是所有基于 glibc 中最小的,只有 16M,里面只包含一个二进制程序应有的最基础的运行环境,没有一个命令提供,包括 shell。

当我准备使用它作为基础镜像来使用的时候,我发现我无法通过它来达成我的目标,因为它里面没有任何命令,当使用 dockerfile 进行构建的时候,我只能通过 ADD 往里面添加文件,但是这样一来,我每添加一个文件都会创建一个镜像层,而我要添加的文件并不少。。

于是我将目光又放到了构建 distroless 的工具 bazel,由于 distroless 是由 bazel 构建的,我有预感它能够实现我心中所想,于是开始研究它。事实证明,我的方向是对的,bazel 完全能够满足我的所有需求。

有了这样的前提之后,我开始规划我的镜像:

  • 要安装 bash,要通过它来设置 jvm 参数;
  • 要安装一些基本的命令,例如 cat、echo 之类;
  • 要安装 jdk,因为需要用到 jdk 中的一些命令;
  • 能不要的命令都不要,除了可以减少镜像的大小,还能提升安全性;
  • 需要自己写脚本启动 tomcat,catalina.sh 脚本中涉及到的命令太多,没必要使用。

下面一步步实现上面的需求。

安装 bash

我的做法是建立一个目录,作为根目录,里面存放需要复制到 distroless 镜像中的所有文件。文件需要放在哪,那就在该目录下创建对应的目录。最终将这个“根目录”打包,并在 distroless 的根目录解压,就能将所有文件一步到位。这也是镜像层只有 3 层的原因,其中 2 层是 distroless 自带的。

第一步是安装 bash,安装 bash 的目的是为了执行脚本,同样也是为了能够登录上去执行 jmap、jstack 之类的命令。前面已经提到了,我这里使用 distroless 作为基础镜像,且因为 distroless 使用的是 debian 的库文件,因此我们可以将 debian 中的命令直接复制下来使用。

Linux 中命令的运行不仅需要命令本身,还需要它依赖的库文件,库文件通过 ldd 命令查看。因此我们不仅需要复制命令本身,还得复制它所需的库文件。

首先启动一个 debian 容器:

# docker run -it --name debian debian:stretch /bin/bash
复制代码

查看 bash 所依赖的库文件:

root@45104fade344:/# ldd /bin/bash
	linux-vdso.so.1 (0x00007ffda05b1000) # 这一行无需理会
	libtinfo.so.5 => /lib/x86_64-linux-gnu/libtinfo.so.5 (0x00007fdf4532f000)
	libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007fdf4512b000)
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fdf44d8c000)
	/lib64/ld-linux-x86-64.so.2 (0x0000564175f7f000)
复制代码

上面总共有四个库文件,我们需要将其复制下来,需要注意的是,上面显示的库文件有可能是软链接文件。

我们先在宿主机上创建所谓的“根目录”,这里取名为 debian_files。

然后在这个目录下创建 lib/x86_64-linux-gnubinlib64 这几个目录。

mkdir -p debian_files/{lib/x86_64-linux-gnu,bin,lib64}
复制代码

接着使用 docker cp 命令将 debian 容器中的命令和库文件复制到我们创建的对应的目录下。

docker cp debian:/bin/bash debian_files/bin/
# -L 表示复制链接文件指向的实际文件
docker cp -L debian:/lib/x86_64-linux-gnu/libtinfo.so.5 debian_files/lib/x86_64-linux-gnu/
docker cp -L debian:/lib/x86_64-linux-gnu/libdl.so.2 debian_files/lib/x86_64-linux-gnu/
docker cp -L debian:/lib/x86_64-linux-gnu/libc.so.6 debian_files/lib/x86_64-linux-gnu/
复制代码

通过这种方式可以将你想要使用的其他命令都拷贝下来,这里就不一一演示了。

准备 jdk

因为这里我打算使用 oracle jdk 而非 openjdk,所以先从 oracle 官网上将 jdk8 下载下来。我这里下载的是 162 版本,没有别的原因,只是因为公司使用的是这个版本。

注意下载 tar 包。因为我准备将其放置在镜像的 /opt 目录下,所以需要在 debian_files 下面创建一个 opt 目录:

# ls debian_files/opt
jdk1.8.0_162
复制代码

完整的 jdk 总共有 371M,里面有很多我们用不到的东西,先将其都删除掉:

# cd debian_files/opt/jdk1.8.0_162
# rm -rf *src.zip \
lib/missioncontrol \
lib/visualvm \
lib/*javafx* \
jre/lib/plugin.jar \
jre/lib/ext/jfxrt.jar \
jre/bin/javaws \
jre/lib/javaws.jar \
jre/lib/desktop \
jre/plugin \
jre/lib/deploy* \
jre/lib/*javafx* \
jre/lib/*jfx* \
jre/lib/amd64/libdecora_sse.so \
jre/lib/amd64/libprism_*.so \
jre/lib/amd64/libfxplugins.so \
jre/lib/amd64/libglass.so \
jre/lib/amd64/libgstreamer-lite.so \
jre/lib/amd64/libjavafx*.so \
jre/lib/amd64/libjfx*.so
复制代码

删除之后,只剩 153M?。然后给 jdk 目录建立一个软链接:

# ln -s jdk1.8.0_162 java
复制代码

jdk 依赖

jdk 的 bin 目录下有很多我们需要的命令,这些命令也有依赖的库文件。虽然 jdk 是我们下载而并不是我们安装,但是由于 jdk 中的命令都是编译好的二进制文件,只要满足内核和 glibc 的需求它们就可以运行。

当然,内核和 glibc 只是硬性要求,软性要求就是它们依赖的库文件。由于我的宿主机 Linux 的 CentOS7 而非 debian,因此我们需要将 jdk 挂载到 debian 镜像中,在镜像中使用 ldd 命令来查看 jdk 中命令所依赖的库文件有哪些。

# cd debian_files/
# docker run -it --name debian -v `pwd`/java:/opt debian:stretch /bin/bash
复制代码

将 jdk 都挂载进容器的 /opt 目录,注意都使用绝对路径。然后你就可以查看 /opt/bin 和 /opt/jre/bin 下面的命令所依赖的库文件有哪些了。

它们所依赖的库文件中,貌似只需要再 cp 一个 /lib/x86_64-linux-gnu/libpthread.so.0 即可。注意这个文件是一个链接文件,你通过 docker cp -L 下载下来之后并不是这个名,你得使用软链接弄成一样的。

接下来就是准备 tomcat 了。

准备 tomcat

tomcat 里面没有任何命令,它本身也是依赖 java 启动的,因此它没有任何的库文件依赖。直接去官网下一个就行,我这里使用的是 apache-tomcat-8.0.36

我这里同样就之放在 opt 目录下:

# ls debian_files/opt/
apache-tomcat-8.0.36  java  jdk1.8.0_162
复制代码

tomcat 的启动我们是通过 catalina.sh 进行的,但是由于它里面用到的命令太多,且我们只需要启动 tomcat,不需要停止或者重启之类的,所以我们完全可以不用 catalina.sh,只需要它启动所需的 java 参数就行。拿到这些参数之后,我们直接传递给 java 后同样可以直接启动。

甚至我怀疑只要在 Linux 服务器上直接执行就行,没必要挂载到 debian 中。

这个参数其实很好获得,你将 tomcat 和 jdk 同时映射到 debian 容器中,然后定义好 JAVA_HOME,就可以通过 sh -x catalina.sh run 看到它最终启动的参数了。

我这里就不演示具体的操作了,直接将它的参数贴出来。当你什么 JAVA 参数都没有设定时,它的启动参数如下:

/opt/java/bin/java -Djava.util.logging.config.file=/opt/tomcat/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Djdk.tls.ephemeralDHKeySize=2048 -Djava.endorsed.dirs=/opt/tomcat/endorsed -classpath /opt/tomcat/bin/bootstrap.jar:/opt/tomcat/bin/tomcat-juli.jar -Dcatalina.base=/opt/tomcat -Dcatalina.home=/opt/tomcat -Djava.io.tmpdir=/opt/tomcat/temp org.apache.catalina.startup.Bootstrap start
复制代码

你如果想要增加 jvm 参数,随便往里面插就行,你定义在 catalina.sh 中的类似与 JAVA_OPS 等,最终都会转换成 java 参数。不过,貌似 jvm 中堆的参数之前要加上 -server

 -server -Xmx128M -Xms128M
复制代码

jmx 监控:

-Djava.rmi.server.hostname=`hostname -I` -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=11111 -Dcom.sun.management.jmxremote.authenticate=true -Dcom.sun.management.jmxremote.password.file=/etc/jmxremote.password -Dcom.sun.management.jmxremote.access.file=/etc/jmxremote.access -Dcom.sun.management.jmxremote.ssl=false
复制代码

gc log:

-Xloggc:/opt/tomcat/logs/gc.log
复制代码

最终你可以定义一个脚本用来启动 tomcat:

#!/bin/bash

/opt/java/bin/java -Djava.util.logging.config.file=/opt/tomcat/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Djdk.tls.ephemeralDHKeySize=2048 -Djava.endorsed.dirs=/opt/tomcat/endorsed -classpath /opt/tomcat/bin/bootstrap.jar:/opt/tomcat/bin/tomcat-juli.jar -Dcatalina.base=/opt/tomcat -Dcatalina.home=/opt/tomcat -Djava.io.tmpdir=/opt/tomcat/temp org.apache.catalina.startup.Bootstrap -server -Xmx128M -Xms128M -Xloggc:/opt/tomcat/logs/gc.log -Djava.rmi.server.hostname=`hostname -I` -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=11111 -Dcom.sun.management.jmxremote.authenticate=true -Dcom.sun.management.jmxremote.password.file=/etc/jmxremote.password -Dcom.sun.management.jmxremote.access.file=/etc/jmxremote.access -Dcom.sun.management.jmxremote.ssl=false start
复制代码

这个脚本你随便放在哪,只要 CMD 指定执行它就行。你还可以写一些简单的逻辑,以便通过环境变量来灵活的控制 jvm 的堆大小,这个今后会提到。

打包

确保所有你需要的文件都在“根目录”下都准备完毕后,我们就可以对该目录(这里是 debian_files)进行打包了,打包的格式必须是 tar/tar.gz/tar.xz 等。

有一点需要注意,使用 tar 命令无法完成这种操作(或许是我不知道方法?),因为 tar 必须在它上级目录打包,但是这样一来 tar 中就包含 debian_files 这个目录名了。这就造成在 distroless 中解压后里面文件都不会直接放在根下,而是还在 debian_files 下。

既然 tar 不行,那就使用 Python,使用 Python 进行打包。你不会 Python 不要紧,跟着我走就不会出任何问题。

首先启动 python3 镜像,注意将 debian_file 映射到 python3 的 /opt 目录下。

docker run -it -v /root/debian_files:/opt python:3.6
复制代码

然后执行下面这些代码:

import tarfile, os
os.chdir("/opt")
tar = tarfile.open("/tmp/x.tar", "w")
for i in os.listdir("."):
  tar.add(i)

tar.close()
复制代码

这会将 debian_files 中的所有文件都打包到容器中的 /tmp/x.tar 文件中。然后使用 docker cp 将其复制到宿主机的 /tmp 下,留作后用。

ok,准备工作都已完成,停止 Python 容器后开始安装 bazel,然后将 /tmp/x.tar 解压到 distroless 镜像中。

安装 bazel

bazel 是 Google 推出的编译工具,用于将各种语言的源代码编译成二进制文件,至于有什么优势我没有具体了解 ?。从这点上来看,编译 docker 镜像只是它附带的功能,事实也确实如此,它并非原生支持 docker 镜像的编译。

关于它的基本概念和基本使用在文章的后面,有需要的可自行查阅。

使用 bazel 编译 docker 镜像的一大优势就是你甚至无需安装 docker。

首先安装 bazel:

# cat >/etc/yum.repos.d/bazel.repo <<'EOF'
[vbatts-bazel]
name=Copr repo for bazel owned by vbatts
baseurl=https://copr-be.cloud.fedoraproject.org/results/vbatts/bazel/epel-7-$basearch/
type=rpm-md
skip_if_unavailable=True
gpgcheck=1
EOF

# yum install bazel
复制代码

bazel 原生并不支持编译 docker 镜像,不过 GitHub 上面有扩展规则可以帮你完成。

首先创建一个目录,以它作为 WORKSPACE:

# mkdir bazel-test
复制代码

然后定义 WORKSPACE,也就是外部依赖。其实我们依赖就是 docker 规则,因此加载它就好。

# cd bazel-test
# vim WORKSPACE
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")

# 加载 docker 规则
http_archive(
    name = "io_bazel_rules_docker",
    sha256 = "aed1c249d4ec8f703edddf35cbe9dfaca0b5f5ea6e4cd9e83e99f3b0d1136c3d",
    strip_prefix = "rules_docker-0.7.0",
    urls = ["https://github.com/bazelbuild/rules_docker/archive/v0.7.0.tar.gz"],
)

# 这一步貌似是和 docker registry 交互用的?具体我也没搞懂,不过不影响使用
load(
    "@io_bazel_rules_docker//repositories:repositories.bzl",
    container_repositories = "repositories",
)
container_repositories()

load(
    "@io_bazel_rules_docker//container:container.bzl",
    "container_pull",
)

# pull 基础镜像,以便在 BUILD 中引用
container_pull(
  name = "base",
  registry = "gcr.io",
  repository = "distroless/base",
)
复制代码

因为 gcr.io 被墙了,我将镜像上传到了 dockerhub 一份,有需要的可以使用我的:

docker pull maxadd/distroless-base
复制代码

当然如果你不放心的话,也可以找找国内的加速平台(搜索“gcr.io加速”)。当然,如果镜像改了,上面 WORKSPACE 文件中的内容也要改。

编译镜像

定义好依赖之后,我们在 WORKSPACE 下创建一个包,用来编译镜像。

# mkdir test
# cp /tmp/x.tar . # 将 tar 包复制到当前目录
# vim BUILD
load(
    "@io_bazel_rules_docker//container:container.bzl",
    "container_image",
)

container_image(
    name = "app",
    base = "@base//image",
    tars = ["x.tar"], # 解压 tar 包
    ports = ["8080"], # 有其他端口可以往里添加
    env = {
        "PATH": "/bin:/opt/java/bin:/opt/java/jre/bin",
        "JAVA_HOME": "/opt/java",
    },
    cmd = ["/opt/tomcat/start.sh"] # 启动镜像后执行的命令
)
复制代码

开始编译:

# cd ..
# bazel build //test:app
复制代码

//test:app 是一个 target,后面会提到,使用它来指定我们要编译哪个目录。test 指的是我们创建的 test 目录,因为它下面有 BUILD 文件,所以它也称为一个包。app 则是指 BUILD 文件中 container_image 下面 name 的值,通过 //test:app 就能定位到它的位置。

我是在国外 vps 上编译,不知道在国内环境会不会有影响。。

编译完成后,在当前目录下执行:

bzael-bin/tese/app
复制代码

然后你可以使用通过 docker images 看到 bazel/test 这个镜像了,注意它的 tag 是 app 而非 latest。

启动镜像

启动它和其他镜像一样:

docker run --cap-add=SYS_PTRACE bazel/test:app
复制代码

之所以加上 --cap-add=SYS_PTRACE 是为了能够使用 jps 等命令。

好了,操作部分到此结束,希望你能从其中学到一些东西,这也是我希望的。下面是 bazel 的一些简单的介绍,完全是我看了官方文档之后写的一些简单的理解,很粗糙,也没时间仔细写。真正有需要的话,还是建议直接阅读官方文档。

Bazel

构建工具,用来编译各类语言的源代码,除此之外,它还可以构建 docker 镜像,也就是 distroless 系列。

基础概念

使用之前,了解其基础概念。

Workspace

workspace 是一个目录,包含你想要编译的源文件,以及指向包含编译输出目录的链接文件。每个 workspace 中包含一个名为 WORKSPACE 的文件,这个文件可以为空,也可以包含此次编译需要的外部依赖。

Workspace 是一个顶级的结构,下面可以有多个包。

Packages

工作空间中代码组织的主要单元是包。包是相关文件的集合,以及它们之间的依赖关系的规范。

包所在的目录下必须存在名为 BUILDBUILD.bazel 的文件。包中包含其目录中的所有文件,以及其下的所有子目录,除了那些本身包含 BUILD 文件的子目录。

包名的命名格式为 my/app,下面的示例中有 my/app 和其子包 my/app/tests 这两个。注意 my/app/data 不是包,但是它属于 my/app 这个包。

src/my/app/BUILD
src/my/app/app.cc
src/my/app/data/input.txt
src/my/app/tests/BUILD
src/my/app/tests/test.cc
复制代码

Targets

包是容器,包中的元素称为 targets。大部分的 targets 分为两种:files 和 rules。除此之外,还有 package groups,不过很少使用。

files 进一步分为两个种类:源文件和生成文件。源文件通过规则转换成生成文件。

第二种主要的 target 是 rule,也就是规则。源代码的编译就是通过规则进行。规则既可以使用源代码作为输入,也可以使用生成文件作为输入,因此你可以自己构成一个构建链。

一个规则的输入也许包含其他规则。

Labels

target 的名称称为它的标签。一个标签的示例:

//my/app/main:app_binary
复制代码

每个标签由两部分构成:包的名称(my/app/main)和 target 的名称(app_binary)。每个标签唯一的标识一个 target。

标签有时还会以另一种形式出现,当冒号 : 和 target 名称都省略时,target 名称会被认为和包名中最后一个单词相同。因此下面两个 target 是等价的:

//my/app
//my/app:app
复制代码

短标签名 //my/app 和包名并不冲突,因为标签名以 // 开头,而包名却不会。因此 my/app 是一个包,并且包含 target //my/app

在一个 BUILD 文件中,标签的包名部分也许会被省略,冒号也同样如此。因此,在一个 my/app 包的 BUILD 文件中(当然这个 BUILD 的标签名为 //my/app:BUILD),下面几种标签的标签形式都是相同的:

//my/app:app
//my/app
:app
app
复制代码

有一个惯例,对于普通文件冒号会被省略,但是规则文件不这样,不过这不重要。

同样,在 BUILD 文件中,属于该包的文件可以直接使用文件名进行引用。

generate.cc
testdata/input.txt
复制代码

不过在其他包中,或者在命令行,你必须将其标签写全,比如 //my/app:generate.cc。即使这个文件在子包中。

命名规范

为了不和 shell 一些保留字符冲突,因此标签名有意做了严格限制。

包名是一个包含 BUILD 文件的目录的名称,相对于顶级的源文件树。

项目名中不能使用 -

Rules

规则指定输入和输出之间的关系,以及构建输出的步骤。规则可以是许多不同的 kinds 或 classes 的一种。

每个规则都有一个名称,通过 name 属性指定。名称必须是一个有效的 target 名称,如命名规范中所述。

有些场景中,名称意义不大;而在另外一些场景中,名称是有意义的,比如在 *_binary*_test 规则中,下面例子中的 name 就确定了编译后二进制文件的名称。

cc_binary(
    name = "my_app",
    srcs = ["my_app.cc"],
    deps = [
        "//absl/base",
        "//absl/strings",
    ],
)
复制代码

每个规则都有很多属性,每个属性也有各自的类型。

BUILD 文件

每个包下面必须存在 BUILD 文件,文件中的内容被 Starlark 语言按顺序解释。

为了鼓励将代码和数据进行分离,BUILD 文件中不能定义函数、for 和 if(列表推导中可以使用 if),函数应该定义在 .bzl 文件中。另外,*args**kwargs 也不允许出现在 BUILD 文件中,你需要明确的给出参数列表。

BUILD 文件使用 Latin-1 字符集进行解释,因此不要在其中使用中文。

load

Bazel 的扩展是以 .bzl 结尾的文件,使用 load 语句用来从一个扩展中导入 symbol(这个 symbol 是扩展中的一个结构)。

load("//foo/bar:file.bzl", "some_library")
复制代码

这个代码会加载 foo/bar/file.bzl 文件,然后将 some_library 添加到当前环境。它可以用来加载新的规则、函数或者常量。多个 symbols 可以使用参数同时导入,参数不能是变量,必须是字符串,且 load 必须顶格写,也就是它不能出现在函数中。

load 的第一个参数是 .bzl 文件的标签名,如果是一个相对标签(使用 : 开头),那么它会被相对于包含当前 bzl 文件的包(而不是目录)进行解析。

load 还支持别名:

load("//foo/bar:file.bzl", library_alias = "some_library")
复制代码

您可以在一个 load 语句中定义多个别名。此外,参数列表可以包含别名和常规符号名称。以下示例完全合法(请注意何时使用引号)。

load(":my_rules.bzl", "some_rule", nice_alias = "some_other_rule")
复制代码

.bzl 文件中,以 _ 开头的 symbol 不会导出,也无法从其他文件加载。

外部依赖

外部依赖定义在 workspace 目录下的 WORKSPACE 文件中。只要你在 WORKSPACE 文件中定义好你想使用的外部依赖,那么你就可以在该 workspace 下面包的 BUILD 文件中通过外部依赖的名称来引用它。

比如,有下面两个项目在文件系统上:

/
  home/
    user/
      project1/
        WORKSPACE
        BUILD
        srcs/
          ...
      project2/
        WORKSPACE
        BUILD
        my-libs/
复制代码

假如 project1 想要依赖一个定义在 /home/user/project2/BUILD 中的 target :foo,那么可以在 project1/BUILD 中添加 @project2//:foo

外部依赖除了可以是其他的 workspace,还可以是其他文件系统上的文件,以及可以是从互联网上下载的文件,你甚至可以自定义 Repository Rules 来提供更复杂的行为。

WORKSPACE 文件的语法和 BUILD 文件相同,不过允许更复杂的规则集。

Bazel 支持以下的外部依赖:

  • 其他 Bazel 项目
  • 非 Bazel 项目
  • 外部包

依赖其他 Bazel 项目

可以使用 local_repository、 git_repositoryhttp_archive

比如引用当前文件系统中的其他 Bazel 项目:

local_repository(
    name = "coworkers_project",
    path = "/path/to/coworkers-project",
)
复制代码

如果 coworkers-project 项目中有一个 target //foo:bar,你可以在 BUILD 中通过 @coworkers_project//foo:bar 来引用。

依赖非 Bazel 项目

使用 new_ 开头的规则,比如 new_local_repository 可以让你从非 Bazel 项目中创建 target。

假设你正在定义 my-project/ 项目,但是这个项目会用到 coworkers-project/ 项目生成的一个 .so 文件,你可以在 my_project/WORKSPACE 文件中这么定义:

new_local_repository(
    name = "coworkers_project",
    path = "/path/to/coworkers-project",
    build_file = "coworker.BUILD",
)
复制代码

build_file 指定一个 BUILD 文件去覆盖已存在的项目,coworker.BUILD 文件内容如下:

cc_library(
    name = "some-lib",
    srcs = glob(["**"]),
    visibility = ["//visibility:public"],
)
复制代码

然后你就可以在你项目的 BUILD 文件中通过 @coworkers_project//:some-lib 进行引用了。

 类似资料: