Anaconda 编译器工具#

Anaconda 5.0 从操作系统提供的编译器工具切换到我们自己的工具集。这提高了编译器功能,包括更好的安全性和性能。本页介绍如何使用这些工具并启用这些优势。

编译器软件包#

在 Anaconda 5.0 之前,编译器是使用系统工具安装的,例如 XCode 或 yum install gcc。现在有适用于 Linux 和 macOS 编译器的 conda 软件包。与以前的 GCC 4.8.5 软件包(包括 GCC、g++ 和 GFortran 在同一个软件包中)不同,这些 conda 软件包被拆分为单独的编译器

macOS

  • clang_osx-64。

  • clangxx_osx-64。

  • gfortran_osx-64。

Linux

  • gcc_linux-64。

  • gxx_linux-64。

  • gfortran_linux-64。

编译器的“构建平台”是编译器运行和构建代码的平台。

编译器的“主机平台”是构建的代码最终将被托管和运行的平台。

请注意,所有这些软件包名称都以平台标识符结尾,该标识符指定主机平台。所有编译器软件包都特定于构建平台和主机平台。

使用编译器软件包#

编译器软件包可以使用 conda 安装。由于它们在设计时考虑了(伪)交叉编译,因此编译器软件包中的所有可执行文件都已“前缀化”。您使用的编译器的可执行文件名将类似于 x86_64-conda_cos6-linux-gnu-gcc,而不是 gcc。这些完整的编译器名称显示在构建日志中,记录主机平台并帮助防止使用错误编译器的常见错误。

许多构建工具(如 makeCMake)默认搜索名为 gcc 的编译器,因此我们设置环境变量以将这些工具指向正确的编译器。

我们在 conda activate.d 脚本中设置这些变量,因此您将在其中使用编译器的任何环境都必须首先激活,以便脚本运行。Conda-build 使用与编译器软件包一起安装在 CONDA_PREFIX/etc/conda/activate.d 中的激活挂钩为您执行此激活,因此无需额外的工作。

您可以使用命令 conda activate root 激活 root 环境。

macOS SDK#

macOS 编译器需要 macOS 10.9 SDK 或更高版本。SDK 许可证阻止将其捆绑在 conda 软件包中。我们知道 macOS SDK 的 2 个当前来源

我们通常将 10.10 SDK 安装在 /opt/MacOSX10.10.sdk,但您可以将其安装在任何位置。编辑您的 conda_build_config.yaml 文件以指向它,如下所示

CONDA_BUILD_SYSROOT:
  - /opt/MacOSX10.10.sdk        # [osx]

在 Anaconda,我们在配方仓库的根目录中有一个集中的 conda_build_config.yaml 文件中设置了此配置。由于我们从该位置运行构建命令,因此该文件和设置用于所有配方。conda_build_config.yaml 搜索顺序在 创建 conda-build 变体配置文件 中进一步描述。

macOS 的构建脚本应使用由 conda-build 设置的变量 MACOSX_DEPLOYMENT_TARGETCONDA_BUILD_SYSROOT(请参阅 环境变量)。这些变量应转换为正确的编译器参数,例如,对于 Clang,这将是

clang .. -isysroot ${CONDA_BUILD_SYSROOT} -mmacosx-version-min=${MACOSX_DEPLOYMENT_TARGET} ..

大多数构建工具(例如 CMake 和 distutils (setuptools))将自动拾取 MACOSX_DEPLOYMENT_TARGET,但您需要显式传递 CONDA_BUILD_SYSROOT。对于 CMake,可以使用选项 -DCMAKE_OSX_SYSROOT=${CONDA_BUILD_SYSROOT} 完成此操作。使用 distutils 构建 Python 扩展时,应始终在调用 setup.py 之前扩展 CFLAGS

export CFLAGS="${CFLAGS} -i sysroot ${CONDA_BUILD_SYSROOT}"

当使用 Cython 构建 C++ 扩展时,必须类似地修改 CXXFLAGS

向后兼容性#

