Compare commits

...

13 Commits

Author SHA1 Message Date
psakiev
5986173e5b Conditional function use 2025-05-14 13:41:38 -06:00
psakiev
9e942bb3a3 completion 2025-05-14 13:08:35 -06:00
psakiev
815cb4f5b2 Style 2025-05-14 12:29:06 -06:00
psakiev
944b3dad3f More tests 2025-05-14 12:26:17 -06:00
psakiev
9fe2796e9d Fix failing test 2025-05-08 13:52:16 -06:00
psakievich
200191cc3d
Merge branch 'develop' into psakiev/cache-no-dev 2025-05-07 09:10:12 -06:00
Philip Sakievich
6a48121ed7 Add include flags 2025-05-06 13:02:27 +02:00
Philip Sakievich
9c03f15cbd Fix circular import 2025-05-06 13:02:27 +02:00
Philip Sakievich
6eda1b4d04 Refactor to organize filter object 2025-05-06 13:02:27 +02:00
Philip Sakievich
0240120d4f Test filter function 2025-05-06 13:02:27 +02:00
Philip Sakievich
88d7249141 Completion 2025-05-06 13:02:27 +02:00
Philip Sakievich
8d9af73d83 Tests and parsing 2025-05-06 13:02:26 +02:00
Philip Sakievich
6fb1ded7c3 Add configurable filters to mirrors
Add the ability to filter what specs get pushed to a mirror.
This allows for a more controlled autopush for binary mirrors.
2025-05-06 13:02:26 +02:00
11 changed files with 286 additions and 7 deletions

View File

@ -120,6 +120,16 @@ what it looks like:
Once this is done, you can tar up the ``spack-mirror-2014-06-24`` directory and
copy it over to the machine you want it hosted on.
Customization of the mirror contents can be done by selectively excluding
specs using the ``--exclude-file`` or ``--exclude-specs`` flags with
``spack mirror create``. Note that these only apply to source mirrors.
You may additionally add an ``exclude`` or ``include``
section to the ``mirrors`` configuration section for pushing to binary mirrors.
These are lists of abstract or concrete specs to configure what gets pushed to your mirror.
If overlapping inclusion and exclusions are applied then inclusion is preferred.
^^^^^^^^^^^^^^^^^^^
Custom package sets
^^^^^^^^^^^^^^^^^^^

View File

