引言
如果你已经知道 spack 是做什么的了,只是想比阅读官方文档更快速上手,请直接跳到快速上手部分。
Spack1 是一个高度灵活的软件包管理器,其主要解决的痛点是多版本软件的共存。这里的多版本不仅指软件的版本本身,还包括编译该软件使用的编译器版本,MPI 版本,LAPACK 支持版本等的区别。因此 Spack 是一个为超算 HPC 而生的管理工具,其主要的默认支持的软件类型,也以科学计算相关的 C,C++,Fortran 等开源项目为主。我们下面将会看到 spack 具有惊人的灵活性,易用性和可扩展性,因此实际上 spack 可以支持任意软件的安装和管理,无论其是开源软件亦或是商业软件,也无论其是否在 spack 默认的软件列表里。
与其他包管理生态的区别
众所周知,操作系统和某些编程语言提供了很方便的软件包管理工具,比如系统级的 yum,apt,和 python 紧密相关的 pip,conda,node 的 npm 等等。这些工具通常默认2直接提供的是二进制版本的软件(当然 binary 在讨论 js,python 这种纯脚本语言的包的时候比较暧昧,但毕竟还是有很多的 python package 包含了大量的 c 代码),这种二进制文件,下载后通过简单设置和移动到特定路径即可完成安装,使得包管理器的任务可以比较简洁,免去了编译需要的大量依赖静态动态连接,以及编译器版本选择等麻烦。
这些包管理软件的缺点也很明显,最重要的痛点就是很难实现多版本的相同软件共存。如果说这一点还可以通过现代包管理多会提供的虚拟环境方案来解决,那么通过包管理器来安装多个版本不同,编译器不同,链接动态库不同,configure 选项不同的相同软件,并使它们可以和谐共处,可以说是无法完成的任务。不用包管理器,当然是可以完成上述任务,大致的工作流是,在非系统路径中,通过配置不同的环境变量,连接路径和编译器等,来将相同的源码编译成不同的 binary。想要激活某个执行文件和动态库时,需要将对应 binary 路径加入到相应的 PATH
, LD_LIBRARY_PATH
等,同时还可能需要修改编译器使用时的 flag。而对这样的大量软件进行手动安装,依赖解决,冲突处理,和使用停用的切换,几乎是不可能做到的,这样的一个系统维护起来宛如灾难。特别是在 HPC 环境中,往往存在着不同的编译器,比如 gcc,clang 和 icc,或者 pgi,nag 系列的编译器等;同时存在着不同的 mpi 实现,比如 openmpi,intelmpi,mpich 等;同时还存在着不同的 LAPACK 实现,比如 atlas,openblas,intel mkl 等。也就是说对于一个软件的同一个版本我们就可能需要准备 \(3\times 3\times 3=27\) 种 binary,来应对用户的不同开发环境的要求。这是因为如果用户开发的软件选择使用 openmpi,那么该软件所需要的依赖动态库,必须是用 openmpi 提供的 mpicxx
所编译的,如果是 intel mpi 编译的,很显然软件会在编译时出现问题,link 失败。其他条件同理,因此 HPC 维护同一软件的不同 binary 是非常必要的,除非可以强迫用户使用统一的开发工具栈,而这显然是不现实的。
为了解决这些问题,spack 横空出世了,其通过和其他包管理器相似的逻辑,来完成同一软件不同配置的共存和管理。只不过 spack install 时,所下载的是对应库的源代码,整个配置,编译,安装的过程是在本地完成的,也只有这样我们才可以拥有统一软件的多个版本。spack 管理,安装的整个过程,对小白来说,可以如丝般顺滑;对喜欢折腾的人来说,也具有着超强的扩展性和定制性。这样一款包管理工具,显然是为了 HPC 而生的,同时任何需要进行科学计算和相应库安装的工作站及电脑,也很适合用 spack 管理。
为什么要写本文
事实上,本文的所有内容,几乎都可以在 spack 官方文档3中找到,写这篇内容倒不是仅仅做文档翻译这样没技术含量的事,实在是官方文档写的不太好。虽然说关键的内容文档里其实都讲到了,但文档的逻辑组织实在不敢恭维,详略失当,前后失当,把重点混在细节里讲解,很难把握主线,甚至我最早看了很长时间文档,也没搞懂 spack install 到底是下载的 binary 还是 source。因此我决定把 spack 使用的主线重新整理出来,方便查看和快速上手。
原理
上手之前,简单了解一下 spack 的工作原理还是有必要的。spack 由 python 语言实现,但并不需要任何如 pip 之类的安装过程,可直接将 github 源 pull 下来开箱即用。spack install <package>
时,spack 会在本地自己安装的目录寻找对应 <package>
名字的脚本,该 python 脚本则通过 spack python 库提供的 API 描述了如何编译安装该软件的细节。这主要包括,软件的多个版本号和对应的源代码下载地址,configure 的参数,依赖的其他动态库,可能会存在冲突的库,在 spack install
命令中提供的额外参数的处理等等。spack 通过该脚本的 instruction,就可以完成软件的源码下载。通过每个软件脚本中的依赖定义,对于每个软件,spack 就会生成一个依赖的 DAG 进行分析。从而从最基本的依赖开始满足,未满足的依赖则依次重复 spack install 过程,最终编译完成的指定软件,而编译的 binary 则会存于 spack 本地安装目录的 opt 文件夹中,因此这些软件默认是无法调用的,除非通过和 spack 紧密结合的 module 工具进行加载,其实质上就相当于完成了各种环境变量的修改,只不过从 module 文件生成到环境变量添加,spack 都自动完成了。
有了以上这个最粗糙的框架,我们就已经能理清不少问题了。首先,和其他包管理器不同,spack 没有中心的 repository 服务器,这是因为每个具体软件的 url 都指向了对应软件的官网,因此 spack 本身并不需要维护一个服务器。spack 中所谓的 repo 概念,也不过是在 spack 本地文件夹特定位置的一组软件安装 python 脚本而已。其次,可以看到 spack 项目的代码基本由两大部分构成,第一部分是 spack 的框架部分,这一部分提供了一些 API,定义了一些描述安装编译时的行为和函数,这一部分又可细分为 make 需要的 API, cmake 需要的 API,python package 需要的 API 等等,不同的 build system 可以参考文档对应部分。另一部分,则是大量的对应不同软件的 python 脚本,这些脚本利用了 spack 提供的 API,描述式的给出了对应软件的编译过程,而由于这一部分的脚本可以自己自由的修改和添加,因此 spack 可以管理的软件不限于官方的列表,事实上所有的软件都可以通过 spack 编译安装,只要自己去写对应的脚本。而 spack github 库贡献最活跃的部分,就是这些不同软件安装脚本的修改和添加。这种软件编译脚本的编写,被 spack 称作打包 (packaging)。spack 代码的这两部分的开发,分别对应文档的 developer guide 和 packaging guide.
快速上手
所有斜体的碎碎念都可以快速开始学习时略去。每一部分最开始都有 TL;DR 的命令行操作,这一部分已足够完成 spack 的基本安装和使用。因此以下教程分为三个层次,最快上手选择只看 TL;DR 给出的命令行操作,基本了解可以选择阅读文本,较详细的一些的内容都是斜体。
安装
TL;DR
$ cd
$ git clone https://github.com/spack/spack # download spack repo to local path ~/spack
$ source ~/spack/share/setup-env.sh # activate the spack installation by adding spack bin to system PATH
Spack 的代码是纯 python 实现的,而且 spack 开发者为了零依赖零开销的使用 spack,甚至连 pip 都不需要,而是直接把源码复制到用户的某个文件夹下即可。具体的可以从 github download 源码 git clone https://github.com/spack/spack
。一步即完成了 spack 的安装。也就是说这里的安装只不过是把 spack 整个代码与文件结构复制到本地任何路径即可。
一般来讲,对于 git clone 获取的源码,最好 checkout 到某个稳定版本的 tag 上,再进行进一步地编译和使用,防止正在进行开发的 master 分支,存在一些 bug。但对于 spack,正如原理部分所言,活跃的开发几乎都是对不同软件安装脚本的完善和添加,因此直接用 head 的代码,反而可以使用最新的安装脚本,因此可以不 checkout 到稳定 tag。
为了方便,我们之后默认 spack 的安装文件夹为 ~/spack/
。其内部的文件结构如下。
~/spack/ <- installation root
bin/
spack <- main spack executable
etc/
spack/ <- Spack config files.
Can be overridden by files in ~/.spack.
var/
spack/ <- build & stage directories
repos/ <- contains package repositories
builtin/ <- pkg repository that comes with Spack
repo.yaml <- descriptor for the builtin repository
packages/ <- directories under here contain packages
cache/ <- saves resources downloaded during installs
opt/
spack/ <- packages are installed here
lib/
spack/
docs/ <- source for this documentation
env/ <- compiler wrappers for build environment
external/ <- external libs included in Spack distro
llnl/ <- some general-use libraries
spack/ <- spack module; contains Python code
cmd/ <- each file in here is a spack subcommand
compilers/ <- compiler description files
test/ <- unit test modules
util/ <- common code
share/
spack/
setup-env.sh <- spack activate script
templates/
modules/ <- jinja templates for module file rendering
modules/ <- module files for installed softwares
注意以上文件结构,每一个子目录,比如 opt 或者 etc 等都包含了目录 spack,这是为了使得 spack 也可以以 /
为安装的根路径,这样上述的诸如 lib 等文件夹其实质上就是 /lib
等,该文件夹中和 spack 有关的内容进一步限制在 spack 子文件夹内也是理所当然的。
但是 spack 最好还是安装在用户文件夹,这样所有包的安装使用,都不需要 sudo 权限,同时这些库还可以通过进一步的配置文件权限设置,提供给其他用户调用(其他用户默认设置即可调用)。也就是对于没有 sudo 的 HPC 普通用户,也可以自行安装使用自己的 spack,来安装想使用并且 HPC 没有默认提供的软件,这往往比自己手动编译所需依赖或者联系管理员高效的多。于此同时,最有趣的部分在于,每个用户自己安装的 spack,可以通过设定 upstream 的方式,自然的继承管理员的 spack 中已安装的所有 packages,upstream 的设置请参考文档的这里。
以上文件结构中,对应我们原理部分谈到的 spack 的两大部分,分别是 lib/spack/spack 对应的 spack 框架 python 源码部分和 var/spack/repos 对应的不同软件的安装脚本部分。而命令行调用需要的 spack binary 文件则是 bin/spack。
关于集群 spack 安装的注脚。一般来说,集群的 /home
文件夹都是通过 NFS 或者其他合适的文件系统的方式,在不同节点之间共享的。因此简单的将整个 spack 文件结构复制到主节点用户文件夹的位置,即可自动实现了整个 spack 和将来所有 spack 安装的软件在全部节点可用。
此时命令行键入 spack,并不会有任何反应,因为很明显,~/spack/bin
并不在 PATH
之中。由于 spack 相应的路径还需要添加进不同的环境变量才可以正常调用,因此我们不去手动添加 PATH,而是直接使用 spack 提供的修改环境变量,完整激活该 spack 的脚本,其位置为 ~/share/setup-env.sh
,通过 source ~/share/setup-env.sh
即可正常使用 spack。如果希望每次登陆都默认激活 spack,只需将这句 source 命令添加到用户(仅本人用)~/.profile
或全局的 /etc/profile
即可。
激活的 spack 的安装根路径由 $SPACK_ROOT
环境变量给出。事实上通过这种激活的方式,不只 spack 管理的同一软件多种版本可以共存,多个 spack 安装也可以共存和分别使用。
基本的命令行用法
TL;DR
$ spack info tar # check the information of a given package, tar here
AutotoolsPackage: tar
Description:
GNU Tar provides the ability to create tar archives, as well as various
other kinds of manipulation.
Homepage: https://www.gnu.org/software/tar/
Tags:
None
Preferred version:
1.30 https://ftpmirror.gnu.org/tar/tar-1.30.tar.gz
# stdout below is omitted
$ spack spec tar # check the compiling spec and dependents
Input spec
--------------------------------
tar
Concretized
--------------------------------
tar@1.30%[email protected] arch=linux-ubuntu18.04-x86_64
# the stdout indicates that tar with version 1.30 compiled by gcc 7.4.0 would be installed if spack install tar is called
$ spack install tar # install tar based on the above spec
$ spack find tar # show all versions of tar installed by spack
==> 1 installed package
-- linux-ubuntu18.04-x86_64 / [email protected] -------------------------
tar@1.30
$ spack location -i tar # show the full path of installed tar
/home/user/spack/opt/spack/linux-ubuntu18.04-x86_64/gcc-7.4.0/tar-1.30-chre7vfevfhfn2bkv6lchiuoqaqwyav4
spack 最基本的操作,当然就是一系列和其他包管理器类似的操作,包括相应包的查找下载等命令。任何时候,你都可以通过 spack help --all
查看基本的操作。然而这基本上只限于你已经会用 spack 只是记不起具体命令名称的时候,不然 help 给出的很多命令,可能并不知道是做什么的。spack list
可以列出当前支持安装的所有软件名,事实上也就是本地 repo
文件夹中各个安装脚本的名称而已,和任何中心服务器无关。
软件 package 的 syntax:对于 spack 管理的软件,无论是查找,安装还是加载,都自然需要指定软件名才能操作,最简单的就是名字的形式,比如 tar
。但为了指定复杂的版本,编译器,依赖等要求,其名称可以进行一定语法的扩展。比如 ^
开头表示依赖,@
开头表示版本号,%
开头表示编译器等。比如查看 armadillo
对应的安装版本,其显示结果就按照这种 syntax。最开始是安装版本的 hash,作为一个软件一个安装的唯一标识,因此软件也可通过 /<hash>
来指定。同 git 一样,hash 只需指定前几位即可自动识别。同样的,版本号也不一定需要写全,可以只写大版本号,交给 spack 自动识别。此外还有 ~
开头的 syntax,表示不包含后续依赖之意。之后的每个 ^
开始的软件,都是 armadillo 的一个依赖,其也有自己的版本,参数和 patch 等信息。之后我们会有 <package>
作为软件的通配符,其合法形式包括 /dadhc
,armadillo
,[email protected]
,armadillo %[email protected]
, armadillo ^[email protected] ^superlu %gcc
等等。而我们用 <pack-name>
表示 armadillo
这种只有软件名称为合法的通配符。
$ spack find -L -d -v armadillo ## show with Long name, dependents and variants
==> 1 installed package
-- linux-ubuntu18.04-x86_64 / [email protected] -------------------------
dadhchezeuep6uctn4d4zok6tzxay6bi [email protected] build_type=RelWithDebInfo ~hdf5 patches=59207b14cd147b5e6e75f4d81f29f8371261c740d33401b2d30d8ca71ff30b87
bd5vju5jlxzyhmgsexgjoyeoepegye27 ^[email protected]+mpi+shared
ocqy4j4zorvcbcwyn46mwwciia44coax ^[email protected] cpu_target= ~ilp64 patches=47cfa7a952ac7b2e4632c73ae199d69fb54490627b66a62c681e21019c4ddc9d,714aea33692304a50bd0ccde42590c176c82ded4a8ac7f06e573dc8071929c33 +pic+shared threads=none ~virtual_machine
jvqr2t7crxpvwbwda37ehz3wt4o6j5dd ^[email protected]~cuda+cxx_exceptions fabrics= ~java~legacylaunchers~memchecker~pmi schedulers= ~sqlite3~thread_multiple+vt
a57o3y55upjnydldzd7pcxxaszu2n72x ^[email protected]+pic
想要安装软件时,我们可以安装之前 spack spec <package>
,查看 <package>
对应的安装参数和版本,以确认其版本与我们预期相符。更高阶的方式,是直接 spack edit <pack-name>
,这一命令会直接打开 repo 中对应软件的 python 脚本,我们可以从脚本中直接获取软件可能的版本和安装时可选的参数,这些参数通常被脚本中的 variant
API 所定义,对应了一些安装时不同的 configure 选项。
安装软件只需要 spack install <package> <variants>
即可,其中 <variants>
可选,代表了相应的编译参数。安装时会按照脚本给出的官网 url 下载 package 源代码并在本地编译。国情提醒: 很多软件的官网,在中国特色网络环境下,你懂的。强烈建议通过 export http_proxy=<some proxy>
的方式设置 http 和 ftp 代理。否则很多软件可能永远也无法下载。
安装完成后,可以通过 spack find <package>
来查找对应的 package 是否安装成功,并通过 spack location -i <package>
,查看软件的安装位置,其通常都在 $SPACK_ROOT/opt/spack/<arch>/<compiler>/
文件夹内。这也就意味着,安装的软件并不在系统路径,无法直接调用。相应的,自然可以使用 spack uninstall <package>
来删除安装软件。
spack find
的用法其实非常强大。首先不加任何 package 参数,则会默认显示所有已安装的库。除了直接按照 package 的 syntax 进行搜索以外,也可以诸如 spack find ^openmpi
来查找安装的所有依赖 openmpi 的软件。又比如-x
可以只显示直接安装的软件,而不显示因为依赖需要而默认安装的软件。此外 spack graph <package>
可以显示 package 依赖的 DAG。
module 使用
TL;DR
$ which tar # the tar binary is the default one by OS, though new tar is installed by spack
/bin/tar
$ spack install lmod # spack install lmod for module management
$ . $(spack location -i lmod)/lmod/lmod/init/bash # activate lmod, i.e. add lmod to env vars
$ spack load tar # wrapper shorthand for module load
$ module list # check modules loaded now
Currently Loaded Modules:
1) tar-1.30-gcc-7.4.0-ch
$ which tar # tar is now the version under spack
/home/user/spack/opt/spack/linux-ubuntu18.04-x86_64/gcc-7.4.0/tar-1.30-chre7vfevfhfn2bkv6lchiuoqaqwyav4/bin/tar
为了能够顺利使用 spack 安装在非系统路径的各个软件和动态库,我们需要通过 module 的方式来管理。所谓 module 就是通过脚本文件(module 文件)来修改相应的系统环境变量,从而使得对应的软件版本进入系统路径后可以直接调用的一套工具。相信熟悉 HPC 的用户都很熟悉 module 的使用了。spack 的设计和 module 系统可以无缝衔接。可以说用 spack 安装软件的话,那么同时使用 module 系统来管理和加载软件是必须的。
对于每个安装的软件,都可以直接自动生成 module 文件。其原理是通过配置文件的选项和模版文件,在每个软件 spack install
时,自动通过 jinja2 引擎渲染生成对应的 module 文件放在相应的文件夹 share/modules
。注意我们无需安装 jinja2,spack 是零开销的,其文件结构中已经自带了 jinja2 的 package 供调用 ,位置在lib/spack/external
,用户什么都不需要做。
常用的 module 软件有 enviroment modules 和 lmod,本文使用 lmod。注意,如果通过 spack bootstrap
来完成初始的 module 软件的下载和配置的话,其对应的是 enviroment modules。安装 lmod 也很简单,直接通过 spack 完成,spack install lmod
。安装完成后,同样的, lmod 无法直接调用。但其本身就是模块系统,因此无法通过模块加载调用,这就有点像 bootstrap,因此需要 . $(spack location -i lmod)/lmod/lmod/init/bash
单独将 lmod 的相应变量加入系统环境变量和路径。此时可以通过 module -v
来查看 lmod 的版本,验证 lmod 已安装并添加到系统路径。如果希望每次登陆都可以默认使用 module,可以将 . $(spack location -i lmod)/lmod/lmod/init/bash
添加到 bash 的 profile 文件。
现在我们就完成了 spack module 系统的安装,可以开始使用之前 spack 安装的 tar 了,只需要 spack load tar
即可加载 spack 安装的 tar 软件,此时 which tar
地址已指向了 spack 中 tar 的安装路径。
事实上,spack load/unload 命令是对于 module load/unload 命令的 wrapper。这是因为 module load 的参数只严格接受 module 文件名,这通常形如 tar-1.30-gcc-7.4.0-ch
这并不方便记忆。而 spack load/unload 则可和其他命令一样根据不完整的 package syntax 智能识别所对应的安装版本,并且再去调用 module 实现加载。
对于系统已安装的软件 module,可以通过 module avail
查看,而对于已经 load 的 module,可以通过 module list
查看。而 module 文件的具体内容,既可以到对应路径下查看文件,也可以直接 module show <module-name>
的方式来查看。更改 module 的配置文件或模版后,可以通过 spack module tcl refresh --delete-tree
来刷新所有的 module 文件内容。
一般情况下,只需要 module load 自己需要的软件即可,不需考虑其依赖。但对于诸如 python 库等软件,可能还是需要适当的 load 依赖才可正常运行,这种情况可以使用 spack module tcl loads --dependencies <package>
命令。
进阶使用
配置文件
Spack 的配置文件利用 yaml 格式(这渐渐成为“现代”工具的标配了),这里关于 yaml 的基本语法和 parse 就不再赘述了。spack 默认读取的配置文件的路径很多,并且可以按顺序相互覆盖,其位置可以参考文档的这里。一般来讲,对于系统管理员和系统级 spack,配置文件建议存放在 $SPACK_ROOT/etc/spack
。这样既可以使得配置对所有用户生效,又不影响其他的 spack 安装。注意不要覆盖和修改改文件夹下 default 中的配置文件。同时你只需要将相应的配置文件放置在该目录下,如果建立一个新的子文件夹放自定义的配置文件,反而不会生效(?)。
对于文件的修改,有两种模式,一种是直接修改对应的配置文件,另一种是通过 spack config edit <param>
的方式来修改,config 的参数有以下加粗的几个,对应不同方向的配置内容。不过命令行这种方式,修改的文件实际上是 ~/.spack/
中的配置文件。通过 spack config get
可以查看最终完整的配置。
配置文件中也可以出现一些 spack 提供的变量,主要的有 $spack
作为 spack 安装路径,$user
作为当前用户等,具体的可以参考文档的这里。
config
config.yaml 中的内容大致是一些基本的路径设置,比如缓存的路径,module file 模板的路径,临时路径等等,一般来讲不太需要调整。且该文件每个选项的含义,都在 $SPACK_ROOT/etc/spack/default/config.yaml
中有具体的注释说明。或者也可以参考文档的这里。
compilers
关于编译器组的配置,每组都包括 c,c++,f77,f90 等一组编译器的路径和相应的编译 flag 与环境变量等,用于提供给 spack install 软件编译时使用和选择。见文档的这里。
# compilers.yaml
compilers:
- compiler:
environment: {}
extra_rpaths: []
flags: {}
modules: []
operating_system: ubuntu16.04
paths:
cc: /usr/bin/clang-3.7
cxx: /usr/bin/clang++-3.7
f77: null
fc: null
spec: [email protected]
target: x86_64
- compiler:
environment: {}
extra_rpaths: []
flags: {}
modules: []
operating_system: ubuntu16.04
paths:
cc: /usr/bin/clang
cxx: /usr/bin/clang++
f77: null
fc: null
spec: [email protected]
target: x86_64
如上的配置文件,就配置了两组编译器,分别由 paths
指定具体使用的编译器,spec
表示调用该组编译器的名字,也即 <package>
中的 %<spec>
部分的名字。其他参量,看名字也能大概理解是用来做什么的。
packages
packages.yaml 可能是 spack 配置文件中最复杂,但也很有必要的一部分。见文档的这里。
# packages.yaml
packages:
all:
compiler: [clang, gcc, intel, pgi, xl, nag]
providers:
mpi: [mpich, openmpi]
permissions:
read: world
write: user
hdf5:
variants: ~mpi
zlib:
paths:
[email protected]%[email protected] arch=linux-ubuntu16.04-x86_64: /usr
buildable: False
这里只简单解释一下上述配置文件出现的部分。all 中 compiler 指定了 compiler 的默认顺序,一般默认第一组编译器编译。provider 则指定了一系列接口的提供者。通过诸如 mpi,lapack 这种接口,使得 spack 软件安装依赖的表达更加灵活。一些软件依赖这些接口,另一些软件则负责提供这些接口。比如上文将 mpi 依赖默认设置为 mpich。permission 则给出了可以写(spack install)和读(spack load)的用户范围。
packages.yaml 还可以就具体软件给出需要的参量和版本等,相当于把待安装软件进一步通过配置的方式固定下来。此外,还可以使得 external package 纳入 spack 的管理。比如上文 zlib 的例子,就将系统安装的 zlib 纳入了 spack 的管理中。而 buildable:False
则告诉 spack 强制使用该版本,而不再编译其他版本的该软件。注意对于这种有 paths
参数的外部库,还需要额外 spack install <package>
一次,才会进入 spack 的管理数据库并自动生成 module files。否则直接 spack find 是无法找到的。
modules
一些关于 modules 的配置。相关配置需要结合 spack module 工作的细节来理解。见文档的这里和这里。通常可以实现的功能,包括模块文件的命名规范,使用的模块软件 hook,相应软件是否需要生成模块文件,模块文件的冲突标记,以及是否使用层级的模块文件加载等。其配置也比较灵活复杂,这里不再赘述。对于基本用户并不太需要去进行这部分的配置。不过这里有个 subtle 的地方,真要配置的话一定要分清 module hook 和最后使用的 module 软件的区别。激活的 module hook 负责在安装软件时自动生成和该 hook 相符的模板文件,因此可以同时激活多个 hook,也就是一次性生成多个不同规范的 module 文件。但 module 软件则是现在激活正在使用的相应软件,而不同软件和不同 hook 生成的 module 文件的支持不太一样,可以参考文档这里的表格。比如 tcl hook 生成的 module 文件也可以被 lmod 所使用,这也是安装 lmod 之后的默认搭配。
除以上介绍的配置文件之外,spack 还具有 repos,mirrors 两个配置文件,将在下一节结合两者的基本概念进行介绍。
repos, mirros and caches
所谓 repos 就是本地的 var/spack/repos
中的各子文件夹,默认的为 builtin。这里的每个文件夹里可以包含若干 python 脚本,每个脚本描述了一个对应软件的安装手续。每个 repo 文件夹都包括一个 repo.yaml
的配置文件和每个软件安装脚本占一个子文件夹。子文件夹名称为对应软件名,而安装脚本为文件夹中的 package.py
。
spack 通过配置文件 repos.yaml
来设置 repo,注意和每个 repo 自带的 repo.yaml
元数据相区别。
# repos.yaml
repos:
- $spack/var/spack/repos/override
- $spack/var/spack/repos/builtin
对于上述设置,当 spack install
时,spack 会按 repo 顺序寻找安装脚本。因此 override repo 中的 openmpi 安装脚本可以覆盖默认的 builtin 脚本,从而更灵活的定制安装手续。
对于每一个 repo,可以在 repo.yaml
设置 namespace 参量,因此每个软件都有一个 namespace,表示其是根据哪个 repo 的脚本安装的。这一 namespace 信息可以通过 spack find -N <package>
来查看。namespace.package
也是一种合法的描述 package 的 syntax。
除了手动创建 repo 文件夹和修改相应配置文件为,spack 也提供了一组更方便的命令行 API。包括 spack repo list/create/add/rm
。其中 create 仅表示创建了对应 repo 的文件夹,而只有 add 之后,spack 才会自动使用该 repo。
总之, repo 是一堆软件安装 python 脚本的集合。
所谓 mirros 就是可以提供 spack install
时源码文件下载地址替代的服务器。基本想法是,提前在服务器的相应路径准备好待安装库及各依赖库的 tar 压缩包,这样 spack 安装软件时,主机就不需要连接到各软件官网或外网,即可直接从 mirror 获取软件源码。
mirror 文件夹的典型结构是,每个软件一个子文件夹,每个子文件夹里可以有多个不同版本的软件压缩包。其文件名需要遵循 <name>-<version>.<extension>
的格式。spack mirror create
可以直接建立一个 mirror,并且从官网下载所有的软件不同版本源代码,目测耗时较长,适合提供镜像服务器的服务。当然可以指定 spack mirror create <package>
,这样的 mirror 就会只下载符合要求的源码到本地。如果需要 package 较多时,也可通过 spack mirror create --file specs.txt
来下载源码,创建镜像。
mirror 的使用:需要 spack mirror add <mirror-name> <mirror-url>
来添加服务器或本地的 mirror,本地 url 采用 file://path/to/mirror
的格式。spack config edit mirrors
可以修改 mirrors.yaml
从而调整 mirror 的使用顺序。
注意到 var/spack/cache/
就是一个会默认优先尝试的特殊的 mirror。其作为 cache 会保存最近下载过的软件源码,并组织成合法的 mirror 文件夹结构。因此当再次安装相应版本的软件时,会直接从该 mirror 获取源码。请注意区分这种系统自动的源码缓存机制和下面将要介绍的 build cache 机制的区别。
总之,mirror 是一堆软件源代码的集合。
所谓 caches 是某个软件编译好的 binary 形式的打包(这一打包也会包括相应依赖的 binary,从而最大可能使得 binary 具有迁移性和复用性),从而 spack 某些情形可以直接复用 binary,免去了下载源码和耗时的编译。
在 build cache 前,需要提前 spack install 和 load patchelf,gnupg 等工具。spack buildcache create <package>
可以创建 binary 压缩包,默认位置在 ~/build_cache/
。注意 buildcache 的时候很可能需要添加参数,比如 -u
就不添加 GPG 签名,还有很多和 rellocate RPATH 相关的参数。对于编译细节不了解的用户可能很难顺利的使用 buildcache 功能,好在这一功能也不在 spack 的核心地位。
在 build cache 之后,需要将 build_cache 文件夹完整的移动到某个 mirror 文件夹内,也即 mirror_root/build_cache/
。并且必须首先 spack mirror add
添加该 mirror。至此即可 spack buildcache list <package>
和 spack buildcache install <package>
来查找和直接安装二进制文件,跳过编译过程。
总之,cache 是一堆软件 binary 的集合。
虚拟环境
这一部分,和 conda 等的虚拟环境的概念很类似。可以通过创建和激活一个虚拟环境,在环境中安装的相应 package 只在环境中有效,用来维持一个协调的开发环境。不过由于 spack 本来就可以多软件多版本和谐共存,因此对于简单的库调用需求,启用虚拟环境可能用途不大。但对于依赖复杂的大型开发项目,每个项目启用一个虚拟环境可能切换起来更方便些。
虚拟环境的使用不详述了,只简单列举一下 API,相信熟悉 python 等虚拟环境概念的读者可以快速上手。
spack env create <env-name>
: 创建一个新虚拟环境spack env list
: 显示所有的虚拟环境spack env activate <env-name>
: 激活名为 env-name 的虚拟环境spack env status
: 显示当前虚拟环境spack env deactive
: 退出当前虚拟环境,回到原始 spack 环境
在激活的虚拟环境中,软件的查看安装等操作,和正常的 spack 命令行相同。但需要注意的是,对于 spack 已安装的软件,在虚拟环境中安装时,会自动指向,不需重新安装(仅这一点就可以让 virtualenv 和 npm 的包管理方式哭泣)。相应的,当在某个虚拟环境 spack uninstall
时,对应软件也不会真的从 spack 路径中删除,除非 spack 已经确认没有任何虚拟环境还依赖该软件。
注意到 spack 虚拟环境默认使用的配置文件与 spack 相同,但可以通过 spack config edit
来进行修改,虚拟环境中的配置修改只影响该环境。spack 虚拟环境文件的实际路径为 var/spack/environments/<env-name>/
,其中的 spack.yaml
即为修改的只影响该环境的配置文件。其中包含了虚拟环境的库信息,因此可以直接 spack install
进行全部安装。事实上,当 pwd
内存在 spack.yaml
时,spack 会默认对应的虚拟环境已激活,而不管是否在命令行激活过。与 spack.yaml
相对应的,虚拟环境文件夹中还存在 spack.lock
文件。熟悉 node 的读者,可能看到名字就知道这个文件的作用了。没错,该文件就是更详细完整的,该虚拟环境安装库的版本和配置记录。
pip in spack
对于 spack 安装的 python,其 sys.path
并不包括全局路径,但是包括 ~/.local/lib/python
中的 packages。也就是说,通过全局的 pip,即 /usr/bin/pip
,在不 sudo 情况下安装的 package,是可以被 spack 内的 python 直接导入和使用的。但这种方式有几个缺陷,第一,全局的 pip 和 spack 里的 python,总觉得有点不配套。第二就是,spack 虽然安装在用户文件夹,但目标是给全体用户提供软件服务,而 ~/.local/lib
这一路径对于其他用户是不同的位置,因此这种 python packages 要想提供给其他用户导入,还需要额外的操作。
因此最好的办法,就是直接 spack 安装 pip。比如 spack install [email protected]
之后,安装对应版本的 pip,也即 spack install py-pip ^[email protected]
,之后需要 spack load py-setuptools
和 spack load py-pip
,此时 which pip
发现 pip 已经更换为 spack 版本,pip install
的 packages 将存入 spack 相应路径,从而直接可提供给全部用户使用。这里需要注意的是,虽然 setuptools 会作为 pip 的依赖自动安装,但 load 模块时,还是需要两者都 load,pip 才可以正常工作。值得指出的是,spack 的 pip,依然可以通过 ~/.pip/pip.conf
位置的配置文件进行配置。
最后,通过 spack extensions python
查看可以直接作为 python package,通过 spack 安装的包。但我其实不太推荐通过这种方式安装 python package。因为这样安装的 package 和 python 相对独立,不在 python 默认的系统路径里,也就是每次使用的时候,都需要单独 spack load
才可以正常在 python 中导入。此外,通过 pip
安装 packages,也可能因为一些独立安装的 package 没有 load 而认为不存在,从而导致重复安装依赖等现象,增大了管理难度。
module 管理非 spack 安装的软件
用 spack 安装的 module 来管理非 spack 安装软件的加载时,最简单的方案可能是在 MODULEPATH
环境变量中添加新的文件夹路径,并把自己写的 module files 放在该路径中,从而用 module add
的方式加载。但这种方式显然无法令人满意。第一,module files 需要我们自己维护,很容易出错和冲突。第二,这些外部软件加载的时候,无法使用 spack load
命令,造成了加载模块接口的不统一。
这里我们以 mathematica 纳入 spack 模块管理为具体例子,来展示一个更好的更协调的外部软件模块管理方案。首先,创建一个新的 repo,也即 mkdir $spack/var/spack/repos/override
,并且添加 repo.yaml 和 packages 子文件夹。repo.yaml 内容为:
repo:
namespace: override
然后通过修改 spack 配置文件 repos.yaml
,将该 repo 源添加到默认的 builtin repo 之前。这样我们就可以在 override repo 的 packages 中 touch mathematica/package.py
,使得 spack 可以管理 mathematica 软件。当然这些关于 repo 的操作,只要你不担心污染 builtin repo,也可以不做,而直接在 builtin repo 的文件夹中添加 mathematica 的安装 recipe。总之,我们需要添加一个 pseudo recipe,使得 spack 可以管理外部软件,其内容如下。
from spack import *
class Mathematica(Package):
homepage = "https://www.wolfram.com/mathematica/"
version('11.0')
可以看到,该脚本无法真的完成 mathematica 的安装,但这不重要,这只是一个用来占位的文件而已。现在修改 spack 的 packages.yaml 配置文件,添加以下内容。
mathematica:
paths:
[email protected]: /root/path/to/mathematica
现在,spack install mathematica
来使得外部安装的 mathematica 纳入 spack 的管理。之后就可以 spack load mathematica
来加载 mathematica 使用了。
And more …
相信可以读到这里,并熟悉以上内容的读者,已经完全可以独立去看 spack 的官方文档或者直接阅读源码,来了解更多的内容了。很多特征尤其是对于软件安装 python 脚本的编写和软件编译系统 API 的使用细节,spack 本身的魔改插件和命令行功能扩展等等,还是值得 spack 重度用户去掌握的。不过有了前述内容的积累,这部分进阶知识就不在此文介绍了,可以去官方文档里继续学习。
Reference
-
事实上很多库管理软件也可以提供源码下载和安装的选项,比如 apt 获取源码和软件安装的方式可以参考: How do I get and modify the source code of packages installed through apt-get? ↩