一些用户希望使用最新的 Anaconda 软件包,但尚不想使用 Anaconda 编译器。为了实现这一点,最新的 Python 软件包构建具有默认的 _sysconfigdata 文件。此文件将系统提供的编译器(例如 gccg++)设置为默认编译器。这种方式允许旧版配方继续工作。

Python 软件包还包括一个替代的 _sysconfigdata 文件,该文件将 Anaconda 编译器设置为默认编译器。Anaconda Python 可执行文件本身是使用这些 Anaconda 编译器制作的。

编译器软件包设置环境变量 _PYTHON_SYSCONFIGDATA_NAME,该变量告诉 Python 要使用哪个 _sysconfigdata 文件。此变量在激活时使用上面描述的激活挂钩设置。

新的 _sysconfigdata 自定义系统仅存在于最新版本的 Python 软件包中。Conda-build 自动尝试使用当前配置的频道中可用的最新 Python 版本,这通常从默认频道获取最新版本。如果您在使用新编译器时使用 conda-build 以外的其他工具,则 conda 不会自动更新 Python,因此请确保通过手动更新 Python 软件包来获得正确的 _sysconfigdata 文件。

Anaconda 编译器和 conda-build 3#

Anaconda 5.0 编译器和 conda-build 3 旨在协同工作。

Conda-build 3 定义了一个特殊的 jinja2 函数 compiler(),以便在多个平台上轻松动态地指定编译器软件包。compiler 函数至少接受 1 个参数,即要使用的编译器的语言

requirements:
  build:
    - {{ compiler('c') }}

“跨平台”配方可用于制作主机平台与 conda-build 运行的构建平台不同的软件包。要编写跨平台配方,您可能还需要在 requirements 部分中使用“host”部分。在此示例中,我们将“host”设置为“zlib”,以告知 conda-build 使用 conda 环境中的 zlib,而不是系统 zlib。这确保 conda-build 使用主机平台的 zlib,而不是构建平台的 zlib。

requirements:
  build:
    - {{ compiler('c') }}
  host:
    - zlib

通常,build 部分应包括编译器和其他构建工具,而 host 部分应包括所有其他内容,包括共享库、Python 和 Python 库。

关于 CMake 和 sysroots 的题外话#

Anaconda 的 Linux 编译器是使用名为 crosstool-ng 的工具构建的。它们不仅包括 GCC,还包括带有 glibc 的“sysroot”,以及工具链的其余部分 (binutils)。通常,sysroot 是您的系统提供的,它建立了已编译代码的 libc 兼容性边界。任何使用系统 sysroot 以外的 sysroot 的编译都被称为“交叉编译”。当目标操作系统和构建操作系统相同时,它被称为“伪交叉编译器”。对于使用 Anaconda 编译器在 Linux 上进行的正常构建来说,情况就是如此。

不幸的是,某些软件工具无法以直观的方式处理 sysroots。CMake 在这方面尤其糟糕。即使编译器本身理解其自身的 sysroot,CMake 仍坚持忽略它。我们已在以下位置提交了问题:

此外,此 Stack Overflow 问题有更多信息:https://stackoverflow.com/questions/36195791/cmake-missing-sysroot-when-cross-compiling

为了向 CMake 介绍 sysroot,您必须执行其他工作。例如,请参阅我们在 AnacondaRecipes/libnetcdf-feedstock 的 libnetcdf 配方

特别是,您需要将 cross-linux.cmake 文件复制到那里,并在您的 build.sh 文件中引用它

CMAKE_PLATFORM_FLAGS+=(-DCMAKE_TOOLCHAIN_FILE="${RECIPE_DIR}/cross-linux.cmake")

cmake -DCMAKE_INSTALL_PREFIX=${PREFIX} \
  ${CMAKE_PLATFORM_FLAGS[@]} \
  ${SRC_DIR}

自定义编译器#

上面列出的编译器软件包是小型软件包,仅包括激活脚本,并将它们提供的大部分软件列为运行时依赖项。

此设计旨在让您通过复制这些配方并更改标志来轻松自定义您自己的编译器软件包。然后,您可以编辑 conda_build_config.yaml 文件以指定您自己的软件包。

