Spec 语法

Spack 有一种特定的语法来描述软件包约束。每个约束单独被称为一个 spec(规格)。Spack 使用 spec 来

  1. 引用软件包的特定构建配置,或

  2. 通过配置文件表达软件包的需求或偏好,或

  3. 查询已安装的软件包或构建缓存

Spec 不仅仅是软件包名称和版本;您可以使用它们来指定构建的编译器、编译器版本、架构、编译选项和依赖项选项。在本节中,我们将介绍 spec 的完整语法。

以下是使用复杂 spec 来安装 mpileaks 特定配置的示例

$ spack install mpileaks@1.2:1.4 +debug ~qt target=x86_64_v3 %gcc@15 ^libelf@1.1 %clang@20

下图有助于您了解构成此 spec 的各个部分

Spack spec with annotations

安装时,您将获得

  • mpileaks 软件包,版本介于 1.21.4 之间(含),

  • 启用了 debug 选项,且没有 qt 支持,

  • 针对 x86_64_v3 架构进行了优化,

  • 使用 gcc 版本 15 构建,

  • 依赖于 libelf 版本 1.1,该依赖项使用 clang 版本 20 构建。

大多数 spec 不会像这个例子这样复杂,但这是 spec 功能的一个很好的演示。我们可以从第一个例子中推断出几条通用规则

  1. 用户在描述软件包构建细节时,可以根据需要模糊或精确

  2. Spec 语法是递归的,即 %^ 之后的每个依赖项本身也是一个 spec

  3. 传递依赖项位于 ^ 符号之后,它们始终指向根软件包

  4. 直接依赖项位于 % 符号之后,它们指向根软件包或最后定义的传递依赖项

Spec 语法在指定构建细节时提供的灵活性使得 Spack 对初学者和专家同样友好。

软件模型

为了真正理解上述内容,我们需要考虑软件是如何构建的。可执行文件或库通常依赖于其他库才能运行。我们可以将软件包与其依赖项之间的关系表示为图。这是 mpileaks 的简化依赖图

digraph { node[ fontname=Monaco, penwidth=2, fontsize=124, margin=.4, shape=box, fillcolor=lightblue, style="rounded,filled" ] mpileaks -> { mpich callpath } callpath -> { mpich dyninst } dyninst -> libdwarf -> libelf dyninst -> libelf }

上图中的每个方框都是一个软件包,每个箭头代表对其他软件包的依赖。例如,我们说 mpileaks 依赖于 callpathmpichmpileaks间接依赖于 dyninstlibdwarflibelf,因为这些库是 callpath 的依赖项。要安装 mpileaks,Spack 必须构建所有这些软件包。Spack 中的依赖图必须是无环的,并且依赖于关系是有方向的,因此这是一个有向无环图,即 DAG

Spec 中的软件包名称标识符是某个依赖 DAG 的根,而 DAG 本身是隐式的。Spack 知道软件包之间精确的依赖关系,但用户不需要知道完整的 DAG 结构。完整 spec 中的每个 ^ 都指代根软件包的传递依赖项。每个 % 都指代直接依赖项,即根软件包或最后定义的传递依赖项。

在需要一致性的情况下,Spack 仅允许每个软件包存在单一配置。在上面的例子中,mpileakscallpath 都依赖于 mpich,但 mpich 在 DAG 中仅出现一次。您不能构建一个依赖于某个 mpich 版本,同时又依赖于一个依赖于其他 mpich 版本的 callpathmpileaks 版本。通常情况下,这种配置在运行时可能会表现异常,Spack 强制执行此规则以确保运行环境的一致性。

Spec 的目的是为 Spack 用户抽象掉整个 DAG。完全不关心 DAG 的用户只需简单地输入以下内容即可引用 mpileaks

mpileaks

如果用户知道 mpileaks 间接使用 dyninst 并希望使用特定版本的 dyninst,spec 会变得稍微复杂一点

mpileaks ^dyninst@8.1

Spack 会在安装 spec 之前补全其余细节。用户只需要了解软件包名称及其关系的最基本细节。您可以在依赖项 spec 上使用与根 spec 相同的修饰符。也就是说,您可以像指定任何其他 spec 一样指定它们的版本、变体和架构。说明符与其左侧最近的软件包名称相关联。

虚拟依赖项

我们上面看到的 mpileaks 依赖图并不完全准确。mpileaks 使用 MPI,这是一种具有多种实现的接口。上面我们展示了 mpileakscallpath 依赖于 mpich,这是 MPI 的一种特定实现。然而,我们可以使用其他实现来构建它们,例如 openmpimvapich

