环境 (spack.yaml, spack.lock)

环境用于将一组为了特定目的而打算以连贯方式构建、重构和部署的规范(spec)进行分组。环境定义了软件安装的各个方面,例如:

  1. 安装哪些规范;

  2. 如何配置这些规范;以及

  3. 具体化(concretized)后的软件安装在何处

与构建和加载单个 Spack 模块的“点菜式”方法相比,将此信息聚合到环境中进行处理具有明显优势。

使用环境,您可以通过单个命令具体化、安装或加载(激活)所有规范。具体化过程会全面配置环境的规范和依赖项,为安装软件做好准备。这比临时安装脚本更稳健。此外,您还可以共享环境,甚至在不同的计算机上重复使用它。

环境定义,特别是规范的配置方式,使得即使在升级 Spack 包时,软件也能保持稳定和可重复。只有当环境被明确地重新具体化时,更改才会生效。

定义规范的安装位置支持环境的文件系统视图。同时,Spack 维护着一份软件的单一安装版本,可在多个环境中重复使用。

激活环境决定了何时加载所有相关(且已安装)的规范,因此将加载的软件限制为环境实际需要的规范。Spack 甚至可以生成一个脚本来加载与环境相关的所有模块。

其他软件包管理系统也提供了在某些方面类似于 Spack 环境的环境;例如,Conda 环境Python 虚拟环境。不过,Spack 环境提供了一些独特的功能:

  1. 安装在“环境”中的规范与安装在 Spack 中任何其他位置的规范没有任何区别。

  2. Spack 环境可以包含同一个软件包的多个不同规范。

Spack 使用类似于 Bundler gemfiles 和其他包管理器的“清单与锁定”模型。环境的用户输入文件(即清单)命名为 spack.yaml。包含完全配置和具体化规范的锁定文件命名为 spack.lock

使用环境

在此,我们按照创建、具体化、安装和加载环境的典型用例进行说明。

创建托管环境

创建环境的方法是:

$ spack env create myenv

系统将创建目录 $SPACK_ROOT/var/spack/environments/myenv 以管理该环境。

注意

默认情况下,所有托管环境都存储在 $SPACK_ROOT/var/spack/environments 文件夹中。可以通过在 config.yaml 中设置 environments_root 变量来更改此位置。

Spack 会在 $SPACK_ROOT/var/spack/environments/myenv 下创建 spack.yaml 文件、隐藏目录 .spack-env 以及 spack.lock 文件。用户通过 spack.yaml 文件及相关的 Spack 命令进行交互。元数据(默认情况下还包括视图)存储在 .spack-env 目录中。当环境被具体化时,Spack 会创建带有完整配置的规范和依赖项的 spack.lock 文件。

.spack-env 子目录还包含:

  • repo/:一个充当仓库的子目录,由环境中使用的 Spack 软件包组成。理论上,它允许环境即使在不同版本或不同软件包的 Spack 上也能实现相同的构建效果!

  • logs/:一个包含此环境中软件包构建日志的子目录。

Spack 环境也可以从另一个环境创建。可以从清单文件(用户输入)、锁定文件或整个环境一次性创建环境。从清单创建环境使用:

$ spack env create myenv spack.yaml

生成的新环境保证具有与原始环境相同的根规范,但在存在不同的显式或默认配置设置(例如不同版本的 Spack 或不同的用户账户)时,具体化的结果可能不同。

从清单创建的环境将复制环境中相对路径内的任何包含配置。来自环境外部的相对路径将导致错误,而绝对路径将保持为绝对路径。例如,如果 spack.yaml 包含:

spack:
  include: [./config.yaml]

那么新创建的环境将拥有从原始环境位置复制过来的 config.yaml 文件的副本。

spack.lock 文件创建环境使用:

$ spack env create myenv spack.lock

在相同或兼容的机器上,生成的新环境保证初始时具有与原始环境相同的具体规范。

从整个环境创建新环境,可以使用环境名称或路径:

$ spack env create myenv /path/to/env
$ spack env create myenv2 myenv

如果原始环境已具体化(如从锁定文件创建),新环境将包含原始环境的具体规范;并包含原始环境清单文件中指定的所有配置选项和抽象规范。它还将包含环境目录中引用的任何其他文件,如仓库或源代码,因为它们可以通过相对路径被引用。

注意

环境创建也接受文件的完整路径。

如果路径不在 $SPACK_ROOT/var/spack/environments 目录下,则该源被称为 独立环境

环境名称可以是嵌套路径,以帮助通过子目录组织环境。

$ spack env create projectA/configA/myenv

这将在 $environments_root/projectA/configA/myenv 下创建一个托管环境。因此,更改 environment_root 也可以用于使一组嵌套环境可用。

激活环境

要激活环境,请使用以下命令:

$ spack env activate myenv

默认情况下,spack env activate 会将与该环境关联的视图加载到用户环境中。-v, --with-view 参数可确保此行为,而 -V, --without-view 参数则在不更改用户环境变量的情况下激活环境。

spack env activate 命令的 -p 选项会修改用户的提示符,使其以括号内的环境名称开头。

$ spack env activate -p myenv
[myenv] $ ...

如果环境尚未定义,activate 命令还可以通过添加 --create 标志来创建新环境。托管环境和独立环境都可以使用 spack env create 接受的相同标志进行创建。如果环境已存在,Spack 将仅激活它并忽略创建相关的标志。

$ spack env activate --create -p myenv
# ...
# [creates if myenv does not exist yet]
# ...
[myenv] $ ...

要停用环境,请使用命令:

$ spack env deactivate

或使用快捷别名:

$ despacktivate

如果环境在激活时包含了视图,停用环境将从用户环境中移除该视图。

独立环境

独立环境可以位于 Spack 外部的任何目录中。