我们一直谨慎地选择良好、通用、安全且快速的标志。我们还在 Anaconda Distribution 5.0.0 中的所有软件包中使用了它们,除了少数配方中的一些微小自定义。更改这些标志时,请记住,选择错误的标志可能会降低安全性、降低性能并导致不兼容。

考虑到这一警告,让我们看看自定义 Clang 的好方法。

  1. anacondarecipes/aggregate 下载或 fork 代码。Clang 软件包配方位于 clang 文件夹中。主要材料在 llvm-compilers-feedstock 文件夹中。

  2. 编辑 clang/recipe/meta.yaml

    package:
      name: clang_{{ target_platform }}
      version: {{ version }}
    

    此处的名称无关紧要,但下面的输出名称很重要。Conda-build 希望任何编译器都遵循 BASENAME_PLATFORMNAME 模式,因此保持名称的 {{target_platform}} 部分很重要。

    {{ version }} 保留为有意未定义的 jinja2 变量。它稍后在 conda_build_config.yaml 中设置。

  3. 在完成任何打包之前,运行 build.sh 脚本:AnacondaRecipes/aggregate

    在此配方中,值在此处更改。这些值插入到稍后安装的激活脚本中。

    #!/bin/bash
    
    CHOST=${macos_machine}
    
    FINAL_CPPFLAGS="-D_FORTIFY_SOURCE=2 -mmacosx-version-min=${macos_min_version}"
    FINAL_CFLAGS="-march=core2 -mtune=haswell -mssse3 -ftree-vectorize -fPIC -fPIE -fstack-protector-strong -O2 -pipe"
    FINAL_CXXFLAGS="-march=core2 -mtune=haswell -mssse3 -ftree-vectorize -fPIC -fPIE -fstack-protector-strong -O2 -pipe -stdlib=libc++ -fvisibility-inlines-hidden -std=c++14 -fmessage-length=0"
    # These are the LDFLAGS for when the linker is being called directly, without "-Wl,"
    FINAL_LDFLAGS="-pie -headerpad_max_install_names"
    # These are the LDFLAGS for when the linker is being driven by a compiler, with "-Wl,"
    FINAL_LDFLAGS_CC="-Wl,-pie -Wl,-headerpad_max_install_names"
    FINAL_DEBUG_CFLAGS="-Og -g -Wall -Wextra -fcheck=all -fbacktrace -fimplicit-none -fvar-tracking-assignments"
    FINAL_DEBUG_CXXFLAGS="-Og -g -Wall -Wextra -fcheck=all -fbacktrace -fimplicit-none -fvar-tracking-assignments"
    FINAL_DEBUG_FFLAGS="-Og -g -Wall -Wextra -fcheck=all -fbacktrace -fimplicit-none -fvar-tracking-assignments"
    
    find "${RECIPE_DIR}" -name "*activate*.sh" -exec cp {} . \;
    
    find . -name "*activate*.sh" -exec sed -i.bak "s|@CHOST@|${CHOST}|g" "{}" \;
    find . -name "*activate*.sh" -exec sed -i.bak "s|@CPPFLAGS@|${FINAL_CPPFLAGS}|g"             "{}" \;
    find . -name "*activate*.sh" -exec sed -i.bak "s|@CFLAGS@|${FINAL_CFLAGS}|g"                 "{}" \;
    find . -name "*activate*.sh" -exec sed -i.bak "s|@DEBUG_CFLAGS@|${FINAL_DEBUG_CFLAGS}|g"     "{}" \;
    find . -name "*activate*.sh" -exec sed -i.bak "s|@CXXFLAGS@|${FINAL_CXXFLAGS}|g"             "{}" \;
    find . -name "*activate*.sh" -exec sed -i.bak "s|@DEBUG_CXXFLAGS@|${FINAL_DEBUG_CXXFLAGS}|g" "{}" \;
    find . -name "*activate*.sh" -exec sed -i.bak "s|@DEBUG_CXXFLAGS@|${FINAL_DEBUG_CXXFLAGS}|g" "{}" \;
    # find . -name "*activate*.sh" -exec sed -i.bak "s|@FFLAGS@|${FINAL_FFLAGS}|g"                 "{}" \;
    # find . -name "*activate*.sh" -exec sed -i.bak "s|@DEBUG_FFLAGS@|${FINAL_DEBUG_FFLAGS}|g"     "{}" \;
    find . -name "*activate*.sh" -exec sed -i.bak "s|@LDFLAGS@|${FINAL_LDFLAGS}|g"               "{}" \;
    find . -name "*activate*.sh" -exec sed -i.bak "s|@LDFLAGS_CC@|${FINAL_LDFLAGS_CC}|g"         "{}" \;
    find . -name "*activate*.sh.bak" -exec rm "{}" \;
    
  4. 在激活脚本中进行这些更改后,就可以继续安装内容了。回顾 clang 文件夹的 meta.yaml。这是我们更改软件包名称的地方。请注意 {{ target_platform }} 之前的内容。

    outputs:
      - name: super_duper_clang_{{ target_platform }}
        script: install-clang.sh
        requirements:
          - clang {{ version }}
    

    此处的脚本引用是您可以添加自定义的另一个位置。您可以更改那些安装脚本的内容,也可以更改那些安装脚本正在安装的脚本。

    请注意,我们使主要材料中的软件包 clang 在版本上与我们的输出版本一致。这与顶级配方隐式相同。clang 软件包根本不设置任何环境变量,因此可能难以直接使用。

  5. 让我们检查脚本 install-clang.sh

    #!/bin/bash
    
    set -e -x
    
    CHOST=${macos_machine}
    
    mkdir -p "${PREFIX}"/etc/conda/{de,}activate.d/
    cp "${SRC_DIR}"/activate-clang.sh "${PREFIX}"/etc/conda/activate.d/activate_"${PKG_NAME}".sh
    cp "${SRC_DIR}"/deactivate-clang.sh "${PREFIX}"/etc/conda/deactivate.d/deactivate_"${PKG_NAME}".sh
    
    pushd "${PREFIX}"/bin
      ln -s clang ${CHOST}-clang
    popd
    

    这里没有什么太不寻常的。

    激活脚本根据我们的软件包名称命名,因此它们不会与其他激活脚本冲突。

    Clang 的符号链接是设置主机平台的 Clang 实现细节。

    我们在 aggregate 的 conda_build_config.yaml 中定义 macos_machineAnacondaRecipes/aggregate

    正在安装的激活脚本是我们实际设置环境变量的地方。请记住,这些脚本已被 build.sh 修改。

  6. 在完成任何所需的更改后,继续构建配方。

    您应该得到一个 super_duper_clang_osx-64 软件包。或者,如果您不在 macOS 上并且正在修改不同的配方,则您应该得到一个适用于您平台的等效软件包。

