diff options
-rw-r--r-- | lib/spack/spack/solver/asp.py | 149 | ||||
-rw-r--r-- | lib/spack/spack/solver/concretize.lp | 36 | ||||
-rw-r--r-- | lib/spack/spack/solver/display.lp | 1 | ||||
-rw-r--r-- | lib/spack/spack/test/concretize.py | 20 |
4 files changed, 178 insertions, 28 deletions
diff --git a/lib/spack/spack/solver/asp.py b/lib/spack/spack/solver/asp.py index 0f4eccb358..597a4bff04 100644 --- a/lib/spack/spack/solver/asp.py +++ b/lib/spack/spack/solver/asp.py @@ -401,16 +401,18 @@ class ClingoDriver(object): return result -def _normalize_body(body): +def _normalize(body): """Accept an AspAnd object or a single Symbol and return a list of symbols. """ if isinstance(body, AspAnd): - args = [f.symbol() for f in body.args] + args = [getattr(f, 'symbol', lambda: f)() for f in body.args] elif isinstance(body, clingo.Symbol): args = [body] + elif hasattr(body, 'symbol'): + args = [body.symbol()] else: - raise TypeError("Invalid typee for rule body: ", type(body)) + raise TypeError("Invalid typee: ", type(body)) return args @@ -475,37 +477,64 @@ class PyclingoDriver(object): def rule(self, head, body): """ASP rule (an implication).""" - args = _normalize_body(body) + head_symbols = _normalize(head) + body_symbols = _normalize(body) - symbols = [head.symbol()] + args + symbols = head_symbols + body_symbols atoms = {} for s in symbols: atoms[s] = self.backend.add_atom(s) # Special assumption atom to allow rules to be in unsat cores - rule_str = "%s :- %s." % ( - head.symbol(), ",".join(str(a) for a in args)) + head_str = ",".join(str(a) for a in head_symbols) + body_str = ",".join(str(a) for a in body_symbols) + rule_str = "%s :- %s." % (head_str, body_str) rule_atoms = self._register_rule_for_cores(rule_str) # print rule before adding self.out.write("%s\n" % rule_str) self.backend.add_rule( - [atoms[head.symbol()]], - [atoms[s] for s in args] + rule_atoms + [atoms[s] for s in head_symbols], + [atoms[s] for s in body_symbols] + rule_atoms ) - def integrity_constraint(self, body): - symbols, atoms = _normalize_body(body), {} - for s in symbols: + def integrity_constraint(self, clauses, default_negated=None): + """Add an integrity constraint to the solver. + + Args: + clauses: clauses to be added to the integrity constraint + default_negated: clauses to be added to the integrity + constraint after with a default negation + """ + symbols, negated_symbols, atoms = _normalize(clauses), [], {} + if default_negated: + negated_symbols = _normalize(default_negated) + + for s in symbols + negated_symbols: atoms[s] = self.backend.add_atom(s) - rule_str = ":- {0}.".format(",".join(str(a) for a in symbols)) + symbols_str = ",".join(str(a) for a in symbols) + if negated_symbols: + negated_symbols_str = ",".join( + "not " + str(a) for a in negated_symbols + ) + symbols_str += ",{0}".format(negated_symbols_str) + rule_str = ":- {0}.".format(symbols_str) rule_atoms = self._register_rule_for_cores(rule_str) # print rule before adding self.out.write("{0}\n".format(rule_str)) - self.backend.add_rule([], [atoms[s] for s in symbols] + rule_atoms) + self.backend.add_rule( + [], + [atoms[s] for s in symbols] + + [-atoms[s] for s in negated_symbols] + + rule_atoms + ) + + def iff(self, expr1, expr2): + self.rule(head=expr1, body=expr2) + self.rule(head=expr2, body=expr1) def one_of_iff(self, head, versions): self.out.write("%s :- %s.\n" % (head, AspOneOf(*versions))) @@ -575,11 +604,15 @@ class PyclingoDriver(object): # once done, construct the solve result result.satisfiable = solve_result.satisfiable + + def stringify(x): + return x.string or str(x) + if result.satisfiable: builder = SpecBuilder(specs) min_cost, best_model = min(models) tuples = [ - (sym.name, [a.string for a in sym.arguments]) + (sym.name, [stringify(a) for a in sym.arguments]) for sym in best_model ] answers = builder.build_specs(tuples) @@ -703,8 +736,11 @@ class SpackSolverSetup(object): 'node_compiler_hard', 'node_compiler_version_satisfies' ] clauses = [x for x in clauses if x.name not in to_be_filtered] + external = fn.external(pkg.name) - self.gen.integrity_constraint(AspAnd(*clauses)) + self.gen.integrity_constraint( + AspAnd(*clauses), AspAnd(external) + ) def available_compilers(self): """Facts about available compilers.""" @@ -868,6 +904,71 @@ class SpackSolverSetup(object): fn.default_provider_preference(v, p, i)) ) + def external_packages(self): + """Facts on external packages, as read from packages.yaml""" + packages_yaml = spack.config.get("packages") + self.gen.h1('External packages') + for pkg_name, data in packages_yaml.items(): + if pkg_name == 'all': + continue + + if 'externals' not in data: + self.gen.fact(fn.external(pkg_name).symbol(positive=False)) + + self.gen.h2('External package: {0}'.format(pkg_name)) + # Check if the external package is buildable. If it is + # not then "external(<pkg>)" is a fact. + external_buildable = data.get('buildable', True) + if not external_buildable: + self.gen.fact(fn.external_only(pkg_name)) + + # Read a list of all the specs for this package + externals = data['externals'] + external_specs = [spack.spec.Spec(x['spec']) for x in externals] + + # Compute versions with appropriate weights + external_versions = [ + (x.version, id) for id, x in enumerate(external_specs) + ] + external_versions = [ + (v, -(w + 1), id) + for w, (v, id) in enumerate(sorted(external_versions)) + ] + for version, weight, id in external_versions: + self.gen.fact(fn.external_version_declared( + pkg_name, str(version), weight, id + )) + + # Establish an equivalence between "external_spec(pkg, id)" + # and the clauses of that spec, so that we have a uniform + # way to identify it + spec_id_list = [] + for id, spec in enumerate(external_specs): + self.gen.newline() + spec_id = fn.external_spec(pkg_name, id) + clauses = self.spec_clauses(spec, body=True) + # This is an iff below, wish it could be written in a + # more compact form + self.gen.rule(head=spec_id.symbol(), body=AspAnd(*clauses)) + for clause in clauses: + self.gen.rule(clause, spec_id.symbol()) + spec_id_list.append(spec_id) + + # If one of the external specs is selected then the package + # is external and viceversa + # TODO: make it possible to declare the rule like below + # self.gen.iff(expr1=fn.external(pkg_name), + # expr2=one_of_the_externals) + self.gen.newline() + # FIXME: self.gen.one_of_iff(fn.external(pkg_name), spec_id_list) + one_of_the_externals = self.gen.one_of(*spec_id_list) + external_str = fn.external(pkg_name) + external_rule = "{0} :- {1}.\n{1} :- {0}.\n".format( + external_str, str(one_of_the_externals) + ) + self.gen.out.write(external_rule) + self.gen.control.add("base", [], external_rule) + def flag_defaults(self): self.gen.h2("Compiler flag defaults") @@ -965,8 +1066,6 @@ class SpackSolverSetup(object): self.gen.fact(f.node_flag(spec.name, flag_type, flag)) # TODO - # external_path - # external_module # namespace return clauses @@ -1204,6 +1303,7 @@ class SpackSolverSetup(object): self.virtual_providers() self.provider_defaults() + self.external_packages() self.flag_defaults() self.gen.h1('Package Constraints') @@ -1291,6 +1391,18 @@ class SpecBuilder(object): def no_flags(self, pkg, flag_type): self._specs[pkg].compiler_flags[flag_type] = [] + def external_spec(self, pkg, idx): + """This means that the external spec and index idx + has been selected for this package. + """ + packages_yaml = spack.config.get('packages') + spec_info = packages_yaml[pkg]['externals'][int(idx)] + self._specs[pkg].external_path = spec_info.get('prefix', None) + self._specs[pkg].external_modules = spec_info.get('modules', []) + self._specs[pkg].extra_attributes = spec_info.get( + 'extra_attributes', {} + ) + def depends_on(self, pkg, dep, type): dependency = self._specs[pkg]._dependencies.get(dep) if not dependency: @@ -1364,7 +1476,6 @@ class SpecBuilder(object): # print out unknown actions so we can display them for debugging if not action: print("%s(%s)" % (name, ", ".join(str(a) for a in args))) - print(" ", args) continue assert action and callable(action) diff --git a/lib/spack/spack/solver/concretize.lp b/lib/spack/spack/solver/concretize.lp index 0fb9353819..dec14cde39 100644 --- a/lib/spack/spack/solver/concretize.lp +++ b/lib/spack/spack/solver/concretize.lp @@ -25,18 +25,23 @@ version_weight(Package, Weight) % Dependencies of any type imply that one package "depends on" another depends_on(Package, Dependency) :- depends_on(Package, Dependency, _). -% declared dependencies are real if they're not virtual +% declared dependencies are real if they're not virtual AND +% the package is not an external depends_on(Package, Dependency, Type) - :- declared_dependency(Package, Dependency, Type), not virtual(Dependency), - node(Package). + :- declared_dependency(Package, Dependency, Type), + node(Package), + not virtual(Dependency), + not external(Package). -% if you declare a dependency on a virtual, you depend on one of its providers +% if you declare a dependency on a virtual AND the package is not an external, +% you depend on one of its providers 1 { depends_on(Package, Provider, Type) : provides_virtual(Provider, Virtual) } 1 :- declared_dependency(Package, Virtual, Type), virtual(Virtual), + not external(Package), node(Package). % if a virtual was required by some root spec, one provider is in the DAG @@ -85,11 +90,34 @@ node(Dependency) :- node(Package), depends_on(Package, Dependency). #defined virtual/1. #defined virtual_node/1. #defined provides_virtual/2. +#defined external/1. +#defined external_spec/2. +#defined external_version_declared/4. +#defined external_only/1. #defined pkg_provider_preference/4. #defined default_provider_preference/3. #defined root/1. %----------------------------------------------------------------------------- +% External semantics +%----------------------------------------------------------------------------- + +% if an external version is declared, it is also declared globally +version_declared(Package, Version, Weight) :- external_version_declared(Package, Version, Weight, _). + +% if a package is external its version must be one of the external versions +1 { version(Package, Version): external_version_declared(Package, Version, _, _) } 1 :- external(Package). + +% if a package is not buildable (external_only), only externals are allowed +external(Package) :- external_only(Package), node(Package). + +% if an external version is selected, the package is external and +% we are using the corresponding spec +external(Package), external_spec(Package, ID) :- + version(Package, Version), version_weight(Package, Weight), + external_version_declared(Package, Version, Weight, ID). + +%----------------------------------------------------------------------------- % Variant semantics %----------------------------------------------------------------------------- % one variant value for single-valued variants. diff --git a/lib/spack/spack/solver/display.lp b/lib/spack/spack/solver/display.lp index 6778c2fbe0..40f6acdb05 100644 --- a/lib/spack/spack/solver/display.lp +++ b/lib/spack/spack/solver/display.lp @@ -26,3 +26,4 @@ #show compiler_weight/2. #show node_target_match/2. #show node_target_weight/2. +#show external_spec/2. diff --git a/lib/spack/spack/test/concretize.py b/lib/spack/spack/test/concretize.py index 4b087f8ad8..da7a5b5a52 100644 --- a/lib/spack/spack/test/concretize.py +++ b/lib/spack/spack/test/concretize.py @@ -301,7 +301,7 @@ class TestConcretize(object): provides one. """ s = Spec('hypre ^openblas-with-lapack ^netlib-lapack') - with pytest.raises(spack.spec.MultipleProviderError): + with pytest.raises(spack.error.SpackError): s.concretize() def test_no_matching_compiler_specs(self, mock_low_high_config): @@ -500,10 +500,20 @@ class TestConcretize(object): s.concretize() assert not s.concrete - def test_no_conflixt_in_external_specs(self, conflict_spec): - # clear deps because external specs cannot depend on anything - ext = Spec(conflict_spec).copy(deps=False) - ext.external_path = '/fake/path' + @pytest.mark.parametrize('spec_str', [ + 'conflict@10.0%clang+foo' + ]) + def test_no_conflict_in_external_specs(self, spec_str): + # Modify the configuration to have the spec with conflict + # registered as an external + ext = Spec(spec_str) + data = { + 'externals': [ + {'spec': spec_str, + 'prefix': '/fake/path'} + ] + } + spack.config.set("packages::{0}".format(ext.name), data) ext.concretize() # failure raises exception def test_regression_issue_4492(self): |