Skip to content

Commit c3f52f0

Browse files
committed
Initial implementation of package specs, including parser.
spec.py can parse full dependence specs like this: openmpi@1.4.3:1.4.5%intel+debug ^hwloc@1.2 These will be used to specify how to install packages and their dependencies, as well as to specify restrictions (e.g., on particular versions) for dependencies. e.g.: class SomePackage(Package): depends_on('boost@1.46,1.49') This would require either of those two versions of boost. This also moves depends_on out to relations.py and adds "provides", which will allow packages to provide virtual dependences. This is just initial implementation of the parsing and objects to represent specs. They're not integrated with packages yet.
1 parent 5dd2c53 commit c3f52f0

File tree

10 files changed

+821
-76
lines changed

10 files changed

+821
-76
lines changed

lib/spack/spack/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22
from utils import *
33
from error import *
44

5-
from package import Package, depends_on
5+
from package import Package
6+
from relations import depends_on, provides
67
from multi_function import platform

lib/spack/spack/dependency.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
"""
2+
This file defines the dependence relation in spack.
3+
4+
"""
5+
6+
import packages
7+
8+
9+
class Dependency(object):
10+
"""Represents a dependency from one package to another.
11+
"""
12+
def __init__(self, name, version):
13+
self.name = name
14+
15+
@property
16+
def package(self):
17+
return packages.get(self.name)
18+
19+
def __str__(self):
20+
return "<dep: %s>" % self.name

lib/spack/spack/package.py

Lines changed: 15 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727

2828
from multi_function import platform
2929
from stage import Stage
30+
from dependency import *
3031

3132

3233
class Package(object):
@@ -228,14 +229,24 @@ class SomePackage(Package):
228229
clean() (some of them do this), and others to provide custom behavior.
229230
"""
230231

231-
def __init__(self, sys_type=arch.sys_type()):
232+
"""By default a package has no dependencies."""
233+
dependencies = []
234+
235+
"""By default we build in parallel. Subclasses can override this."""
236+
parallel = True
237+
238+
"""Remove tarball and build by default. If this is true, leave them."""
239+
dirty = False
240+
241+
"""Controls whether install and uninstall check deps before running."""
242+
ignore_dependencies = False
243+
244+
def __init__(self, sys_type = arch.sys_type()):
245+
# Check for attributes that derived classes must set.
232246
attr.required(self, 'homepage')
233247
attr.required(self, 'url')
234248
attr.required(self, 'md5')
235249

236-
attr.setdefault(self, 'dependencies', [])
237-
attr.setdefault(self, 'parallel', True)
238-
239250
# Architecture for this package.
240251
self.sys_type = sys_type
241252

@@ -258,15 +269,9 @@ def __init__(self, sys_type=arch.sys_type()):
258269
# This adds a bunch of convenience commands to the package's module scope.
259270
self.add_commands_to_module()
260271

261-
# Controls whether install and uninstall check deps before acting.
262-
self.ignore_dependencies = False
263-
264272
# Empty at first; only compute dependents if necessary
265273
self._dependents = None
266274

267-
# Whether to remove intermediate build/install when things go wrong.
268-
self.dirty = False
269-
270275
# stage used to build this package.
271276
self.stage = Stage(self.stage_name, self.url)
272277

@@ -569,37 +574,6 @@ def do_clean_dist(self):
569574
tty.msg("Successfully cleaned %s" % self.name)
570575

571576

572-
class Dependency(object):
573-
"""Represents a dependency from one package to another."""
574-
def __init__(self, name, **kwargs):
575-
self.name = name
576-
for key in kwargs:
577-
setattr(self, key, kwargs[key])
578-
579-
@property
580-
def package(self):
581-
return packages.get(self.name)
582-
583-
def __repr__(self):
584-
return "<dep: %s>" % self.name
585-
586-
def __str__(self):
587-
return self.__repr__()
588-
589-
590-
def depends_on(*args, **kwargs):
591-
"""Adds a dependencies local variable in the locals of
592-
the calling class, based on args.
593-
"""
594-
# This gets the calling frame so we can pop variables into it
595-
locals = sys._getframe(1).f_locals
596-
597-
# Put deps into the dependencies variable
598-
dependencies = locals.setdefault("dependencies", [])
599-
for name in args:
600-
dependencies.append(Dependency(name))
601-
602-
603577
class MakeExecutable(Executable):
604578
"""Special Executable for make so the user can specify parallel or
605579
not on a per-invocation basis. Using 'parallel' as a kwarg will

lib/spack/spack/packages/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
import spack.attr as attr
1313
import spack.error as serr
1414

15-
# Valid package names
16-
valid_package = r'^[a-zA-Z0-9_-]*$'
15+
# Valid package names -- can contain - but can't start with it.
16+
valid_package = r'^\w[\w-]*$'
1717

1818
# Don't allow consecutive [_-] in package names
1919
invalid_package = r'[_-][_-]+'

