Skip to content

Commit f3cd36d

Browse files
authored
Multiple package repos in config.yaml (#294)
Extends config.yaml to accept not only ```yaml ... packages: repo: https://github.com/foo/spack-packages.git commit: abcdef ``` but also multiple repos: ```yaml ... packages: myrepo: repo: https://github.com/bar/other-packages.git commit: fedcba path: path/to/repo builtin: repo: https://github.com/foo/spack-packages.git commit: abcdef ``` Order of repos is significant in the same way it's significant in spack config files (https://spack.readthedocs.io/en/latest/repositories.html#search-order-and-overriding-packages). Packages from repos higher up on the list take precedence. I made a small change so that there are also two implicit repos (currently on main there's one: `alps`). A repo `recipe` exists if and only if there are packages in the recipe. `alps` still exists, but doesn't have the recipe packages. The example above would produce the following list of repos: - `recipe` - `alps` - `myrepo` - `builtin` Currently there's no enforcement that the repo entry has the same name as the repo namespace (similar to spack itself, if I understand it correctly). Is this ok? There's an optional `path` property that can be used to point to a non-default subdirectory within the cloned git repo. For spack-packages this is always in `repos/spack_repo/builtin`, so the default is `repos/spack_repo/{name}`. In spack-packages there is an index file https://github.com/spack/spack-packages/blob/develop/spack-repo-index.yaml which _could_ be used to locate all the package repos within a git repo. Spack uses this when it clones package repos through git. For now I decided it's overkill to support that. Thoughts?
1 parent 72a278e commit f3cd36d

20 files changed