Spack 使用虚拟依赖项来表示此类接口。对于 mpileaks,真正的依赖 DAG 看起来像这样

digraph { node[ fontname=Monaco, penwidth=2, fontsize=124, margin=.4, shape=box, fillcolor=lightblue, style="rounded,filled" ] mpi [color=red] mpileaks -> mpi mpileaks -> callpath -> mpi callpath -> dyninst dyninst -> libdwarf -> libelf dyninst -> libelf }

请注意,mpich 现在已被 mpi 取代。没有真实的 MPI 软件包,但某些软件包提供 MPI 接口,当构建 mpileaks 时,这些软件包可以替代 mpi

Spack 的独特之处在于其虚拟软件包可以像普通软件包一样进行版本控制。软件包的特定版本可以提供虚拟软件包的特定版本。软件包可以依赖于虚拟软件包的特定版本。例如,如果应用程序需要 MPI-2 功能,它可以依赖于 mpi@2:,以表明它需要提供 MPI-2 功能的实现。

以下是关于您可以添加到 spec 中的说明符的更多详细信息。

版本说明符

版本说明符

pkg@specifier

位于软件包名称之后,并以 @ 开头。它可以是匹配多个已知版本的抽象描述,也可以是特定版本。

版本说明符通常表示版本范围

# All versions between v1.0 and v1.5.
# This includes any v1.5.x version
@1.0:1.5

# All versions up to and including v3
# This would include v3.4 etc.
@:3

# All versions above and including v4.2
@4.2:

但也可以是特定版本

# Exactly version v3.2, will NOT match v3.2.1 etc.
@=3.2

作为简写,@3 等同于范围 @3:3,包含主要版本为 3 的任何版本。版本按其组件进行字典序排序。有关排序的更多详细信息,请参阅 打包指南

请注意,您可以区分特定版本 @=3.2 和范围 @3.2。这对于遵循省略零补丁版本号的软件包很有用:3.23.2.13.2.2 等。通常,使用范围语法 @3.2 更可取,因为范围也匹配带有后缀的版本,例如 3.2-custom

版本说明符也可以是以逗号分隔的范围和特定版本的列表。例如

@1.0:1.5,=1.7.1

匹配范围 1.0:1.5 中的任何版本和特定版本 1.7.1

Git 版本

注意

希望仅匹配基于分支或标签的特定提交版本的用户应分配 commit 变体(commit=<40 char sha>)。Spack 专门保留此变体来跟踪基于 git 的版本的来源。Spack 将尝试在具体化(concretization)过程中自动计算此值,如果无法分配提交,则会发出警告。更多详细信息可在 Git 版本来源 中找到。

对于具有 git 属性的软件包,可以指定 git 引用(即分支、标签和提交)来代替数字版本。Spack 将基于所提供的 git 引用进行暂存和构建。可接受的语法包括

# commit hashes
foo@abcdef1234abcdef1234abcdef1234abcdef1234  # 40 character hashes are automatically treated as git commits
foo@git.abcdef1234abcdef1234abcdef1234abcdef1234

# branches and tags
foo@git.develop  # use the develop branch
foo@git.0.19  # use the 0.19 tag

Spack 总是需要将 Spack 版本与 git 引用关联,该版本用于版本比较。此 Spack 版本是通过启发式方法从 git 引用的祖先中最接近的有效 git 标签获取的。

一旦 Spack 版本与 git 引用关联,它总是会与 git 引用一起打印。例如,如果提交 @git.abcdefg 被标记为 0.19,则 spec 将显示为 @git.abcdefg=0.19

如果 git 引用不完全是标签,则到最近标签的距离也是解析版本的一部分。@git.abcdefg=0.19.git.8 意味着该提交距离 0.19 标签有 8 次提交。

在 Spack 无法从 git 引用解析出合理版本的情况下,用户可以指定用于该 git 引用的 Spack 版本。这可以通过在 git 引用后追加 = 和 Spack 版本来实现。例如

foo@git.my_ref=3.2 # use the my_ref tag or branch, but treat it as version 3.2 for version comparisons
foo@git.abcdef1234abcdef1234abcdef1234abcdef1234=develop # use the given commit, but treat it as develop for version comparisons

关于版本如何比较以及 Spack 如何确定一个版本是否小于另一个版本的详细信息,请参阅开发者指南。

变体(Variants)

变体是与特定软件包关联的命名选项,通常用于在构建时启用或禁用某些功能。它们是可选的,因为每个软件包必须为其提供的每个变体提供默认值。

特定软件包可用的变体由软件包作者定义。spack info <package> 将提供有关可用构建变体的信息。