@ -64,6 +64,7 @@
import spack.util.web as web_util
from spack import traverse
from spack.caches import misc_cache_location
from spack.mirrors.filter import MirrorSpecFilter
from spack.oci.image import (
Digest,
ImageReference,
@ -1149,6 +1150,7 @@ def push(
force=self.force,
tmpdir=self.tmpdir,
executor=self.executor,
filter=MirrorSpecFilter(self.mirror),
)
self._base_images = base_images
@ -1199,6 +1201,7 @@ def push(
signing_key=self.signing_key,
tmpdir=self.tmpdir,
executor=self.executor,
filter=MirrorSpecFilter(self.mirror),
)
@ -1269,6 +1272,7 @@ def _url_push(
update_index: bool,
tmpdir: str,
executor: concurrent.futures.Executor,
filter: Optional[MirrorSpecFilter] = None,
) -> Tuple[List[spack.spec.Spec], List[Tuple[spack.spec.Spec, BaseException]]]:
"""Pushes to the provided build cache, and returns a list of skipped specs that were already
present (when force=False), and a list of errors. Does not raise on error."""
@ -1303,6 +1307,11 @@ def _url_push(
if not specs_to_upload:
return skipped, errors
if filter:
filtered, filtrate = filter(specs_to_upload)
skipped.extend(filtrate)
specs_to_upload = filtered
total = len(specs_to_upload)
if total != len(specs):
@ -1573,6 +1582,7 @@ def _oci_push(
tmpdir: str,
executor: concurrent.futures.Executor,
force: bool = False,
filter: Optional[MirrorSpecFilter] = None,
) -> Tuple[
List[spack.spec.Spec],
Dict[str, Tuple[dict, dict]],
@ -1609,6 +1619,11 @@ def _oci_push(
if not blobs_to_upload:
return skipped, base_images, checksums, []
if filter:
filtered, filtrate = filter(blobs_to_upload)
skipped.extend(filtrate)
blobs_to_upload = filtered
if len(blobs_to_upload) != len(installed_specs_with_deps):
tty.info(
f"{len(blobs_to_upload)} specs need to be pushed to "

View File

@ -134,6 +134,24 @@ def setup_parser(subparser):
default=None,
dest="signed",
)
add_parser.add_argument(
"--include-file",
help="specs which Spack should always try to add to a mirror"
" (listed in a file, one per line)",
)
add_parser.add_argument(
"--include-specs",
help="specs which Spack should always try to add to a mirror (specified on command line)",
)
add_parser.add_argument(
"--exclude-file",
help="specs which Spack should not try to add to a mirror"
" (listed in a file, one per line)",
)
add_parser.add_argument(
"--exclude-specs",
help="specs which Spack should not try to add to a mirror (specified on command line)",
)
arguments.add_connection_args(add_parser, False)
# Remove
remove_parser = sp.add_parser("remove", aliases=["rm"], help=mirror_remove.__doc__)
@ -222,6 +240,24 @@ def setup_parser(subparser):
default=lambda: spack.config.default_modify_scope(),
help="configuration scope to modify",
)
set_parser.add_argument(
"--include-file",
help="specs which Spack should always try to add to a mirror"
" (listed in a file, one per line)",
)
set_parser.add_argument(
"--include-specs",
help="specs which Spack should always try to add to a mirror (specified on command line)",
)
set_parser.add_argument(
"--exclude-file",
help="specs which Spack should not try to add to a mirror"
" (listed in a file, one per line)",
)
set_parser.add_argument(
"--exclude-specs",
help="specs which Spack should not try to add to a mirror (specified on command line)",
)
arguments.add_connection_args(set_parser, False)
# List
@ -299,6 +335,30 @@ def _default_variable(id_):
return None
def _manage_filters(args, mirror) -> bool:
include_specs = []
if args.include_file:
include_specs.extend(specs_from_text_file(args.include_file, concretize=False))
if args.include_specs:
include_specs.extend(spack.cmd.parse_specs(str(args.include_specs).split()))
if include_specs:
# round trip specs to assure they are valid
mirror.update({"include": [str(s) for s in include_specs]})
exclude_specs = []
if args.exclude_file:
exclude_specs.extend(specs_from_text_file(args.exclude_file, concretize=False))
if args.exclude_specs:
exclude_specs.extend(spack.cmd.parse_specs(str(args.exclude_specs).split()))
if exclude_specs:
# round trip specs to assure they are valid
mirror.update({"exclude": [str(s) for s in exclude_specs]})
if include_specs or exclude_specs:
return True
else:
return False
def mirror_add(args):
"""add a mirror to Spack"""
if (
@ -368,6 +428,9 @@ def mirror_add(args):
mirror = spack.mirrors.mirror.Mirror(connection, name=args.name)
else:
mirror = spack.mirrors.mirror.Mirror(args.url, name=args.name)
_manage_filters(args, mirror)
spack.mirrors.utils.add(mirror, args.scope)
@ -428,6 +491,8 @@ def _configure_mirror(args):
changes["source"] = "source" in args.type
changed = entry.update(changes, direction)
if hasattr(args, "include_file"):
changed = changed | _manage_filters(args, entry)
if changed:
mirrors[args.name] = entry.to_dict()
@ -470,7 +535,10 @@ def specs_from_text_file(filename, concretize=False):
with open(filename, "r", encoding="utf-8") as f:
specs_in_file = f.readlines()
specs_in_file = [s.strip() for s in specs_in_file]
return spack.cmd.parse_specs(" ".join(specs_in_file), concretize=concretize)
if concretize:
return spack.cmd.parse_specs(" ".join(specs_in_file), concretize=True)
else:
return spack.cmd.parse_specs(specs_in_file, concretize=False)
def concrete_specs_from_user(args):

View File

@ -0,0 +1,38 @@
# Copyright Spack Project Developers. See COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
from typing import List
import spack.spec
from spack.mirrors.mirror import Mirror
class MirrorSpecFilter:
def __init__(self, mirror: Mirror):
self.exclude = [spack.spec.Spec(spec) for spec in mirror.exclusions]
self.include = [spack.spec.Spec(spec) for spec in mirror.inclusions]
def __call__(self, specs: List[spack.spec.Spec]):
"""
Determine the intersection of include/exclude filters
Tie goes to keeping
skip | keep | outcome
------------------------
False | False | Keep
True | True | Keep
False | True | Keep
True | False | Skip
"""
filter = []
filtrate = []
for spec in specs:
skip = any([spec.satisfies(test) for test in self.exclude])
keep = any([spec.satisfies(test) for test in self.include])
if skip and not keep:
filtrate.append(spec)
else:
filter.append(spec)
return filter, filtrate

View File

@ -5,7 +5,7 @@
import operator
import os
import urllib.parse
from typing import Any, Dict, Optional, Tuple, Union
from typing import Any, Dict, List, Optional, Tuple, Union
import llnl.util.tty as tty
@ -99,6 +99,11 @@ def display(self, max_len=0):
binary = "b" if self.binary else " "
print(f"{self.name: <{max_len}} [{source}{binary}] {url}")
def _process_spec_filters(self, key: str) -> List[str]:
if isinstance(self._data, str):
return []
return self._data.get(key, [])
@property
def name(self):
return self._name or "<unnamed>"
@ -131,6 +136,14 @@ def push_url(self):
"""Get the valid, canonicalized fetch URL"""
return self.get_url("push")
@property
def exclusions(self):
return self._process_spec_filters("exclude")
@property
def inclusions(self):
return self._process_spec_filters("include")
def ensure_mirror_usable(self, direction: str = "push"):
access_pair = self._get_value("access_pair", direction)
access_token_variable = self._get_value("access_token_variable", direction)
@ -192,7 +205,7 @@ def _update_connection_dict(self, current_data: dict, new_data: dict, top_level:
"endpoint_url",
]
if top_level:
keys += ["binary", "source", "signed", "autopush"]
keys += ["binary", "source", "signed", "autopush", "exclude", "include"]
changed = False
for key in keys:
if key in new_data and current_data.get(key) != new_data[key]:

View File

@ -77,6 +77,8 @@
"fetch": fetch_and_push,
"push": fetch_and_push,
"autopush": {"type": "boolean"},
"exclude": {"type": "array", "items": {"type": "string"}},
"include": {"type": "array", "items": {"type": "string"}},
**connection, # type: ignore
},
**connection_ext, # type: ignore

View File

@ -195,6 +195,38 @@ def test_buildcache_autopush(tmp_path, install_mockery, mock_fetch):
assert (mirror_autopush_dir / specs_dirs / manifest_file).exists()
def test_buildcache_exclude(tmp_path, install_mockery, mock_fetch):
"""Test buildcache with autopush can exclude"""
mirror_dir = tmp_path / "mirror_a"
mirror(
"add",
"--autopush",
"--exclude-specs",
"libelf",
"--unsigned",
"mirror-autopush",
mirror_dir.as_uri(),
)
s = spack.concretize.concretize_one("libdwarf")
# Install and generate build cache index
PackageInstaller([s.package], fake=True, explicit=True).install()
found_file = URLBuildcacheEntry.get_manifest_filename(s)
missing_file = URLBuildcacheEntry.get_manifest_filename(s["libelf"])
found_dirs = os.path.join(
*URLBuildcacheEntry.get_relative_path_components(BuildcacheComponent.SPEC), s.name
)
missing_dirs = os.path.join(
*URLBuildcacheEntry.get_relative_path_components(BuildcacheComponent.SPEC),
s["libelf"].name,
)
assert (mirror_dir / found_dirs / found_file).exists()
assert not (mirror_dir / missing_dirs / missing_file).exists()
def test_buildcache_sync(
mutable_mock_env_path, install_mockery, mock_packages, mock_fetch, mock_stage, tmpdir
):

View File

@ -561,3 +561,36 @@ def test_mirror_add_set_autopush(mutable_config):
mirror("set", "--autopush", "example")
assert spack.config.get("mirrors:example") == {"url": "http://example.com", "autopush": True}
mirror("remove", "example")
def test_mirror_add_filters(mutable_config, tmpdir):
exclude_path = os.path.join(str(tmpdir), "test-exclude.txt")
with open(exclude_path, "w", encoding="utf-8") as exclude_file:
exclude_file.write(
"""\
mpich@3.0.1:3.0.2
mpich@1.0
"""
)
include_path = os.path.join(str(tmpdir), "test-include.txt")
with open(include_path, "w", encoding="utf-8") as include_file:
include_file.write(
"""\
build_type=Debug
gcc-runtime
"""
)
mirror("add", "--exclude-specs", "foo", "example", "http://example.com")
assert spack.config.get("mirrors:example") == {"url": "http://example.com", "exclude": ["foo"]}
mirror("set", "--include-specs", "+shared", "example")
assert spack.config.get("mirrors:example") == {
"url": "http://example.com",
"exclude": ["foo"],
"include": ["+shared"],
}
mirror("set", "--include-file", include_path, "--exclude-file", exclude_path, "example")
assert spack.config.get("mirrors:example") == {
"url": "http://example.com",
"exclude": ["mpich@3.0.1:3.0.2", "mpich@1.0"],
"include": ["build_type=Debug", "gcc-runtime"],
}

View File

@ -18,11 +18,13 @@
import spack.mirrors.mirror
import spack.mirrors.utils
import spack.patch
import spack.spec
import spack.stage
import spack.util.executable
import spack.util.spack_json as sjson
import spack.util.url as url_util
from spack.cmd.common.arguments import mirror_name_or_url
from spack.mirrors.filter import MirrorSpecFilter
from spack.spec import Spec
from spack.util.executable import which
from spack.util.spack_yaml import SpackYAMLError
@ -342,6 +344,15 @@ def test_update_4():
assert m.fetch_url == "https://example.com"
@pytest.mark.parametrize("filter", ["exclude", "include"])
def test_update_filters(filter):
# Change push url, ensure minimal config
m = spack.mirrors.mirror.Mirror("https://example.com")
assert m.update({filter: ["foo", "bar"]})
assert m.to_dict() == {"url": "https://example.com", filter: ["foo", "bar"]}
assert m.fetch_url == "https://example.com"
@pytest.mark.parametrize("direction", ["fetch", "push"])
def test_update_connection_params(direction, tmpdir, monkeypatch):
"""Test whether new connection params expand the mirror config to a dict."""
@ -434,3 +445,44 @@ def test_mirror_name_or_url_dir_parsing(tmp_path):
with working_dir(curdir):
assert mirror_name_or_url(".").fetch_url == curdir.as_uri()
assert mirror_name_or_url("..").fetch_url == tmp_path.as_uri()
def test_mirror_parse_exclude_include():
mirror_raw = {
"url": "https://example.com",
"exclude": ["dev_path=*", "+shared"],
"include": ["+foo"],
}
m = spack.mirrors.mirror.Mirror(mirror_raw)
assert "dev_path=*" in m.exclusions
assert "+foo" in m.inclusions
INPUT_SPEC_STRS = ["foo@main", "foo@main dev_path=/tmp", "foo@2.1.3"]
@pytest.mark.parametrize(
"include,exclude,gold",
[
([], [], [0, 1, 2]),
(["dev_path=*", "@main"], [], [0, 1, 2]),
([], ["dev_path=*", "@main"], [2]),
(["dev_path=*"], ["@main"], [1, 2]),
],
)
def test_filter_specs(include, exclude, gold):
input_specs = [spack.spec.Spec(s) for s in INPUT_SPEC_STRS]
data = {"include": include, "exclude": exclude}
m = spack.mirrors.mirror.Mirror(data)
filter = MirrorSpecFilter(m)
filtered, filtrate = filter(input_specs)
assert filtered is not None
assert filtrate is not None
# lossless
assert (set(filtered) | set(filtrate)) == set(input_specs)
for i in gold:
assert input_specs[i] in filtered

View File

@ -1468,7 +1468,7 @@ _spack_mirror_destroy() {
_spack_mirror_add() {
if $list_options
then
SPACK_COMPREPLY="-h --help --scope --type --autopush --unsigned --signed --s3-access-key-id --s3-access-key-id-variable --s3-access-key-secret --s3-access-key-secret-variable --s3-access-token --s3-access-token-variable --s3-profile --s3-endpoint-url --oci-username --oci-username-variable --oci-password --oci-password-variable"
SPACK_COMPREPLY="-h --help --scope --type --autopush --unsigned --signed --include-file --include-specs --exclude-file --exclude-specs --s3-access-key-id --s3-access-key-id-variable --s3-access-key-secret --s3-access-key-secret-variable --s3-access-token --s3-access-token-variable --s3-profile --s3-endpoint-url --oci-username --oci-username-variable --oci-password --oci-password-variable"
else
_mirrors
fi
@ -1504,7 +1504,7 @@ _spack_mirror_set_url() {
_spack_mirror_set() {
if $list_options
then
SPACK_COMPREPLY="-h --help --push --fetch --type --url --autopush --no-autopush --unsigned --signed --scope --s3-access-key-id --s3-access-key-id-variable --s3-access-key-secret --s3-access-key-secret-variable --s3-access-token --s3-access-token-variable --s3-profile --s3-endpoint-url --oci-username --oci-username-variable --oci-password --oci-password-variable"
SPACK_COMPREPLY="-h --help --push --fetch --type --url --autopush --no-autopush --unsigned --signed --scope --include-file --include-specs --exclude-file --exclude-specs --s3-access-key-id --s3-access-key-id-variable --s3-access-key-secret --s3-access-key-secret-variable --s3-access-token --s3-access-token-variable --s3-profile --s3-endpoint-url --oci-username --oci-username-variable --oci-password --oci-password-variable"
else
_mirrors
fi

View File

@ -2314,7 +2314,7 @@ complete -c spack -n '__fish_spack_using_command mirror destroy' -l mirror-url -
complete -c spack -n '__fish_spack_using_command mirror destroy' -l mirror-url -r -d 'find mirror to destroy by url'
# spack mirror add
set -g __fish_spack_optspecs_spack_mirror_add h/help scope= type= autopush unsigned signed s3-access-key-id= s3-access-key-id-variable= s3-access-key-secret= s3-access-key-secret-variable= s3-access-token= s3-access-token-variable= s3-profile= s3-endpoint-url= oci-username= oci-username-variable= oci-password= oci-password-variable=
set -g __fish_spack_optspecs_spack_mirror_add h/help scope= type= autopush unsigned signed include-file= include-specs= exclude-file= exclude-specs= s3-access-key-id= s3-access-key-id-variable= s3-access-key-secret= s3-access-key-secret-variable= s3-access-token= s3-access-token-variable= s3-profile= s3-endpoint-url= oci-username= oci-username-variable= oci-password= oci-password-variable=
complete -c spack -n '__fish_spack_using_command_pos 0 mirror add' -f
complete -c spack -n '__fish_spack_using_command mirror add' -s h -l help -f -a help
complete -c spack -n '__fish_spack_using_command mirror add' -s h -l help -d 'show this help message and exit'
@ -2328,6 +2328,14 @@ complete -c spack -n '__fish_spack_using_command mirror add' -l unsigned -f -a s
complete -c spack -n '__fish_spack_using_command mirror add' -l unsigned -d 'do not require signing and signature verification when pushing and installing from this build cache'
complete -c spack -n '__fish_spack_using_command mirror add' -l signed -f -a signed
complete -c spack -n '__fish_spack_using_command mirror add' -l signed -d 'require signing and signature verification when pushing and installing from this build cache'
complete -c spack -n '__fish_spack_using_command mirror add' -l include-file -r -f -a include_file
complete -c spack -n '__fish_spack_using_command mirror add' -l include-file -r -d 'specs which Spack should always try to add to a mirror (listed in a file, one per line)'
complete -c spack -n '__fish_spack_using_command mirror add' -l include-specs -r -f -a include_specs
complete -c spack -n '__fish_spack_using_command mirror add' -l include-specs -r -d 'specs which Spack should always try to add to a mirror (specified on command line)'
complete -c spack -n '__fish_spack_using_command mirror add' -l exclude-file -r -f -a exclude_file
complete -c spack -n '__fish_spack_using_command mirror add' -l exclude-file -r -d 'specs which Spack should not try to add to a mirror (listed in a file, one per line)'
complete -c spack -n '__fish_spack_using_command mirror add' -l exclude-specs -r -f -a exclude_specs
complete -c spack -n '__fish_spack_using_command mirror add' -l exclude-specs -r -d 'specs which Spack should not try to add to a mirror (specified on command line)'
complete -c spack -n '__fish_spack_using_command mirror add' -l s3-access-key-id -r -f -a s3_access_key_id
complete -c spack -n '__fish_spack_using_command mirror add' -l s3-access-key-id -r -d 'ID string to use to connect to this S3 mirror'
complete -c spack -n '__fish_spack_using_command mirror add' -l s3-access-key-id-variable -r -f -a s3_access_key_id_variable
@ -2406,7 +2414,7 @@ complete -c spack -n '__fish_spack_using_command mirror set-url' -l oci-password
complete -c spack -n '__fish_spack_using_command mirror set-url' -l oci-password-variable -r -d 'environment variable containing password to use to connect to this OCI mirror'
# spack mirror set
set -g __fish_spack_optspecs_spack_mirror_set h/help push fetch type= url= autopush no-autopush unsigned signed scope= s3-access-key-id= s3-access-key-id-variable= s3-access-key-secret= s3-access-key-secret-variable= s3-access-token= s3-access-token-variable= s3-profile= s3-endpoint-url= oci-username= oci-username-variable= oci-password= oci-password-variable=
set -g __fish_spack_optspecs_spack_mirror_set h/help push fetch type= url= autopush no-autopush unsigned signed scope= include-file= include-specs= exclude-file= exclude-specs= s3-access-key-id= s3-access-key-id-variable= s3-access-key-secret= s3-access-key-secret-variable= s3-access-token= s3-access-token-variable= s3-profile= s3-endpoint-url= oci-username= oci-username-variable= oci-password= oci-password-variable=
complete -c spack -n '__fish_spack_using_command_pos 0 mirror set' -f -a '(__fish_spack_mirrors)'
complete -c spack -n '__fish_spack_using_command mirror set' -s h -l help -f -a help
complete -c spack -n '__fish_spack_using_command mirror set' -s h -l help -d 'show this help message and exit'
@ -2428,6 +2436,14 @@ complete -c spack -n '__fish_spack_using_command mirror set' -l signed -f -a sig
complete -c spack -n '__fish_spack_using_command mirror set' -l signed -d 'require signing and signature verification when pushing and installing from this build cache'
complete -c spack -n '__fish_spack_using_command mirror set' -l scope -r -f -a '_builtin defaults system site user command_line'
complete -c spack -n '__fish_spack_using_command mirror set' -l scope -r -d 'configuration scope to modify'
complete -c spack -n '__fish_spack_using_command mirror set' -l include-file -r -f -a include_file
complete -c spack -n '__fish_spack_using_command mirror set' -l include-file -r -d 'specs which Spack should always try to add to a mirror (listed in a file, one per line)'
complete -c spack -n '__fish_spack_using_command mirror set' -l include-specs -r -f -a include_specs
complete -c spack -n '__fish_spack_using_command mirror set' -l include-specs -r -d 'specs which Spack should always try to add to a mirror (specified on command line)'
complete -c spack -n '__fish_spack_using_command mirror set' -l exclude-file -r -f -a exclude_file
complete -c spack -n '__fish_spack_using_command mirror set' -l exclude-file -r -d 'specs which Spack should not try to add to a mirror (listed in a file, one per line)'
complete -c spack -n '__fish_spack_using_command mirror set' -l exclude-specs -r -f -a exclude_specs
complete -c spack -n '__fish_spack_using_command mirror set' -l exclude-specs -r -d 'specs which Spack should not try to add to a mirror (specified on command line)'
complete -c spack -n '__fish_spack_using_command mirror set' -l s3-access-key-id -r -f -a s3_access_key_id
complete -c spack -n '__fish_spack_using_command mirror set' -l s3-access-key-id -r -d 'ID string to use to connect to this S3 mirror'
complete -c spack -n '__fish_spack_using_command mirror set' -l s3-access-key-id-variable -r -f -a s3_access_key_id_variable