Lines changed: 401 additions & 119 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
@@ -129,8 +129,8 @@ If custom package definitions are provided for the same package in more than one
129129
130130
The following precedence is applied, where 1 has higher precedence than 2 or 3:
131131
132-
1. packages defined in the (optional) `repo` path in the [recipe](recipes.md#custom-spack-packages)
133-
2. packages defined in the (optional) site repo(s) defined in the `repo/repos.yaml` file of cluster configuration (documented here)
134-
3. packages provided by Spack (in the `var/spack/repos/builtin` path)
132+
1. packages defined in the (optional) `repo` path in the [recipe][custom-spack-packages]
133+
2. packages defined in the (optional) site repo(s) defined in the `repo/repos.yaml` file of the cluster configuration (documented here)
134+
3. packages defined in the package repositories configured in `config.yaml:spack:packages`, in the order specified (which typically includes `builtin`)
135135
136-
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
136+
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
@@ -40,6 +40,37 @@ version: 2
4040
* `version`: _default = 1_ the version of the uenv recipe (see below)
4141
* `modules`: (_deprecated_) _optional_ enable/disable module file generation.
4242

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

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

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

492-
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.
524+
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.
493525

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

@@ -502,6 +534,8 @@ repo
502534
└─ package.py
503535
```
504536

537+
Packages provided together with a recipe will be installed in a separate `recipe` namespace.
538+
505539
Additional custom packages can be provided as part of the cluster configuration, as well as additional site packages.
506540
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.
507541
See the documentation for [cluster configuration](cluster-config.md) for more detail.
@@ -523,12 +557,13 @@ See the documentation for [cluster configuration](cluster-config.md) for more de
523557
In the above case, the package `fmt` is backported from `origin/develop` into the `stackinator-recipe`.
524558

525559
!!! alps
526-
All packages are installed under a single spack package repository called `alps`.
560+
All cluster configuration packages are installed under a single spack package repository called `alps`.
527561
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.
562+
These site packages are installed under the `alps` Spack package repository namespace.
528563

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

533568
## Post install configuration
534569

stackinator/builder.py

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

1414
from . import VERSION, cache, 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.
@@ -191,26 +197,16 @@ def generate(self, recipe):
191197

192198
spack_git_commit_result = self._git_clone("spack", spack_repo, spack_commit, spack_path)
193199

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

207205
spack_meta = {
208206
"url": spack_repo,
209207
"ref": spack_commit,
210208
"commit": spack_git_commit_result,
211-
"packages_url": spack_packages_repo,
212-
"packages_ref": spack_packages_commit,
213-
"packages_commit": spack_packages_git_commit_result,
209+
"packages": package_repos,
214210
}
215211

216212
# load the jinja templating environment
@@ -331,16 +327,12 @@ def generate(self, recipe):
331327
# 2. cluster-config/repos.yaml
332328
# - if the repos.yaml file exists it will contain a list of relative paths
333329
# to search for package
334-
# 1. builtin repo
330+
# 1. package repos from config.yaml in the order specified (typically
331+
# only spack-packages builtin repo)
335332

336-
# Build a list of repos with packages to install.
333+
# Build a list of repos with packages to install from system config and recipe.
337334
repos = []
338335

339-
# check for a repo in the recipe
340-
if recipe.spack_repo is not None:
341-
self._logger.debug(f"adding recipe spack package repo: {recipe.spack_repo}")
342-
repos.append(recipe.spack_repo)
343-
344336
# look for repos.yaml file in the system configuration
345337
repo_yaml = recipe.system_config_path / "repos.yaml"
346338
if repo_yaml.exists() and repo_yaml.is_file():
@@ -361,7 +353,7 @@ def generate(self, recipe):
361353
self._logger.error(f"{repo_path} from {repo_yaml} is not a spack package repository")
362354
raise RuntimeError("invalid system-provided package repository")
363355

364-
self._logger.debug(f"full list of spack package repo: {repos}")
356+
self._logger.debug(f"full list of system spack package repos: {repos}")
365357

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

380372
# create the repository step 2: create the repo.yaml file that
381-
# configures alps and builtin repos
373+
# configures the alps repo
382374
with (repo_dst / "repo.yaml").open("w") as f:
383-
f.write(
384-
"""\
385-
repo:
386-
namespace: alps
387-
api: v2.0
388-
"""
389-
)
375+
f.write(_REPO_YAML.format(namespace="alps"))
376+
377+
# If the recipe provides a package repo, install it as a separate
378+
# "recipe" repo in the store with highest precedence.
379+
has_recipe_repo = recipe.spack_repo is not None
380+
if has_recipe_repo:
381+
recipe_dst = repos_path / "recipe"
382+
self._logger.debug(f"creating the recipe spack repo in {recipe_dst}")
383+
if recipe_dst.exists():
384+
self._logger.debug(f"{recipe_dst} exists ... deleting")
385+
shutil.rmtree(recipe_dst)
386+
387+
recipe_pkg_dst = recipe_dst / "packages"
388+
recipe_pkg_dst.mkdir(mode=0o755, parents=True)
389+
390+
with (recipe_dst / "repo.yaml").open("w") as f:
391+
f.write(_REPO_YAML.format(namespace="recipe"))
392+
393+
packages_path = recipe.spack_repo / "packages"
394+
for pkg_path in packages_path.iterdir():
395+
dst = recipe_pkg_dst / pkg_path.name
396+
if pkg_path.is_dir():
397+
self._logger.debug(f" installing recipe package {pkg_path} to {recipe_pkg_dst}")
398+
install(pkg_path, dst)
390399

391400
# create the repository step 2: create the repos.yaml file in build_path/config
392401
repos_yaml_template = jinja_env.get_template("repos.yaml")
393402
with (config_path / "repos.yaml").open("w") as f:
394403
repo_path = recipe.mount / "repos" / "spack_repo" / "alps"
395-
builtin_repo_path = recipe.mount / "repos" / "spack_repo" / "builtin"
404+
recipe_repo_path = recipe.mount / "repos" / "spack_repo" / "recipe"
405+
package_repos = [
406+
{
407+
"name": pkg_repo["name"],
408+
"path": (recipe.mount / "repos" / "spack_repo" / pkg_repo["name"]).as_posix(),
409+
}
410+
for pkg_repo in spack_meta["packages"]
411+
]
396412
f.write(
397413
repos_yaml_template.render(
398414
repo_path=repo_path.as_posix(),
399-
builtin_repo_path=builtin_repo_path.as_posix(),
415+
package_repos=package_repos,
416+
recipe_repo_path=recipe_repo_path.as_posix(),
417+
has_recipe_repo=has_recipe_repo,
400418
verbose=False,
401419
)
402420
)
403421
f.write("\n")
404422

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

429450
# Generate the makefile and spack.yaml files that describe the compilers
430451
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

stackinator/recipe.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,39 @@ def spack_repo(self):
213213
return repo_path
214214
return None
215215

216+
_RESERVED_REPO_NAMES = {"alps", "recipe"}
217+
218+
@property
219+
def spack_package_repos(self):
220+
packages = self.config["spack"]["packages"]
221+
if isinstance(packages.get("repo"), str):
222+
return [
223+
{
224+
"name": "builtin",
225+
"url": packages["repo"],
226+
"ref": packages.get("commit"),
227+
"repo_path": packages.get("path", "repos/spack_repo/builtin"),
228+
}
229+
]
230+
repos = [
231+
{
232+
"name": name,
233+
"url": val["repo"],
234+
"ref": val.get("commit"),
235+
"repo_path": val.get("path", f"repos/spack_repo/{name}"),
236+
}
237+
for name, val in packages.items()
238+
]
239+
for repo in repos:
240+
name = repo["name"]
241+
if name in self._RESERVED_REPO_NAMES:
242+
raise RuntimeError(
243+
f"The package repo name '{name}' is reserved for stackinator internal use. "
244+
f"Reserved names are: {self._RESERVED_REPO_NAMES}. "
245+
"Choose a different name in config.yaml:spack:packages."
246+
)
247+
return repos
248+
216249
# Returns:
217250
# Path: of the recipe extra path if it exists
218251
# None: if there is no user-provided extra path in the recipe

0 commit comments

Comments
 (0)