变体有不同类型。

布尔变体

通常用于在编译时启用或禁用功能。例如,软件包可能具有一个 debug 变体,可以通过以下方式显式启用

+debug

并通过以下方式禁用

~debug

单值变体

通常用于设置默认值。例如,软件包可能有一个 compression 变体来确定默认压缩算法,用户可以将其设置为

compression=gzip

compression=zstd

多值变体

软件包可能具有一个 fabrics 变体,用于确定支持哪些网络架构(fabrics)。用户可以同时激活多个值。例如

fabrics=verbs,ofi

同时启用了 InfiniBand verbs 和 OpenFabrics 接口。这些值以逗号分隔。

fabrics=verbs,ofi 的含义是启用至少指定的架构,但也可能启用其他架构。如果意图是启用指定的架构,则应使用

fabrics:=verbs,ofi

语法及 := 运算符。

变体向依赖项的传播

Spack 允许变体通过对布尔变体使用 ++--~~ 将其值传播到软件包的依赖项。例如,对于 debug 变体

mpileaks ++debug   # enabled debug will be propagated to dependencies
mpileaks +debug    # only mpileaks will have debug enabled

为了传播非布尔变体的值,Spack 使用 name==value。例如,对于 stackstart 变体

mpileaks stackstart==4   # variant will be propagated to dependencies
mpileaks stackstart=4    # only mpileaks will have this variant value

Spack 还允许变体从不具备该变体的软件包中进行传播。

编译器标志

编译器标志使用与非布尔变体相同的语法,但实现不同的目的。变体的功能由软件包设置,而编译器标志则被编译器包装器用于将标志注入构建的编译命令行。此外,可以通过使用 == 将编译器标志继承给依赖项。spack install libdwarf cppflags=="-g" 将安装 libdwarf 和 libelf,并将 -g 标志注入它们的编译命令行中。

请注意,如果编译器标志的值包含空格,则必须加引号。cppflags=-O3cppflags="-O3"cppflags='-O3'cppflags="-O3 -fPIC" 都是可接受的,但 cppflags=-O3 -fPIC 不可接受。此外,如果编译器标志的值不是行尾内容,则必须后跟一个空格。命令 spack install libelf cppflags="-O3"%intel 将被解释为尝试设置 cppflags="-O3%intel"

这六个编译器标志按 GNU Autotools 中隐式 make 命令相同的顺序注入。如果设置了所有标志,对于 C 和 C++,顺序为 $cppflags $cflags|$cxxflags $ldflags <command> $ldlibs;对于 Fortran,顺序为 $fflags $cppflags $ldflags <command> $ldlibs

架构说明符

Spec 依赖图中的每个节点都有一个架构属性。该属性是平台、操作系统和处理器的三元组。您可以通过使用保留关键字 platformostarget 分别指定这些元素

$ spack install libelf platform=linux
$ spack install libelf os=ubuntu18.04
$ spack install libelf target=broadwell

通常,如果用户正在为当前主机安装软件,则无需费心指定架构,因为在这种情况下会自动检测这些值。如果您需要对哪些软件包使用哪些目标(或对所有软件包的默认目标)进行精细控制,请参阅 软件包偏好设置

对特定微架构的支持

Spack 知道如何检测许多特定的微架构并对其进行优化,并将此信息编码在架构规范的 target 部分中。可以通过以下方式获取 Spack 已知的微架构完整列表

$ spack arch --known-targets
Generic architectures (families)
    aarch64  armv8.1a  armv8.3a  armv8.5a  armv9.0a  ppc64    ppcle    sparc    x86     x86_64_v2  x86_64_v4
    arm      armv8.2a  armv8.4a  armv8.6a  ppc       ppc64le  riscv64  sparc64  x86_64  x86_64_v3

GenuineIntel - x86
    i686  pentium2  pentium3  pentium4  prescott

GenuineIntel - x86_64
    nocona  nehalem   sandybridge  haswell    skylake  cannonlake      cascadelake  sapphirerapids
    core2   westmere  ivybridge    broadwell  mic_knl  skylake_avx512  icelake

AuthenticAMD - x86_64
    k10  bulldozer  piledriver  zen  steamroller  zen2  zen3  excavator  zen4  zen5

IBM - ppc64
    power7  power8  power9  power10

IBM - ppc64le
    power8le  power9le  power10le

Cavium - aarch64
    thunderx2

Fujitsu - aarch64
    a64fx

ARM - aarch64
    cortex_a72  neoverse_n1  neoverse_v1  neoverse_v2  neoverse_n2

Ampere - aarch64
    ampere1  ampere1a