注意

卸载软件包时,Spack 会要求用户确认删除托管环境中仍在使用中的软件包。对于独立环境,则不会执行此检查。

要创建独立环境,请使用以下命令之一:

$ spack env create --dir my_env
$ spack env create ./my_env

作为简写,如果独立环境尚未存在,您也可以在激活时创建它:

$ spack env activate --create ./my_env

为方便起见,Spack 还可以为您将独立环境放置在临时目录中:

$ spack env activate --temp

环境感知命令

Spack 命令具有环境感知能力。例如,如果激活了环境,find 命令仅显示当前活跃环境中的规范。否则,它将显示 Spack 实例中的所有规范。同样的规则也适用于 installuninstall 命令。

$ spack find
==> 0 installed packages

$ spack install zlib@1.2.11
[+] q6cqrdt zlib@1.2.11 ~/spack/opt/spack/linux-rhel7-broadwell/gcc-8.1.0/zlib-1.2.11-q6cqrdto4iktfg6qyqcc5u4vmfmwb7iv (12s)

$ spack env activate myenv

$ spack find
==> In environment myenv
==> No root specs
==> 0 installed packages

$ spack install zlib@1.2.8
[+] yfc7epf zlib@1.2.8 ~/spack/opt/spack/linux-rhel7-broadwell/gcc-8.1.0/zlib-1.2.8-yfc7epf57nsfn2gn4notccaiyxha6z7x (12s)
==> Updating view at ~/spack/var/spack/environments/myenv/.spack-env/view

$ spack find
==> In environment myenv
==> Root specs
zlib@1.2.8

==> 1 installed package
-- linux-rhel7-broadwell / gcc@8.1.0 ----------------------------
zlib@1.2.8

$ despacktivate

$ spack find
==> 2 installed packages
-- linux-rhel7-broadwell / gcc@8.1.0 ----------------------------
zlib@1.2.8  zlib@1.2.11

请注意,当我们安装抽象规范 zlib@1.2.8 时,它被呈现为环境的根。所有显式安装的软件包都将列为环境的根。

所有作用于已安装规范列表的 Spack 命令都是环境感知的,包括 installuninstallfindextensions 等。在 配置环境 一节中,我们将进一步讨论环境感知命令。

添加抽象规范

抽象规范是用户在 Spack 应用默认值或依赖信息之前指定的规范。

您可以使用 spack add 命令将抽象规范添加到环境中。这会将该抽象规范作为环境的根添加到 spack.yaml 文件中。环境最重要的组成部分就是抽象规范列表。

添加抽象规范不会立即安装任何内容,也不会影响 spack.lock 文件。要更新锁定文件,必须对环境进行 重新具体化,而要更新任何安装,必须对环境进行 (重新)安装

spack add 命令是环境感知的。它将规范添加到当前处于活跃状态的环境中。如果没有活跃环境,则会生成错误。

$ spack env activate myenv
$ spack add mpileaks

$ spack -e myenv add python

注意

所有环境感知命令也可以使用 spack -e 标志来指定环境。

具体化 (Concretizing)

一旦用户规范被添加到环境中,就可以对其进行具体化。具体化环境有三种不同的操作模式,详见 规范具体化。无论选择哪种模式,以下命令都能确保所有根规范都按照配置中规定的约束进行具体化:

[myenv]$ spack concretize

对于未合并具体化的规范,上述命令将仅具体化尚未具体化的规范。要强制对所有规范进行重新具体化,可以添加 -f 选项:

[myenv]$ spack concretize -f

如果不使用该选项,Spack 保证已具体化的规范在环境中保持不变。

concretize 命令不会安装任何软件包。对于已经在环境外部安装过的软件包,添加并具体化规范的过程与安装该规范相同,前提是其具体化后的结果与外部安装的规范完全一致。

spack find 命令可以使用 -c (--concretized) 标志将具体化规范与已安装规范分开显示。

[myenv]$ spack add zlib
[myenv]$ spack concretize
[myenv]$ spack find -c
==> In environment myenv
==> Root specs
zlib

==> Concretized roots
-- linux-rhel7-x86_64 / gcc@4.9.3 -------------------------------
zlib@1.2.11

==> 0 installed packages

安装环境

除了向环境添加单个规范外,还可以使用以下命令一次性安装整个环境:

[myenv]$ spack install

如果环境已经过具体化,Spack 将安装已具体化的规范。否则,spack install 将在安装具体化规范之前先具体化环境。

注意

每个 spack install 过程都会在多个构建任务(由 -j 标志和 config:build_jobs 选项控制,参见 build_jobs)的配合下一次构建一个软件包。为了进一步加快环境构建速度,可以通过启动更多的 Spack 实例来并行安装独立的软件包。例如,以下命令将使用三个后台任务并行构建最多四个软件包:

[myenv]$ spack install & spack install & spack install & spack install

另一种选择是生成一个 Makefile 并运行 make -j<N> 来控制并行安装进程的数量。详见 从环境生成依赖文件

在安装过程中,spack install 会在环境的 logs/ 目录中创建符号链接,方便查看与该环境相关的构建日志。spack install 命令还会将安装时使用的包含 package.py 的 Spack 仓库存储在环境的 repos/ 目录中。

在具体化的环境中,可以使用 --no-add 选项告知 Spack 只安装环境中已存在的规范,而不要添加任何新的根规范。对于在命令行中传递给 spack install 的根规范,--no-add 是默认行为;而对于依赖规范,则是可选的。换句话说,如果命令行中提供的根规范在当前具体化的环境中存在明确的匹配,则 Spack 不需要用户指定 --no-add 选项来防止规范被再次添加。同时,如果一个规范已经存在于环境中,但仅作为依赖项,则如果没有 --no-add 选项,它将被作为根规范添加到环境中。

