conda中使用ecCodes不支持JPEG解码的问题查错

目录

本文是对笔者近期遇到的一次 Python + ecCodes 解码出错问题的总结。

背景

最近在 CMADaaS 容器中测试处理 GRIB2 数据的 Python 脚本,使用 CMA 镜像安装 Anaconda 环境和软件包,运行后报错,提示 ecCodes 不支持 JPEG 解码。 报错信息如下:

>>> import eccodes
>>> f = open("Z_NAFP_C_BABJ_20230409180000_P_NWPC-GRAPES-GFS-HNEHE-12000.grib2", "rb")
>>> m = eccodes.codes_grib_new_from_file(f)
>>> eccodes.codes_get_double_array(m, "values")
ECCODES ERROR   :  JPEG support not enabled.
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/space/cmadaas/dpl/CEMC_OPER_PROD/anaconda3/envs/py311-plot/lib/python3.11/site-packages/gribapi/gribapi.py", line 1196, in grib_get_double_array
    GRIB_CHECK(err)
  File "/space/cmadaas/dpl/CEMC_OPER_PROD/anaconda3/envs/py311-plot/lib/python3.11/site-packages/gribapi/gribapi.py", line 230, in GRIB_CHECK
    errors.raise_grib_error(errid)
  File "/space/cmadaas/dpl/CEMC_OPER_PROD/anaconda3/envs/py311-plot/lib/python3.11/site-packages/gribapi/errors.py", line 382, in raise_grib_error
    raise ERROR_MAP[errid](errid)
gribapi.errors.FunctionalityNotEnabledError: Functionality not enabled

原因

报错原因是在该容器在 /usr/local 目录下安装了没有启用JPEG支持的 eccodes 库。

检查 /usr/local/bin/grib_ls 的依赖库

$ ldd ./grib_ls
        linux-vdso.so.1 =>  (0x00007ffd999ef000)
        libeccodes.so => /usr/local/bin/./../lib64/libeccodes.so (0x00007f9a074c3000)
        libm.so.6 => /lib64/libm.so.6 (0x00007f9a071c1000)
        libc.so.6 => /lib64/libc.so.6 (0x00007f9a06df4000)
        libpthread.so.0 => /lib64/libpthread.so.0 (0x00007f9a06bd8000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f9a07989000)

没有 JPEG 的依赖库。

检查 conda 环境中安装的 /anaconda3/envs/py311-plot/bin/grib_ls 的依赖库

$ ldd ./grib_ls
        linux-vdso.so.1 =>  (0x00007fff48cd6000)
        libeccodes.so => /anaconda3/envs/py311-plot/bin/./../lib/libeccodes.so (0x00007f4caf931000)
        libc.so.6 => /lib64/libc.so.6 (0x00007f4caf564000)
        libm.so.6 => /lib64/libm.so.6 (0x00007f4caf262000)
        libjasper.so.4 => /anaconda3/envs/py311-plot/bin/./../lib/./libjasper.so.4 (0x00007f4cafca6000)
        libpthread.so.0 => /lib64/libpthread.so.0 (0x00007f4caf046000)
        libaec.so.0 => /anaconda3/envs/py311-plot/bin/./../lib/./libaec.so.0 (0x00007f4cafc9c000)
        libpng16.so.16 => /anaconda3/envs/py311-plot/bin/./../lib/./libpng16.so.16 (0x00007f4cafc5e000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f4cafc08000)
        libjpeg.so.9 => /anaconda3/envs/py311-plot/bin/./../lib/././libjpeg.so.9 (0x00007f4caf007000)
        libz.so.1 => /anaconda3/envs/py311-plot/bin/./../lib/././libz.so.1 (0x00007f4cafc43000)

可以看到 conda 安装的 eccodes 使用了 libjasper 库支持 JPEG 解码。

以上信息可以推测出 eccodes python API 首先定位到 /usr/local 下面的 eccodes,而没有使用 conda 环境中的库。

下面就更进一步分析为什么 eccodes python API 没有直接使用 conda 安装的自带库。

深入分析

浏览 eccodes python 的源代码,在 gribapi/bindings.py 中发现如下代码:

try:
    import ecmwflibs as findlibs
except ImportError:
    import findlibs

library_path = findlibs.find("eccodes")
if library_path is None:
    raise RuntimeError("Cannot find the ecCodes library")