Apple - aarch64
    m1  m2  m3  m4

SiFive - riscv64
    u74mc

SpacemiT - riscv64
    x60

安装 spec 时,Spack 会将所使用的编译器与目标微架构进行匹配,以便在编译时注入适当的优化标志。给出如下命令

$ spack install zlib target=icelake %gcc@14

将生成类似于以下的编译行

$ /usr/bin/gcc-14 -march=icelake-client -mtune=icelake-client -c ztest10532.c
$ /usr/bin/gcc-14 -march=icelake-client -mtune=icelake-client -c -fPIC -O2 ztest10532.
...

其中标志 -march=icelake-client -mtune=icelake-client 由 Spack 根据请求的目标和编译器注入。

如果 Spack 确定所请求的编译器无法为所请求的目标进行优化,或者完全无法为该目标构建二进制文件,它将退出并显示有意义的错误消息

$ spack install zlib target=icelake %gcc@5
==> Error: cannot produce optimized binary for micro-architecture "icelake" with gcc@5.5.0 [supported compiler versions are 8:]

相反,如果为较新的微架构选择了较旧的编译器,Spack 将针对最佳匹配进行优化,而不会失败

$ spack arch
linux-ubuntu18.04-broadwell

$ spack spec zlib%gcc@4.8
Input spec
--------------------------------
zlib%gcc@4.8

Concretized
--------------------------------
zlib@1.2.11%gcc@4.8+optimize+pic+shared arch=linux-ubuntu18.04-haswell

$ spack spec zlib%gcc@9.0.1
Input spec
--------------------------------
zlib%gcc@9.0.1

Concretized
--------------------------------
zlib@1.2.11%gcc@9.0.1+optimize+pic+shared arch=linux-ubuntu18.04-broadwell

例如,在上面的代码片段中,当使用 gcc@4.8 编译时,微架构被降级为 haswell,因为 broadwell 的优化支持从 gcc@4.9: 开始。

最后,如果 Spack 没有匹配编译器和目标的信息,它将继续安装,但避免注入任何特定于微架构的标志。

依赖项

DAG 中的每个节点都可以使用 %^ 符号指定依赖项

  • % 符号标识直接依赖项,这意味着必须有一条边将依赖项连接到它们所引用的节点。

  • ^ 符号标识传递依赖项,这意味着依赖项只需要存在于它们所引用的节点的子 DAG 中。

编写 spec 时,传递依赖项的顺序无关紧要。例如,这两个 spec 代表完全相同的配置

mpileaks ^callpath@1.0 ^libelf@0.8.3
mpileaks ^libelf@0.8.3 ^callpath@1.0

使用 % 指定的直接依赖项要么应用于最近的传递依赖项(^),如果不存在,则应用于 spec 中的根软件包。因此,在 spec 中

root %dep1 ^transitive %dep2 %dep3

dep1root 的直接依赖项,而 dep2dep3 都是 transitive 的直接依赖项。

约束虚拟软件包

安装依赖于虚拟软件包的软件包时,请参阅 虚拟依赖项,您可以选择指定要使用的特定提供程序,或者让 Spack 进行选择。例如,如果您只是输入

$ spack install mpileaks

那么 Spack 将根据站点策略为您选择一个 mpi 提供程序。如果您确实想要特定的版本,例如 mpich,那么您可以改为运行

$ spack install mpileaks ^mpich

这强制 Spack 使用某个版本的 mpich 作为其实现。一如既往,您可以更加具体,要求特定的 mpich 版本

$ spack install mpileaks ^mpich@3

特别是 mpileaks 软件包只需要 MPI-1 命令,因此任何 MPI 实现都可以。如果另一个软件包依赖于 mpi@2,而您尝试为其提供不足的 MPI 实现(例如,仅提供 mpi@:1),则 Spack 将引发错误。同样,如果您尝试插入某些不提供 MPI 的软件包,Spack 也会引发错误。

虚拟依赖项的显式绑定

有些软件包提供的不仅仅是一个虚拟依赖项。在与它们交互时,用户可能只想使用它们可以提供的一小部分,而使用其他提供程序来满足他们需要的虚拟依赖项。

可以使用特殊的语法更明确地告诉 Spack 哪个依赖项应该提供哪个虚拟依赖项

$ spack spec strumpack ^mpi=intel-parallel-studio+mkl ^lapack=openblas

将上面的 spec 具体化会产生以下 DAG

_images/strumpack_virtuals.svg

其中 intel-parallel-studio 可以提供 mpilapackblas,但仅被用于前者。lapackblas 依赖项由 openblas 满足。