lib/spack/spack/parse.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import re
2+
import spack.error as err
3+
import itertools
4+
5+
class UnlexableInputError(err.SpackError):
6+
"""Raised when we don't know how to lex something."""
7+
def __init__(self, message):
8+
super(UnlexableInputError, self).__init__(message)
9+
10+
11+
class ParseError(err.SpackError):
12+
"""Raised when we don't hit an error while parsing."""
13+
def __init__(self, message):
14+
super(ParseError, self).__init__(message)
15+
16+
17+
class Token:
18+
"""Represents tokens; generated from input by lexer and fed to parse()."""
19+
def __init__(self, type, value=''):
20+
self.type = type
21+
self.value = value
22+
23+
def __repr__(self):
24+
return str(self)
25+
26+
def __str__(self):
27+
return "'%s'" % self.value
28+
29+
def is_a(self, type):
30+
return self.type == type
31+
32+
def __cmp__(self, other):
33+
return cmp((self.type, self.value),
34+
(other.type, other.value))
35+
36+
class Lexer(object):
37+
"""Base class for Lexers that keep track of line numbers."""
38+
def __init__(self, lexicon):
39+
self.scanner = re.Scanner(lexicon)
40+
41+
def token(self, type, value=''):
42+
return Token(type, value)
43+
44+
def lex(self, text):
45+
tokens, remainder = self.scanner.scan(text)
46+
if remainder:
47+
raise UnlexableInputError("Unlexable input:\n%s\n" % remainder)
48+
return tokens
49+
50+
51+
class Parser(object):
52+
"""Base class for simple recursive descent parsers."""
53+
def __init__(self, lexer):
54+
self.tokens = iter([]) # iterators over tokens, handled in order. Starts empty.
55+
self.token = None # last accepted token
56+
self.next = None # next token
57+
self.lexer = lexer
58+
59+
def gettok(self):
60+
"""Puts the next token in the input stream into self.next."""
61+
try:
62+
self.next = self.tokens.next()
63+
except StopIteration:
64+
self.next = None
65+
66+
def push_tokens(self, iterable):
67+
"""Adds all tokens in some iterable to the token stream."""
68+
self.tokens = itertools.chain(iter(iterable), iter([self.next]), self.tokens)
69+
self.gettok()
70+
71+
def accept(self, id):
72+
"""Puts the next symbol in self.token if we like it. Then calls gettok()"""
73+
if self.next and self.next.is_a(id):
74+
self.token = self.next
75+
self.gettok()
76+
return True
77+
return False
78+
79+
def unexpected_token(self):
80+
raise ParseError("Unexpected token: %s." % self.next)
81+
82+
def expect(self, id):
83+
"""Like accept(), but fails if we don't like the next token."""
84+
if self.accept(id):
85+
return True
86+
else:
87+
if self.next:
88+
self.unexpected_token()
89+
else:
90+
raise ParseError("Unexpected end of file.")
91+
sys.exit(1)
92+
93+
def parse(self, text):
94+
self.push_tokens(self.lexer.lex(text))
95+
return self.do_parse()

lib/spack/spack/relations.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
"""
2+
This package contains relationships that can be defined among packages.
3+
Relations are functions that can be called inside a package definition,
4+
for example:
5+
6+
class OpenMPI(Package):
7+
depends_on("hwloc")
8+
provides("mpi")
9+
...
10+
11+
The available relations are:
12+
13+
depends_on
14+
Above, the OpenMPI package declares that it "depends on" hwloc. This means
15+
that the hwloc package needs to be installed before OpenMPI can be
16+
installed. When a user runs 'spack install openmpi', spack will fetch
17+
hwloc and install it first.
18+
19+
provides
20+
This is useful when more than one package can satisfy a dependence. Above,
21+
OpenMPI declares that it "provides" mpi. Other implementations of the MPI
22+
interface, like mvapich and mpich, also provide mpi, e.g.:
23+
24+
class Mvapich(Package):
25+
provides("mpi")
26+
...
27+
28+
class Mpich(Package):
29+
provides("mpi")
30+
...
31+
32+
Instead of depending on openmpi, mvapich, or mpich, another package can
33+
declare that it depends on "mpi":
34+
35+
class Mpileaks(Package):
36+
depends_on("mpi")
37+
...
38+
39+
Now the user can pick which MPI they would like to build with when they
40+
install mpileaks. For example, the user could install 3 instances of
41+
mpileaks, one for each MPI version, by issuing these three commands:
42+
43+
spack install mpileaks ^openmpi
44+
spack install mpileaks ^mvapich
45+
spack install mpileaks ^mpich
46+
"""
47+
from dependency import Dependency
48+
49+
50+
def depends_on(*args):
51+
"""Adds a dependencies local variable in the locals of
52+
the calling class, based on args.
53+
"""
54+
# Get the enclosing package's scope and add deps to it.
55+
locals = sys._getframe(1).f_locals
56+
dependencies = locals.setdefault("dependencies", [])
57+
for name in args:
58+
dependencies.append(Dependency(name))
59+
60+
61+
def provides(*args):
62+
"""Allows packages to provide a virtual dependency. If a package provides
63+
"mpi", other packages can declare that they depend on "mpi", and spack
64+
can use the providing package to satisfy the dependency.
65+
"""
66+
# Get the enclosing package's scope and add deps to it.
67+
locals = sys._getframe(1).f_locals
68+
provides = locals.setdefault("provides", [])
69+
for name in args:
70+
provides.append(name)

0 commit comments

Comments
 (0)