将您的自定义编译器软件包与 conda-build 3 一起使用#

还记得 Jinja2 函数 {{ compiler('c') }} 吗?这就是它的用途。conda_build_config.yaml 中的特定键以该 jinja2 函数的语言参数命名。在您的 conda_build_config.yaml 中,添加以下内容

c_compiler:
  - super_duper_clang

请注意,我们没有添加 target_platform 部分,它是独立的。您也可以定义该键

c_compiler:
  - super_duper_clang
target_platform:
  - win-64

定义这两个键后,conda-build 将尝试使用名为 super_duper_clang_win-64 的编译器软件包。该软件包需要存在于您的本机平台上。例如,如果您在 macOS 上,则您的本机平台是 osx-64

您的本机平台的软件包子目录是构建平台。构建平台和 target_platform 可以相同,默认情况下它们是相同的,但它们也可以不同。当它们不同时,您正在进行交叉编译。

如果您曾经为同一种语言需要不同的编译器键,请记住语言键是任意的。例如,我们可能希望在一个生态系统内为 Python 和 R 使用不同的编译器。在 Windows 上,Python 生态系统使用 Microsoft Visual C 编译器,而 R 生态系统使用 Mingw 编译器。

让我们从 conda_build_config.yaml 开始

python_c_compiler:
  - vs2015
r_c_compiler:
  - m2w64-gcc
target_platform:
  - win-64

在 Python 配方中,您将拥有