依赖边属性

某些 spec 需要关于软件包与其依赖项之间关系的额外信息。此信息位于两者之间的边上,并且可以通过在依赖项符号后加上方括号 [] 来指定。边属性总是作为键值对指定

root ^[key=value] dep

在以下章节中,我们将讨论 spec 语法中当前允许的边属性。

虚拟依赖(Virtuals)

软件包可以提供或依赖多个虚拟软件包。用户可以通过指定 virtuals 边属性来选择使用哪个依赖项的哪些虚拟依赖项

$ spack install mpich %[virtuals=c,cxx] clang %[virtuals=fortran] gcc

上面的命令告诉 Spack 使用 clang 提供 ccxx 虚拟依赖,并使用 gcc 提供 fortran 虚拟依赖。

我们在 虚拟依赖项的显式绑定 中看到的特殊语法是指定 virtuals 边属性的一种更紧凑的方式。例如,上述命令的等效写法是

$ spack install mpich %c,cxx=clang %fortran=gcc

条件依赖项

条件依赖项允许仅在特定条件下应用依赖约束。我们可以通过指定 when 边属性来表达条件约束

$ spack install hdf5 ^[when=+mpi] mpich@3.1

这告诉 Spack,如果 hdf5 配置了 MPI 支持,则它应依赖于 mpich@3.1

依赖传播

节点上的依赖说明可以使用双百分号 %% 符号进行传播。这在指定编译器时特别有用。例如,以下命令

$ spack install hdf5+cxx+fortran %%c,cxx=clang %%fortran=gfortran

告诉 Spack 使用 Clang 作为 C 和 C++ 编译器,并使用 GCC 作为 Fortran 编译器来安装 hdf5。它还告诉 Spack 将相同的选择作为 强偏好 传播给 hdf5 的运行时子 DAG。构建工具不受影响,仍可以选择使用不同的编译器。

通过哈希指定 Spec

复杂的 spec 在命令行中输入可能会变得繁琐,尤其是当需要许多限定条件来区分相似的安装时。为避免这种情况,在引用现有 spec 时,Spack 允许您通过哈希值引用它们。我们之前讨论过 Spack 计算的 spec 哈希值。在任何命令中,用 /<hash> 替换 spec,其中 <hash> 是 spec 哈希值开头的任意长度部分。

例如,假设您意外安装了两个不同的 mvapich2 安装。如果您想卸载其中一个但不知道区别是什么,您可以运行

$ spack find --long mvapich2
==> 2 installed packages.
-- linux-centos7-x86_64 / gcc@6.3.0 ----------
qmt35td mvapich2@2.2%gcc
er3die3 mvapich2@2.2%gcc

然后,您可以使用以下命令卸载后者

$ spack uninstall /er3die3

或者,如果您想将特定的安装作为依赖项进行构建,可以使用

$ spack install trilinos ^/er3die3

如果给定的 spec 哈希足够长且唯一,Spack 将用它所指代的 spec 替换该引用。否则,它会提示输入更具体的哈希。

注意,对于由 spack uninstall --force 卸载的依赖项,此方法无法用于重新安装。

命令行上的 Spec

Spec 语法中使用的字符是经过精心选择的,以便与大多数 Shell 良好配合。然而,在某些情况下,Shell 可能会在 Spack 有机会解析 spec 之前对其进行解释,从而导致意外结果。在此,我们记录两种此类情况以及如何避免它们。

Unix Shell

在类 Unix 系统上,Shell 可能会将 ~foo 扩展为名为 foo 的用户的主目录,因此 Spack 不会将其视为 已禁用的布尔变体 foo。要在不使用引号的情况下解决此问题,您可以避免在软件包名称和布尔变体之间留空格

mpileaks ~debug   # shell may expand this to `mpileaks /home/debug`
mpileaks~debug    # use this instead

或者,您可以使用连字符 - 字符来禁用变体,但请注意这需要在软件包名称和变体之间留出空格

mpileaks-debug     # wrong: refers to a package named "mpileaks-debug"
mpileaks -debug    # right: refers to a package named mpileaks with debug disabled

作为最后手段,debug=False 也可以用来禁用布尔变体。

Windows CMD

在 Windows CMD 中,插入符号 ^ 是一个转义字符,需要对其自身进行转义。同样,等号 = 在 CMD 中也具有特殊含义。

要在 spec 中使用插入符号和等号字符,您可以像这样引用并转义它们

C:\> spack install mpileaks "^^libelf" "foo=bar"

PowerShell 中不存在这些问题。有关更多详细信息,请参阅 GitHub issue #42833#43348