Conan Docker 工具 - ConanCenter C++ 包构建的官方 Docker 镜像
为了实现包的统一分发,Conan 需要一个基础设施,允许构建包的环境保持一致。这个方案从项目伊始就被考虑,并在整个开发过程中占据了很大一部分。在这篇文章中,我们将介绍 Conan Docker 工具 项目的历史、问题和解决方案,该项目是 Conan 附属项目,专注于 Docker 镜像。
序言
为了在 Conan 社区和 Bincrafters 项目的 CI 服务中建立构建环境的标准化,使用 Docker 是最被接受的方案,因为它生成的镜像大小适中,并且易于维护。
因此,Conan Docker 工具项目启动了,支持最少数量的 Docker 镜像,所有镜像均基于 Ubuntu,安装了标准编译器(根据发行版的版本),以及一些其他支持系统包。
发展
最初的镜像在 Ubuntu 14.04 (Trusty) 和 17.10 (Artful) 之间有所不同,具体取决于发行版之间可用的 GCC 和 Clang 版本。因此,可以获得 GCC 4.8 到 GCC 7 的镜像,以及 Clang 3.9 及更高版本的镜像。当时使用的 Python 版本还是 2.7,并且现在在 Conan Center Index 中分发的很大一部分包都是通过系统获得的。
随着时间的推移,添加了新的镜像,要么是因为新的编译器版本可用,要么是因为社区的要求。除了与每个编译器较小版本相关的其他镜像(仅限于 GCC 6)之外,还生成了专门用于交叉编译(ARM)的镜像。
每个镜像的大小约为 900MB,其中包含支持构建 64 位和 32 位项目的编译器,以及 Python 2.7、Conan 和各种实用程序,如 wget、vim、curl、ninja 和 valgrind。虽然最终大小小于使用虚拟机,但它仍然远大于用于 CI 目的的常规镜像。一段时间后,一些实用程序被移除,例如 vim 和 valgrind,因为它们仅用于调试目的,而调试仅占不到 10% 的情况。此外,Python 已升级到版本 3,但它是通过 pyenv 项目安装的,从而在所需版本方面提供了更大的灵活性,而不管发行版如何。此重构使镜像大小减少了 15%。
问题的征兆
尽管努力跟上 GCC 和 Clang 编译器新版本的发布,但无法使用相同的发行版,因为相同的编译器只能在新的版本中可用,这意味着多个发行版。然而,存在一个更大的问题,每个发行版都有一个特定的 glibc 库版本,该库版本仅向后兼容,在 Conan 包之间造成了微妙的状况。这种情况已被社区报告,并在问题 #1321 中得到了很好的解释。
安装程序的包,即仅包含可执行文件(cmake、7z、ninja 等)的包,在包 ID 中不考虑编译器,因此,无论使用哪个版本的编译器,每个平台的包都应该相同。这导致了一个与 glibc 版本相关的问题,其中需要将此类包构建到尽可能旧的版本,以避免在使用该包时出现错误。为此,构建了一个基于 CentOS 6 的镜像,并与 GCC 7 关联,其中 glibc 版本足够旧,可以与项目中可用的任何其他镜像兼容。
随着时间的推移和编译器的演变,我们来到了以下场景
如您所见,对于每个新的编译器版本,我们可能会拥有一个新的发行版,并且由于 glibc 版本的原因,它将与之前的版本不兼容。为了更好地说明这种情况,让我们使用以下示例:ninja/1.10.0
包仅提供可执行文件,因此 settings.compiler
不是其包 ID 的一部分,因此,编译器及其版本未被考虑在内。因此,如果使用 Ubuntu 18.04 (Bionic)、GCC 8 和 glibc 版本 2.27 构建 ninja/1.10.0
包,它将仅与具有相同或更高 glibc 版本的发行版兼容,否则,由于缺少请求版本中的库,在运行时会发生错误。
Conan 社区了解此类问题 (#213),并且今天它是 Conan Center Index 中需要解决的挑战之一。
独特处理
尽管遵循新版本的演变,但维护成本和不兼容因素随着每个新镜像的增加而增加,最终达到无法维持的程度。因此,我们决定最好选择重新设计镜像,仅使用一个通用的版本,该版本足够旧以支持 glibc 的先前版本,但又足够新以获得发行版支持(长期支持)。
选择的基镜像是 Ubuntu Xenial (16.04),因为它属于 LTS 版本,仍然得到支持,并且足够旧,可使用 glibc 2.23。选择单个发行版版本并支持所有编译器及其版本所带来的成本是,需要从源代码构建它们,从而增加了 CI 中每个工作的时长。时长增加了多少?大约 2 倍。以下是 GCC 9 的当前 Conan Docker 构建脚本与新的集中版本之间的比较
当前版本
FROM ubuntu:eoan
...
RUN dpkg --add-architecture i386 \
&& apt-get -qq update \
&& apt-get -qq install -y --no-install-recommends \
sudo \
binutils \
wget \
git \
libc6-dev-i386 \
libc6-dev \
linux-libc-dev:i386 \
g++-9-multilib \
...
&& update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-9 100 \
...
&& useradd -ms /bin/bash conan -g 1001 -G 1000,2000,999 \
...
&& wget --no-check-certificate --quiet -O /tmp/pyenv-installer \
https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer \
&& chmod +x /tmp/pyenv-installer \
&& /tmp/pyenv-installer \
&& rm /tmp/pyenv-installer \
...
&& update-alternatives --install /usr/bin/python python /opt/pyenv/shims/python 100 \
...
USER conan
WORKDIR /home/conan
RUN mkdir -p /home/conan/.conan \
&& printf 'eval "$(pyenv init -)"\n' >> ~/.bashrc \
&& printf 'eval "$(pyenv virtualenv-init -)"\n' >> ~/.bashrc
新版本
FROM ubuntu:xenial
...
RUN apt-get -qq update \
&& apt-get -qq install -y --no-install-recommends \
sudo \
build-essential \
wget \
git \
libc6-dev \
gcc \
...
&& rm -rf /var/lib/apt/lists/*
RUN wget --no-check-certificate --quiet -O /opt/gcc-${GCC_VERSION}.tar.gz \
https://github.com/gcc-mirror/gcc/archive/releases/gcc-${GCC_VERSION}.tar.gz \
&& tar zxf /opt/gcc-${GCC_VERSION}.tar.gz -C /opt \
&& cd /opt/gcc-releases-gcc-${GCC_VERSION} \
&& ./configure --prefix=/usr/local \
--enable-languages=c,c++ \
--disable-bootstrap \
...
&& make -j "$(nproc)" \
&& make install-strip \
&& cd - \
&& rm -rf /opt/gcc* \
&& apt-get remove -y gcc gcc-5 \
&& update-alternatives --install /usr/bin/gcc gcc /usr/local/bin/gcc 100 \
...
RUN groupadd 1001 -g 1001 \
...
&& useradd -ms /bin/bash conan -g 1001 -G 1000,2000,999 \
...
RUN wget --no-check-certificate --quiet -O /tmp/pyenv-installer \
https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer \
&& chmod +x /tmp/pyenv-installer \
&& /tmp/pyenv-installer \
...
RUN pip install -q --no-cache-dir conan conan-package-tools cmake==${CMAKE_VERSION} \
&& chown -R conan:1001 /opt/pyenv \
&& find /opt/pyenv -iname __pycache__ -print0 | xargs -0 rm -rf \
&& update-alternatives --install /usr/bin/python python /opt/pyenv/shims/python 100 \
...
USER conan
WORKDIR /home/conan
RUN mkdir -p /home/conan/.conan \
&& printf 'eval "$(pyenv init -)"\n' >> ~/.bashrc \
&& printf 'eval "$(pyenv virtualenv-init -)"\n' >> ~/.bashrc
FROM base as release
RUN pip install -U conan conan-package-tools
新的构建脚本版本比当前版本更大,因为它从源代码构建 GCC,并使用了 多阶段构建 功能。但是,保留了一些要点
- 系统包 (APT) 是基本实用程序(例如 wget、git 等)和构建所需的预先库(例如 python 的 libsqlite3)所必需的。
- GCC 和 Python 通过
update-alternatives
命令配置为默认值。 - 添加了一个非 root 用户 (conan),并默认使用它。这是 Docker 社区提出的安全建议。
- pyenv-installer 用于安装 pyenv,因此 Python 版本是灵活的。
这些镜像是全新的,因此它们将获得一个新的唯一名称,以避免与以前的镜像发生任何可能的冲突。可能名称只带一个后缀,例如:conan-gcc10-cci。这样,就不会破坏旧的镜像,并且将有助于向新的镜像过渡。
新版本使用多阶段构建功能来缓存 base 镜像并在任何新的 Conan 版本中重复使用它。此策略可以节省构建 Docker 镜像的时间。构建 Docker 镜像的正常时间大约为 15 分钟。但是,使用多阶段构建并为将来的构建保留基础层,此时间应该仅降至 1 分钟,以便在每次有新版本发布时更新 Conan 版本。
目前,拉取请求 #204 正在实现基镜像的新版本,该版本应该包含当前和将来的编译器。此外,此镜像将来将用于在 Conan Center Index 中生成 Conan 包。
结论
对于包分发和行为复制,构建环境的实现对于包管理器至关重要,如果没有它,每次遇到构建错误时都会增加许多未知因素,每个开发环境最终都会变得不同,并且难以重现。但是,构建环境的开发和维护同样具有挑战性,并且可能导致多个环境,这些环境与没有特定环境一样,是不同的且不兼容的。
Conan Docker 工具证明,选择更新所使用的发行版而不是通过源代码构建编译器来维护使用已在发行版中可用的相同编译器版本的易用性和敏捷性,会付出更高的代价。随着新的编译器及其版本的引入,glibc 版本之间的不兼容性以及维护时间的增加开始比仅更新编译器花费更多。
作为此用例的演变,一个新的镜像将基于单个发行版版本,用于所有编译器及其版本,从而修正了大量镜像及其多个版本。
如果您有兴趣了解这些镜像,请访问 conan-docker-tools 存储库,或者如果您想对这种新方法发表评论,请在问题 #205 中发表评论。