Refer to mirrors by name, path, or url (#34891)

With this change we get the invariant that `mirror.fetch_url` and
`mirror.push_url` return valid URLs, even when the backing config
file is actually using (relative) paths with potentially `$spack` and
`$env` like variables.

Secondly it avoids expanding mirror path / URLs too early,
so if I say `spack mirror add name ./path`, it stays `./path` in my
config. When it's retrieved through MirrorCollection() we
exand it to say `file://<env dir>/path` if `./path` was set in an
environment scope.

Thirdly, the interface is simplified for the relevant buildcache
commands, so it's more like `git push`:

```
spack buildcache create [mirror] [specs...]
```

`mirror` is either a mirror name, a path, or a URL.

Resolving the relevant mirror goes as follows:
    
- If it contains either / or \ it is used as an anonymous mirror with
   path or url.
- Otherwise, it's interpreted as a named mirror, which must exist.

This helps to guard against typos, e.g. typing `my-mirror` when there
is no such named mirror now errors with:

```
$ spack -e . buildcache create my-mirror
==> Error: no mirror named "my-mirror". Did you mean ./my-mirror?
```

instead of creating a directory in the current working directory. I
think this is reasonable, as the alternative (requiring that a local dir
exists) feels a bit pendantic in the general case -- spack is happy to
create the build cache dir when needed, saving a `mkdir`.

The old (now deprecated) format will still be available in Spack 0.20,
but is scheduled to be removed in 0.21:

```
spack buildcache create (--directory | --mirror-url | --mirror-name) [specs...]
```

This PR also touches `tmp_scope` in tests, because it didn't really
work for me, since spack fixes the possible --scope values once and
for all across tests, so tests failed when run out of order.
This commit is contained in:
Harmen Stoppels 2023-01-16 19:14:41 +01:00 committed by GitHub
parent 9a25e21da8
commit 3489cc0a9b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 349 additions and 312 deletions

View File

@ -13,49 +13,51 @@ Some sites may encourage users to set up their own test environments
before carrying out central installations, or some users may prefer to set
up these environments on their own motivation. To reduce the load of
recompiling otherwise identical package specs in different installations,
installed packages can be put into build cache tarballs, uploaded to
installed packages can be put into build cache tarballs, pushed to
your Spack mirror and then downloaded and installed by others.
Whenever a mirror provides prebuilt packages, Spack will take these packages
into account during concretization and installation, making ``spack install``
signficantly faster.
--------------------------
Creating build cache files
--------------------------
A compressed tarball of an installed package is created. Tarballs are created
for all of its link and run dependency packages as well. Compressed tarballs are
signed with gpg and signature and tarball and put in a ``.spack`` file. Optionally,
the rpaths (and ids and deps on macOS) can be changed to paths relative to
the Spack install tree before the tarball is created.
.. note::
We use the terms "build cache" and "mirror" often interchangeably. Mirrors
are used during installation both for sources and prebuilt packages. Build
caches refer to mirrors that provide prebuilt packages.
----------------------
Creating a build cache
----------------------
Build caches are created via:
.. code-block:: console
$ spack buildcache create <spec>
$ spack buildcache create <path/url/mirror name> <spec>
This command takes the locally installed spec and its dependencies, and
creates tarballs of their install prefixes. It also generates metadata files,
signed with GPG. These tarballs and metadata files are then pushed to the
provided binary cache, which can be a local directory or a remote URL.
If you wanted to create a build cache in a local directory, you would provide
the ``-d`` argument to target that directory, again also specifying the spec.
Here is an example creating a local directory, "spack-cache" and creating
build cache files for the "ninja" spec:
Here is an example where a build cache is created in a local directory named
"spack-cache", to which we push the "ninja" spec:
.. code-block:: console
$ mkdir -p ./spack-cache
$ spack buildcache create -d ./spack-cache ninja
==> Buildcache files will be output to file:///home/spackuser/spack/spack-cache/build_cache
gpgconf: socketdir is '/run/user/1000/gnupg'
gpg: using "E6DF6A8BD43208E4D6F392F23777740B7DBD643D" as default secret key for signing
$ spack buildcache create --allow-root ./spack-cache ninja
==> Pushing binary packages to file:///home/spackuser/spack/spack-cache/build_cache
Note that the targeted spec must already be installed. Once you have a build cache,
you can add it as a mirror, discussed next.
Not that ``ninja`` must be installed locally for this to work.
.. warning::
We're using the ``--allow-root`` flag to tell Spack that is OK when any of
the binaries we're pushing contain references to the local Spack install
directory.
Spack improved the format used for binary caches in v0.18. The entire v0.18 series
will be able to verify and install binary caches both in the new and in the old format.
Support for using the old format is expected to end in v0.19, so we advise users to
recreate relevant buildcaches using Spack v0.18 or higher.
Once you have a build cache, you can add it as a mirror, discussed next.
---------------------------------------
Finding or installing build cache files
@ -66,10 +68,10 @@ with:
.. code-block:: console
$ spack mirror add <name> <url>
$ spack mirror add <name> <url or path>
Note that the url can be a web url _or_ a local filesystem location. In the previous
Both web URLs and local paths on the filesystem can be specified. In the previous
example, you might add the directory "spack-cache" and call it ``mymirror``:
@ -94,7 +96,7 @@ this new build cache as follows:
.. code-block:: console
$ spack buildcache update-index -d spack-cache/
$ spack buildcache update-index ./spack-cache
Now you can use list:
@ -105,46 +107,38 @@ Now you can use list:
-- linux-ubuntu20.04-skylake / gcc@9.3.0 ------------------------
ninja@1.10.2
Great! So now let's say you have a different spack installation, or perhaps just
a different environment for the same one, and you want to install a package from
that build cache. Let's first uninstall the actual library "ninja" to see if we can
re-install it from the cache.
With ``mymirror`` configured and an index available, Spack will automatically
use it during concretization and installation. That means that you can expect
``spack install ninja`` to fetch prebuilt packages from the mirror. Let's
verify by re-installing ninja:
.. code-block:: console
$ spack uninstall ninja
And now reinstall from the buildcache
.. code-block:: console
$ spack buildcache install ninja
==> buildcache spec(s) matching ninja
==> Fetching file:///home/spackuser/spack/spack-cache/build_cache/linux-ubuntu20.04-skylake/gcc-9.3.0/ninja-1.10.2/linux-ubuntu20.04-skylake-gcc-9.3.0-ninja-1.10.2-i4e5luour7jxdpc3bkiykd4imke3mkym.spack
####################################################################################################################################### 100.0%
==> Installing buildcache for spec ninja@1.10.2%gcc@9.3.0 arch=linux-ubuntu20.04-skylake
gpgconf: socketdir is '/run/user/1000/gnupg'
gpg: Signature made Tue 23 Mar 2021 10:16:29 PM MDT
gpg: using RSA key E6DF6A8BD43208E4D6F392F23777740B7DBD643D
gpg: Good signature from "spackuser (GPG created for Spack) <spackuser@noreply.users.github.com>" [ultimate]
$ spack install ninja
==> Installing ninja-1.11.1-yxferyhmrjkosgta5ei6b4lqf6bxbscz
==> Fetching file:///home/spackuser/spack/spack-cache/build_cache/linux-ubuntu20.04-skylake-gcc-9.3.0-ninja-1.10.2-yxferyhmrjkosgta5ei6b4lqf6bxbscz.spec.json.sig
gpg: Signature made Do 12 Jan 2023 16:01:04 CET
gpg: using RSA key 61B82B2B2350E171BD17A1744E3A689061D57BF6
gpg: Good signature from "example (GPG created for Spack) <example@example.com>" [ultimate]
==> Fetching file:///home/spackuser/spack/spack-cache/build_cache/linux-ubuntu20.04-skylake/gcc-9.3.0/ninja-1.10.2/linux-ubuntu20.04-skylake-gcc-9.3.0-ninja-1.10.2-yxferyhmrjkosgta5ei6b4lqf6bxbscz.spack
==> Extracting ninja-1.10.2-yxferyhmrjkosgta5ei6b4lqf6bxbscz from binary cache
==> ninja: Successfully installed ninja-1.11.1-yxferyhmrjkosgta5ei6b4lqf6bxbscz
Search: 0.00s. Fetch: 0.17s. Install: 0.12s. Total: 0.29s
[+] /home/harmen/spack/opt/spack/linux-ubuntu20.04-skylake/gcc-9.3.0/ninja-1.11.1-yxferyhmrjkosgta5ei6b4lqf6bxbscz
It worked! You've just completed a full example of creating a build cache with
a spec of interest, adding it as a mirror, updating it's index, listing the contents,
a spec of interest, adding it as a mirror, updating its index, listing the contents,
and finally, installing from it.
Note that the above command is intended to install a particular package to a
build cache you have created, and not to install a package from a build cache.
For the latter, once a mirror is added, by default when you do ``spack install`` the ``--use-cache``
flag is set, and you will install a package from a build cache if it is available.
If you want to always use the cache, you can do:
By default Spack falls back to building from sources when the mirror is not available
or when the package is simply not already available. To force Spack to only install
prebuilt packages, you can use
.. code-block:: console
$ spack install --cache-only <package>
$ spack install --use-buildcache only <package>
For example, to combine all of the commands above to add the E4S build cache
and then install from it exclusively, you would do:
@ -153,7 +147,7 @@ and then install from it exclusively, you would do:
$ spack mirror add E4S https://cache.e4s.io
$ spack buildcache keys --install --trust
$ spack install --cache-only <package>
$ spack install --use-buildache only <package>
We use ``--install`` and ``--trust`` to say that we are installing keys to our
keyring, and trusting all downloaded keys.

View File

@ -94,22 +94,15 @@ class Bootstrapper:
def __init__(self, conf):
self.conf = conf
self.name = conf["name"]
self.url = conf["info"]["url"]
self.metadata_dir = spack.util.path.canonicalize_path(conf["metadata"])
@property
def mirror_url(self):
"""Mirror url associated with this bootstrapper"""
# Absolute paths
if os.path.isabs(self.url):
return spack.util.url.format(self.url)
# Check for :// and assume it's an url if we find it
if "://" in self.url:
return self.url
# Otherwise, it's a relative path
return spack.util.url.format(os.path.join(self.metadata_dir, self.url))
# Promote (relative) paths to file urls
url = conf["info"]["url"]
if spack.util.url.is_path_instead_of_url(url):
if not os.path.isabs(url):
url = os.path.join(self.metadata_dir, url)
url = spack.util.url.path_to_file_url(url)
self.url = url
@property
def mirror_scope(self):
@ -117,7 +110,7 @@ def mirror_scope(self):
this bootstrapper.
"""
return spack.config.InternalConfigScope(
self.config_scope_name, {"mirrors:": {self.name: self.mirror_url}}
self.config_scope_name, {"mirrors:": {self.name: self.url}}
)
def try_import(self, module: str, abstract_spec_str: str) -> bool:

View File

@ -1482,7 +1482,7 @@ def _push_mirror_contents(env, specfile_path, sign_binaries, mirror_url):
tty.debug("Creating buildcache ({0})".format("unsigned" if unsigned else "signed"))
hashes = env.all_hashes() if env else None
matches = spack.store.specfile_matches(specfile_path, hashes=hashes)
push_url = spack.mirror.push_url_from_mirror_url(mirror_url)
push_url = spack.mirror.Mirror.from_url(mirror_url).push_url
spec_kwargs = {"include_root": True, "include_dependencies": False}
kwargs = {"force": True, "allow_root": True, "unsigned": unsigned}
bindist.push(matches, push_url, spec_kwargs, **kwargs)

View File

@ -43,6 +43,8 @@
"The sha256 checksum of binaries is checked before installation."
),
"info": {
# This is a mis-nomer since it's not a URL; but file urls cannot
# represent relative paths, so we have to live with it for now.
"url": os.path.join("..", "..", LOCAL_MIRROR_DIR),
"homepage": "https://github.com/spack/spack-bootstrap-mirrors",
"releases": "https://github.com/spack/spack-bootstrap-mirrors/releases",
@ -58,7 +60,11 @@
SOURCE_METADATA = {
"type": "install",
"description": "Mirror with software needed to bootstrap Spack",
"info": {"url": os.path.join("..", "..", LOCAL_MIRROR_DIR)},
"info": {
# This is a mis-nomer since it's not a URL; but file urls cannot
# represent relative paths, so we have to live with it for now.
"url": os.path.join("..", "..", LOCAL_MIRROR_DIR)
},
}

View File

@ -8,7 +8,6 @@
import shutil
import sys
import tempfile
import urllib.parse
import llnl.util.tty as tty
@ -66,27 +65,37 @@ def setup_parser(subparser):
create.add_argument(
"-k", "--key", metavar="key", type=str, default=None, help="Key for signing."
)
output = create.add_mutually_exclusive_group(required=True)
output = create.add_mutually_exclusive_group(required=False)
# TODO: remove from Spack 0.21
output.add_argument(
"-d",
"--directory",
metavar="directory",
type=str,
help="local directory where buildcaches will be written.",
dest="mirror_flag",
type=arguments.mirror_directory,
help="local directory where buildcaches will be written. (deprecated)",
)
# TODO: remove from Spack 0.21
output.add_argument(
"-m",
"--mirror-name",
metavar="mirror-name",
type=str,
help="name of the mirror where buildcaches will be written.",
dest="mirror_flag",
type=arguments.mirror_name,
help="name of the mirror where buildcaches will be written. (deprecated)",
)
# TODO: remove from Spack 0.21
output.add_argument(
"--mirror-url",
metavar="mirror-url",
type=str,
help="URL of the mirror where buildcaches will be written.",
dest="mirror_flag",
type=arguments.mirror_url,
help="URL of the mirror where buildcaches will be written. (deprecated)",
)
# Unfortunately we cannot add this to the mutually exclusive group above,
# because we have further positional arguments.
# TODO: require from Spack 0.21
create.add_argument("mirror", type=str, help="Mirror name, path, or URL.", nargs="?")
create.add_argument(
"--rebuild-index",
action="store_true",
@ -179,7 +188,7 @@ def setup_parser(subparser):
"-m",
"--mirror-url",
default=None,
help="Override any configured mirrors with this mirror url",
help="Override any configured mirrors with this mirror URL",
)
check.add_argument(
@ -266,55 +275,108 @@ def setup_parser(subparser):
help="A quoted glob pattern identifying copy manifest files",
)
source = sync.add_mutually_exclusive_group(required=False)
# TODO: remove in Spack 0.21
source.add_argument(
"--src-directory", metavar="DIRECTORY", type=str, help="Source mirror as a local file path"
"--src-directory",
metavar="DIRECTORY",
dest="src_mirror_flag",
type=arguments.mirror_directory,
help="Source mirror as a local file path (deprecated)",
)
# TODO: remove in Spack 0.21
source.add_argument(
"--src-mirror-name", metavar="MIRROR_NAME", type=str, help="Name of the source mirror"
"--src-mirror-name",
metavar="MIRROR_NAME",
dest="src_mirror_flag",
type=arguments.mirror_name,
help="Name of the source mirror (deprecated)",
)
# TODO: remove in Spack 0.21
source.add_argument(
"--src-mirror-url", metavar="MIRROR_URL", type=str, help="URL of the source mirror"
"--src-mirror-url",
metavar="MIRROR_URL",
dest="src_mirror_flag",
type=arguments.mirror_url,
help="URL of the source mirror (deprecated)",
)
# TODO: only support this in 0.21
source.add_argument(
"src_mirror",
metavar="source mirror",
type=arguments.mirror_name_or_url,
help="Source mirror name, path, or URL",
nargs="?",
)
dest = sync.add_mutually_exclusive_group(required=False)
# TODO: remove in Spack 0.21
dest.add_argument(
"--dest-directory",
metavar="DIRECTORY",
type=str,
help="Destination mirror as a local file path",
dest="dest_mirror_flag",
type=arguments.mirror_directory,
help="Destination mirror as a local file path (deprecated)",
)
# TODO: remove in Spack 0.21
dest.add_argument(
"--dest-mirror-name",
metavar="MIRROR_NAME",
type=str,
help="Name of the destination mirror",
type=arguments.mirror_name,
dest="dest_mirror_flag",
help="Name of the destination mirror (deprecated)",
)
# TODO: remove in Spack 0.21
dest.add_argument(
"--dest-mirror-url", metavar="MIRROR_URL", type=str, help="URL of the destination mirror"
"--dest-mirror-url",
metavar="MIRROR_URL",
dest="dest_mirror_flag",
type=arguments.mirror_url,
help="URL of the destination mirror (deprecated)",
)
# TODO: only support this in 0.21
dest.add_argument(
"dest_mirror",
metavar="destination mirror",
type=arguments.mirror_name_or_url,
help="Destination mirror name, path, or URL",
nargs="?",
)
sync.set_defaults(func=sync_fn)
# Update buildcache index without copying any additional packages
update_index = subparsers.add_parser("update-index", help=update_index_fn.__doc__)
update_index_out = update_index.add_mutually_exclusive_group(required=True)
# TODO: remove in Spack 0.21
update_index_out.add_argument(
"-d",
"--directory",
metavar="directory",
type=str,
help="local directory where buildcaches will be written.",
dest="mirror_flag",
type=arguments.mirror_directory,
help="local directory where buildcaches will be written (deprecated)",
)
# TODO: remove in Spack 0.21
update_index_out.add_argument(
"-m",
"--mirror-name",
metavar="mirror-name",
type=str,
help="name of the mirror where buildcaches will be written.",
dest="mirror_flag",
type=arguments.mirror_name,
help="name of the mirror where buildcaches will be written (deprecated)",
)
# TODO: remove in Spack 0.21
update_index_out.add_argument(
"--mirror-url",
metavar="mirror-url",
type=str,
help="URL of the mirror where buildcaches will be written.",
dest="mirror_flag",
type=arguments.mirror_url,
help="URL of the mirror where buildcaches will be written (deprecated)",
)
# TODO: require from Spack 0.21
update_index_out.add_argument(
"mirror",
type=arguments.mirror_name_or_url,
help="Destination mirror name, path, or URL",
nargs="?",
)
update_index.add_argument(
"-k",
@ -326,26 +388,17 @@ def setup_parser(subparser):
update_index.set_defaults(func=update_index_fn)
def _mirror_url_from_args(args):
if args.directory:
return spack.mirror.push_url_from_directory(args.directory)
if args.mirror_name:
return spack.mirror.push_url_from_mirror_name(args.mirror_name)
if args.mirror_url:
return spack.mirror.push_url_from_mirror_url(args.mirror_url)
def _matching_specs(args):
def _matching_specs(specs, spec_file):
"""Return a list of matching specs read from either a spec file (JSON or YAML),
a query over the store or a query over the active environment.
"""
env = ev.active_environment()
hashes = env.all_hashes() if env else None
if args.spec_file:
return spack.store.specfile_matches(args.spec_file, hashes=hashes)
if spec_file:
return spack.store.specfile_matches(spec_file, hashes=hashes)
if args.specs:
constraints = spack.cmd.parse_specs(args.specs)
if specs:
constraints = spack.cmd.parse_specs(specs)
return spack.store.find(constraints, hashes=hashes)
if env:
@ -383,10 +436,30 @@ def _concrete_spec_from_args(args):
def create_fn(args):
"""create a binary package and push it to a mirror"""
push_url = _mirror_url_from_args(args)
matches = _matching_specs(args)
if args.mirror_flag:
mirror = args.mirror_flag
elif not args.mirror:
raise ValueError("No mirror provided")
else:
mirror = arguments.mirror_name_or_url(args.mirror)
msg = "Pushing binary packages to {0}/build_cache".format(push_url)
if args.mirror_flag:
tty.warn(
"Using flags to specify mirrors is deprecated and will be removed in "
"Spack 0.21, use positional arguments instead."
)
# TODO: remove this in 0.21. If we have mirror_flag, the first
# spec is in the positional mirror arg due to argparse limitations.
specs = args.specs
if args.mirror_flag and args.mirror:
specs.insert(0, args.mirror)
url = mirror.push_url
matches = _matching_specs(specs, args.spec_file)
msg = "Pushing binary packages to {0}/build_cache".format(url)
tty.msg(msg)
specs_kwargs = {
"include_root": "package" in args.things_to_install,
@ -400,7 +473,7 @@ def create_fn(args):
"allow_root": args.allow_root,
"regenerate_index": args.rebuild_index,
}
bindist.push(matches, push_url, specs_kwargs, **kwargs)
bindist.push(matches, url, specs_kwargs, **kwargs)
def install_fn(args):
@ -593,51 +666,24 @@ def sync_fn(args):
manifest_copy(glob.glob(args.manifest_glob))
return 0
# Figure out the source mirror
source_location = None
if args.src_directory:
source_location = args.src_directory
scheme = urllib.parse.urlparse(source_location, scheme="<missing>").scheme
if scheme != "<missing>":
raise ValueError('"--src-directory" expected a local path; got a URL, instead')
# Ensure that the mirror lookup does not mistake this for named mirror
source_location = url_util.path_to_file_url(source_location)
elif args.src_mirror_name:
source_location = args.src_mirror_name
result = spack.mirror.MirrorCollection().lookup(source_location)
if result.name == "<unnamed>":
raise ValueError('no configured mirror named "{name}"'.format(name=source_location))
elif args.src_mirror_url:
source_location = args.src_mirror_url
scheme = urllib.parse.urlparse(source_location, scheme="<missing>").scheme
if scheme == "<missing>":
raise ValueError('"{url}" is not a valid URL'.format(url=source_location))
# If no manifest_glob, require a source and dest mirror.
# TODO: Simplify in Spack 0.21
if not (args.src_mirror_flag or args.src_mirror) or not (
args.dest_mirror_flag or args.dest_mirror
):
raise ValueError("Source and destination mirror are required.")
src_mirror = spack.mirror.MirrorCollection().lookup(source_location)
src_mirror_url = url_util.format(src_mirror.fetch_url)
if args.src_mirror_flag or args.dest_mirror_flag:
tty.warn(
"Using flags to specify mirrors is deprecated and will be removed in "
"Spack 0.21, use positional arguments instead."
)
# Figure out the destination mirror
dest_location = None
if args.dest_directory:
dest_location = args.dest_directory
scheme = urllib.parse.urlparse(dest_location, scheme="<missing>").scheme
if scheme != "<missing>":
raise ValueError('"--dest-directory" expected a local path; got a URL, instead')
# Ensure that the mirror lookup does not mistake this for named mirror
dest_location = url_util.path_to_file_url(dest_location)
elif args.dest_mirror_name:
dest_location = args.dest_mirror_name
result = spack.mirror.MirrorCollection().lookup(dest_location)
if result.name == "<unnamed>":
raise ValueError('no configured mirror named "{name}"'.format(name=dest_location))
elif args.dest_mirror_url:
dest_location = args.dest_mirror_url
scheme = urllib.parse.urlparse(dest_location, scheme="<missing>").scheme
if scheme == "<missing>":
raise ValueError('"{url}" is not a valid URL'.format(url=dest_location))
src_mirror = args.src_mirror_flag if args.src_mirror_flag else args.src_mirror
dest_mirror = args.dest_mirror_flag if args.dest_mirror_flag else args.dest_mirror
dest_mirror = spack.mirror.MirrorCollection().lookup(dest_location)
dest_mirror_url = url_util.format(dest_mirror.fetch_url)
src_mirror_url = src_mirror.fetch_url
dest_mirror_url = dest_mirror.push_url
# Get the active environment
env = spack.cmd.require_active_env(cmd_name="buildcache sync")
@ -698,38 +744,28 @@ def manifest_copy(manifest_file_list):
copy_buildcache_file(copy_file["src"], copy_file["dest"])
def update_index(mirror_url, update_keys=False):
mirror = spack.mirror.MirrorCollection().lookup(mirror_url)
outdir = url_util.format(mirror.push_url)
def update_index(mirror: spack.mirror.Mirror, update_keys=False):
url = mirror.push_url
bindist.generate_package_index(url_util.join(outdir, bindist.build_cache_relative_path()))
bindist.generate_package_index(url_util.join(url, bindist.build_cache_relative_path()))
if update_keys:
keys_url = url_util.join(
outdir, bindist.build_cache_relative_path(), bindist.build_cache_keys_relative_path()
url, bindist.build_cache_relative_path(), bindist.build_cache_keys_relative_path()
)
bindist.generate_key_index(keys_url)
def _mirror_url_from_args_deprecated_format(args):
# In Spack 0.19 the -d flag was equivalent to --mirror-url.
# Spack 0.20 deprecates this, so in 0.21 -d means --directory.
if args.directory and url_util.validate_scheme(urllib.parse.urlparse(args.directory).scheme):
tty.warn(
"Passing a URL to `update-index -d <url>` is deprecated "
"and will be removed in Spack 0.21. "
"Use `update-index --mirror-url <url>` instead."
)
return spack.mirror.push_url_from_mirror_url(args.directory)
else:
return _mirror_url_from_args(args)
def update_index_fn(args):
"""Update a buildcache index."""
push_url = _mirror_url_from_args_deprecated_format(args)
update_index(push_url, update_keys=args.keys)
if args.mirror_flag:
tty.warn(
"Using flags to specify mirrors is deprecated and will be removed in "
"Spack 0.21, use positional arguments instead."
)
mirror = args.mirror_flag if args.mirror_flag else args.mirror
update_index(mirror, update_keys=args.keys)
def buildcache(parser, args):

View File

@ -241,8 +241,9 @@ def ci_reindex(args):
ci_mirrors = yaml_root["mirrors"]
mirror_urls = [url for url in ci_mirrors.values()]
remote_mirror_url = mirror_urls[0]
mirror = spack.mirror.Mirror(remote_mirror_url)
buildcache.update_index(remote_mirror_url, update_keys=True)
buildcache.update_index(mirror, update_keys=True)
def ci_rebuild(args):

View File

@ -5,6 +5,7 @@
import argparse
import os.path
from llnl.util.lang import stable_partition
@ -12,6 +13,7 @@
import spack.config
import spack.dependency as dep
import spack.environment as ev
import spack.mirror
import spack.modules
import spack.reporters
import spack.spec
@ -552,3 +554,42 @@ def is_valid(arg):
dependencies = val
return package, dependencies
def mirror_name_or_url(m):
# Look up mirror by name or use anonymous mirror with path/url.
# We want to guard against typos in mirror names, to avoid pushing
# accidentally to a dir in the current working directory.
# If there's a \ or / in the name, it's interpreted as a path or url.
if "/" in m or "\\" in m:
return spack.mirror.Mirror(m)
# Otherwise, the named mirror is required to exist.
try:
return spack.mirror.require_mirror_name(m)
except ValueError as e:
raise argparse.ArgumentTypeError(
str(e) + ". Did you mean {}?".format(os.path.join(".", m))
)
def mirror_url(url):
try:
return spack.mirror.Mirror.from_url(url)
except ValueError as e:
raise argparse.ArgumentTypeError(str(e))
def mirror_directory(path):
try:
return spack.mirror.Mirror.from_local_path(path)
except ValueError as e:
raise argparse.ArgumentTypeError(str(e))
def mirror_name(name):
try:
return spack.mirror.require_mirror_name(name)
except ValueError as e:
raise argparse.ArgumentTypeError(str(e))

View File

@ -145,8 +145,7 @@ def setup_parser(subparser):
def mirror_add(args):
"""Add a mirror to Spack."""
url = url_util.format(args.url)
spack.mirror.add(args.name, url, args.scope, args)
spack.mirror.add(args.name, args.url, args.scope, args)
def mirror_remove(args):
@ -156,7 +155,7 @@ def mirror_remove(args):
def mirror_set_url(args):
"""Change the URL of a mirror."""
url = url_util.format(args.url)
url = args.url
mirrors = spack.config.get("mirrors", scope=args.scope)
if not mirrors:
mirrors = syaml_dict()

View File

@ -31,15 +31,15 @@
import spack.mirror
import spack.spec
import spack.url as url
import spack.util.path
import spack.util.spack_json as sjson
import spack.util.spack_yaml as syaml
import spack.util.url as url_util
from spack.util.spack_yaml import syaml_dict
from spack.version import VersionList
def _is_string(url):
return isinstance(url, str)
#: What schemes do we support
supported_url_schemes = ("file", "http", "https", "sftp", "ftp", "s3", "gs")
def _display_mirror_entry(size, name, url, type_=None):
@ -51,6 +51,19 @@ def _display_mirror_entry(size, name, url, type_=None):
print("%-*s%s%s" % (size + 4, name, url, type_))
def _url_or_path_to_url(url_or_path: str) -> str:
"""For simplicity we allow mirror URLs in config files to be local, relative paths.
This helper function takes care of distinguishing between URLs and paths, and
canonicalizes paths before transforming them into file:// URLs."""
# Is it a supported URL already? Then don't do path-related canonicalization.
parsed = urllib.parse.urlparse(url_or_path)
if parsed.scheme in supported_url_schemes:
return url_or_path
# Otherwise we interpret it as path, and we should promote it to file:// URL.
return url_util.path_to_file_url(spack.util.path.canonicalize_path(url_or_path))
class Mirror(object):
"""Represents a named location for storing source tarballs and binary
packages.
@ -90,6 +103,21 @@ def from_json(stream, name=None):
except Exception as e:
raise sjson.SpackJSONError("error parsing JSON mirror:", str(e)) from e
@staticmethod
def from_local_path(path: str):
return Mirror(fetch_url=url_util.path_to_file_url(path))
@staticmethod
def from_url(url: str):
"""Create an anonymous mirror by URL. This method validates the URL."""
if not urllib.parse.urlparse(url).scheme in supported_url_schemes:
raise ValueError(
'"{}" is not a valid mirror URL. Scheme must be once of {}.'.format(
url, ", ".join(supported_url_schemes)
)
)
return Mirror(fetch_url=url)
def to_dict(self):
if self._push_url is None:
return syaml_dict([("fetch", self._fetch_url), ("push", self._fetch_url)])
@ -201,7 +229,11 @@ def set_access_token(self, url_type, connection_token):
@property
def fetch_url(self):
return self._fetch_url if _is_string(self._fetch_url) else self._fetch_url["url"]
"""Get the valid, canonicalized fetch URL"""
url_or_path = (
self._fetch_url if isinstance(self._fetch_url, str) else self._fetch_url["url"]
)
return _url_or_path_to_url(url_or_path)
@fetch_url.setter
def fetch_url(self, url):
@ -210,9 +242,12 @@ def fetch_url(self, url):
@property
def push_url(self):
"""Get the valid, canonicalized push URL. Returns fetch URL if no custom
push URL is defined"""
if self._push_url is None:
return self._fetch_url if _is_string(self._fetch_url) else self._fetch_url["url"]
return self._push_url if _is_string(self._push_url) else self._push_url["url"]
return self.fetch_url
url_or_path = self._push_url if isinstance(self._push_url, str) else self._push_url["url"]
return _url_or_path_to_url(url_or_path)
@push_url.setter
def push_url(self, url):
@ -663,31 +698,12 @@ def create_mirror_from_package_object(pkg_obj, mirror_cache, mirror_stats):
return True
def push_url_from_directory(output_directory):
"""Given a directory in the local filesystem, return the URL on
which to push binary packages.
"""
if url_util.validate_scheme(urllib.parse.urlparse(output_directory).scheme):
raise ValueError("expected a local path, but got a URL instead")
mirror_url = url_util.path_to_file_url(output_directory)
mirror = spack.mirror.MirrorCollection().lookup(mirror_url)
return url_util.format(mirror.push_url)
def push_url_from_mirror_name(mirror_name):
"""Given a mirror name, return the URL on which to push binary packages."""
mirror = spack.mirror.MirrorCollection().lookup(mirror_name)
if mirror.name == "<unnamed>":
def require_mirror_name(mirror_name):
"""Find a mirror by name and raise if it does not exist"""
mirror = spack.mirror.MirrorCollection().get(mirror_name)
if not mirror:
raise ValueError('no mirror named "{0}"'.format(mirror_name))
return url_util.format(mirror.push_url)
def push_url_from_mirror_url(mirror_url):
"""Given a mirror URL, return the URL on which to push binary packages."""
if not url_util.validate_scheme(urllib.parse.urlparse(mirror_url).scheme):
raise ValueError('"{0}" is not a valid URL'.format(mirror_url))
mirror = spack.mirror.MirrorCollection().lookup(mirror_url)
return url_util.format(mirror.push_url)
return mirror
class MirrorError(spack.error.SpackError):

View File

@ -6,10 +6,8 @@
"""Schema for mirrors.yaml configuration file.
.. literalinclude:: _spack_root/lib/spack/spack/schema/mirrors.py
:lines: 13-
"""
#: Properties for inclusion in other schemas
properties = {
"mirrors": {

View File

@ -23,22 +23,6 @@
import spack.util.spack_yaml as syaml
@pytest.fixture
def tmp_scope():
"""Creates a temporary configuration scope"""
base_name = "internal-testing-scope"
current_overrides = set(x.name for x in cfg.config.matching_scopes(r"^{0}".format(base_name)))
num_overrides = 0
scope_name = base_name
while scope_name in current_overrides:
scope_name = "{0}{1}".format(base_name, num_overrides)
num_overrides += 1
with cfg.override(cfg.InternalConfigScope(scope_name)):
yield scope_name
def test_urlencode_string():
s = "Spack Test Project"

View File

@ -3,7 +3,6 @@
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
import argparse
import errno
import os
import platform
@ -268,13 +267,3 @@ def test_buildcache_create_install(
tarball = spack.binary_distribution.tarball_name(spec, ".spec.json")
assert os.path.exists(os.path.join(str(tmpdir), "build_cache", tarball_path))
assert os.path.exists(os.path.join(str(tmpdir), "build_cache", tarball))
def test_deprecation_mirror_url_dir_flag(capfd):
# Test that passing `update-index -d <url>` gives a deprecation warning.
parser = argparse.ArgumentParser()
spack.cmd.buildcache.setup_parser(parser)
url = spack.util.url.path_to_file_url(os.getcwd())
args = parser.parse_args(["update-index", "-d", url])
spack.cmd.buildcache._mirror_url_from_args_deprecated_format(args)
assert "Passing a URL to `update-index -d <url>` is deprecated" in capfd.readouterr()[1]

View File

@ -1218,7 +1218,7 @@ def test_push_mirror_contents(
working_dir = tmpdir.join("working_dir")
mirror_dir = working_dir.join("mirror")
mirror_url = "file://{0}".format(mirror_dir.strpath)
mirror_url = url_util.path_to_file_url(mirror_dir.strpath)
ci.import_signing_key(_signing_key())

View File

@ -25,25 +25,6 @@
pytestmark = pytest.mark.skipif(sys.platform == "win32", reason="does not run on windows")
@pytest.fixture
def tmp_scope():
"""Creates a temporary configuration scope"""
base_name = "internal-testing-scope"
current_overrides = set(
x.name for x in spack.config.config.matching_scopes(r"^{0}".format(base_name))
)
num_overrides = 0
scope_name = base_name
while scope_name in current_overrides:
scope_name = "{0}{1}".format(base_name, num_overrides)
num_overrides += 1
with spack.config.override(spack.config.InternalConfigScope(scope_name)):
yield scope_name
# test gpg command detection
@pytest.mark.parametrize(
"cmd_name,version",
@ -81,7 +62,7 @@ def test_no_gpg_in_path(tmpdir, mock_gnupghome, monkeypatch, mutable_config):
@pytest.mark.maybeslow
def test_gpg(tmpdir, tmp_scope, mock_gnupghome):
def test_gpg(tmpdir, mutable_config, mock_gnupghome):
# Verify a file with an empty keyring.
with pytest.raises(ProcessError):
gpg("verify", os.path.join(mock_gpg_data_path, "content.txt"))
@ -211,6 +192,6 @@ def test_gpg(tmpdir, tmp_scope, mock_gnupghome):
test_path = tmpdir.join("named_cache")
os.makedirs("%s" % test_path)
mirror_url = "file://%s" % test_path
mirror("add", "--scope", tmp_scope, "gpg", mirror_url)
mirror("add", "gpg", mirror_url)
gpg("publish", "--rebuild-index", "-m", "gpg")
assert os.path.exists("%s/build_cache/_pgp/index.json" % test_path)

View File

@ -26,25 +26,6 @@
pytestmark = pytest.mark.skipif(sys.platform == "win32", reason="does not run on windows")
@pytest.fixture
def tmp_scope():
"""Creates a temporary configuration scope"""
base_name = "internal-testing-scope"
current_overrides = set(
x.name for x in spack.config.config.matching_scopes(r"^{0}".format(base_name))
)
num_overrides = 0
scope_name = base_name
while scope_name in current_overrides:
scope_name = "{0}{1}".format(base_name, num_overrides)
num_overrides += 1
with spack.config.override(spack.config.InternalConfigScope(scope_name)):
yield scope_name
@pytest.mark.disable_clean_stage_check
@pytest.mark.regression("8083")
def test_regression_8083(tmpdir, capfd, mock_packages, mock_fetch, config):
@ -154,48 +135,44 @@ def test_exclude_file(mock_packages, tmpdir, config):
assert not any(spec.satisfies(y) for spec in mirror_specs for y in expected_exclude)
def test_mirror_crud(tmp_scope, capsys):
def test_mirror_crud(mutable_config, capsys):
with capsys.disabled():
mirror("add", "--scope", tmp_scope, "mirror", "http://spack.io")
mirror("add", "mirror", "http://spack.io")
output = mirror("remove", "--scope", tmp_scope, "mirror")
output = mirror("remove", "mirror")
assert "Removed mirror" in output
mirror("add", "--scope", tmp_scope, "mirror", "http://spack.io")
mirror("add", "mirror", "http://spack.io")
# no-op
output = mirror("set-url", "--scope", tmp_scope, "mirror", "http://spack.io")
output = mirror("set-url", "mirror", "http://spack.io")
assert "No changes made" in output
output = mirror("set-url", "--scope", tmp_scope, "--push", "mirror", "s3://spack-public")
output = mirror("set-url", "--push", "mirror", "s3://spack-public")
assert "Changed (push) url" in output
# no-op
output = mirror("set-url", "--scope", tmp_scope, "--push", "mirror", "s3://spack-public")
output = mirror("set-url", "--push", "mirror", "s3://spack-public")
assert "No changes made" in output
output = mirror("remove", "--scope", tmp_scope, "mirror")
output = mirror("remove", "mirror")
assert "Removed mirror" in output
# Test S3 connection info token
mirror(
"add",
"--scope",
tmp_scope,
"--s3-access-token",
"aaaaaazzzzz",
"mirror",
"s3://spack-public",
)
output = mirror("remove", "--scope", tmp_scope, "mirror")
output = mirror("remove", "mirror")
assert "Removed mirror" in output
# Test S3 connection info id/key
mirror(
"add",
"--scope",
tmp_scope,
"--s3-access-key-id",
"foo",
"--s3-access-key-secret",
@ -204,14 +181,12 @@ def test_mirror_crud(tmp_scope, capsys):
"s3://spack-public",
)
output = mirror("remove", "--scope", tmp_scope, "mirror")
output = mirror("remove", "mirror")
assert "Removed mirror" in output
# Test S3 connection info with endpoint URL
mirror(
"add",
"--scope",
tmp_scope,
"--s3-access-token",
"aaaaaazzzzz",
"--s3-endpoint-url",
@ -220,32 +195,32 @@ def test_mirror_crud(tmp_scope, capsys):
"s3://spack-public",
)
output = mirror("remove", "--scope", tmp_scope, "mirror")
output = mirror("remove", "mirror")
assert "Removed mirror" in output
output = mirror("list", "--scope", tmp_scope)
output = mirror("list")
assert "No mirrors configured" in output
# Test GCS Mirror
mirror("add", "--scope", tmp_scope, "mirror", "gs://spack-test")
mirror("add", "mirror", "gs://spack-test")
output = mirror("remove", "--scope", tmp_scope, "mirror")
output = mirror("remove", "mirror")
assert "Removed mirror" in output
def test_mirror_nonexisting(tmp_scope):
def test_mirror_nonexisting(mutable_config):
with pytest.raises(SpackCommandError):
mirror("remove", "--scope", tmp_scope, "not-a-mirror")
mirror("remove", "not-a-mirror")
with pytest.raises(SpackCommandError):
mirror("set-url", "--scope", tmp_scope, "not-a-mirror", "http://spack.io")
mirror("set-url", "not-a-mirror", "http://spack.io")
def test_mirror_name_collision(tmp_scope):
mirror("add", "--scope", tmp_scope, "first", "1")
def test_mirror_name_collision(mutable_config):
mirror("add", "first", "1")
with pytest.raises(SpackCommandError):
mirror("add", "--scope", tmp_scope, "first", "1")
mirror("add", "first", "1")
def test_mirror_destroy(

View File

@ -71,6 +71,20 @@ def file_url_string_to_path(url):
return urllib.request.url2pathname(urllib.parse.urlparse(url).path)
def is_path_instead_of_url(path_or_url):
"""Historically some config files and spack commands used paths
where urls should be used. This utility can be used to validate
and promote paths to urls."""
scheme = urllib.parse.urlparse(path_or_url).scheme
# On non-Windows, no scheme means it's likely a path
if not sys.platform == "win32":
return not scheme
# On Windows, we may have drive letters.
return "A" <= scheme <= "Z"
def format(parsed_url):
"""Format a URL string

View File

@ -500,7 +500,7 @@ _spack_buildcache_create() {
then
SPACK_COMPREPLY="-h --help -r --rel -f --force -u --unsigned -a --allow-root -k --key -d --directory -m --mirror-name --mirror-url --rebuild-index --spec-file --only"
else
_all_packages
_mirrors
fi
}
@ -552,11 +552,21 @@ _spack_buildcache_save_specfile() {
}
_spack_buildcache_sync() {
if $list_options
then
SPACK_COMPREPLY="-h --help --manifest-glob --src-directory --src-mirror-name --src-mirror-url --dest-directory --dest-mirror-name --dest-mirror-url"
else
SPACK_COMPREPLY=""
fi
}
_spack_buildcache_update_index() {
if $list_options
then
SPACK_COMPREPLY="-h --help -d --directory -m --mirror-name --mirror-url -k --keys"
else
_mirrors
fi
}
_spack_cd() {