由此可知 eccodes 接口使用 findlibs 查找 eccodes 的库文件。 findlibs 也是 ecmwf 开发的开源库。

直接执行上述代码:

>>> import findlibs
>>> findlibs.find("eccodes")
'/usr/local/lib64/libeccodes.so'

发现 eccodes python 库确实首先找到了 /usr/local 下面的 eccodes 库。

进一步查看 findlibs/__init__.py 的源代码:

__version__ = "0.0.2"

EXTENSIONS = {
    "darwin": ".dylib",
    "win32": ".dll",
}


def find(name):
    """Returns the path to the selected library, or None if not found."""

    extension = EXTENSIONS.get(sys.platform, ".so")

    for what in ("HOME", "DIR"):
        LIB_HOME = "{}_{}".format(name.upper(), what)
        if LIB_HOME in os.environ:
            home = os.environ[LIB_HOME]
            fullname = os.path.join(home, "lib", "lib{}{}".format(name, extension))
            if os.path.exists(fullname):
                return fullname

    for path in (
        "LD_LIBRARY_PATH",
        "DYLD_LIBRARY_PATH",
    ):
        for home in os.environ.get(path, "").split(":"):
            fullname = os.path.join(home, "lib{}{}".format(name, extension))
            if os.path.exists(fullname):
                return fullname

    for root in ("/", "/usr/", "/usr/local/", "/opt/"):
        for lib in ("lib", "lib64"):
            fullname = os.path.join(home, "{}{}/lib{}{}".format(root, lib, name, extension))
            if os.path.exists(fullname):
                return fullname

    return ctypes.util.find_library(name)

在该版本中 (0.0.2),查找分为 4 个步骤

  1. 在环境变量 ECCODES_HOMEECCODES_DIR 中的 liblib64 目录中查找
  2. 在环境变量 LD_LIBRARY_PATHDYLD_LIBRARY_PATH 中的 liblib64 目录中查找
  3. 在目录 //usr/usr/local/optliblib64 目录中查找
  4. 调用 Python 内置的 ctypes.util.find_library 函数

因为容器环境存在 /usr/local/lib64/libeccodes.so 文件,所以在第3步中返回该路径。

手动执行第4步:

>>> import ctypes.util
>>> ctypes.util.find_library("eccodes")
'/anaconda3/envs/py311-plot/lib/libeccodes.so'

可以找到 conda 环境安装的 eccodes 库,由此可见问题就出在 findlibs 库的实现逻辑上。

解决方案

简单方案

笔者在开发中选用的方案是将 conda 库路径添加到 LD_LIBRARY_PATH 环境变量中。 但该方案仅为了能快速跑通脚本,存在各种问题,比如如果环境迁移了脚本就失效了。

也可以直接删掉 /usr/local 下的 eccodes 库,或者重新编译支持 JPEG 的版本。

更新 findlibs 库

容器 conda 环境安装的 findlibs 是 0.0.2 版本。

实际上,findlibs 在 0.0.3 及以后版本中就对本文的问题进行了修正。 在最新的 0.0.5 版本 (2023.04.21检索) 中,find 函数开头有了下面的代码:

# sys.prefix/lib, $CONDA_PREFIX/lib has highest priority;
# otherwise, system library may mess up anaconda's virtual environment.

roots = [sys.prefix]
if "CONDA_PREFIX" in os.environ:
    roots.append(os.environ["CONDA_PREFIX"])

for root in roots:
    for lib in ("lib", "lib64"):
        fullname = os.path.join(root, lib, "lib{}{}".format(lib_name, extension))
        if os.path.exists(fullname):
            return fullname

从注释就可以看到新版本将 conda 安装的 eccodes 库作为最高优先级,而不是最后一步才检查。

更新 findlibs 到最新版就可以解决这个问题。 如果 conda forge 没有更新可以手动下载源码安装。

后记

笔者实际的查错过程比较费劲。 首先怀疑是 CMA 镜像的问题。在容器中使用清华镜像重新安装,运行报错。在超算上使用CMA镜像安装同样的环境,运行正常。 经过多次尝试后,才开始浏览 eccodes python 接口源码,直到手动运行 findlibs.find("eccodes") 函数才最终定位到问题所在。

快速定位问题是编程熟练工种的必备技能。 如果下次遇到类似的问题,可能就更有经验了。

参考

findlibs源码:https://github.com/ecmwf/findlibs/