在 Spack 环境中开发软件包

spack develop 命令允许用户在环境中开发 Spack 软件包。它会将 Spack 配置为从本地源代码安装该软件包。默认情况下,spack develop 还会将该软件包克隆到环境中的子目录以作为本地源码。这些选择可以通过 --path--no-clone 参数进行覆盖。提供给 --path 参数的相对路径将解析为相对于环境目录的路径。所有这些选项都会记录在环境清单中,尽管默认值可能保持隐含。

$ spack develop --path src/foo foo@develop
$ cat `spack location -e`/spack.yaml
spack:
  ...
  develop
    foo:
      spec: foo@develop
      path: src/foo

在具体化环境中运行 spack develop 时,Spack 将修改环境中的具体规范以反映修改后的出处。任何从本地源码构建的软件包都将拥有一个 dev_path 变体,且这些软件包的任何依赖项的哈希值都将被修改以反映此变化。dev_path 变体的值将是软件包源代码目录的绝对路径。如果开发规范与环境中的具体规范冲突,Spack 将引发异常并要求使用 spack develop --no-modify-concrete-specs 选项,随后执行 spack concretize --force 以应用 dev_path 变体和开发规范中的约束。

当使用开发规范具体化环境时,传递给 spack develop 命令的规范版本、变体及其他属性将被具体化器视为约束(除了包 specs 列表中的任何约束)。如果软件包的 develop 配置不包含版本,Spack 将选择该软件包的最高版本。这意味着任何“无穷大”版本(如 develop, main 等)都将被优先用于标记有 spack develop 命令的规范,这与 Spack 偏好最高数字版本的标准行为不同。具体化器会自动为这些软件包添加一个 dev_path 变体,其值为 Spack 构建所用的本地源码的绝对路径。

如果软件包的本地源代码已被修改,Spack 将确保在每次安装环境时重新构建该软件包及其依赖项。Spack 的原生实现是检查 mtime 是否比安装时间更新。可以通过在软件包类中重写 detect_dev_src_change 方法来创建自定义检查。这对于使用自定义 Spack 仓库来驱动开发并希望优化性能的项目特别有用。

当在没有任何参数的情况下运行 spack develop 时,Spack 将克隆环境中任何指定路径尚不存在的开发规范。

在图形深度工作时,通常希望有多个规范被标记为 develop,这样就不必在每次调用 spack install 时重新暂存和/或进行全面重建。--recursive 标志可用于确保您提供的初始规范的所有依赖项也被标记为开发规范。--recursive 标志需要一个已预先具体化的环境,以便可以从提供的规范遍历到根规范。

对于具有 git 属性的软件包,git 分支、标签和提交也可以用作有效的具体版本(参见 版本指定符)。这意味着对于软件包 foospack develop foo@git.main 将克隆该软件包的 main 分支,如果 foo 在环境中,spack install 将从该 git 克隆安装。对 foo 的进一步开发可以通过重新安装环境进行测试,并最终提交并推送到上游 git 仓库。

如果正在开发的软件包支持源码外构建(out-of-source builds),用户可以使用 --build_directory 标志来控制构建目录的位置和名称。这是在 packages 配置中设置 package_attributes:build_directory 的快捷方式(参见 分配软件包属性)。所提供的位置将成为该软件包未来所有构建的构建目录。

设置构建目录的潜在隐患

Spack 不会检查软件包的源码外构建兼容性,因此确保软件包支持源码外构建的责任由用户承担。例如,大多数 autotoolsmakefile 软件包不支持源码外构建,而所有 CMake 软件包都支持。理解这些细微差别取决于软件开发人员,我们强烈建议开发人员只有在了解其软件包构建系统的情况下才重定向构建目录。

修改环境中的规范

spack change 命令允许用户更改 Spack 环境中的单个规范。

默认情况下,spack change 对环境的抽象规范进行操作。该命令接受一个规范参数列表。对于每个参数,具有与所提供规范相同名称的根规范将被修改以满足所提供的规范。例如,在具有根规范 hdf5+mpi+fortran 的环境中:

spack change hdf5~mpi+cxx

将把根规范更改为 hdf5~mpi+cxx+fortran

当需要更复杂的匹配语义时,--match-spec 参数将替代规范名称作为选择标准。使用 --match-spec 参数时,不需要规范名称。在同一个环境中:

spack change --match-spec "+fortran" +hl

将把 hdf5 规范约束为 +hl

默认情况下,如果修改将导致一个以上的抽象规范被更改,spack change 命令将导致错误且不对环境进行任何更改。使用 --all 选项以允许 spack change 修改多个抽象规范。

--concrete 选项允许 spack change 修改环境的具体规范以及抽象规范。即使是仅修改单个抽象规范的变更,也可能修改多个具体规范。--all 选项不影响可以修改多少个具体规范。

警告

具体规范的修改不会受到来自软件包的任何约束。spack change --concrete 命令如果使用不当,可能会创建无法正常构建的无效规范。

--concrete-only 选项允许在不修改抽象规范的情况下修改具体规范。它允许对环境中的非根节点应用更改,以及其他不修改任何根规范的更改。

加载

一旦安装了环境,以下命令将为其创建加载脚本:

$ spack env loads -r

这会在环境目录中创建一个名为 loads 的文件。在 Bash 中执行该文件将使环境对用户可用,并可以包含在 .bashrc 文件等中。loads 文件也可以从环境中复制出来、重命名等。

包含具体化环境

Spack 可以创建一个包含来自已具体化环境信息的新环境。您可以将新环境视为现有环境的组合。它在创建新环境时使用了现有环境 spack.lock 文件中的信息。当此类环境被具体化时,它将生成自己的 spack.lock 文件,其中包含来自所包含环境的相关信息。

