Skip to content

Commit 3e6efc1

Browse files
committed
merge main
2 parents b62f24d + 6e49f96 commit 3e6efc1

22 files changed

Lines changed: 445 additions & 134 deletions

File tree

.github/workflows/main.yaml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ jobs:
2323
done
2424
exit $errors
2525
- name: Install Dependencies
26-
run: uv sync --group dev
26+
run: |
27+
uv sync --group dev
28+
sudo apt-get install -y jq
2729
- name: Run Unit Tests
2830
run: uv run pytest
31+
- name: Run Envvars Test
32+
working-directory: unittests
33+
run: uv run bash ./test-envvars.sh

docs/cluster-config.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,8 @@ If custom package definitions are provided for the same package in more than one
137137
138138
The following precedence is applied, where 1 has higher precedence than 2 or 3:
139139
140-
1. packages defined in the (optional) `repo` path in the [recipe](recipes.md#custom-spack-packages)
141-
2. packages defined in the (optional) site repo(s) defined in the `repo/repos.yaml` file of cluster configuration (documented here)
142-
3. packages provided by Spack (in the `var/spack/repos/builtin` path)
140+
1. packages defined in the (optional) `repo` path in the [recipe][custom-spack-packages]
141+
2. packages defined in the (optional) site repo(s) defined in the `repo/repos.yaml` file of the cluster configuration (documented here)
142+
3. packages defined in the package repositories configured in `config.yaml:spack:packages`, in the order specified (which typically includes `builtin`)
143143
144-
As of Stackinator v4, the definitions of some custom repositories (mainly CSCS' custom cray-mpich and its dependencies) was removed from Stackinator, and moved to the the site configuration
144+
As of Stackinator v4, the definitions of some custom repositories (mainly CSCS' custom cray-mpich and its dependencies) was removed from Stackinator, and moved to the site configuration.

docs/recipes.md

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,37 @@ version: 2
4141
* `version`: _default = 1_ the version of the uenv recipe (see below)
4242
* `modules`: (_deprecated_) _optional_ enable/disable module file generation.
4343

44+
It's possible to configure multiple package repositories for the uenv build by providing a dictionary of spack repositories. For example:
45+
46+
```yaml title="config.yaml"
47+
name: prgenv-gnu
48+
store: /user-environment
49+
spack:
50+
repo: https://github.com/spack/spack.git
51+
commit: releases/v1.0
52+
packages:
53+
builtin:
54+
repo: https://github.com/spack/spack-packages.git
55+
commit: develop
56+
foo:
57+
repo: https://github.com/foo/spack-packages.git
58+
commit: v13.2
59+
path: repos/spack_repo/foo
60+
version: 2
61+
```
62+
63+
The `path` entry is optional and defaults to `repos/spack_repo/${name}`, where the dictionary key is the `name`.
64+
For the upstream spack-packages repository, the default value can be used.
65+
66+
!!! info
67+
The order of package repositories is significant.
68+
stackinator follows the same semantics as spack itself, where package repositories further up in the list take precedence over ones later in the list.
69+
Refer to the [spack documentation](https://spack.readthedocs.io/en/latest/repositories.html#search-order-and-overriding-packages) for more information.
70+
71+
!!! info
72+
`recipe` and `alps` are reserved repository names for internal stackinator use and can't be used for user-specified package repositories.
73+
The `recipe` and `alps` repositories have higher precedence than repositories configured in `config.yaml` (see [custom spack packages][ref-custom-spack-packages] for more details).
74+
4475
!!! note "uenv recipe versions"
4576
Stackinator 6 introduces breaking changes to the uenv recipe format, introduced to support Spack v1.0.
4677

@@ -488,9 +519,10 @@ Modules are generated for the installed compilers and packages by spack.
488519
- `modules:default:arch_folder` defaults to `false`. If set to `true` an error is raised, as Stackinator does not support this feature;
489520
- `modules:default:roots:tcl` is ignored, as Stackinator automatically configures the module root to be inside the uenv mount point.
490521

522+
[](){#ref-custom-spack-packages}
491523
## Custom Spack Packages
492524

493-
An optional package repository can be added to a recipe to provide new or customized Spack packages in addition to Spack's `builtin` package repository, if a `repo` path is provided in the recipe.
525+
An optional package repository can be added to a recipe to provide new or customized Spack packages in addition to the package repositories configured in `config.yaml`, if a `repo` path is provided in the recipe.
494526

495527
For example, the following `repo` path will add custom package definitions for the `hdf5` and `nvhpc` packages:
496528

@@ -503,6 +535,8 @@ repo
503535
└─ package.py
504536
```
505537

538+
Packages provided together with a recipe will be installed in a separate `recipe` namespace.
539+
506540
Additional custom packages can be provided as part of the cluster configuration, as well as additional site packages.
507541
These packages are all optional, and will be installed together in a single Spack package repository that is made available to downstream users of the generated uenv stack.
508542
See the documentation for [cluster configuration](cluster-config.md) for more detail.
@@ -524,12 +558,13 @@ See the documentation for [cluster configuration](cluster-config.md) for more de
524558
In the above case, the package `fmt` is backported from `origin/develop` into the `stackinator-recipe`.
525559

526560
!!! alps
527-
All packages are installed under a single spack package repository called `alps`.
561+
All cluster configuration packages are installed under a single spack package repository called `alps`.
528562
The CSCS configurations in [github.com/eth-cscs/alps-cluster-config](https://github.com/eth-cscs/alps-cluster-config) provides a site configuration that defines cray-mpich, its dependencies, and the most up to date versions of cuda, nvhpc etc to all clusters on Alps.
563+
These site packages are installed under the `alps` Spack package repository namespace.
529564

530565
!!! warning
531566
Unlike Spack package repositories, any `repos.yaml` file in the `repo` path will be ignored.
532-
This is because the provided packages are added to the `alps` namespace.
567+
This is because the provided packages are installed in the `recipe` namespace.
533568

534569
## Post install configuration
535570

stackinator/builder.py

Lines changed: 84 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@
1313

1414
from . import VERSION, root_logger, spack_util
1515

16+
_REPO_YAML = """\
17+
repo:
18+
namespace: {namespace}
19+
api: v2.0
20+
"""
21+
1622

1723
def install(src, dst, *, ignore=None, symlinks=False):
1824
"""Call shutil.copytree or shutil.copy2. copy2 is used if `src` is not a directory.
@@ -194,26 +200,16 @@ def generate(self, recipe):
194200

195201
spack_git_commit_result = self._git_clone("spack", spack_repo, spack_commit, spack_path)
196202

197-
# Clone the spack-packages repository and check out commit if one was given
198-
spack_packages = spack["packages"]
199-
spack_packages_repo = spack_packages["repo"]
200-
spack_packages_commit = spack_packages["commit"]
201-
spack_packages_path = self.path / "spack-packages"
202-
203-
spack_packages_git_commit_result = self._git_clone(
204-
"spack-packages",
205-
spack_packages_repo,
206-
spack_packages_commit,
207-
spack_packages_path,
208-
)
203+
package_repos = recipe.spack_package_repos
204+
for pkg_repo in package_repos:
205+
pkg_repo["path"] = self.path / "repos" / pkg_repo["name"]
206+
pkg_repo["commit"] = self._git_clone(pkg_repo["name"], pkg_repo["url"], pkg_repo["ref"], pkg_repo["path"])
209207

210208
spack_meta = {
211209
"url": spack_repo,
212210
"ref": spack_commit,
213211
"commit": spack_git_commit_result,
214-
"packages_url": spack_packages_repo,
215-
"packages_ref": spack_packages_commit,
216-
"packages_commit": spack_packages_git_commit_result,
212+
"packages": package_repos,
217213
}
218214

219215
# load the jinja templating environment
@@ -236,10 +232,17 @@ def generate(self, recipe):
236232
# generate top level makefiles
237233
makefile_template = jinja_env.get_template("Makefile")
238234

235+
# Extract module types that were configured in recipe.py
236+
module_types = []
237+
if recipe.with_modules and recipe.modules:
238+
roots = recipe.modules.get("modules", {}).get("default", {}).get("roots", {})
239+
module_types = list(roots.keys())
240+
239241
with (self.path / "Makefile").open("w") as f:
240242
f.write(
241243
makefile_template.render(
242244
modules=recipe.with_modules,
245+
module_types=module_types,
243246
post_install_hook=recipe.post_install_hook,
244247
pre_install_hook=recipe.pre_install_hook,
245248
spack_version=spack_version,
@@ -336,16 +339,12 @@ def generate(self, recipe):
336339
# 2. cluster-config/repos.yaml
337340
# - if the repos.yaml file exists it will contain a list of relative paths
338341
# to search for package
339-
# 1. builtin repo
342+
# 1. package repos from config.yaml in the order specified (typically
343+
# only spack-packages builtin repo)
340344

341-
# Build a list of repos with packages to install.
345+
# Build a list of repos with packages to install from system config and recipe.
342346
repos = []
343347

344-
# check for a repo in the recipe
345-
if recipe.spack_repo is not None:
346-
self._logger.debug(f"adding recipe spack package repo: {recipe.spack_repo}")
347-
repos.append(recipe.spack_repo)
348-
349348
# look for repos.yaml file in the system configuration
350349
repo_yaml = recipe.system_config_path / "repos.yaml"
351350
if repo_yaml.exists() and repo_yaml.is_file():
@@ -366,7 +365,7 @@ def generate(self, recipe):
366365
self._logger.error(f"{repo_path} from {repo_yaml} is not a spack package repository")
367366
raise RuntimeError("invalid system-provided package repository")
368367

369-
self._logger.debug(f"full list of spack package repo: {repos}")
368+
self._logger.debug(f"full list of system spack package repos: {repos}")
370369

371370
# Delete the store/repo path, if it already exists.
372371
# Do this so that incremental builds (though not officially supported) won't break if a repo is updated.
@@ -383,53 +382,82 @@ def generate(self, recipe):
383382
self._logger.debug(f"created the repo packages path {pkg_dst}")
384383

385384
# create the repository step 2: create the repo.yaml file that
386-
# configures alps and builtin repos
385+
# configures the alps repo
387386
with (repo_dst / "repo.yaml").open("w") as f:
388-
f.write(
389-
"""\
390-
repo:
391-
namespace: alps
392-
api: v2.0
393-
"""
394-
)
387+
f.write(_REPO_YAML.format(namespace="alps"))
388+
389+
# If the recipe provides a package repo, install it as a separate
390+
# "recipe" repo in the store with highest precedence.
391+
has_recipe_repo = recipe.spack_repo is not None
392+
if has_recipe_repo:
393+
recipe_dst = repos_path / "recipe"
394+
self._logger.debug(f"creating the recipe spack repo in {recipe_dst}")
395+
if recipe_dst.exists():
396+
self._logger.debug(f"{recipe_dst} exists ... deleting")
397+
shutil.rmtree(recipe_dst)
398+
399+
recipe_pkg_dst = recipe_dst / "packages"
400+
recipe_pkg_dst.mkdir(mode=0o755, parents=True)
401+
402+
with (recipe_dst / "repo.yaml").open("w") as f:
403+
f.write(_REPO_YAML.format(namespace="recipe"))
404+
405+
packages_path = recipe.spack_repo / "packages"
406+
for pkg_path in packages_path.iterdir():
407+
dst = recipe_pkg_dst / pkg_path.name
408+
if pkg_path.is_dir():
409+
self._logger.debug(f" installing recipe package {pkg_path} to {recipe_pkg_dst}")
410+
install(pkg_path, dst)
395411

396412
# create the repository step 2: create the repos.yaml file in build_path/config
397413
repos_yaml_template = jinja_env.get_template("repos.yaml")
398414
with (config_path / "repos.yaml").open("w") as f:
399415
repo_path = recipe.mount / "repos" / "spack_repo" / "alps"
400-
builtin_repo_path = recipe.mount / "repos" / "spack_repo" / "builtin"
416+
recipe_repo_path = recipe.mount / "repos" / "spack_repo" / "recipe"
417+
package_repos = [
418+
{
419+
"name": pkg_repo["name"],
420+
"path": (recipe.mount / "repos" / "spack_repo" / pkg_repo["name"]).as_posix(),
421+
}
422+
for pkg_repo in spack_meta["packages"]
423+
]
401424
f.write(
402425
repos_yaml_template.render(
403426
repo_path=repo_path.as_posix(),
404-
builtin_repo_path=builtin_repo_path.as_posix(),
427+
package_repos=package_repos,
428+
recipe_repo_path=recipe_repo_path.as_posix(),
429+
has_recipe_repo=has_recipe_repo,
405430
verbose=False,
406431
)
407432
)
408433
f.write("\n")
409434

410-
# Iterate over the source repositories copying their contents to the consolidated repo in the uenv.
411-
# Do overwrite packages that have been copied from an earlier source repo, enforcing a descending
412-
# order of precidence.
413-
if len(repos) > 0:
414-
for repo_src in repos:
415-
self._logger.debug(f"installing repo {repo_src}")
416-
packages_path = repo_src / "packages"
417-
for pkg_path in packages_path.iterdir():
418-
dst = pkg_dst / pkg_path.name
419-
if pkg_path.is_dir() and not dst.exists():
420-
self._logger.debug(f" installing package {pkg_path} to {pkg_dst}")
421-
install(pkg_path, dst)
422-
elif dst.exists():
423-
self._logger.debug(f" NOT installing package {pkg_path}")
424-
425-
# Copy the builtin repo to store, delete if it already exists.
426-
spack_packages_builtin_path = spack_packages_path / "repos" / "spack_repo" / "builtin"
427-
spack_packages_store_path = store_path / "repos" / "spack_repo" / "builtin"
428-
self._logger.debug(f"copying builtin repo from {spack_packages_builtin_path} to {spack_packages_store_path}")
429-
if spack_packages_store_path.exists():
430-
self._logger.debug(f"{spack_packages_store_path} exists ... deleting")
431-
shutil.rmtree(spack_packages_store_path)
432-
install(spack_packages_builtin_path, spack_packages_store_path)
435+
# Iterate over the alps and recipe repositories copying their contents
436+
# to the final repo locations. Because of the order of repos in the
437+
# repos.yaml config file, recipe packages have precedence.
438+
for repo_src in repos:
439+
self._logger.debug(f"installing repo {repo_src}")
440+
packages_path = repo_src / "packages"
441+
for pkg_path in packages_path.iterdir():
442+
dst = pkg_dst / pkg_path.name
443+
if pkg_path.is_dir() and not dst.exists():
444+
self._logger.debug(f" installing package {pkg_path} to {pkg_dst}")
445+
install(pkg_path, dst)
446+
elif dst.exists():
447+
self._logger.debug(f" NOT installing package {pkg_path}")
448+
449+
# Copy all package repos defined in config.yaml to their final repo
450+
# locations.
451+
for pkg_repo in spack_meta["packages"]:
452+
clone_path = pkg_repo["path"]
453+
name = pkg_repo["name"]
454+
src_path = clone_path / pkg_repo["repo_path"]
455+
dst_path = store_path / "repos" / "spack_repo" / name
456+
self._logger.debug(f"copying repo '{name}' from {src_path} to {dst_path}")
457+
if dst_path.exists():
458+
self._logger.debug(f"{dst_path} exists ... deleting")
459+
shutil.rmtree(dst_path)
460+
install(src_path, dst_path)
433461

434462
# Generate the makefile and spack.yaml files that describe the compilers
435463
compiler_files = recipe.compiler_files

stackinator/etc/envvars.py

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -621,12 +621,27 @@ def meta_impl(args):
621621

622622
if args.spack is not None:
623623
spack_url, spack_ref, spack_commit = args.spack.split(",")
624-
spack_packages_url = None
625-
spack_packages_ref = None
626-
spack_packages_commit = None
627-
if args.spack_packages is not None:
628-
spack_packages_url, spack_packages_ref, spack_packages_commit = args.spack_packages.split(",")
629624
spack_path = f"{args.mount}/config".replace("//", "/")
625+
scalar_vars = {
626+
"UENV_SPACK_CONFIG_PATH": spack_path,
627+
"UENV_SPACK_URL": spack_url,
628+
"UENV_SPACK_REF": spack_ref,
629+
"UENV_SPACK_COMMIT": spack_commit,
630+
}
631+
if args.spack_package_repo:
632+
repo_names = []
633+
for entry in args.spack_package_repo:
634+
name, url, ref, commit = entry.split(",")
635+
repo_names.append(name)
636+
name_upper = name.upper().replace("-", "_")
637+
scalar_vars[f"UENV_PACKAGE_REPO_{name_upper}_URL"] = url
638+
scalar_vars[f"UENV_PACKAGE_REPO_{name_upper}_REF"] = ref
639+
scalar_vars[f"UENV_PACKAGE_REPO_{name_upper}_COMMIT"] = commit
640+
if name == "builtin":
641+
scalar_vars["UENV_SPACK_PACKAGES_URL"] = url
642+
scalar_vars["UENV_SPACK_PACKAGES_REF"] = ref
643+
scalar_vars["UENV_SPACK_PACKAGES_COMMIT"] = commit
644+
scalar_vars["UENV_PACKAGE_REPOS"] = ",".join(repo_names)
630645
meta["views"]["spack"] = {
631646
"activate": "/dev/null",
632647
"description": "configure spack upstream",
@@ -636,15 +651,7 @@ def meta_impl(args):
636651
"type": "augment",
637652
"values": {
638653
"list": {},
639-
"scalar": {
640-
"UENV_SPACK_CONFIG_PATH": spack_path,
641-
"UENV_SPACK_URL": spack_url,
642-
"UENV_SPACK_REF": spack_ref,
643-
"UENV_SPACK_COMMIT": spack_commit,
644-
"UENV_SPACK_PACKAGES_URL": spack_packages_url,
645-
"UENV_SPACK_PACKAGES_REF": spack_packages_ref,
646-
"UENV_SPACK_PACKAGES_COMMIT": spack_packages_commit,
647-
},
654+
"scalar": scalar_vars,
648655
},
649656
},
650657
}
@@ -686,9 +693,11 @@ def meta_impl(args):
686693
default=None,
687694
)
688695
uenv_parser.add_argument(
689-
"--spack-packages",
690-
help='configure spack-packages repository metadata. Format is "spack_url,git_ref,git_commit"',
696+
"--spack-package-repo",
697+
help="configure spack package repository metadata. "
698+
'Format is "name,spack_url,git_ref,git_commit". Can be repeated.',
691699
type=str,
700+
action="append",
692701
default=None,
693702
)
694703

0 commit comments

Comments
 (0)