所谓的“最小” tomcat 镜像是相对的,它的大小取决于如下几点:
- 基础镜像是否使用 glibc,也就是是否使用 alpine 作为基础镜像;
- 使用 jdk 还是只使用 jre 作为 tomcat 运行环境;
- 使用 openjdk 还是 oracle jdk。
上述的条件决定了 tomcat 镜像的大小。
总所周知,alpine 算是基础镜像中最小的了,它还自带包管理器,但是它的缺点也同样明显,就是它没有使用 glibc,因此会带来兼容性的问题。
本文会使用基于 glibc 的 distroless 作为基础镜像,然后使用 oracle jdk8 + tomcat8,最终镜像大小为 171M。这个大小是有一定浮动空间的,因为即使是使用 oracle jdk8,不同的小版本之间大小也会相差很大。
以下是和官方镜像的一个对比:
镜像名 | java | 大小 | 镜像层 | glibc |
---|---|---|---|---|
tomcat:8 | openjdk jre | 463MB | 31 | glibc |
tomcat:8-alpine | openjdk jre | 106MB | 25 | muslc |
tomcat:8-slim | openjdk jre | 223MB | 29 | glibc |
本文编译使用 | oracle jdk | 171M | 3 | glibc |
可以看出 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-gnu
、bin
、lib64
这几个目录。
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
工作空间中代码组织的主要单元是包。包是相关文件的集合,以及它们之间的依赖关系的规范。
包所在的目录下必须存在名为 BUILD
或 BUILD.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_repository
和 http_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
进行引用了。