创建组合的具体化环境

要创建组合的具体化环境,必须至少有一个现有的具体化环境。您将使用带有 --include-concrete 参数的 spack env create 命令,后跟您想要包含的环境名称或路径。以下是从命令行创建组合环境的示例:

$ spack env create myenv
$ spack -e myenv add python
$ spack -e myenv concretize
$ spack env create --include-concrete myenv combined_env

您还可以直接在 spack.yaml 文件中包含具体化环境。这涉及在环境的 include 标题下添加具体化环境 spack.lock 的绝对路径。Spack 特有的配置变量(如 $spack)和环境变量只要能扩展为绝对路径,就可以用于包含路径。(详见 配置文件变量 以获取更多信息。)

例如:

spack:
  include:
  - /absolute/path/to/environment1/spack.lock
  - $spack/../path/to/environment2/spack.lock
  specs: []
  concretizer:
    unify: true

将包含来自 environment1environment2 的规范,其中第二个环境的路径是相对于 spack 根目录的绝对路径。

注意

一旦 spack.yaml 文件更新,您必须具体化新环境以从包含的环境中获取具体规范。这将产生组合后的 spack.lock 文件。

更新组合环境

如果您希望在组合环境中反映对某个被包含环境所做的更改,则需要重新具体化被包含的环境,然后重新具体化组合环境,以便合并更改。例如:

$ spack env create myenv
$ spack -e myenv add python
$ spack -e myenv concretize
$ spack env create --include-concrete myenv combined_env

$ spack -e myenv find
==> In environment myenv
==> Root specs
python

==> 0 installed packages

$ spack -e combined_env find
==> In environment combined_env
==> No root specs
==> Included specs
python

==> 0 installed packages

在这里我们可以看到 combined_env 包含了来自 myenv 环境的 python 软件包。但是,如果我们向 myenv 添加另一个规范,combined_env 将不知道该规范。

$ spack -e myenv add perl
$ spack -e myenv concretize
$ spack -e myenv find
==> In environment myenv
==> Root specs
perl  python

==> 0 installed packages

$ spack -e combined_env find
==> In environment combined_env
==> No root specs
==> Included specs
python

==> 0 installed packages

直到运行 spack concretize 命令时,组合环境才会从重新具体化后的 myenv 获取更新信息。

$ spack -e combined_env concretize
$ spack -e combined_env find
==> In environment combined_env
==> No root specs
==> Included specs
perl  python

==> 0 installed packages

配置环境

各种 Spack 行为都是通过 Spack 配置文件更改的,详见 配置文件 一节。

Spack 环境在配置文件文档中讨论的自定义范围和用户范围之间提供了额外的配置范围级别。

有两种方式在 Spack 环境中包含配置信息:

  1. spack.yaml 文件中内联。

  2. spack.yaml 文件中从另一个文件包含。

许多 Spack 命令也会自动影响文件中的配置信息。这些命令接受 --scope 参数,环境可以通过 env:NAME 指定(要影响环境 foo,请设置 --scope env:foo)。这些命令将自动在 spack.yaml 文件中内联操纵配置。

内联配置

内联环境级配置使用与标准 Spack 配置范围相同的 YAML 格式,详见 配置文件 一节。每个部分都包含在一个顶层的 YAML 对象中。例如,包含某些软件包首选项配置(如 packages.yaml 文件)的 spack.yaml 清单文件可以包含:

spack:
  # ...
  packages:
    all:
      providers:
        mpi: [openmpi]
  # ...

此配置将默认的 mpi 提供程序设置为 openmpi

包含配置

Spack 环境在其 YAML 模式中允许一个 include 标题。该标题用于引入外部配置文件并将其应用于环境。

spack:
  include:
  - environment/relative/path/to/config.yaml
  - path: https://github.com/path/to/raw/config/compilers.yaml
    sha256: 26e871804a92cd07bb3d611b31b4156ae93d35b6a6d6e0ef3a67871fcb1d258b
  - /absolute/path/to/packages.yaml
  - path: /path/to/$os/$target/environment
    optional: true
  - path: /path/to/os-specific/config-dir
    when: os == "ventura"

引入的配置文件是必须的,除非它们被显式指定为可选的,或者条目的条件计算结果为 false。可选的包含项使用 optional 子句指定,条件项使用 when 子句指定。(详见 包含设置 (include.yaml) 获取有关可选和条件条目的更多信息。)

