前段时间帮别人安装了大气科学相关的一套建模系统——Weather Research and Forecasting Model,以及其预处理工具 WPS(WRF Preprocessing System,不是金山那个 WPS)。其构建过程不可谓不复杂,遂考虑用容器化的方式实现可控的构建环境,并写一篇随笔记录一下成品的使用和遇到的困难。

Note: 此前没怎么写过容器相关的东西,不太明白各种情况下容器化的 Best Practice,欢迎各位指正 & 批判 & 提出改进意见。

需要考虑的问题

受限的生产环境

用户使用 WPS/WRF 的服务器使用的是 AlmaLinux 8.10 系统(RHEL 兼容的系统,大概是 CentOS 的精神后继),并且没有 root 权限。直接构建的过程中遇到了 conan 不好装等问题。同时在这台服务器上安装 Podman/Docker 也较为繁琐,即使安装 Rootless Docker。

但在笔者哼哧哼哧装完 Rootless Docker 后才十分小丑地发现,这台机器已经装了 Podman,并且可以 Rootless 地运行(已经给每个用户配置好了 subgid/subuid)。

构建依赖

WPS 和 WRF 有一堆依赖,除了烂大街的 glibc/gfortran/zlib/curl 之类外,还包括:

  • MPICH,最简单的一个,没有 C/Fortran 系统库以外的依赖,但会给 WPS/WRF 产生一个动态链接的依赖,同时后续的构建也需要用 MPICH 提供的 mpif90 编译器(或许应该称为某种编译器前端)
  • HDF5,依赖 zlib/szip 还算简单,问题是 Conan 版的 Recipe 没有构建 Fortran 库,因此需要自己用 CMake 构建
  • NetCDF 4:需要用到 HDF5 和 zlib/szip,conan 上的版本有点旧,并且没有 Fortran 版,因此自己构建。C 和 Fortran 的库是分开的,先构建 netcdf-c 再在其基础上构建 netcdf-fortran
  • jasper/libpng/libjpeg:图片压缩库,从 conan 拉就好了

其中 zlib/curl/jasper/libpng/libjpeg 之类的没有修改构建配置,用的人又比较多的库,可以直接用 conan 安装,但 HDF5、NetCDF 4、MPICH 这三者必须手动编译。

成品使用教程

构建脚本已经全部传了 GitHub :https://github.com/cqjjjzr/WRF-WPS-Docker-Containerized 。使用分为两种情况:在容器里面运行成品,和需要把成品二进制取出来在外面跑。同时提供了两个版本,区别在基础镜像为 Ubuntu 24.04 或 Alma Linux 8.10。

Usage 1: 在容器里运行

  1. 克隆代码库:git clone https://github.com/cqjjjzr/WRF-WPS-Docker-Containerized.git 并 cd 进去
  2. Podman 用户请运行 podman build . -f Containerfile --build-arg-file=argfile.conf --tag=wrf_wps:latest。Docker 用户请运行 docker build -f Containerfile --tag=wrf_wps:latest .(不要漏掉最后的点)。如果想要构建以 Alma Linux 为基础镜像的容器,把 Containerfile 换成 Containerfile_almalinux 即可
  3. 上一命令执行完后会输出一个镜像 ID。直接用这个镜像 ID 或 wrf_wps:latest 进行 podman run 即可,WRF 和 WPS 的命令分别在 /build/WRF/run/build/WPS 下面。你可能需要挂载你的数据目录到容器里面,并 podman run bash 再从里面工作。你可能还需要阅读一下官方的 WRF_DOCKER 项目的教程

如果想要修改 WRF 和 WPS 在构建过程中的配置,请修改 argfile.conf(仅 Podman 可用)或通过 build-arg 参数传入。具体的配置项有:

  • wrf_config_1wrf_config_2:运行 WRF 的 configure 时分别传入的两个数字
  • wrf_type:运行 WRF 的 compile 时跟的参数
  • wps_config:运行 WPS 的 configure 时传入的数字

这里一个问题是,WRF 和 WPS 会读取其构建目录内的文件(namelist.wps 等),而我们是可能修改这些文件的(调参等),因此即用即丢的容器会造成这些配置修改的丢失。一个解决方案是先创建一个容器,把 WRF、WPS 目录 podman cp 拷出来,再在后续的 podman run 时,把两个目录挂载进去。

Usage 2: 将二进制文件复制出来用

尝试构建了容器后用着哪哪都别扭,又是挂载数据目录又是要把 WRF、WPS 的目录复制出来的。因此考虑用一个不那么合乎周礼的方法:先用一个和生产环境一样的系统版本作为基础镜像构建项目,再把构建好的文件取出来。

  1. 克隆代码库:git clone https://github.com/cqjjjzr/WRF-WPS-Docker-Containerized.git 并 cd 进去
  2. Podman 用户请运行 podman build . -f Containerfile --build-arg-file=argfile.conf --tag=wrf_wps:latest。Docker 用户请运行 docker build -f Containerfile --tag=wrf_wps:latest .(不要漏掉最后的点)。如果想要构建以 Alma Linux 为基础镜像的容器,把 Containerfile 换成 Containerfile_almalinux 即可。编译配置的修改同上。
  3. 复制出来二进制文件,在一个合适的地方(用于存放结果文件的目录):
