LLVM 构建 Dockerfile 指南

简介

您可以在 llvm/utils/docker 中找到许多用于构建包含 LLVM 组件的 Docker 镜像的源代码。任何想要为自身构建 Docker 镜像的人,或者想要编写自己的 Dockerfile 的人,都可以使用这些源代码。 它们也可以作为起点。

我们目前提供了基于 debian10nvidia-cuda 基础镜像的 Dockerfile。我们还提供了一个 example 镜像,其中包含了创建新 Docker 镜像的 Dockerfile 所需的占位符。

为什么?

Docker 镜像提供了一种在受控环境中生成软件二进制发行版的方式。在 LLVM 代码库中提供构建 Docker 镜像的 Dockerfile,使得它们比放在其他任何地方都更容易被发现。

Docker 基础知识

如果您以前从未听说过 Docker,那么本节可能会对您有所帮助,它提供了 Docker 的基本解释。Docker 是一种流行的解决方案,用于在隔离且可重现的环境中运行程序,尤其是在维护部署到大型分布式集群的软件版本时。它使用 Linux 内核命名空间和控制组,在当前运行的 Linux 内核中提供轻量级的隔离。Docker 化环境的一个活动实例称为Docker 容器。Docker 容器文件系统的快照称为Docker 镜像。可以从预构建的 Docker 镜像启动容器。

Docker 镜像是根据所谓的Dockerfile构建的,Dockerfile 是用一种专门的语言编写的源文件,其中定义了构建 Docker 镜像时要使用的指令(有关更多详细信息,请参阅官方文档)。一个最小的 Dockerfile 通常包含一个基础镜像和一些必须执行的 RUN 命令,以构建镜像。在构建新镜像时,Docker 首先会下载您的基础镜像,将其文件系统以只读方式挂载,然后在其顶部添加一个可写覆盖层,以跟踪在构建镜像期间执行的所有文件系统修改。构建过程完成后,将存储构建的镜像与基础镜像文件系统之间的差异。

概述

llvm/utils/docker 文件夹包含 Dockerfile 和简单的 bash 脚本,作为任何想要创建包含 LLVM 组件的 Docker 镜像(从源代码编译)的基础。在构建镜像时,源代码将从上游 Git 代码库中检出。

生成的镜像仅包含请求的 LLVM 组件和一些额外的软件包,以使镜像最适合 C++ 开发,例如 libstdc++ 和 binutils。

运行构建的接口是 build_docker_image.sh 脚本。它接受要检出的 LLVM 代码库列表以及 CMake 调用参数。

如果您想编写自己的 Docker 镜像,请从 example/ 子文件夹开始。它提供了一个不完整的 Dockerfile,其中包含 (很少的) FIXME,解释了使您的 Dockerfile 可运行所需采取的步骤。

用法

llvm/utils/build_docker_image.sh 脚本提供了对如何运行构建的高度控制。它允许您指定要从 Git 中检出的项目,并提供在 Docker 容器内构建 LLVM 时要使用的 CMake 参数列表。

以下是一个使用系统编译器在 debian10 镜像中编译 clang 二进制文件的 Docker 镜像的简单示例:

./llvm/utils/docker/build_docker_image.sh \
    --source debian10 \
    --docker-repository clang-debian10 --docker-tag "staging" \
    -p clang -i install-clang -i install-clang-resource-headers \
    -- \
    -DCMAKE_BUILD_TYPE=Release

请注意,这样的构建不使用您可能需要的 clang 的两阶段构建过程。运行两阶段构建稍微复杂一些,以下命令可以做到这一点:

# Run a 2-stage build.
#   LLVM_TARGETS_TO_BUILD=Native is to reduce stage1 compile time.
#   Options, starting with BOOTSTRAP_* are passed to stage2 cmake invocation.
./build_docker_image.sh \
    --source debian10 \
    --docker-repository clang-debian10 --docker-tag "staging" \
    -p clang -i stage2-install-clang -i stage2-install-clang-resource-headers \
    -- \
    -DLLVM_TARGETS_TO_BUILD=Native -DCMAKE_BUILD_TYPE=Release \
    -DBOOTSTRAP_CMAKE_BUILD_TYPE=Release \
    -DCLANG_ENABLE_BOOTSTRAP=ON -DCLANG_BOOTSTRAP_TARGETS="install-clang;install-clang-resource-headers"

这将从最新的上游修订版生成一个新的镜像 clang-debian10:staging。构建镜像后,您可以像这样在基于您的镜像的容器中运行 bash:

docker run -ti clang-debian10:staging bash

现在您可以像往常一样运行 bash 命令了。

root@80f351b51825:/# clang -v
clang version 5.0.0 (trunk 305064)
Target: x86_64-unknown-linux-gnu
Thread model: posix
InstalledDir: /bin
Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-gnu/4.8
Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-gnu/4.8.4
Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-gnu/4.9
Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-gnu/4.9.2
Selected GCC installation: /usr/lib/gcc/x86_64-linux-gnu/4.9
Candidate multilib: .;@m64
Selected multilib: .;@m64

我应该选择哪个镜像?

我们目前提供了两个镜像:基于 Debian10 和基于 nvidia-cuda 的镜像。它们的区别在于它们使用的基础镜像,即它们具有不同的预安装二进制文件集。Debian10 非常精简,nvidia-cuda 较大,但预安装了 CUDA 库,并允许访问您机器上安装的 GPU。

如果您需要一个仅包含 clang 和 libstdc++ 的最小 Linux 发行版,则应尝试基于 Debian10 的镜像。

如果您想使用 CUDA 库并访问您机器上的 GPU,则应选择基于 nvidia-cuda 的镜像并使用 nvidia-docker 运行 Docker 容器。请注意,您不需要 nvidia-docker 来构建镜像,但需要它才能从运行已构建镜像的 Docker 容器访问 GPU。

如果您有不同的用例,您可以根据 example/ 文件夹创建自己的镜像。

任何 Docker 镜像都可以仅使用 Docker 二进制文件构建和运行,即您可以在 Fedora 或任何其他 Linux 发行版上运行 debian10 构建。您无需安装 CMake、编译器或任何其他 clang 依赖项。所有这些都在 Docker 隔离环境中的构建过程中处理。

稳定构建

如果您想要一个相对较新且稳定的构建,请使用 branches/google/stable 分支,即以下命令将为您生成一个基于 Debian10 的镜像,使用最新的 google/stable 源代码:

./llvm/utils/docker/build_docker_image.sh \
    -s debian10 --d clang-debian10 -t "staging" \
    --branch branches/google/stable \
    -p clang -i install-clang -i install-clang-resource-headers \
    -- \
    -DCMAKE_BUILD_TYPE=Release

最小化 Docker 镜像大小

由于 Docker 文件系统的工作方式,所有中间写入都会保留在生成的镜像中,即使在后续命令中删除它们也是如此。为了最小化生成的镜像大小,我们使用 多阶段 Docker 构建。Docker 在内部构建两个镜像。第一个镜像完成所有工作:安装构建依赖项、检出 LLVM 源代码、编译 LLVM 等。第一个镜像仅在构建期间使用,并且没有描述性名称,即它只能在构建完成后通过哈希值访问。第二个镜像是我们的结果镜像。它仅包含构建的二进制文件,不包含任何构建依赖项。它也可以通过描述性名称访问(由 -d 和 -t 标志指定)。