文件使用指向单个文件或包含它们的目录的路径列出。路径条目可以是绝对路径、相对于环境的相对路径,或是 URL。指向单个文件的 URL 必须链接到文件的 原始 (raw) 内容形式(例如 GitHubGitLab并且包含该文件的有效 sha256。仅支持 fileftphttphttps 协议。可以使用 Spack 特有的、环境和用户路径变量。(详见 配置文件变量 获取更多信息。)

警告

递归包含目前未以广度优先的方式处理,因此被多个包含文件更改的配置选项的值可能不是您预期的。这个问题将在未来的更新中解决。

配置优先级

内联配置的优先级高于包含配置,因此您无需更改共享配置文件即可对单个环境进行小幅修改。较早列出的包含配置具有更高的优先级,因为包含的配置是按相反顺序应用的。

手动编辑规范列表

环境中抽象/根规范的列表维护在 spack.yaml 清单中 specs 标题下。

spack:
  specs:
  - ncview
  - netcdf
  - nco
  - py-sphinx

在 YAML 中追加此列表与从命令行使用 spack add 命令完全相同。然而,YAML 文件提供了更强大的功能。

规范具体化

环境可以以三种不同的模式进行具体化,其行为取决于 concretizer:unify 配置选项。

默认模式是统一所有规范:

spack:
  specs:
  - hdf5+mpi
  - zlib@1.2.8
  concretizer:
    unify: true

这意味着环境中的任何软件包都对应于单个具体规范。在上面的例子中,当 hdf5 依赖链依赖于 zlib 时,它必须使用 zlib@1.2.8 而不是更新的版本。这种具体化模式在使用环境视图时特别有用:如果每个软件包只有一种版本,通常可以将所有安装目录合并为一个视图。

统一具体化的一个缺点是它可能过于严格。例如,当在环境中同时指定了 hdf5+mpihdf5~mpi 时,会发生具体化错误。

第二种模式是 尽可能统一:这使得根规范的具体化更独立。它不仅要求在不同根规范之间重用依赖项,而且最大化了这种重用:

spack:
  specs:
  - hdf5~mpi
  - hdf5+mpi
  - zlib@1.2.8
  concretizer:
    unify: when_possible

这意味着即使有更新版本的库可用,两个 hdf5 安装都将使用 zlib@1.2.8 作为依赖项。

第三种操作模式是通过禁用统一具体化,完全独立地具体化根规范:

spack:
  specs:
  - hdf5~mpi
  - hdf5+mpi
  - zlib@1.2.8
  concretizer:
    unify: false

在这个例子中,hdf5 被分别具体化,并不将 zlib@1.2.8 视为约束或首选项。相反,它将采用尽可能新的版本。

最后两种具体化选项通常对系统管理员和为 HPC 中心提供大型软件栈的用户支持组很有用。

注意

concretizer:unify 配置选项是在 Spack 0.18 中引入的,旨在取代 concretization 属性。供参考,concretization: togetherconcretizer:unify:true 取代,而 concretization: separatelyconcretizer:unify:false 取代。

用户规范的重新具体化

spack concretize 命令如果不带附加参数,将不会更改任何先前具体化的规范。这可能导致在使用 unify: true 时无法找到解决方案,或者在使用 unify: when_possible 时无法找到最优解。您可以使用 spack concretize -f 强制 Spack 忽略现有的具体环境。

规范矩阵

specs 列表中的条目可以是单个抽象规范,也可以是规范矩阵。

规范矩阵是一个包含多个规范列表的 YAML 对象,计算结果为其规范的叉积。规范矩阵还包含一个 excludes 指令,用于从计算结果中消除某些组合。

以下两个环境清单是完全相同的:

spack:
  specs:
  - zlib %gcc@7.1.0
  - zlib %gcc@4.9.3
  - libelf %gcc@7.1.0
  - libelf %gcc@4.9.3
  - libdwarf %gcc@7.1.0
  - cmake
spack:
  specs:
  - matrix:
    - [zlib, libelf, libdwarf]
    - ["%gcc@7.1.0", "%gcc@4.9.3"]
    exclude:
    - libdwarf%gcc@4.9.3
  - cmake

规范矩阵可用于在各种工具链中安装大批软件。

规范列表引用

规范列表中可能存在的最后一种条目类型是引用。

Spack 环境清单 YAML 模式包含一个额外的标题 definitions。在 definitions 下是一个 YAML 对象数组。每个对象具有一个或两个字段。一个是必需字段 name,可选字段是 when 子句。

名为 field 的是规范列表。规范列表使用与 specs 条目相同的语法。规范列表中的每个条目都可以是规范、规范矩阵或对先前命名列表的引用。引用使用 $ 符号指定,并被“展开”到原位(即被引用者的元素处于与单独列出的元素相同的层级)。例如,以下两个清单文件是完全相同的:

spack:
  definitions:
  - first: [libelf, libdwarf]
  - compilers: ["%gcc", "%intel"]
  - second:
    - $first
    - matrix:
      - [zlib]
      - [$compilers]
  specs:
  - $second
  - cmake
spack:
  specs:
  - libelf
  - libdwarf
  - zlib%gcc
  - zlib%intel
  - cmake

注意

definitions 部分中的命名规范列表只能引用自身之前定义的命名列表。顺序很重要。

在像示例这样的短文件中,直接列出包含的规范可能更容易。但是,对于涉及跨多个工具链的许多软件包的复杂示例,单独列出的因素使得环境更易于管理。

此外,spack add 命令的 -l 选项允许直接从命令行向清单文件的 definitions 部分中的命名列表添加内容。

when 指令可用于有条件地向命名列表添加规范。when 指令接受一串引用受限变量集的 Python 代码,并计算为布尔值。如果 when 字符串计算结果为 True,则列出的规范将被追加到命名列表中。在以下片段中,在 x86_64 系统上,命名列表 compilers["%gcc", "%clang", "%intel"],而在所有其他系统上则为 ["%gcc", "%clang"]

spack:
  definitions:
  - compilers: ["%gcc", "%clang"]
  - when: arch.satisfies("target=x86_64:")
    compilers: ["%intel"]

注意

任何具有相同命名列表且 when 子句为真(或缺少 when 子句)的定义将被追加在一起。

when 子句的有效变量包括:

  1. platform:系统默认 Spack 架构的平台字符串。

  2. os:系统默认 Spack 架构的操作系统字符串。

  3. target:系统默认 Spack 架构的目标字符串。

  4. architecturearch:满足系统默认 Spack 架构的 Spack 规范。这支持通过 satisfies 方法进行查询,如上所示。

  5. arch_str:系统默认 Spack 架构的架构字符串。

  6. re:Python 中的标准正则表达式模块。

  7. env:用户环境(通常是 Python 中的 os.environ)。

  8. hostname:系统的 hostname(如果 hostname 是用户 PATH 中的可执行文件)。

SpecLists 作为约束

Spack 中的依赖项和编译器既可以是环境中的软件包,也可以是对其他软件包的约束。对 SpecLists 的引用允许使用 $%$^ 语法将列表中的软件包分别视为编译器或依赖项的简写。

例如,以下环境有三个根软件包:gcc@8.1.0, mvapich2@2.3.1 %gcc@8.1.0, 以及 hdf5+mpi %gcc@8.1.0 ^mvapich2@2.3.1

spack:
  definitions:
  - compilers: [gcc@8.1.0]
  - mpis: [mvapich2@2.3.1]
  - packages: [hdf5+mpi]

  specs:
  - $compilers
  - matrix:
    - [$mpis]
    - [$%compilers]
  - matrix:
    - [$packages]
    - [$^mpis]
    - [$%compilers]

这允许极大地减少软件包和约束之间的冗余。

规范组

在 1.2 版本中添加。

环境可以通过命名规范组来组织,使您能够应用本地化的配置覆盖并建立具体化依赖项。这在下面详述的几种常见场景中非常有用。

在单个环境中构建和使用编译器

一种常见的用例是在现有系统之上构建一个更新的编译器,然后用它来编译一系列软件。例如,假设我们有兴趣使用 gcc@15.2 构建 hdf5libtree。以下清单文件将准确实现这一点:

spack:
  specs:
  - group: compiler
    specs:
    - gcc@15.2

  - group: apps
    needs: [compiler]
    specs:
    - hdf5 %gcc@15.2
    - libtree %gcc@15.2

group: 属性允许命名一组规范,这些规范随后被列在同一对象的 specs: 属性下。最简单的例子是仅由 gcc@15.2 规范组成的 compiler 组。

要表达规范组之间的依赖关系,请使用 needs: 属性,它是一个对应于我们所依赖的组名的列表。其工作方式是:组依赖项总是在当前组之前被具体化,并且当当前组被具体化时,它们的规范总是可用于重用。

配置规范组

另一种常见场景是部署同一组软件的不同配置(例如 CUDA 启用 vs. ROCm 启用)。例如,假设我们想要针对 target=x86_64_v3target=x86_64_v4 安装 gromacsquantum-espresso。可以使用以下清单文件完成:

spack:
  specs:
  - group: apps-x86_64_v3
    specs:
    - gromacs
    - quantum-espresso
    override:
      packages:
        all:
          prefer:
          - target=x86_64_v3

  - group: apps-x86_64_v4
    specs:
    - gromacs
    - quantum-espresso
    override:
      packages:
        all:
          prefer:
          - target=x86_64_v4

override: 属性允许我们覆盖单个规范组的配置。覆盖部分在当前组具体化时总是作为最高范围添加。这确保了覆盖总是优先于其他配置源。

使用 explicit: false 控制垃圾回收

默认情况下,每个规范组都被视为一组显式根。这意味着即使没有其他东西依赖它们,它们的规范也会被 spack gc 保留。在组上设置 explicit: false 会将其规范标记为隐式,使其在没有其他已安装规范依赖它们时,有资格进行垃圾回收。

spack:
  specs:
  - group: compiler
    explicit: false
    specs:
    - gcc@15.2

  - group: apps
    needs: [compiler]
    specs:
    - hdf5 %gcc@15.2
    - libtree %gcc@15.2

应用程序安装后,当没有已安装的规范对其有链接或运行依赖时,spack gc 将删除该编译器。

注意

在已安装的组上切换 explicit: false 不会追溯更新已安装规范的数据库记录。该标志仅对更改后安装或重新安装的规范生效。要立即将现有规范标记为隐式,请使用 spack mark -i <spec>

修改环境变量

Spack 环境在激活时可以修改活跃 shell 的环境变量。环境可以配置为使用 spack.yaml 中的 env_vars 配置来设置、取消设置、前置或追加变量。

spack:
  env_vars:
    set:
      ENVAR_TO_SET_IN_ENV_LOAD: "FOO"
    unset:
    - ENVAR_TO_UNSET_IN_ENV_LOAD
    prepend_path:
      PATH_LIST: "path/to/prepend"
    append_path:
      PATH_LIST: "path/to/append"
    remove_path:
      PATH_LIST: "path/to/remove"

环境视图

Spack 环境可以具有关联的文件系统视图,这是一个具有更传统结构(<view>/bin, <view>/lib, <view>/include)的目录,其中链接了已安装软件包的所有文件。

由于 spack.yaml 清单文件中的 view: true 选项,默认情况下会为每个环境创建一个视图。

spack:
  specs: [perl, python]
  view: true

视图是在相对于环境的隐藏目录 .spack-env/view 中创建的。如果您使用了 spack env activate,您可能已经与此视图交互过。当环境激活时,Spack 会将其 <view>/bin 目录前置到 PATH 中,以便您可以直接运行环境中所有已安装软件包的可执行文件。

视图是高度可定制的:您可以控制它们的放置位置、修改其结构、包含和排除规范、更改文件链接方式,甚至可以为一个环境生成多个视图。

最小视图配置

最小配置:

spack:
  # ...
  view: true

让 Spack 在环境的 .spack-env/view 目录下生成单个具有默认设置的视图。

配置视图的另一种简便方法是仅指定其放置位置:

spack:
  # ...
  view: /path/to/view

通过设置 view: false 也可以禁用视图。

高级视图配置

可以在 view 下定义一个或多个 视图描述符,并以名称作为键。上一节中带有 view: /path/to/view 的示例等同于定义了一个名为 default 且带有 root 属性的视图描述符:

spack:
  # ...
  view:
    default:  # name of the view
      root: /path/to/view  # view descriptor attribute

default 视图描述符名称是特殊的:当您 spack env activate 环境时,此视图将用于更新您的 PATH 变量(以及其他内容)。

视图描述符必须包含视图的根目录,并可选地包含投影 (projections)、selectexclude 列表,以及通过 linklink_type 实现的链接信息。

作为一个更高级的示例,在下面的清单文件片段中,我们定义了一个名为 mpis 的视图,根目录位于 /path/to/view,其中所有投影都使用软件包名称、版本和编译器名称来确定给定软件包的路径。该视图选择了所有依赖于 MPI 的软件包,并排除了那些使用 GCC 8.5 版本构建的软件包。由于 link: all 选项,其(传递性)链接和运行类型依赖的根规范将被放入视图中,并且视图中的文件将是 Spack 安装目录的符号链接。

spack:
  # ...
  view:
    mpis:
      root: /path/to/view
      select: [^mpi]
      exclude: ["%gcc@8.5"]
      projections:
        all: "{name}/{version}-{compiler.name}"
      link: all
      link_type: symlink
      link_dirs: true

selectexclude 值的默认设置是选择所有内容且不排除任何内容。默认投影是默认视图投影 ({})。link 属性允许以下值:

  1. link: all:包含根规范及其传递性运行和链接类型依赖(默认);

  2. link: run:包含根规范及其传递性运行类型依赖;

  3. link: roots:仅包含根规范,不包含其依赖项。

link_type 默认为 symlink,但也支持 hardlinkcopy

在 1.2 版本中新增:link_dirs 选项用于控制是否对目录进行符号链接。这是 Spack v1.2 及更高版本的默认行为。这是一种优化手段,可显著缩短创建视图的时间,并减少视图的 inode 使用量。它仅在 link_type 设置为 symlink 时生效。如果您只想链接非目录文件,请设置 link_dirs: false

提示

选项 link: run 可用于为 Python 包创建小型环境视图。即使未激活该环境,Python 也能够导入视图内部的包,并且得益于 rpath,链接的库将位于视图外部

在命令行中,spack env create 命令接受一个 --with-view [PATH] 参数,用于设置单个默认视图的路径。如果未指定路径,则使用默认路径(view: true)。可以使用 --without-view 参数来创建一个未配置任何视图的环境。

spack env view 命令可用于管理环境视图。子命令 spack env view enable 会向环境添加一个名为 default 的视图。它接受一个可选参数来指定新默认视图的路径。子命令 spack env view disable 将从环境中移除名为 default 的视图(如果存在)。子命令 spack env view regenerate 将重新生成环境的视图。这将应用环境配置中尚未应用的任何更新。

视图投影 (View Projections)

视图的默认投影是将每个包链接到视图的根目录。projections 属性是部分规范(partial specs)到规范格式字符串的映射,由 format() 函数定义,如下例所示

projections:
  zlib: "{name}-{version}"
  ^mpi: "{name}-{version}/{^mpi.name}-{^mpi.version}-{compiler.name}-{compiler.version}"
  all: "{name}-{version}/{compiler.name}-{compiler.version}"

投影还允许展开环境变量和 Spack 配置变量,如下所示

projections:
  all: "{name}-{version}/{compiler.name}-{compiler.version}/$date/$SYSTEM_ENV_VARIABLE"

其中 $date 是将以 YYYY-MM-DD 格式展开的 Spack 配置变量,而 $SYSTEM_ENV_VARIABLE 是 Shell 中定义的环境变量。

投影配置文件中的条目必须是规范(specs)或关键字 all。对于每个规范,所使用的投影将是该规范满足的第一个非 all 条目;如果存在 all 条目且规范不满足任何其他条目,则使用 all。关键字 all 在文件中出现的位置无关紧要。

根据上面的示例,规范 zlib@1.2.8 将被链接到 /my/view/zlib-1.2.8/,规范 hdf5@1.8.10+mpi %gcc@4.9.3 ^mvapich2@2.2 将被链接到 /my/view/hdf5-1.8.10/mvapich2-2.2-gcc-4.9.3,而规范 hdf5@1.8.10~mpi %gcc@4.9.3 将被链接到 /my/view/hdf5-1.8.10/gcc-4.9.3

如果投影配置文件中没有出现关键字 all,则任何不满足文件中任何条目的规范都将像在单前缀视图中一样链接到视图根目录。投影配置文件中出现在 all 关键字下方的任何条目都不会被使用,因为所有规范在到达这些条目之前都会使用 all 下的投影。

规范组 (Group of Specs)

视图也可以应用于所选的 规范组 列表。这可以通过在视图配置中指定 group: 属性来完成。例如,使用以下清单

spack:
  concretizer:
    unify: true

  packages:
    all:
      require:
      - target=x86_64_v4

  specs:
  - group: compiler
    specs:
    - gcc@15.2

  - group: apps
    needs: [compiler]
    specs:
    - hdf5~mpi %gcc@15.2
    - libtree %gcc@15.2

  view:
    apps:
      root: ./views/apps
      group: apps

该视图将仅包含来自 apps 组的条目,并且不会包含来自 compiler 组的规范。

激活环境视图

spack env activate <env> 命令有两个作用:

  1. 它激活环境,使得后续的 Spack 命令(如 spack install)将在该环境的上下文中运行。

  2. 它激活视图,使得诸如 PATH 之类的环境变量被更新以包含该视图。

如果不带其他参数,将激活环境的 default 视图。如果需要激活具有不同名称的视图,可以使用 spack env activate --with-view <name> <env>。您还可以使用 --without-view 在不修改环境变量的情况下激活环境。

spack env activate 命令影响的环境变量以及用于更新它们的路径由模块配置中定义的前缀检查(prefix inspections)决定;下表总结了默认设置。

变量

路径

PATH

bin

MANPATH

man, share/man

ACLOCAL_PATH

share/aclocal

PKG_CONFIG_PATH

lib/pkgconfig, lib64/pkgconfig, share/pkgconfig

CMAKE_PREFIX_PATH

.

这些路径中的每一个都会被附加到视图根目录下,并在该路径存在时添加到相关变量中。因此,不建议在环境的默认视图中使用非默认投影。

spack env deactivate 命令将从用户的环境变量中移除当前处于活动状态的 Spack 环境视图。

从环境生成依赖文件 (Depfiles)

Spack 可以生成 Makefile,以便更容易地并行构建环境中的多个包。

注意

从 Spack v1.1 开始,有一个新的实验性安装程序,它通过 POSIX jobserver 支持开箱即用的包级并行性。您可以通过 spack config add config:installer:new 启用它。对于主要关注加速环境安装的用户来说,这个新安装程序可能比本节中描述的 spack env depfile 工作流提供了更简单的替代方案。

生成的 Makefile 暴露了一些可以包含在现有 Makefile 中的目标,从而允许其他目标依赖于环境安装。

典型的工作流如下:

$ spack env create -d .
$ spack -e . add perl
$ spack -e . concretize
$ spack -e . env depfile -o Makefile
$ make -j64

这会在当前工作目录中根据已具体化(concretized)的环境生成一个 Makefile,而 make -j64 将安装该环境,尽可能利用跨包的并行性。Spack 会遵守 Make 的 jobserver 并将其转发给包的构建环境,这意味着即使并行构建多个包,单个 -j 标志也足以控制负载。

默认情况下,提供以下伪便利目标:

  • make all:安装环境(默认目标);

  • make clean:清理 make 使用的文件,但不会卸载包。

提示

GNU Make 4.3 及以上版本通过 -O--output-sync 标志对输出同步提供了良好的支持,确保每个包安装的输出能按序打印。若要获得带颜色的同步输出,请使用 make -j<N> SPACK_COLOR=always --output-sync=recurse

指定对生成的 make 目标的依赖

一个有趣的问题是如何在自己的 Makefile 中包含生成的 Makefile。当您想要安装一个提供特定命令所需的可执行文件的环境,并将其用于自己的 make 目标时,就会出现这种情况。

下面的示例展示了如何实现这一点:env 目标将生成的 spack/env 目标指定为前提条件,这意味着环境会在 env 目标中使用前被安装并可用。

SPACK ?= spack

.PHONY: all clean env

all: env

spack.lock: spack.yaml
	$(SPACK) -e . concretize -f

env.mk: spack.lock
	$(SPACK) -e . env depfile -o $@ --make-prefix spack

env: spack/env
	$(info environment installed!)

clean:
	rm -rf spack.lock env.mk spack/

ifeq (,$(filter clean,$(MAKECMDGOALS)))
include env.mk
endif

其工作原理如下:当调用 make 时,它首先会“重做”缺失的包含文件 env.mk,因为有对应的目标。这会触发环境的具体化(concretization),并使 Spack 输出 env.mk。此时,生成的目标 spack/env 将通过 include env.mk 变为可用。

由于通常不希望在 make clean 时重做 env.mk,因此包含是条件性的。

注意

当包含生成的 Makefile 时,重要的是使用 --make-prefix 标志,并使用非伪目标 <prefix>/env 作为前提条件,而不是使用伪目标 <prefix>/all

构建环境的子集

生成的 Makefile 包含每个规范的安装目标,标识为 <name>-<version>-<hash>。这允许您仅安装环境中的一部分包。当包在环境中是唯一的时,只需知道名称,并让制表符自动补全版本和哈希即可。

提供以下伪目标:install/<spec> 用于安装带有依赖项的规范,install-deps/<spec> 安装其依赖项。当某些标志仅应用于依赖项时,这很有用。下面我们展示了一个用例,其中一个规范以详细输出方式(spack install --verbose)安装,而其依赖项则静默安装。

$ spack env depfile -o Makefile

# Install dependencies in parallel, only show a log on error.
$ make -j16 install-deps/python-3.11.0-<hash> SPACK_INSTALL_FLAGS=--show-log-on-error

# Install the root spec with verbose output.
$ make -j16 install/python-3.11.0-<hash> SPACK_INSTALL_FLAGS=--verbose

添加安装后钩子 (post-install hooks)

生成的 Makefile 的另一个高级用例是为每个包运行安装后命令。这些“钩子”可以是任何操作,例如打印安装后消息、运行测试或将刚构建的二进制文件推送到构建缓存。

这可以通过生成的 [<prefix>/]SPACK_PACKAGE_IDS 变量来实现。假设我们有一个处于活动状态且具体化的环境,我们以 example 为前缀生成关联的 Makefile

$ spack env depfile -o env.mk --make-prefix example

现在我们将其包含在另一个 Makefile 中,并创建一个目标 example/push/%,其中 % 指代包标识符。此目标依赖于特定的包安装。在此目标中,我们可以自动使用特定于目标的 HASHSPEC 变量。它们分别是规范哈希(不带前导 /)和人类可读的规范。最后,我们有一个入口目标 push,它会在推送所有包后更新构建缓存索引。注意该目标是如何使用生成的 example/SPACK_PACKAGE_IDS 变量来定义其前提条件的。

SPACK ?= spack
BUILDCACHE_DIR = $(CURDIR)/tarballs

.PHONY: all

all: push

include env.mk

example/push/%: example/install/%
	@mkdir -p $(dir $@)
	$(info About to push $(SPEC) to a buildcache)
	$(SPACK) -e . buildcache push --only=package $(BUILDCACHE_DIR) /$(HASH)
	@touch $@

push: $(addprefix example/push/,$(example/SPACK_PACKAGE_IDS))
	$(info Updating the buildcache index)
	$(SPACK) -e . buildcache update-index $(BUILDCACHE_DIR)
	$(info Done!)
	@touch $@