podman create wrf_wps:latest # 先建一个容器方便取文件,记下返回的ID,替换到所有下列命令的<cont-id>里面
podman cp <cont-id>:/build/WRF ./out/WRF
podman cp <cont-id>:/build/WPS ./out/WPS
podman cp <cont-id>:/install_dir/mpich ./out/mpich
podman rm <cont-id>
  1. 此时 ./out/ 中已经有了可以用的二进制文件。但直接执行会报找不到 MPICH 的动态库,因此还需要设置 LD_LIBRARY_PATH 追加上到 ./out/mpich/lib 的绝对路径。推荐把这一设置加到 bash/zsh/fish 的 Profile 里面。未来改进应该把这个写进 rpath 里面!

关于版本更新

虽然笔者有支持升级版本的想法(比如各种依赖版本是写在 conanfile 和 argfile.conf 里面可以改的,而非硬编码),但由于项目里用到了一堆 Patch,因此这并不一定是一个简单的工作。基本的升级思路就是:先修改 argfile 和 conanfile 升级依赖版本与 checkout 时的 tag,再尝试构建。如果出现失败(特别是 Patch 应用失败),则修改各个 Patch 直到成功。

另外 HDF5 和 NetCDF-C 可以确定最新分支与目前使用的版本代码不匹配、无法应用 Patch。hdf5-cmake-d507b7c.patchnetcdf-c-cmake-0ad7164.patch 给出了最新分支上(截至写稿时)可以用的 Patch,仅作为参考。

编写过程中遇到的问题

项目目录里一堆 Patch 就已经很能说明问题了。

Conan 和 Build Type

conan 2 改进了 CMake 构建脚本的生成方式,从原先的手动修改 prefix 和 module 路径指向 conan 安装路径的方式改进到了 conan 吐一组 CMakePresets 和 CMakeUserPresets.json 文件的方式。这种情况下只需要在配置的时候用 cmake .. --preset conan-release 就带上各种 conan 的配置。

然而,conan 生成的 CMake 配置文件的问题在于,它内部通过 Generator Expression 的方式指定了只有当当前构建的 build type 与 conan install 时的设置相同(这里是 Release)时,才会正确设置各种 Target 的值,否则只会提供空值。一开始笔者没有发现这一点,同时也没有发现 NetCDF 会偷偷修改 build type,因此十分痛苦地调了很久,明明已经通过 target_link_libraries 传入了库,却没有进入最终的 Ninja 配置里面。期间怀疑过:

  • 间接依赖并没有被正确处理——添加为直接依赖也没有解决
  • 依赖定序问题,即先写被依赖的再写依赖的会造成丢失——再写一遍也没有解决
  • target 的依赖被整个覆盖——在 configure 阶段最后打印一遍依赖,发现没有被覆盖

最后是不知道在哪个阶段,笔者发现链接库里面有一大串 Generator Expression 指定条件为 Build Type = Release,才怀疑起 build type 不匹配,最后通过全文搜索发现 NetCDF 的 CMake 代码里面有覆盖了 build type 的地方,Patch 掉才解决。

各种依赖写漏

CMake 构建时依赖是通过 target_link_libraries 一个个传入的,而不是通过设置 LDFLAGSLIBS 漫灌,因此在写漏、写错依赖名的时候,就是不会 Link。这里没有容错的空间,因此 NetCDF 需要大量 Patch 补齐写漏、写错的依赖。几个典型的包括 szip 的大小写写错、HDF5::HDF5 应该换成 hdf5-static 等等,再补上一堆没有写出来的 find_package

WRF/WPS 补齐依赖的过程还要 Cursed 一些。因为这两个程序的构建既不是 autoconf 又不是 CMake(事实上是给了一份 CMake 的,但似乎不是特别完善),因此需要手动 Patch 其文件来提供参数。同时漏的一些库中,有一部分是来自 conan 的,笔者采取的办法是通过 conan 生成 JSON 格式的配置(conan install . --build missing --format json > conaninfo.json),再通过 jq 提取其中库的打包安装路径(export JASPER=$(cat conaninfo.json | jq -r '.graph.nodes | [.[] | select(.name == "jasper")] | first | .package_folder'))。同时为了保险,又让 conan 生成了一份 autoconf 版本的配置(事实上就是生成一个会设置 LDFLAGS 和 LIBS 的脚本),获得一组包含所有依赖的 LDFLAGS 与 LIBS 环境变量配置进去。实在是太罪恶了。

这两者的 configure 事实上是生成了一个 configure.wps 文件,其中包括各种参数,再在 Makefile 内去 include 这个配置文件。笔者的做法是修改了生成这个配置文件的脚本和模板,而非去修改生成后的配置文件,意图在使其能适应更多的构建环境,以及确保一定程度上的向上兼容。

P. S.

积攒起来还没写的东西有点多(NAS 和宿舍网络配置的文章已经拖了一年多了,都快拖到我毕业了……),缓更……


不想被自己的惰性打败。