requirements:
  build:
    - {{ compiler('python_c') }}

在 R 配方中,您将拥有

requirements:
  build:
    - {{ compiler('r_c') }}

此示例有点牵强,因为 m2w64-gcc_win-64 软件包不可用。您需要创建一个元软件包 m2w64-gcc_win-64 以指向 m2w64-gcc 软件包,该软件包确实存在于 repo.anaconda.com 上的 msys2 频道上。

表达编译器及其标准库之间的关系#

对于大多数语言,当然对于“c”和“cxx”,编译任何给定的程序可能会创建对来自各自标准库的符号的运行时依赖性。例如,Linux 上 C 的标准库通常是 glibc,它是操作系统的核心组件。Conda 无法更改或取代此库(尝试这样做风险太大)。MacOS 和 Windows 上也存在类似的情况。

编译器软件包通常有两种方法来处理这种依赖性

  • 假设软件包必须存在(如 Linux 上的 glibc)。

  • 始终添加对各自 stdlib 的运行时要求(例如 MacOS 上的 libcxx)。

但是,即使我们假设软件包必须存在,关于 glibc 版本的信息仍然是非常相关的信息,这也是为什么它反映在 __glibc 虚拟软件包 中的原因。

例如,较新的软件包可能会随着时间的推移决定增加它们支持的 glibc 最低版本。因此,我们需要一种方法来表达这种依赖性,以便 conda 能够理解,以便(与 __glibc 虚拟软件包结合使用)环境解析器不会考虑那些 glibc 版本太旧的计算机上的软件包。

执行此操作的方法是使用 Jinja2 函数 {{ stdlib('c') }},它在尽可能多的方面与 {{ compiler('c') }} 匹配。让我们再次从 conda_build_config.yaml 开始

c_stdlib:
  - sysroot                     # [linux]
  - macosx_deployment_target    # [osx]
c_stdlib_version:
  - 2.17                        # [linux]
  - 10.13                       # [osx]

然后在配方中,我们将使用

requirements:
  build:
    - {{ compiler('c') }}
    - {{ stdlib('c') }}

这将表达结果软件包需要在构建环境中分别在 Linux 上需要 sysroot ==2.17(对应于 glibc)和在 MacOS 上需要 macosx_deployment_target ==10.13。这如何转化为运行时依赖性可以在表示标准库的相应 conda(元)软件包的元数据中定义(即那些在 c_stdlib 下定义的)。

在此示例中,sysroot 2.17 将在 __glibc >=2.17 上生成运行时导出,而 macosx_deployment_target 10.13 将类似地生成 __osx >=10.13。通过这种方式,我们使软件包能够以统一的方式定义它们自己对标准库的期望,而无需隐式地依赖于关于给定平台上的较低版本的某些全局假设。

原则上,此工具还可以表达对单独的 stdlib 实现(如 musl 而不是 glibc)的依赖性,或者消除假设 C++ 编译器始终需要在 C++ stdlib 上添加运行时导出的需要——然后可以由软件包本身决定它们是否需要 {{ stdlib('cxx') }}

Anaconda 编译器隐式添加指向 conda 环境的 RPATH#

您可能希望在 conda-build 之外使用 Anaconda 编译器,以便您使用相同的版本、标志和配置,以最大程度地兼容 Anaconda 软件包(但在您想要简单 tarball 的情况下)。在这种情况下,有一个陷阱。

即使从 conda-build 外部使用 Anaconda 编译器,GCC 规范也已自定义,以便在链接可执行文件或共享库时,添加指向当前环境前缀目录 ($CONDA_PREFIX/lib) 内 lib/ 的 RPATH。这是通过更改 GCC specs 文件内的 link_libgcc: 部分完成的,并且进行此更改是为了不需要 LD_LIBRARY_PATH 用于基本库。

conda-build 知道如何自动使其可重定位,以便此 RPATH 将在安装时由 conda 更改为指向软件包正在安装的环境。但是,如果您仅将此二进制文件打包在 tarball 中,它将继续包含此硬编码的 RPATH 到您计算机上的环境。在这种情况下,建议手动删除 RPATH