parser: refactor with coarser token granularity (#34151)
## Motivation Our parser grew to be quite complex, with a 2-state lexer and logic in the parser that has up to 5 levels of nested conditionals. In the future, to turn compilers into proper dependencies, we'll have to increase the complexity further as we foresee the need to add: 1. Edge attributes 2. Spec nesting to the spec syntax (see https://github.com/spack/seps/pull/5 for an initial discussion of those changes). The main attempt here is thus to _simplify the existing code_ before we start extending it later. We try to do that by adopting a different token granularity, and by using more complex regexes for tokenization. This allow us to a have a "flatter" encoding for the parser. i.e., it has fewer nested conditionals and a near-trivial lexer. There are places, namely in `VERSION`, where we have to use negative lookahead judiciously to avoid ambiguity. Specifically, this parse is ambiguous without `(?!\s*=)` in `VERSION_RANGE` and an extra final `\b` in `VERSION`: ``` @ 1.2.3 : develop # This is a version range 1.2.3:develop @ 1.2.3 : develop=foo # This is a version range 1.2.3: followed by a key-value pair ``` ## Differences with the previous parser ~There are currently 2 known differences with the previous parser, which have been added on purpose:~ - ~No spaces allowed after a sigil (e.g. `foo @ 1.2.3` is invalid while `foo @1.2.3` is valid)~ - ~`/<hash> @1.2.3` can be parsed as a concrete spec followed by an anonymous spec (before was invalid)~ ~We can recover the previous behavior on both ones but, especially for the second one, it seems the current behavior in the PR is more consistent.~ The parser is currently 100% backward compatible. ## Error handling Being based on more complex regexes, we can possibly improve error handling by adding regexes for common issues and hint users on that. I'll leave that for a following PR, but there's a stub for this approach in the PR. ## Performance To be sure we don't add any performance penalty with this new encoding, I measured: ```console $ spack python -m timeit -s "import spack.spec" -c "spack.spec.Spec(<spec>)" ``` for different specs on my machine: * **Spack:** 0.20.0.dev0 (c9db4e50ba045f5697816187accaf2451cb1aae7) * **Python:** 3.8.10 * **Platform:** linux-ubuntu20.04-icelake * **Concretizer:** clingo results are: | Spec | develop | this PR | | ------------- | ------------- | ------- | | `trilinos` | 28.9 usec | 13.1 usec | | `trilinos @1.2.10:1.4.20,2.0.1` | 131 usec | 120 usec | | `trilinos %gcc` | 44.9 usec | 20.9 usec | | `trilinos +foo` | 44.1 usec | 21.3 usec | | `trilinos foo=bar` | 59.5 usec | 25.6 usec | | `trilinos foo=bar ^ mpich foo=baz` | 120 usec | 82.1 usec | so this new parser seems to be consistently faster than the previous one. ## Modifications In this PR we just substituted the Spec parser, which means: - [x] Deleted in `spec.py` the `SpecParser` and `SpecLexer` classes. deleted `spack/parse.py` - [x] Added a new parser in `spack/parser.py` - [x] Hooked the new parser in all the places the previous one was used - [x] Adapted unit tests in `test/spec_syntax.py` ## Possible future improvements Random thoughts while working on the PR: - Currently we transform hashes and files into specs during parsing. I think we might want to introduce an additional step and parse special objects like a `FileSpec` etc. in-between parsing and concretization.
This commit is contained in:
		
				
					committed by
					
						
						GitHub
					
				
			
			
				
	
			
			
			
						parent
						
							412bec45aa
						
					
				
				
					commit
					ab6499ce1e
				
			@@ -175,14 +175,11 @@ Spec-related modules
 | 
			
		||||
^^^^^^^^^^^^^^^^^^^^
 | 
			
		||||
 | 
			
		||||
:mod:`spack.spec`
 | 
			
		||||
  Contains :class:`~spack.spec.Spec` and :class:`~spack.spec.SpecParser`.
 | 
			
		||||
  Also implements most of the logic for normalization and concretization
 | 
			
		||||
  Contains :class:`~spack.spec.Spec`. Also implements most of the logic for concretization
 | 
			
		||||
  of specs.
 | 
			
		||||
 | 
			
		||||
:mod:`spack.parse`
 | 
			
		||||
  Contains some base classes for implementing simple recursive descent
 | 
			
		||||
  parsers: :class:`~spack.parse.Parser` and :class:`~spack.parse.Lexer`.
 | 
			
		||||
  Used by :class:`~spack.spec.SpecParser`.
 | 
			
		||||
:mod:`spack.parser`
 | 
			
		||||
  Contains :class:`~spack.parser.SpecParser` and functions related to parsing specs.
 | 
			
		||||
 | 
			
		||||
:mod:`spack.concretize`
 | 
			
		||||
  Contains :class:`~spack.concretize.Concretizer` implementation,
 | 
			
		||||
 
 | 
			
		||||
@@ -26,6 +26,7 @@
 | 
			
		||||
import spack.environment as ev
 | 
			
		||||
import spack.error
 | 
			
		||||
import spack.extensions
 | 
			
		||||
import spack.parser
 | 
			
		||||
import spack.paths
 | 
			
		||||
import spack.spec
 | 
			
		||||
import spack.store
 | 
			
		||||
@@ -217,7 +218,7 @@ def parse_specs(args, **kwargs):
 | 
			
		||||
    unquoted_flags = _UnquotedFlags.extract(sargs)
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        specs = spack.spec.parse(sargs)
 | 
			
		||||
        specs = spack.parser.parse(sargs)
 | 
			
		||||
        for spec in specs:
 | 
			
		||||
            if concretize:
 | 
			
		||||
                spec.concretize(tests=tests)  # implies normalize
 | 
			
		||||
 
 | 
			
		||||
@@ -495,6 +495,8 @@ def provides(*specs, **kwargs):
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    def _execute_provides(pkg):
 | 
			
		||||
        import spack.parser  # Avoid circular dependency
 | 
			
		||||
 | 
			
		||||
        when = kwargs.get("when")
 | 
			
		||||
        when_spec = make_when_spec(when)
 | 
			
		||||
        if not when_spec:
 | 
			
		||||
@@ -505,7 +507,7 @@ def _execute_provides(pkg):
 | 
			
		||||
        when_spec.name = pkg.name
 | 
			
		||||
 | 
			
		||||
        for string in specs:
 | 
			
		||||
            for provided_spec in spack.spec.parse(string):
 | 
			
		||||
            for provided_spec in spack.parser.parse(string):
 | 
			
		||||
                if pkg.name == provided_spec.name:
 | 
			
		||||
                    raise CircularReferenceError("Package '%s' cannot provide itself." % pkg.name)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,174 +0,0 @@
 | 
			
		||||
# Copyright 2013-2022 Lawrence Livermore National Security, LLC and other
 | 
			
		||||
# Spack Project Developers. See the top-level COPYRIGHT file for details.
 | 
			
		||||
#
 | 
			
		||||
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
 | 
			
		||||
 | 
			
		||||
import itertools
 | 
			
		||||
import re
 | 
			
		||||
import shlex
 | 
			
		||||
import sys
 | 
			
		||||
 | 
			
		||||
import spack.error
 | 
			
		||||
import spack.util.path as sp
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Token(object):
 | 
			
		||||
    """Represents tokens; generated from input by lexer and fed to parse()."""
 | 
			
		||||
 | 
			
		||||
    __slots__ = "type", "value", "start", "end"
 | 
			
		||||
 | 
			
		||||
    def __init__(self, type, value="", start=0, end=0):
 | 
			
		||||
        self.type = type
 | 
			
		||||
        self.value = value
 | 
			
		||||
        self.start = start
 | 
			
		||||
        self.end = end
 | 
			
		||||
 | 
			
		||||
    def __repr__(self):
 | 
			
		||||
        return str(self)
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return "<%d: '%s'>" % (self.type, self.value)
 | 
			
		||||
 | 
			
		||||
    def is_a(self, type):
 | 
			
		||||
        return self.type == type
 | 
			
		||||
 | 
			
		||||
    def __eq__(self, other):
 | 
			
		||||
        return (self.type == other.type) and (self.value == other.value)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Lexer(object):
 | 
			
		||||
    """Base class for Lexers that keep track of line numbers."""
 | 
			
		||||
 | 
			
		||||
    __slots__ = "scanner0", "scanner1", "mode", "mode_switches_01", "mode_switches_10"
 | 
			
		||||
 | 
			
		||||
    def __init__(self, lexicon0, mode_switches_01=[], lexicon1=[], mode_switches_10=[]):
 | 
			
		||||
        self.scanner0 = re.Scanner(lexicon0)
 | 
			
		||||
        self.mode_switches_01 = mode_switches_01
 | 
			
		||||
        self.scanner1 = re.Scanner(lexicon1)
 | 
			
		||||
        self.mode_switches_10 = mode_switches_10
 | 
			
		||||
        self.mode = 0
 | 
			
		||||
 | 
			
		||||
    def token(self, type, value=""):
 | 
			
		||||
        if self.mode == 0:
 | 
			
		||||
            return Token(type, value, self.scanner0.match.start(0), self.scanner0.match.end(0))
 | 
			
		||||
        else:
 | 
			
		||||
            return Token(type, value, self.scanner1.match.start(0), self.scanner1.match.end(0))
 | 
			
		||||
 | 
			
		||||
    def lex_word(self, word):
 | 
			
		||||
        scanner = self.scanner0
 | 
			
		||||
        mode_switches = self.mode_switches_01
 | 
			
		||||
        if self.mode == 1:
 | 
			
		||||
            scanner = self.scanner1
 | 
			
		||||
            mode_switches = self.mode_switches_10
 | 
			
		||||
 | 
			
		||||
        tokens, remainder = scanner.scan(word)
 | 
			
		||||
        remainder_used = 0
 | 
			
		||||
 | 
			
		||||
        for i, t in enumerate(tokens):
 | 
			
		||||
            if t.type in mode_switches:
 | 
			
		||||
                # Combine post-switch tokens with remainder and
 | 
			
		||||
                # scan in other mode
 | 
			
		||||
                self.mode = 1 - self.mode  # swap 0/1
 | 
			
		||||
                remainder_used = 1
 | 
			
		||||
                tokens = tokens[: i + 1] + self.lex_word(
 | 
			
		||||
                    word[word.index(t.value) + len(t.value) :]
 | 
			
		||||
                )
 | 
			
		||||
                break
 | 
			
		||||
 | 
			
		||||
        if remainder and not remainder_used:
 | 
			
		||||
            msg = "Invalid character, '{0}',".format(remainder[0])
 | 
			
		||||
            msg += " in '{0}' at index {1}".format(word, word.index(remainder))
 | 
			
		||||
            raise LexError(msg, word, word.index(remainder))
 | 
			
		||||
 | 
			
		||||
        return tokens
 | 
			
		||||
 | 
			
		||||
    def lex(self, text):
 | 
			
		||||
        lexed = []
 | 
			
		||||
        for word in text:
 | 
			
		||||
            tokens = self.lex_word(word)
 | 
			
		||||
            lexed.extend(tokens)
 | 
			
		||||
        return lexed
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Parser(object):
 | 
			
		||||
    """Base class for simple recursive descent parsers."""
 | 
			
		||||
 | 
			
		||||
    __slots__ = "tokens", "token", "next", "lexer", "text"
 | 
			
		||||
 | 
			
		||||
    def __init__(self, lexer):
 | 
			
		||||
        self.tokens = iter([])  # iterators over tokens, handled in order.
 | 
			
		||||
        self.token = Token(None)  # last accepted token
 | 
			
		||||
        self.next = None  # next token
 | 
			
		||||
        self.lexer = lexer
 | 
			
		||||
        self.text = None
 | 
			
		||||
 | 
			
		||||
    def gettok(self):
 | 
			
		||||
        """Puts the next token in the input stream into self.next."""
 | 
			
		||||
        try:
 | 
			
		||||
            self.next = next(self.tokens)
 | 
			
		||||
        except StopIteration:
 | 
			
		||||
            self.next = None
 | 
			
		||||
 | 
			
		||||
    def push_tokens(self, iterable):
 | 
			
		||||
        """Adds all tokens in some iterable to the token stream."""
 | 
			
		||||
        self.tokens = itertools.chain(iter(iterable), iter([self.next]), self.tokens)
 | 
			
		||||
        self.gettok()
 | 
			
		||||
 | 
			
		||||
    def accept(self, id):
 | 
			
		||||
        """Put the next symbol in self.token if accepted, then call gettok()"""
 | 
			
		||||
        if self.next and self.next.is_a(id):
 | 
			
		||||
            self.token = self.next
 | 
			
		||||
            self.gettok()
 | 
			
		||||
            return True
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
    def next_token_error(self, message):
 | 
			
		||||
        """Raise an error about the next token in the stream."""
 | 
			
		||||
        raise ParseError(message, self.text[0], self.token.end)
 | 
			
		||||
 | 
			
		||||
    def last_token_error(self, message):
 | 
			
		||||
        """Raise an error about the previous token in the stream."""
 | 
			
		||||
        raise ParseError(message, self.text[0], self.token.start)
 | 
			
		||||
 | 
			
		||||
    def unexpected_token(self):
 | 
			
		||||
        self.next_token_error("Unexpected token: '%s'" % self.next.value)
 | 
			
		||||
 | 
			
		||||
    def expect(self, id):
 | 
			
		||||
        """Like accept(), but fails if we don't like the next token."""
 | 
			
		||||
        if self.accept(id):
 | 
			
		||||
            return True
 | 
			
		||||
        else:
 | 
			
		||||
            if self.next:
 | 
			
		||||
                self.unexpected_token()
 | 
			
		||||
            else:
 | 
			
		||||
                self.next_token_error("Unexpected end of input")
 | 
			
		||||
            sys.exit(1)
 | 
			
		||||
 | 
			
		||||
    def setup(self, text):
 | 
			
		||||
        if isinstance(text, str):
 | 
			
		||||
            # shlex does not handle Windows path
 | 
			
		||||
            # separators, so we must normalize to posix
 | 
			
		||||
            text = sp.convert_to_posix_path(text)
 | 
			
		||||
            text = shlex.split(str(text))
 | 
			
		||||
        self.text = text
 | 
			
		||||
        self.push_tokens(self.lexer.lex(text))
 | 
			
		||||
 | 
			
		||||
    def parse(self, text):
 | 
			
		||||
        self.setup(text)
 | 
			
		||||
        return self.do_parse()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ParseError(spack.error.SpackError):
 | 
			
		||||
    """Raised when we don't hit an error while parsing."""
 | 
			
		||||
 | 
			
		||||
    def __init__(self, message, string, pos):
 | 
			
		||||
        super(ParseError, self).__init__(message)
 | 
			
		||||
        self.string = string
 | 
			
		||||
        self.pos = pos
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class LexError(ParseError):
 | 
			
		||||
    """Raised when we don't know how to lex something."""
 | 
			
		||||
 | 
			
		||||
    def __init__(self, message, string, pos):
 | 
			
		||||
        super(LexError, self).__init__(message, string, pos)
 | 
			
		||||
							
								
								
									
										522
									
								
								lib/spack/spack/parser.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										522
									
								
								lib/spack/spack/parser.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,522 @@
 | 
			
		||||
# Copyright 2013-2022 Lawrence Livermore National Security, LLC and other
 | 
			
		||||
# Spack Project Developers. See the top-level COPYRIGHT file for details.
 | 
			
		||||
#
 | 
			
		||||
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
 | 
			
		||||
"""Parser for spec literals
 | 
			
		||||
 | 
			
		||||
Here is the EBNF grammar for a spec::
 | 
			
		||||
 | 
			
		||||
    spec          = [name] [node_options] { ^ node } |
 | 
			
		||||
                    [name] [node_options] hash |
 | 
			
		||||
                    filename
 | 
			
		||||
 | 
			
		||||
    node          =  name [node_options] |
 | 
			
		||||
                     [name] [node_options] hash |
 | 
			
		||||
                     filename
 | 
			
		||||
 | 
			
		||||
    node_options  = [@(version_list|version_pair)] [%compiler] { variant }
 | 
			
		||||
 | 
			
		||||
    hash          = / id
 | 
			
		||||
    filename      = (.|/|[a-zA-Z0-9-_]*/)([a-zA-Z0-9-_./]*)(.json|.yaml)
 | 
			
		||||
 | 
			
		||||
    name          = id | namespace id
 | 
			
		||||
    namespace     = { id . }
 | 
			
		||||
 | 
			
		||||
    variant       = bool_variant | key_value | propagated_bv | propagated_kv
 | 
			
		||||
    bool_variant  =  +id |  ~id |  -id
 | 
			
		||||
    propagated_bv = ++id | ~~id | --id
 | 
			
		||||
    key_value     =  id=id |  id=quoted_id
 | 
			
		||||
    propagated_kv = id==id | id==quoted_id
 | 
			
		||||
 | 
			
		||||
    compiler      = id [@version_list]
 | 
			
		||||
 | 
			
		||||
    version_pair  = git_version=vid
 | 
			
		||||
    version_list  = (version|version_range) [ { , (version|version_range)} ]
 | 
			
		||||
    version_range = vid:vid | vid: | :vid | :
 | 
			
		||||
    version       = vid
 | 
			
		||||
 | 
			
		||||
    git_version   = git.(vid) | git_hash
 | 
			
		||||
    git_hash      = [A-Fa-f0-9]{40}
 | 
			
		||||
 | 
			
		||||
    quoted_id     = " id_with_ws " | ' id_with_ws '
 | 
			
		||||
    id_with_ws    = [a-zA-Z0-9_][a-zA-Z_0-9-.\\s]*
 | 
			
		||||
    vid           = [a-zA-Z0-9_][a-zA-Z_0-9-.]*
 | 
			
		||||
    id            = [a-zA-Z0-9_][a-zA-Z_0-9-]*
 | 
			
		||||
 | 
			
		||||
Identifiers using the <name>=<value> command, such as architectures and
 | 
			
		||||
compiler flags, require a space before the name.
 | 
			
		||||
 | 
			
		||||
There is one context-sensitive part: ids in versions may contain '.', while
 | 
			
		||||
other ids may not.
 | 
			
		||||
 | 
			
		||||
There is one ambiguity: since '-' is allowed in an id, you need to put
 | 
			
		||||
whitespace space before -variant for it to be tokenized properly.  You can
 | 
			
		||||
either use whitespace, or you can just use ~variant since it means the same
 | 
			
		||||
thing.  Spack uses ~variant in directory names and in the canonical form of
 | 
			
		||||
specs to avoid ambiguity.  Both are provided because ~ can cause shell
 | 
			
		||||
expansion when it is the first character in an id typed on the command line.
 | 
			
		||||
"""
 | 
			
		||||
import enum
 | 
			
		||||
import pathlib
 | 
			
		||||
import re
 | 
			
		||||
from typing import Iterator, List, Match, Optional
 | 
			
		||||
 | 
			
		||||
from llnl.util.tty import color
 | 
			
		||||
 | 
			
		||||
import spack.error
 | 
			
		||||
import spack.spec
 | 
			
		||||
import spack.variant
 | 
			
		||||
import spack.version
 | 
			
		||||
 | 
			
		||||
#: Valid name for specs and variants. Here we are not using
 | 
			
		||||
#: the previous "w[\w.-]*" since that would match most
 | 
			
		||||
#: characters that can be part of a word in any language
 | 
			
		||||
IDENTIFIER = r"([a-zA-Z_0-9][a-zA-Z_0-9\-]*)"
 | 
			
		||||
DOTTED_IDENTIFIER = rf"({IDENTIFIER}(\.{IDENTIFIER})+)"
 | 
			
		||||
GIT_HASH = r"([A-Fa-f0-9]{40})"
 | 
			
		||||
GIT_VERSION = rf"((git\.({DOTTED_IDENTIFIER}|{IDENTIFIER}))|({GIT_HASH}))"
 | 
			
		||||
 | 
			
		||||
NAME = r"[a-zA-Z_0-9][a-zA-Z_0-9\-.]*"
 | 
			
		||||
 | 
			
		||||
HASH = r"[a-zA-Z_0-9]+"
 | 
			
		||||
 | 
			
		||||
#: A filename starts either with a "." or a "/" or a "{name}/"
 | 
			
		||||
FILENAME = r"(\.|\/|[a-zA-Z0-9-_]*\/)([a-zA-Z0-9-_\.\/]*)(\.json|\.yaml)"
 | 
			
		||||
 | 
			
		||||
VALUE = r"([a-zA-Z_0-9\-+\*.,:=\~\/\\]+)"
 | 
			
		||||
QUOTED_VALUE = r"[\"']+([a-zA-Z_0-9\-+\*.,:=\~\/\\\s]+)[\"']+"
 | 
			
		||||
 | 
			
		||||
VERSION = r"([a-zA-Z0-9_][a-zA-Z_0-9\-\.]*\b)"
 | 
			
		||||
VERSION_RANGE = rf"({VERSION}\s*:\s*{VERSION}(?!\s*=)|:\s*{VERSION}(?!\s*=)|{VERSION}\s*:|:)"
 | 
			
		||||
VERSION_LIST = rf"({VERSION_RANGE}|{VERSION})(\s*[,]\s*({VERSION_RANGE}|{VERSION}))*"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TokenBase(enum.Enum):
 | 
			
		||||
    """Base class for an enum type with a regex value"""
 | 
			
		||||
 | 
			
		||||
    def __new__(cls, *args, **kwargs):
 | 
			
		||||
        # See
 | 
			
		||||
        value = len(cls.__members__) + 1
 | 
			
		||||
        obj = object.__new__(cls)
 | 
			
		||||
        obj._value_ = value
 | 
			
		||||
        return obj
 | 
			
		||||
 | 
			
		||||
    def __init__(self, regex):
 | 
			
		||||
        self.regex = regex
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return f"{self._name_}"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TokenType(TokenBase):
 | 
			
		||||
    """Enumeration of the different token kinds in the spec grammar.
 | 
			
		||||
 | 
			
		||||
    Order of declaration is extremely important, since text containing specs is parsed with a
 | 
			
		||||
    single regex obtained by ``"|".join(...)`` of all the regex in the order of declaration.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    # Dependency
 | 
			
		||||
    DEPENDENCY = r"(\^)"
 | 
			
		||||
    # Version
 | 
			
		||||
    VERSION_HASH_PAIR = rf"(@({GIT_VERSION})=({VERSION}))"
 | 
			
		||||
    VERSION = rf"(@\s*({VERSION_LIST}))"
 | 
			
		||||
    # Variants
 | 
			
		||||
    PROPAGATED_BOOL_VARIANT = rf"((\+\+|~~|--)\s*{NAME})"
 | 
			
		||||
    BOOL_VARIANT = rf"([~+-]\s*{NAME})"
 | 
			
		||||
    PROPAGATED_KEY_VALUE_PAIR = rf"({NAME}\s*==\s*({VALUE}|{QUOTED_VALUE}))"
 | 
			
		||||
    KEY_VALUE_PAIR = rf"({NAME}\s*=\s*({VALUE}|{QUOTED_VALUE}))"
 | 
			
		||||
    # Compilers
 | 
			
		||||
    COMPILER_AND_VERSION = rf"(%\s*({NAME})([\s]*)@\s*({VERSION_LIST}))"
 | 
			
		||||
    COMPILER = rf"(%\s*({NAME}))"
 | 
			
		||||
    # FILENAME
 | 
			
		||||
    FILENAME = rf"({FILENAME})"
 | 
			
		||||
    # Package name
 | 
			
		||||
    FULLY_QUALIFIED_PACKAGE_NAME = rf"({DOTTED_IDENTIFIER})"
 | 
			
		||||
    UNQUALIFIED_PACKAGE_NAME = rf"({IDENTIFIER})"
 | 
			
		||||
    # DAG hash
 | 
			
		||||
    DAG_HASH = rf"(/({HASH}))"
 | 
			
		||||
    # White spaces
 | 
			
		||||
    WS = r"(\s+)"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ErrorTokenType(TokenBase):
 | 
			
		||||
    """Enum with regexes for error analysis"""
 | 
			
		||||
 | 
			
		||||
    # Unexpected character
 | 
			
		||||
    UNEXPECTED = r"(.[\s]*)"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Token:
 | 
			
		||||
    """Represents tokens; generated from input by lexer and fed to parse()."""
 | 
			
		||||
 | 
			
		||||
    __slots__ = "kind", "value", "start", "end"
 | 
			
		||||
 | 
			
		||||
    def __init__(
 | 
			
		||||
        self, kind: TokenType, value: str, start: Optional[int] = None, end: Optional[int] = None
 | 
			
		||||
    ):
 | 
			
		||||
        self.kind = kind
 | 
			
		||||
        self.value = value
 | 
			
		||||
        self.start = start
 | 
			
		||||
        self.end = end
 | 
			
		||||
 | 
			
		||||
    def __repr__(self):
 | 
			
		||||
        return str(self)
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return f"({self.kind}, {self.value})"
 | 
			
		||||
 | 
			
		||||
    def __eq__(self, other):
 | 
			
		||||
        return (self.kind == other.kind) and (self.value == other.value)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#: List of all the regexes used to match spec parts, in order of precedence
 | 
			
		||||
TOKEN_REGEXES = [rf"(?P<{token}>{token.regex})" for token in TokenType]
 | 
			
		||||
#: List of all valid regexes followed by error analysis regexes
 | 
			
		||||
ERROR_HANDLING_REGEXES = TOKEN_REGEXES + [
 | 
			
		||||
    rf"(?P<{token}>{token.regex})" for token in ErrorTokenType
 | 
			
		||||
]
 | 
			
		||||
#: Regex to scan a valid text
 | 
			
		||||
ALL_TOKENS = re.compile("|".join(TOKEN_REGEXES))
 | 
			
		||||
#: Regex to analyze an invalid text
 | 
			
		||||
ANALYSIS_REGEX = re.compile("|".join(ERROR_HANDLING_REGEXES))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def tokenize(text: str) -> Iterator[Token]:
 | 
			
		||||
    """Return a token generator from the text passed as input.
 | 
			
		||||
 | 
			
		||||
    Raises:
 | 
			
		||||
        SpecTokenizationError: if we can't tokenize anymore, but didn't reach the
 | 
			
		||||
            end of the input text.
 | 
			
		||||
    """
 | 
			
		||||
    scanner = ALL_TOKENS.scanner(text)  # type: ignore[attr-defined]
 | 
			
		||||
    match: Optional[Match] = None
 | 
			
		||||
    for match in iter(scanner.match, None):
 | 
			
		||||
        yield Token(
 | 
			
		||||
            TokenType.__members__[match.lastgroup],  # type: ignore[attr-defined]
 | 
			
		||||
            match.group(),  # type: ignore[attr-defined]
 | 
			
		||||
            match.start(),  # type: ignore[attr-defined]
 | 
			
		||||
            match.end(),  # type: ignore[attr-defined]
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    if match is None and not text:
 | 
			
		||||
        # We just got an empty string
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    if match is None or match.end() != len(text):
 | 
			
		||||
        scanner = ANALYSIS_REGEX.scanner(text)  # type: ignore[attr-defined]
 | 
			
		||||
        matches = [m for m in iter(scanner.match, None)]  # type: ignore[var-annotated]
 | 
			
		||||
        raise SpecTokenizationError(matches, text)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TokenContext:
 | 
			
		||||
    """Token context passed around by parsers"""
 | 
			
		||||
 | 
			
		||||
    __slots__ = "token_stream", "current_token", "next_token"
 | 
			
		||||
 | 
			
		||||
    def __init__(self, token_stream: Iterator[Token]):
 | 
			
		||||
        self.token_stream = token_stream
 | 
			
		||||
        self.current_token = None
 | 
			
		||||
        self.next_token = None
 | 
			
		||||
        self.advance()
 | 
			
		||||
 | 
			
		||||
    def advance(self):
 | 
			
		||||
        """Advance one token"""
 | 
			
		||||
        self.current_token, self.next_token = self.next_token, next(self.token_stream, None)
 | 
			
		||||
 | 
			
		||||
    def accept(self, kind: TokenType):
 | 
			
		||||
        """If the next token is of the specified kind, advance the stream and return True.
 | 
			
		||||
        Otherwise return False.
 | 
			
		||||
        """
 | 
			
		||||
        if self.next_token and self.next_token.kind == kind:
 | 
			
		||||
            self.advance()
 | 
			
		||||
            return True
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SpecParser:
 | 
			
		||||
    """Parse text into specs"""
 | 
			
		||||
 | 
			
		||||
    __slots__ = "literal_str", "ctx"
 | 
			
		||||
 | 
			
		||||
    def __init__(self, literal_str: str):
 | 
			
		||||
        self.literal_str = literal_str
 | 
			
		||||
        self.ctx = TokenContext(filter(lambda x: x.kind != TokenType.WS, tokenize(literal_str)))
 | 
			
		||||
 | 
			
		||||
    def tokens(self) -> List[Token]:
 | 
			
		||||
        """Return the entire list of token from the initial text. White spaces are
 | 
			
		||||
        filtered out.
 | 
			
		||||
        """
 | 
			
		||||
        return list(filter(lambda x: x.kind != TokenType.WS, tokenize(self.literal_str)))
 | 
			
		||||
 | 
			
		||||
    def next_spec(self, initial_spec: Optional[spack.spec.Spec] = None) -> spack.spec.Spec:
 | 
			
		||||
        """Return the next spec parsed from text.
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
            initial_spec: object where to parse the spec. If None a new one
 | 
			
		||||
                will be created.
 | 
			
		||||
 | 
			
		||||
        Return
 | 
			
		||||
            The spec that was parsed
 | 
			
		||||
        """
 | 
			
		||||
        initial_spec = initial_spec or spack.spec.Spec()
 | 
			
		||||
        root_spec = SpecNodeParser(self.ctx).parse(initial_spec)
 | 
			
		||||
        while True:
 | 
			
		||||
            if self.ctx.accept(TokenType.DEPENDENCY):
 | 
			
		||||
                dependency = SpecNodeParser(self.ctx).parse(spack.spec.Spec())
 | 
			
		||||
 | 
			
		||||
                if dependency == spack.spec.Spec():
 | 
			
		||||
                    msg = (
 | 
			
		||||
                        "this dependency sigil needs to be followed by a package name "
 | 
			
		||||
                        "or a node attribute (version, variant, etc.)"
 | 
			
		||||
                    )
 | 
			
		||||
                    raise SpecParsingError(msg, self.ctx.current_token, self.literal_str)
 | 
			
		||||
 | 
			
		||||
                if root_spec.concrete:
 | 
			
		||||
                    raise spack.spec.RedundantSpecError(root_spec, "^" + str(dependency))
 | 
			
		||||
 | 
			
		||||
                root_spec._add_dependency(dependency, ())
 | 
			
		||||
 | 
			
		||||
            else:
 | 
			
		||||
                break
 | 
			
		||||
 | 
			
		||||
        return root_spec
 | 
			
		||||
 | 
			
		||||
    def all_specs(self) -> List[spack.spec.Spec]:
 | 
			
		||||
        """Return all the specs that remain to be parsed"""
 | 
			
		||||
        return list(iter(self.next_spec, spack.spec.Spec()))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SpecNodeParser:
 | 
			
		||||
    """Parse a single spec node from a stream of tokens"""
 | 
			
		||||
 | 
			
		||||
    __slots__ = "ctx", "has_compiler", "has_version", "has_hash"
 | 
			
		||||
 | 
			
		||||
    def __init__(self, ctx):
 | 
			
		||||
        self.ctx = ctx
 | 
			
		||||
        self.has_compiler = False
 | 
			
		||||
        self.has_version = False
 | 
			
		||||
        self.has_hash = False
 | 
			
		||||
 | 
			
		||||
    def parse(self, initial_spec: spack.spec.Spec) -> spack.spec.Spec:
 | 
			
		||||
        """Parse a single spec node from a stream of tokens
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
            initial_spec: object to be constructed
 | 
			
		||||
 | 
			
		||||
        Return
 | 
			
		||||
            The object passed as argument
 | 
			
		||||
        """
 | 
			
		||||
        import spack.environment  # Needed to retrieve by hash
 | 
			
		||||
 | 
			
		||||
        # If we start with a package name we have a named spec, we cannot
 | 
			
		||||
        # accept another package name afterwards in a node
 | 
			
		||||
        if self.ctx.accept(TokenType.UNQUALIFIED_PACKAGE_NAME):
 | 
			
		||||
            initial_spec.name = self.ctx.current_token.value
 | 
			
		||||
        elif self.ctx.accept(TokenType.FULLY_QUALIFIED_PACKAGE_NAME):
 | 
			
		||||
            parts = self.ctx.current_token.value.split(".")
 | 
			
		||||
            name = parts[-1]
 | 
			
		||||
            namespace = ".".join(parts[:-1])
 | 
			
		||||
            initial_spec.name = name
 | 
			
		||||
            initial_spec.namespace = namespace
 | 
			
		||||
        elif self.ctx.accept(TokenType.FILENAME):
 | 
			
		||||
            return FileParser(self.ctx).parse(initial_spec)
 | 
			
		||||
 | 
			
		||||
        while True:
 | 
			
		||||
            if self.ctx.accept(TokenType.COMPILER):
 | 
			
		||||
                self.hash_not_parsed_or_raise(initial_spec, self.ctx.current_token.value)
 | 
			
		||||
                if self.has_compiler:
 | 
			
		||||
                    raise spack.spec.DuplicateCompilerSpecError(
 | 
			
		||||
                        f"{initial_spec} cannot have multiple compilers"
 | 
			
		||||
                    )
 | 
			
		||||
 | 
			
		||||
                compiler_name = self.ctx.current_token.value[1:]
 | 
			
		||||
                initial_spec.compiler = spack.spec.CompilerSpec(compiler_name.strip(), ":")
 | 
			
		||||
                self.has_compiler = True
 | 
			
		||||
            elif self.ctx.accept(TokenType.COMPILER_AND_VERSION):
 | 
			
		||||
                self.hash_not_parsed_or_raise(initial_spec, self.ctx.current_token.value)
 | 
			
		||||
                if self.has_compiler:
 | 
			
		||||
                    raise spack.spec.DuplicateCompilerSpecError(
 | 
			
		||||
                        f"{initial_spec} cannot have multiple compilers"
 | 
			
		||||
                    )
 | 
			
		||||
 | 
			
		||||
                compiler_name, compiler_version = self.ctx.current_token.value[1:].split("@")
 | 
			
		||||
                initial_spec.compiler = spack.spec.CompilerSpec(
 | 
			
		||||
                    compiler_name.strip(), compiler_version
 | 
			
		||||
                )
 | 
			
		||||
                self.has_compiler = True
 | 
			
		||||
            elif self.ctx.accept(TokenType.VERSION) or self.ctx.accept(
 | 
			
		||||
                TokenType.VERSION_HASH_PAIR
 | 
			
		||||
            ):
 | 
			
		||||
                self.hash_not_parsed_or_raise(initial_spec, self.ctx.current_token.value)
 | 
			
		||||
                if self.has_version:
 | 
			
		||||
                    raise spack.spec.MultipleVersionError(
 | 
			
		||||
                        f"{initial_spec} cannot have multiple versions"
 | 
			
		||||
                    )
 | 
			
		||||
 | 
			
		||||
                version_list = spack.version.VersionList()
 | 
			
		||||
                version_list.add(spack.version.from_string(self.ctx.current_token.value[1:]))
 | 
			
		||||
                initial_spec.versions = version_list
 | 
			
		||||
 | 
			
		||||
                # Add a git lookup method for GitVersions
 | 
			
		||||
                if (
 | 
			
		||||
                    initial_spec.name
 | 
			
		||||
                    and initial_spec.versions.concrete
 | 
			
		||||
                    and isinstance(initial_spec.version, spack.version.GitVersion)
 | 
			
		||||
                ):
 | 
			
		||||
                    initial_spec.version.generate_git_lookup(initial_spec.fullname)
 | 
			
		||||
 | 
			
		||||
                self.has_version = True
 | 
			
		||||
            elif self.ctx.accept(TokenType.BOOL_VARIANT):
 | 
			
		||||
                self.hash_not_parsed_or_raise(initial_spec, self.ctx.current_token.value)
 | 
			
		||||
                variant_value = self.ctx.current_token.value[0] == "+"
 | 
			
		||||
                initial_spec._add_flag(
 | 
			
		||||
                    self.ctx.current_token.value[1:].strip(), variant_value, propagate=False
 | 
			
		||||
                )
 | 
			
		||||
            elif self.ctx.accept(TokenType.PROPAGATED_BOOL_VARIANT):
 | 
			
		||||
                self.hash_not_parsed_or_raise(initial_spec, self.ctx.current_token.value)
 | 
			
		||||
                variant_value = self.ctx.current_token.value[0:2] == "++"
 | 
			
		||||
                initial_spec._add_flag(
 | 
			
		||||
                    self.ctx.current_token.value[2:].strip(), variant_value, propagate=True
 | 
			
		||||
                )
 | 
			
		||||
            elif self.ctx.accept(TokenType.KEY_VALUE_PAIR):
 | 
			
		||||
                self.hash_not_parsed_or_raise(initial_spec, self.ctx.current_token.value)
 | 
			
		||||
                name, value = self.ctx.current_token.value.split("=", maxsplit=1)
 | 
			
		||||
                name = name.strip("'\" ")
 | 
			
		||||
                value = value.strip("'\" ")
 | 
			
		||||
                initial_spec._add_flag(name, value, propagate=False)
 | 
			
		||||
            elif self.ctx.accept(TokenType.PROPAGATED_KEY_VALUE_PAIR):
 | 
			
		||||
                self.hash_not_parsed_or_raise(initial_spec, self.ctx.current_token.value)
 | 
			
		||||
                name, value = self.ctx.current_token.value.split("==", maxsplit=1)
 | 
			
		||||
                name = name.strip("'\" ")
 | 
			
		||||
                value = value.strip("'\" ")
 | 
			
		||||
                initial_spec._add_flag(name, value, propagate=True)
 | 
			
		||||
            elif not self.has_hash and self.ctx.accept(TokenType.DAG_HASH):
 | 
			
		||||
                dag_hash = self.ctx.current_token.value[1:]
 | 
			
		||||
                matches = []
 | 
			
		||||
                if spack.environment.active_environment():
 | 
			
		||||
                    matches = spack.environment.active_environment().get_by_hash(dag_hash)
 | 
			
		||||
                if not matches:
 | 
			
		||||
                    matches = spack.store.db.get_by_hash(dag_hash)
 | 
			
		||||
                if not matches:
 | 
			
		||||
                    raise spack.spec.NoSuchHashError(dag_hash)
 | 
			
		||||
 | 
			
		||||
                if len(matches) != 1:
 | 
			
		||||
                    raise spack.spec.AmbiguousHashError(
 | 
			
		||||
                        f"Multiple packages specify hash beginning '{dag_hash}'.", *matches
 | 
			
		||||
                    )
 | 
			
		||||
                spec_by_hash = matches[0]
 | 
			
		||||
                if not spec_by_hash.satisfies(initial_spec):
 | 
			
		||||
                    raise spack.spec.InvalidHashError(initial_spec, spec_by_hash.dag_hash())
 | 
			
		||||
                initial_spec._dup(spec_by_hash)
 | 
			
		||||
 | 
			
		||||
                self.has_hash = True
 | 
			
		||||
            else:
 | 
			
		||||
                break
 | 
			
		||||
 | 
			
		||||
        return initial_spec
 | 
			
		||||
 | 
			
		||||
    def hash_not_parsed_or_raise(self, spec, addition):
 | 
			
		||||
        if not self.has_hash:
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        raise spack.spec.RedundantSpecError(spec, addition)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class FileParser:
 | 
			
		||||
    """Parse a single spec from a JSON or YAML file"""
 | 
			
		||||
 | 
			
		||||
    __slots__ = ("ctx",)
 | 
			
		||||
 | 
			
		||||
    def __init__(self, ctx):
 | 
			
		||||
        self.ctx = ctx
 | 
			
		||||
 | 
			
		||||
    def parse(self, initial_spec: spack.spec.Spec) -> spack.spec.Spec:
 | 
			
		||||
        """Parse a spec tree from a specfile.
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
            initial_spec: object where to parse the spec
 | 
			
		||||
 | 
			
		||||
        Return
 | 
			
		||||
            The initial_spec passed as argument, once constructed
 | 
			
		||||
        """
 | 
			
		||||
        file = pathlib.Path(self.ctx.current_token.value)
 | 
			
		||||
 | 
			
		||||
        if not file.exists():
 | 
			
		||||
            raise spack.spec.NoSuchSpecFileError(f"No such spec file: '{file}'")
 | 
			
		||||
 | 
			
		||||
        with file.open("r", encoding="utf-8") as stream:
 | 
			
		||||
            if str(file).endswith(".json"):
 | 
			
		||||
                spec_from_file = spack.spec.Spec.from_json(stream)
 | 
			
		||||
            else:
 | 
			
		||||
                spec_from_file = spack.spec.Spec.from_yaml(stream)
 | 
			
		||||
        initial_spec._dup(spec_from_file)
 | 
			
		||||
        return initial_spec
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def parse(text: str) -> List[spack.spec.Spec]:
 | 
			
		||||
    """Parse text into a list of strings
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        text (str): text to be parsed
 | 
			
		||||
 | 
			
		||||
    Return:
 | 
			
		||||
        List of specs
 | 
			
		||||
    """
 | 
			
		||||
    return SpecParser(text).all_specs()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def parse_one_or_raise(
 | 
			
		||||
    text: str, initial_spec: Optional[spack.spec.Spec] = None
 | 
			
		||||
) -> spack.spec.Spec:
 | 
			
		||||
    """Parse exactly one spec from text and return it, or raise
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        text (str): text to be parsed
 | 
			
		||||
        initial_spec: buffer where to parse the spec. If None a new one will be created.
 | 
			
		||||
    """
 | 
			
		||||
    stripped_text = text.strip()
 | 
			
		||||
    parser = SpecParser(stripped_text)
 | 
			
		||||
    result = parser.next_spec(initial_spec)
 | 
			
		||||
    last_token = parser.ctx.current_token
 | 
			
		||||
 | 
			
		||||
    if last_token is not None and last_token.end != len(stripped_text):
 | 
			
		||||
        message = "a single spec was requested, but parsed more than one:"
 | 
			
		||||
        message += f"\n{text}"
 | 
			
		||||
        if last_token is not None:
 | 
			
		||||
            underline = f"\n{' ' * last_token.end}{'^' * (len(text) - last_token.end)}"
 | 
			
		||||
            message += color.colorize(f"@*r{{{underline}}}")
 | 
			
		||||
        raise ValueError(message)
 | 
			
		||||
 | 
			
		||||
    return result
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SpecSyntaxError(Exception):
 | 
			
		||||
    """Base class for Spec syntax errors"""
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SpecTokenizationError(SpecSyntaxError):
 | 
			
		||||
    """Syntax error in a spec string"""
 | 
			
		||||
 | 
			
		||||
    def __init__(self, matches, text):
 | 
			
		||||
        message = "unexpected tokens in the spec string\n"
 | 
			
		||||
        message += f"{text}"
 | 
			
		||||
 | 
			
		||||
        underline = "\n"
 | 
			
		||||
        for match in matches:
 | 
			
		||||
            if match.lastgroup == str(ErrorTokenType.UNEXPECTED):
 | 
			
		||||
                underline += f"{'^' * (match.end() - match.start())}"
 | 
			
		||||
                continue
 | 
			
		||||
            underline += f"{' ' * (match.end() - match.start())}"
 | 
			
		||||
 | 
			
		||||
        message += color.colorize(f"@*r{{{underline}}}")
 | 
			
		||||
        super().__init__(message)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SpecParsingError(SpecSyntaxError):
 | 
			
		||||
    """Error when parsing tokens"""
 | 
			
		||||
 | 
			
		||||
    def __init__(self, message, token, text):
 | 
			
		||||
        message += f"\n{text}"
 | 
			
		||||
        underline = f"\n{' '*token.start}{'^'*(token.end - token.start)}"
 | 
			
		||||
        message += color.colorize(f"@*r{{{underline}}}")
 | 
			
		||||
        super().__init__(message)
 | 
			
		||||
@@ -8,14 +8,14 @@
 | 
			
		||||
import llnl.util.lang
 | 
			
		||||
import llnl.util.tty
 | 
			
		||||
 | 
			
		||||
import spack.spec
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# jsonschema is imported lazily as it is heavy to import
 | 
			
		||||
# and increases the start-up time
 | 
			
		||||
def _make_validator():
 | 
			
		||||
    import jsonschema
 | 
			
		||||
 | 
			
		||||
    import spack.parser
 | 
			
		||||
 | 
			
		||||
    def _validate_spec(validator, is_spec, instance, schema):
 | 
			
		||||
        """Check if the attributes on instance are valid specs."""
 | 
			
		||||
        import jsonschema
 | 
			
		||||
@@ -25,11 +25,9 @@ def _validate_spec(validator, is_spec, instance, schema):
 | 
			
		||||
 | 
			
		||||
        for spec_str in instance:
 | 
			
		||||
            try:
 | 
			
		||||
                spack.spec.parse(spec_str)
 | 
			
		||||
            except spack.spec.SpecParseError as e:
 | 
			
		||||
                yield jsonschema.ValidationError(
 | 
			
		||||
                    '"{0}" is an invalid spec [{1}]'.format(spec_str, str(e))
 | 
			
		||||
                )
 | 
			
		||||
                spack.parser.parse(spec_str)
 | 
			
		||||
            except spack.parser.SpecSyntaxError as e:
 | 
			
		||||
                yield jsonschema.ValidationError(str(e))
 | 
			
		||||
 | 
			
		||||
    def _deprecated_properties(validator, deprecated, instance, schema):
 | 
			
		||||
        if not (validator.is_type(instance, "object") or validator.is_type(instance, "array")):
 | 
			
		||||
 
 | 
			
		||||
@@ -47,37 +47,6 @@
 | 
			
		||||
 | 
			
		||||
6. The architecture to build with.  This is needed on machines where
 | 
			
		||||
   cross-compilation is required
 | 
			
		||||
 | 
			
		||||
Here is the EBNF grammar for a spec::
 | 
			
		||||
 | 
			
		||||
  spec-list    = { spec [ dep-list ] }
 | 
			
		||||
  dep_list     = { ^ spec }
 | 
			
		||||
  spec         = id [ options ]
 | 
			
		||||
  options      = { @version-list | ++variant | +variant |
 | 
			
		||||
                   --variant | -variant | ~~variant | ~variant |
 | 
			
		||||
                   variant=value | variant==value | %compiler |
 | 
			
		||||
                   arch=architecture | [ flag ]==value | [ flag ]=value}
 | 
			
		||||
  flag         = { cflags | cxxflags | fcflags | fflags | cppflags |
 | 
			
		||||
                   ldflags | ldlibs }
 | 
			
		||||
  variant      = id
 | 
			
		||||
  architecture = id
 | 
			
		||||
  compiler     = id [ version-list ]
 | 
			
		||||
  version-list = version [ { , version } ]
 | 
			
		||||
  version      = id | id: | :id | id:id
 | 
			
		||||
  id           = [A-Za-z0-9_][A-Za-z0-9_.-]*
 | 
			
		||||
 | 
			
		||||
Identifiers using the <name>=<value> command, such as architectures and
 | 
			
		||||
compiler flags, require a space before the name.
 | 
			
		||||
 | 
			
		||||
There is one context-sensitive part: ids in versions may contain '.', while
 | 
			
		||||
other ids may not.
 | 
			
		||||
 | 
			
		||||
There is one ambiguity: since '-' is allowed in an id, you need to put
 | 
			
		||||
whitespace space before -variant for it to be tokenized properly.  You can
 | 
			
		||||
either use whitespace, or you can just use ~variant since it means the same
 | 
			
		||||
thing.  Spack uses ~variant in directory names and in the canonical form of
 | 
			
		||||
specs to avoid ambiguity.  Both are provided because ~ can cause shell
 | 
			
		||||
expansion when it is the first character in an id typed on the command line.
 | 
			
		||||
"""
 | 
			
		||||
import collections
 | 
			
		||||
import collections.abc
 | 
			
		||||
@@ -101,7 +70,6 @@
 | 
			
		||||
import spack.dependency as dp
 | 
			
		||||
import spack.error
 | 
			
		||||
import spack.hash_types as ht
 | 
			
		||||
import spack.parse
 | 
			
		||||
import spack.paths
 | 
			
		||||
import spack.platforms
 | 
			
		||||
import spack.provider_index
 | 
			
		||||
@@ -125,8 +93,6 @@
 | 
			
		||||
__all__ = [
 | 
			
		||||
    "CompilerSpec",
 | 
			
		||||
    "Spec",
 | 
			
		||||
    "SpecParser",
 | 
			
		||||
    "parse",
 | 
			
		||||
    "SpecParseError",
 | 
			
		||||
    "ArchitecturePropagationError",
 | 
			
		||||
    "DuplicateDependencyError",
 | 
			
		||||
@@ -584,9 +550,9 @@ def __init__(self, *args):
 | 
			
		||||
            # If there is one argument, it's either another CompilerSpec
 | 
			
		||||
            # to copy or a string to parse
 | 
			
		||||
            if isinstance(arg, str):
 | 
			
		||||
                c = SpecParser().parse_compiler(arg)
 | 
			
		||||
                self.name = c.name
 | 
			
		||||
                self.versions = c.versions
 | 
			
		||||
                spec = spack.parser.parse_one_or_raise(f"%{arg}")
 | 
			
		||||
                self.name = spec.compiler.name
 | 
			
		||||
                self.versions = spec.compiler.versions
 | 
			
		||||
 | 
			
		||||
            elif isinstance(arg, CompilerSpec):
 | 
			
		||||
                self.name = arg.name
 | 
			
		||||
@@ -602,7 +568,8 @@ def __init__(self, *args):
 | 
			
		||||
            name, version = args
 | 
			
		||||
            self.name = name
 | 
			
		||||
            self.versions = vn.VersionList()
 | 
			
		||||
            self.versions.add(vn.ver(version))
 | 
			
		||||
            versions = vn.ver(version)
 | 
			
		||||
            self.versions.add(versions)
 | 
			
		||||
 | 
			
		||||
        else:
 | 
			
		||||
            raise TypeError("__init__ takes 1 or 2 arguments. (%d given)" % nargs)
 | 
			
		||||
@@ -1285,6 +1252,7 @@ def __init__(
 | 
			
		||||
        self.external_path = external_path
 | 
			
		||||
        self.external_module = external_module
 | 
			
		||||
        """
 | 
			
		||||
        import spack.parser
 | 
			
		||||
 | 
			
		||||
        # Copy if spec_like is a Spec.
 | 
			
		||||
        if isinstance(spec_like, Spec):
 | 
			
		||||
@@ -1335,11 +1303,7 @@ def __init__(
 | 
			
		||||
        self._build_spec = None
 | 
			
		||||
 | 
			
		||||
        if isinstance(spec_like, str):
 | 
			
		||||
            spec_list = SpecParser(self).parse(spec_like)
 | 
			
		||||
            if len(spec_list) > 1:
 | 
			
		||||
                raise ValueError("More than one spec in string: " + spec_like)
 | 
			
		||||
            if len(spec_list) < 1:
 | 
			
		||||
                raise ValueError("String contains no specs: " + spec_like)
 | 
			
		||||
            spack.parser.parse_one_or_raise(spec_like, self)
 | 
			
		||||
 | 
			
		||||
        elif spec_like is not None:
 | 
			
		||||
            raise TypeError("Can't make spec out of %s" % type(spec_like))
 | 
			
		||||
@@ -4974,421 +4938,6 @@ def __missing__(self, key):
 | 
			
		||||
spec_id_re = r"\w[\w.-]*"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SpecLexer(spack.parse.Lexer):
 | 
			
		||||
 | 
			
		||||
    """Parses tokens that make up spack specs."""
 | 
			
		||||
 | 
			
		||||
    def __init__(self):
 | 
			
		||||
        # Spec strings require posix-style paths on Windows
 | 
			
		||||
        # because the result is later passed to shlex
 | 
			
		||||
        filename_reg = (
 | 
			
		||||
            r"[/\w.-]*/[/\w/-]+\.(yaml|json)[^\b]*"
 | 
			
		||||
            if not is_windows
 | 
			
		||||
            else r"([A-Za-z]:)*?[/\w.-]*/[/\w/-]+\.(yaml|json)[^\b]*"
 | 
			
		||||
        )
 | 
			
		||||
        super(SpecLexer, self).__init__(
 | 
			
		||||
            [
 | 
			
		||||
                (
 | 
			
		||||
                    r"\@([\w.\-]*\s*)*(\s*\=\s*\w[\w.\-]*)?",
 | 
			
		||||
                    lambda scanner, val: self.token(VER, val),
 | 
			
		||||
                ),
 | 
			
		||||
                (r"\:", lambda scanner, val: self.token(COLON, val)),
 | 
			
		||||
                (r"\,", lambda scanner, val: self.token(COMMA, val)),
 | 
			
		||||
                (r"\^", lambda scanner, val: self.token(DEP, val)),
 | 
			
		||||
                (r"\+\+", lambda scanner, val: self.token(D_ON, val)),
 | 
			
		||||
                (r"\+", lambda scanner, val: self.token(ON, val)),
 | 
			
		||||
                (r"\-\-", lambda scanner, val: self.token(D_OFF, val)),
 | 
			
		||||
                (r"\-", lambda scanner, val: self.token(OFF, val)),
 | 
			
		||||
                (r"\~\~", lambda scanner, val: self.token(D_OFF, val)),
 | 
			
		||||
                (r"\~", lambda scanner, val: self.token(OFF, val)),
 | 
			
		||||
                (r"\%", lambda scanner, val: self.token(PCT, val)),
 | 
			
		||||
                (r"\=\=", lambda scanner, val: self.token(D_EQ, val)),
 | 
			
		||||
                (r"\=", lambda scanner, val: self.token(EQ, val)),
 | 
			
		||||
                # Filenames match before identifiers, so no initial filename
 | 
			
		||||
                # component is parsed as a spec (e.g., in subdir/spec.yaml/json)
 | 
			
		||||
                (filename_reg, lambda scanner, v: self.token(FILE, v)),
 | 
			
		||||
                # Hash match after filename. No valid filename can be a hash
 | 
			
		||||
                # (files end w/.yaml), but a hash can match a filename prefix.
 | 
			
		||||
                (r"/", lambda scanner, val: self.token(HASH, val)),
 | 
			
		||||
                # Identifiers match after filenames and hashes.
 | 
			
		||||
                (spec_id_re, lambda scanner, val: self.token(ID, val)),
 | 
			
		||||
                (r"\s+", lambda scanner, val: None),
 | 
			
		||||
            ],
 | 
			
		||||
            [D_EQ, EQ],
 | 
			
		||||
            [
 | 
			
		||||
                (r"[\S].*", lambda scanner, val: self.token(VAL, val)),
 | 
			
		||||
                (r"\s+", lambda scanner, val: None),
 | 
			
		||||
            ],
 | 
			
		||||
            [VAL],
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Lexer is always the same for every parser.
 | 
			
		||||
_lexer = SpecLexer()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SpecParser(spack.parse.Parser):
 | 
			
		||||
    """Parses specs."""
 | 
			
		||||
 | 
			
		||||
    __slots__ = "previous", "_initial"
 | 
			
		||||
 | 
			
		||||
    def __init__(self, initial_spec=None):
 | 
			
		||||
        """Construct a new SpecParser.
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
            initial_spec (Spec, optional): provide a Spec that we'll parse
 | 
			
		||||
                directly into. This is used to avoid construction of a
 | 
			
		||||
                superfluous Spec object in the Spec constructor.
 | 
			
		||||
        """
 | 
			
		||||
        super(SpecParser, self).__init__(_lexer)
 | 
			
		||||
        self.previous = None
 | 
			
		||||
        self._initial = initial_spec
 | 
			
		||||
 | 
			
		||||
    def do_parse(self):
 | 
			
		||||
        specs = []
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            while self.next:
 | 
			
		||||
                # Try a file first, but if it doesn't succeed, keep parsing
 | 
			
		||||
                # as from_file may backtrack and try an id.
 | 
			
		||||
                if self.accept(FILE):
 | 
			
		||||
                    spec = self.spec_from_file()
 | 
			
		||||
                    if spec:
 | 
			
		||||
                        specs.append(spec)
 | 
			
		||||
                        continue
 | 
			
		||||
 | 
			
		||||
                if self.accept(ID):
 | 
			
		||||
                    self.previous = self.token
 | 
			
		||||
                    if self.accept(EQ) or self.accept(D_EQ):
 | 
			
		||||
                        # We're parsing an anonymous spec beginning with a
 | 
			
		||||
                        # key-value pair.
 | 
			
		||||
                        if not specs:
 | 
			
		||||
                            self.push_tokens([self.previous, self.token])
 | 
			
		||||
                            self.previous = None
 | 
			
		||||
                            specs.append(self.spec(None))
 | 
			
		||||
                        else:
 | 
			
		||||
                            if specs[-1].concrete:
 | 
			
		||||
                                # Trying to add k-v pair to spec from hash
 | 
			
		||||
                                raise RedundantSpecError(specs[-1], "key-value pair")
 | 
			
		||||
                            # We should never end up here.
 | 
			
		||||
                            # This requires starting a new spec with ID, EQ
 | 
			
		||||
                            # After another spec that is not concrete
 | 
			
		||||
                            # If the previous spec is not concrete, this is
 | 
			
		||||
                            # handled in the spec parsing loop
 | 
			
		||||
                            # If it is concrete, see the if statement above
 | 
			
		||||
                            # If there is no previous spec, we don't land in
 | 
			
		||||
                            # this else case.
 | 
			
		||||
                            self.unexpected_token()
 | 
			
		||||
                    else:
 | 
			
		||||
                        # We're parsing a new spec by name
 | 
			
		||||
                        self.previous = None
 | 
			
		||||
                        specs.append(self.spec(self.token.value))
 | 
			
		||||
                elif self.accept(HASH):
 | 
			
		||||
                    # We're finding a spec by hash
 | 
			
		||||
                    specs.append(self.spec_by_hash())
 | 
			
		||||
 | 
			
		||||
                elif self.accept(DEP):
 | 
			
		||||
                    if not specs:
 | 
			
		||||
                        # We're parsing an anonymous spec beginning with a
 | 
			
		||||
                        # dependency. Push the token to recover after creating
 | 
			
		||||
                        # anonymous spec
 | 
			
		||||
                        self.push_tokens([self.token])
 | 
			
		||||
                        specs.append(self.spec(None))
 | 
			
		||||
                    else:
 | 
			
		||||
                        dep = None
 | 
			
		||||
                        if self.accept(FILE):
 | 
			
		||||
                            # this may return None, in which case we backtrack
 | 
			
		||||
                            dep = self.spec_from_file()
 | 
			
		||||
 | 
			
		||||
                        if not dep and self.accept(HASH):
 | 
			
		||||
                            # We're finding a dependency by hash for an
 | 
			
		||||
                            # anonymous spec
 | 
			
		||||
                            dep = self.spec_by_hash()
 | 
			
		||||
                            dep = dep.copy(deps=("link", "run"))
 | 
			
		||||
 | 
			
		||||
                        if not dep:
 | 
			
		||||
                            # We're adding a dependency to the last spec
 | 
			
		||||
                            if self.accept(ID):
 | 
			
		||||
                                self.previous = self.token
 | 
			
		||||
                                if self.accept(EQ):
 | 
			
		||||
                                    # This is an anonymous dep with a key=value
 | 
			
		||||
                                    # push tokens to be parsed as part of the
 | 
			
		||||
                                    # dep spec
 | 
			
		||||
                                    self.push_tokens([self.previous, self.token])
 | 
			
		||||
                                    dep_name = None
 | 
			
		||||
                                else:
 | 
			
		||||
                                    # named dep (standard)
 | 
			
		||||
                                    dep_name = self.token.value
 | 
			
		||||
                                self.previous = None
 | 
			
		||||
                            else:
 | 
			
		||||
                                # anonymous dep
 | 
			
		||||
                                dep_name = None
 | 
			
		||||
                            dep = self.spec(dep_name)
 | 
			
		||||
 | 
			
		||||
                        # Raise an error if the previous spec is already
 | 
			
		||||
                        # concrete (assigned by hash)
 | 
			
		||||
                        if specs[-1].concrete:
 | 
			
		||||
                            raise RedundantSpecError(specs[-1], "dependency")
 | 
			
		||||
                        # command line deps get empty deptypes now.
 | 
			
		||||
                        # Real deptypes are assigned later per packages.
 | 
			
		||||
                        specs[-1]._add_dependency(dep, ())
 | 
			
		||||
 | 
			
		||||
                else:
 | 
			
		||||
                    # If the next token can be part of a valid anonymous spec,
 | 
			
		||||
                    # create the anonymous spec
 | 
			
		||||
                    if self.next.type in (VER, ON, D_ON, OFF, D_OFF, PCT):
 | 
			
		||||
                        # Raise an error if the previous spec is already
 | 
			
		||||
                        # concrete (assigned by hash)
 | 
			
		||||
                        if specs and specs[-1]._hash:
 | 
			
		||||
                            raise RedundantSpecError(specs[-1], "compiler, version, " "or variant")
 | 
			
		||||
                        specs.append(self.spec(None))
 | 
			
		||||
                    else:
 | 
			
		||||
                        self.unexpected_token()
 | 
			
		||||
 | 
			
		||||
        except spack.parse.ParseError as e:
 | 
			
		||||
            raise SpecParseError(e) from e
 | 
			
		||||
 | 
			
		||||
        # Generate lookups for git-commit-based versions
 | 
			
		||||
        for spec in specs:
 | 
			
		||||
            # Cannot do lookups for versions in anonymous specs
 | 
			
		||||
            # Only allow Version objects to use git for now
 | 
			
		||||
            # Note: VersionRange(x, x) is currently concrete, hence isinstance(...).
 | 
			
		||||
            if spec.name and spec.versions.concrete and isinstance(spec.version, vn.GitVersion):
 | 
			
		||||
                spec.version.generate_git_lookup(spec.fullname)
 | 
			
		||||
 | 
			
		||||
        return specs
 | 
			
		||||
 | 
			
		||||
    def spec_from_file(self):
 | 
			
		||||
        """Read a spec from a filename parsed on the input stream.
 | 
			
		||||
 | 
			
		||||
        There is some care taken here to ensure that filenames are a last
 | 
			
		||||
        resort, and that any valid package name is parsed as a name
 | 
			
		||||
        before we consider it as a file. Specs are used in lots of places;
 | 
			
		||||
        we don't want the parser touching the filesystem unnecessarily.
 | 
			
		||||
 | 
			
		||||
        The parse logic is as follows:
 | 
			
		||||
 | 
			
		||||
        1. We require that filenames end in .yaml, which means that no valid
 | 
			
		||||
           filename can be interpreted as a hash (hashes can't have '.')
 | 
			
		||||
 | 
			
		||||
        2. We avoid treating paths like /path/to/spec.json as hashes, or paths
 | 
			
		||||
           like subdir/spec.json as ids by lexing filenames before hashes.
 | 
			
		||||
 | 
			
		||||
        3. For spec names that match file and id regexes, like 'builtin.yaml',
 | 
			
		||||
           we backtrack from spec_from_file() and treat them as spec names.
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        path = self.token.value
 | 
			
		||||
 | 
			
		||||
        # Special case where someone omits a space after a filename. Consider:
 | 
			
		||||
        #
 | 
			
		||||
        #     libdwarf^/some/path/to/libelf.yamllibdwarf ^../../libelf.yaml
 | 
			
		||||
        #
 | 
			
		||||
        # The error is clearly an omitted space. To handle this, the FILE
 | 
			
		||||
        # regex admits text *beyond* .yaml, and we raise a nice error for
 | 
			
		||||
        # file names that don't end in .yaml.
 | 
			
		||||
        if not (path.endswith(".yaml") or path.endswith(".json")):
 | 
			
		||||
            raise SpecFilenameError("Spec filename must end in .yaml or .json: '{0}'".format(path))
 | 
			
		||||
 | 
			
		||||
        if not os.path.exists(path):
 | 
			
		||||
            raise NoSuchSpecFileError("No such spec file: '{0}'".format(path))
 | 
			
		||||
 | 
			
		||||
        with open(path) as f:
 | 
			
		||||
            if path.endswith(".json"):
 | 
			
		||||
                return Spec.from_json(f)
 | 
			
		||||
            return Spec.from_yaml(f)
 | 
			
		||||
 | 
			
		||||
    def parse_compiler(self, text):
 | 
			
		||||
        self.setup(text)
 | 
			
		||||
        return self.compiler()
 | 
			
		||||
 | 
			
		||||
    def spec_by_hash(self):
 | 
			
		||||
        # TODO: Remove parser dependency on active environment and database.
 | 
			
		||||
        import spack.environment
 | 
			
		||||
 | 
			
		||||
        self.expect(ID)
 | 
			
		||||
        dag_hash = self.token.value
 | 
			
		||||
        matches = []
 | 
			
		||||
        if spack.environment.active_environment():
 | 
			
		||||
            matches = spack.environment.active_environment().get_by_hash(dag_hash)
 | 
			
		||||
        if not matches:
 | 
			
		||||
            matches = spack.store.db.get_by_hash(dag_hash)
 | 
			
		||||
        if not matches:
 | 
			
		||||
            raise NoSuchHashError(dag_hash)
 | 
			
		||||
 | 
			
		||||
        if len(matches) != 1:
 | 
			
		||||
            raise AmbiguousHashError(
 | 
			
		||||
                "Multiple packages specify hash beginning '%s'." % dag_hash, *matches
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        return matches[0]
 | 
			
		||||
 | 
			
		||||
    def spec(self, name):
 | 
			
		||||
        """Parse a spec out of the input. If a spec is supplied, initialize
 | 
			
		||||
        and return it instead of creating a new one."""
 | 
			
		||||
        spec_namespace = None
 | 
			
		||||
        spec_name = None
 | 
			
		||||
        if name:
 | 
			
		||||
            spec_namespace, dot, spec_name = name.rpartition(".")
 | 
			
		||||
            if not spec_namespace:
 | 
			
		||||
                spec_namespace = None
 | 
			
		||||
            self.check_identifier(spec_name)
 | 
			
		||||
 | 
			
		||||
        if self._initial is None:
 | 
			
		||||
            spec = Spec()
 | 
			
		||||
        else:
 | 
			
		||||
            # this is used by Spec.__init__
 | 
			
		||||
            spec = self._initial
 | 
			
		||||
            self._initial = None
 | 
			
		||||
 | 
			
		||||
        spec.namespace = spec_namespace
 | 
			
		||||
        spec.name = spec_name
 | 
			
		||||
 | 
			
		||||
        while self.next:
 | 
			
		||||
            if self.accept(VER):
 | 
			
		||||
                vlist = self.version_list()
 | 
			
		||||
                spec._add_versions(vlist)
 | 
			
		||||
 | 
			
		||||
            elif self.accept(D_ON):
 | 
			
		||||
                name = self.variant()
 | 
			
		||||
                spec.variants[name] = vt.BoolValuedVariant(name, True, propagate=True)
 | 
			
		||||
 | 
			
		||||
            elif self.accept(ON):
 | 
			
		||||
                name = self.variant()
 | 
			
		||||
                spec.variants[name] = vt.BoolValuedVariant(name, True, propagate=False)
 | 
			
		||||
 | 
			
		||||
            elif self.accept(D_OFF):
 | 
			
		||||
                name = self.variant()
 | 
			
		||||
                spec.variants[name] = vt.BoolValuedVariant(name, False, propagate=True)
 | 
			
		||||
 | 
			
		||||
            elif self.accept(OFF):
 | 
			
		||||
                name = self.variant()
 | 
			
		||||
                spec.variants[name] = vt.BoolValuedVariant(name, False, propagate=False)
 | 
			
		||||
 | 
			
		||||
            elif self.accept(PCT):
 | 
			
		||||
                spec._set_compiler(self.compiler())
 | 
			
		||||
 | 
			
		||||
            elif self.accept(ID):
 | 
			
		||||
                self.previous = self.token
 | 
			
		||||
                if self.accept(D_EQ):
 | 
			
		||||
                    # We're adding a key-value pair to the spec
 | 
			
		||||
                    self.expect(VAL)
 | 
			
		||||
                    spec._add_flag(self.previous.value, self.token.value, propagate=True)
 | 
			
		||||
                    self.previous = None
 | 
			
		||||
                elif self.accept(EQ):
 | 
			
		||||
                    # We're adding a key-value pair to the spec
 | 
			
		||||
                    self.expect(VAL)
 | 
			
		||||
                    spec._add_flag(self.previous.value, self.token.value, propagate=False)
 | 
			
		||||
                    self.previous = None
 | 
			
		||||
                else:
 | 
			
		||||
                    # We've found the start of a new spec. Go back to do_parse
 | 
			
		||||
                    # and read this token again.
 | 
			
		||||
                    self.push_tokens([self.token])
 | 
			
		||||
                    self.previous = None
 | 
			
		||||
                    break
 | 
			
		||||
 | 
			
		||||
            elif self.accept(HASH):
 | 
			
		||||
                # Get spec by hash and confirm it matches any constraints we
 | 
			
		||||
                # already read in
 | 
			
		||||
                hash_spec = self.spec_by_hash()
 | 
			
		||||
                if hash_spec.satisfies(spec):
 | 
			
		||||
                    spec._dup(hash_spec)
 | 
			
		||||
                    break
 | 
			
		||||
                else:
 | 
			
		||||
                    raise InvalidHashError(spec, hash_spec.dag_hash())
 | 
			
		||||
 | 
			
		||||
            else:
 | 
			
		||||
                break
 | 
			
		||||
 | 
			
		||||
        return spec
 | 
			
		||||
 | 
			
		||||
    def variant(self, name=None):
 | 
			
		||||
        if name:
 | 
			
		||||
            return name
 | 
			
		||||
        else:
 | 
			
		||||
            self.expect(ID)
 | 
			
		||||
            self.check_identifier()
 | 
			
		||||
            return self.token.value
 | 
			
		||||
 | 
			
		||||
    def version(self):
 | 
			
		||||
 | 
			
		||||
        start = None
 | 
			
		||||
        end = None
 | 
			
		||||
 | 
			
		||||
        def str_translate(value):
 | 
			
		||||
            # return None for empty strings since we can end up with `'@'.strip('@')`
 | 
			
		||||
            if not (value and value.strip()):
 | 
			
		||||
                return None
 | 
			
		||||
            else:
 | 
			
		||||
                return value
 | 
			
		||||
 | 
			
		||||
        if self.token.type is COMMA:
 | 
			
		||||
            # need to increment commas, could be ID or COLON
 | 
			
		||||
            self.accept(ID)
 | 
			
		||||
 | 
			
		||||
        if self.token.type in (VER, ID):
 | 
			
		||||
            version_spec = self.token.value.lstrip("@")
 | 
			
		||||
            start = str_translate(version_spec)
 | 
			
		||||
 | 
			
		||||
        if self.accept(COLON):
 | 
			
		||||
            if self.accept(ID):
 | 
			
		||||
                if self.next and self.next.type is EQ:
 | 
			
		||||
                    # This is a start: range followed by a key=value pair
 | 
			
		||||
                    self.push_tokens([self.token])
 | 
			
		||||
                else:
 | 
			
		||||
                    end = self.token.value
 | 
			
		||||
        elif start:
 | 
			
		||||
            # No colon, but there was a version
 | 
			
		||||
            return vn.Version(start)
 | 
			
		||||
        else:
 | 
			
		||||
            # No colon and no id: invalid version
 | 
			
		||||
            self.next_token_error("Invalid version specifier")
 | 
			
		||||
 | 
			
		||||
        if start:
 | 
			
		||||
            start = vn.Version(start)
 | 
			
		||||
        if end:
 | 
			
		||||
            end = vn.Version(end)
 | 
			
		||||
        return vn.VersionRange(start, end)
 | 
			
		||||
 | 
			
		||||
    def version_list(self):
 | 
			
		||||
        vlist = []
 | 
			
		||||
        vlist.append(self.version())
 | 
			
		||||
        while self.accept(COMMA):
 | 
			
		||||
            vlist.append(self.version())
 | 
			
		||||
        return vlist
 | 
			
		||||
 | 
			
		||||
    def compiler(self):
 | 
			
		||||
        self.expect(ID)
 | 
			
		||||
        self.check_identifier()
 | 
			
		||||
 | 
			
		||||
        compiler = CompilerSpec.__new__(CompilerSpec)
 | 
			
		||||
        compiler.name = self.token.value
 | 
			
		||||
        compiler.versions = vn.VersionList()
 | 
			
		||||
        if self.accept(VER):
 | 
			
		||||
            vlist = self.version_list()
 | 
			
		||||
            compiler._add_versions(vlist)
 | 
			
		||||
        else:
 | 
			
		||||
            compiler.versions = vn.VersionList(":")
 | 
			
		||||
        return compiler
 | 
			
		||||
 | 
			
		||||
    def check_identifier(self, id=None):
 | 
			
		||||
        """The only identifiers that can contain '.' are versions, but version
 | 
			
		||||
        ids are context-sensitive so we have to check on a case-by-case
 | 
			
		||||
        basis. Call this if we detect a version id where it shouldn't be.
 | 
			
		||||
        """
 | 
			
		||||
        if not id:
 | 
			
		||||
            id = self.token.value
 | 
			
		||||
        if "." in id:
 | 
			
		||||
            self.last_token_error("{0}: Identifier cannot contain '.'".format(id))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def parse(string):
 | 
			
		||||
    """Returns a list of specs from an input string.
 | 
			
		||||
    For creating one spec, see Spec() constructor.
 | 
			
		||||
    """
 | 
			
		||||
    return SpecParser().parse(string)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def save_dependency_specfiles(
 | 
			
		||||
    root_spec_info, output_directory, dependencies=None, spec_format="json"
 | 
			
		||||
):
 | 
			
		||||
 
 | 
			
		||||
@@ -26,6 +26,7 @@
 | 
			
		||||
import spack.util.executable
 | 
			
		||||
from spack.error import SpackError
 | 
			
		||||
from spack.main import SpackCommand
 | 
			
		||||
from spack.parser import SpecSyntaxError
 | 
			
		||||
from spack.spec import CompilerSpec, Spec
 | 
			
		||||
 | 
			
		||||
install = SpackCommand("install")
 | 
			
		||||
@@ -362,7 +363,7 @@ def test_install_conflicts(conflict_spec):
 | 
			
		||||
)
 | 
			
		||||
def test_install_invalid_spec(invalid_spec):
 | 
			
		||||
    # Make sure that invalid specs raise a SpackError
 | 
			
		||||
    with pytest.raises(SpackError, match="Unexpected token"):
 | 
			
		||||
    with pytest.raises(SpecSyntaxError, match="unexpected tokens"):
 | 
			
		||||
        install(invalid_spec)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,7 @@
 | 
			
		||||
 | 
			
		||||
import spack.environment as ev
 | 
			
		||||
import spack.error
 | 
			
		||||
import spack.parser
 | 
			
		||||
import spack.spec
 | 
			
		||||
import spack.store
 | 
			
		||||
from spack.main import SpackCommand, SpackCommandError
 | 
			
		||||
@@ -181,13 +182,11 @@ def test_spec_returncode():
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_spec_parse_error():
 | 
			
		||||
    with pytest.raises(spack.error.SpackError) as e:
 | 
			
		||||
    with pytest.raises(spack.parser.SpecSyntaxError) as e:
 | 
			
		||||
        spec("1.15:")
 | 
			
		||||
 | 
			
		||||
    # make sure the error is formatted properly
 | 
			
		||||
    error_msg = """\
 | 
			
		||||
    1.15:
 | 
			
		||||
        ^"""
 | 
			
		||||
    error_msg = "unexpected tokens in the spec string\n1.15:\n    ^"
 | 
			
		||||
    assert error_msg in str(e.value)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -68,22 +68,18 @@ def test_validate_spec(validate_spec_schema):
 | 
			
		||||
 | 
			
		||||
    # Check that invalid data throws
 | 
			
		||||
    data["^python@3.7@"] = "baz"
 | 
			
		||||
    with pytest.raises(jsonschema.ValidationError) as exc_err:
 | 
			
		||||
    with pytest.raises(jsonschema.ValidationError, match="unexpected tokens"):
 | 
			
		||||
        v.validate(data)
 | 
			
		||||
 | 
			
		||||
    assert "is an invalid spec" in str(exc_err.value)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.regression("9857")
 | 
			
		||||
def test_module_suffixes(module_suffixes_schema):
 | 
			
		||||
    v = spack.schema.Validator(module_suffixes_schema)
 | 
			
		||||
    data = {"tcl": {"all": {"suffixes": {"^python@2.7@": "py2.7"}}}}
 | 
			
		||||
 | 
			
		||||
    with pytest.raises(jsonschema.ValidationError) as exc_err:
 | 
			
		||||
    with pytest.raises(jsonschema.ValidationError, match="unexpected tokens"):
 | 
			
		||||
        v.validate(data)
 | 
			
		||||
 | 
			
		||||
    assert "is an invalid spec" in str(exc_err.value)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.regression("10246")
 | 
			
		||||
@pytest.mark.parametrize(
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,7 @@
 | 
			
		||||
 | 
			
		||||
import spack.error
 | 
			
		||||
import spack.package_base
 | 
			
		||||
import spack.parser
 | 
			
		||||
import spack.repo
 | 
			
		||||
import spack.util.hash as hashutil
 | 
			
		||||
from spack.dependency import Dependency, all_deptypes, canonical_deptype
 | 
			
		||||
@@ -961,7 +962,7 @@ def test_canonical_deptype(self):
 | 
			
		||||
 | 
			
		||||
    def test_invalid_literal_spec(self):
 | 
			
		||||
        # Can't give type 'build' to a top-level spec
 | 
			
		||||
        with pytest.raises(spack.spec.SpecParseError):
 | 
			
		||||
        with pytest.raises(spack.parser.SpecSyntaxError):
 | 
			
		||||
            Spec.from_literal({"foo:build": None})
 | 
			
		||||
 | 
			
		||||
        # Can't use more than one ':' separator
 | 
			
		||||
 
 | 
			
		||||
@@ -707,13 +707,9 @@ def test_constrain_dependency_not_changed(self):
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_exceptional_paths_for_constructor(self):
 | 
			
		||||
 | 
			
		||||
        with pytest.raises(TypeError):
 | 
			
		||||
            Spec((1, 2))
 | 
			
		||||
 | 
			
		||||
        with pytest.raises(ValueError):
 | 
			
		||||
            Spec("")
 | 
			
		||||
 | 
			
		||||
        with pytest.raises(ValueError):
 | 
			
		||||
            Spec("libelf foo")
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -937,7 +937,7 @@ def __init__(self, vlist=None):
 | 
			
		||||
        self.versions = []
 | 
			
		||||
        if vlist is not None:
 | 
			
		||||
            if isinstance(vlist, str):
 | 
			
		||||
                vlist = _string_to_version(vlist)
 | 
			
		||||
                vlist = from_string(vlist)
 | 
			
		||||
                if type(vlist) == VersionList:
 | 
			
		||||
                    self.versions = vlist.versions
 | 
			
		||||
                else:
 | 
			
		||||
@@ -1165,7 +1165,7 @@ def __repr__(self):
 | 
			
		||||
        return str(self.versions)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _string_to_version(string):
 | 
			
		||||
def from_string(string):
 | 
			
		||||
    """Converts a string to a Version, VersionList, or VersionRange.
 | 
			
		||||
    This is private.  Client code should use ver().
 | 
			
		||||
    """
 | 
			
		||||
@@ -1191,9 +1191,9 @@ def ver(obj):
 | 
			
		||||
    if isinstance(obj, (list, tuple)):
 | 
			
		||||
        return VersionList(obj)
 | 
			
		||||
    elif isinstance(obj, str):
 | 
			
		||||
        return _string_to_version(obj)
 | 
			
		||||
        return from_string(obj)
 | 
			
		||||
    elif isinstance(obj, (int, float)):
 | 
			
		||||
        return _string_to_version(str(obj))
 | 
			
		||||
        return from_string(str(obj))
 | 
			
		||||
    elif type(obj) in (VersionBase, GitVersion, VersionRange, VersionList):
 | 
			
		||||
        return obj
 | 
			
		||||
    else:
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user