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

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

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

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

macOS SDK#

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

我们通常将 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 的构建脚本应使用变量 MACOSX_DEPLOYMENT_TARGETCONDA_BUILD_SYSROOT,这些变量由 conda-build 设置(请参阅 环境变量)。 这些变量应转换为正确的编译器参数,例如,对于 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 函数至少需要一个参数,即要使用的编译器的语言

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 和 sysroot 的说明#

Anaconda 用于 Linux 的编译器使用名为 crosstool-ng 的东西构建。 它们不仅包含 GCC,还包含一个带 glibc 的“sysroot”,以及其他工具链(binutils)。 通常,sysroot 是系统提供的,它为编译的代码建立 libc 兼容性边界。 任何使用与系统 sysroot 不同的 sysroot 的编译都被称为“交叉编译”。 当目标操作系统和构建操作系统相同时,它被称为“伪交叉编译”。 这是 Anaconda 在 Linux 上使用其编译器的正常构建情况。

不幸的是,一些软件工具不会以直观的方式处理 sysroot。 CMake 在这方面尤其糟糕。 即使编译器本身了解它自己的 sysroot,CMake 也坚持忽略它。 我们在以下地址提交了问题

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

为了向 CMake 教授有关 sysroot 的知识,您必须做一些额外的工作。 作为示例,请参阅我们针对 libnetcdf 的食谱,位于 AnacondaRecipes/libnetcdf-feedstock

特别是,您需要复制那里的 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 包兼容(但在您想要简单的 tarballs 的情况下,例如)。在这种情况下,有一个问题。

即使 Anaconda 编译器在 conda-build 之外使用,GCC 规范也会被定制,因此,在链接可执行文件或共享库时,会添加指向当前环境前缀目录 ($CONDA_PREFIX/lib) 内的 lib/ 的 RPATH。这是通过更改 GCC specs 文件内的 link_libgcc: 部分来实现的,这种更改是为了使 LD_LIBRARY_PATH 对于基本库不是必需的。

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