concretizer: rework operating system semantics for installed packages

The OS logic in the concretizer is still the way it was in the first version.
Defaults are implemented in a fairly inflexible way using straight logic. Most
of the other sections have been reworked to leave these kinds of decisions to
optimization. This commit does that for OS's as well.

As with targets, we optimize for target matches. We also try to optimize for
OS matches between nodes. Additionally, this commit adds the notion of
"OS compatibility" where we allow for builds to depend on binaries for certain
other OS's. e.g, for macos, a bigsur build can depend on an already installed
(concrete) catalina build. One cool thing about this is that we can declare
additional compatible OS's later, e.g. CentOS and RHEL.
This commit is contained in:
Todd Gamblin 2021-08-08 12:37:33 -07:00
parent 9c70d51a4f
commit 2c142f9dd4
2 changed files with 104 additions and 64 deletions

View file

@ -568,6 +568,7 @@ def __init__(self):
self.possible_virtuals = None
self.possible_compilers = []
self.possible_oses = set()
self.variant_values_from_specs = set()
self.version_constraints = set()
self.target_constraints = set()
@ -1234,18 +1235,30 @@ def os_defaults(self, specs):
platform = spack.platforms.host()
# create set of OS's to consider
possible = set([
buildable = set([
platform.front_os, platform.back_os, platform.default_os])
for spec in specs:
if spec.architecture and spec.architecture.os:
possible.add(spec.architecture.os)
# TODO: does this make sense?
buildable.add(spec.architecture.os)
# make directives for possible OS's
for possible_os in sorted(possible):
self.gen.fact(fn.os(possible_os))
# make directives for buildable OS's
for build_os in sorted(buildable):
self.gen.fact(fn.buildable_os(build_os))
# mark this one as default
self.gen.fact(fn.node_os_default(platform.default_os))
def keyfun(os):
return (
os == platform.default_os, # prefer default
os not in buildable, # then prefer buildables
os, # then sort by name
)
all_oses = buildable.union(self.possible_oses)
ordered_oses = sorted(all_oses, key=keyfun, reverse=True)
# output the preference order of OS's for the concretizer to choose
for i, os_name in enumerate(ordered_oses):
self.gen.fact(fn.os(os_name, i))
def target_defaults(self, specs):
"""Add facts about targets and target compatibility."""
@ -1508,6 +1521,9 @@ def define_installed_packages(self, possible):
self.impose(h, spec, body=True)
self.gen.newline()
# add OS to possible OS's
self.possible_oses.add(spec.os)
def setup(self, driver, specs, tests=False, reuse=False):
"""Generate an ASP program with relevant constraints for specs.
@ -1552,6 +1568,10 @@ def setup(self, driver, specs, tests=False, reuse=False):
# traverse all specs and packages to build dict of possible versions
self.build_version_dict(possible, specs)
if reuse:
self.gen.h1("Installed packages")
self.define_installed_packages(possible)
self.gen.h1('General Constraints')
self.available_compilers()
self.compiler_defaults()
@ -1610,10 +1630,6 @@ def setup(self, driver, specs, tests=False, reuse=False):
self.gen.h1("Target Constraints")
self.define_target_constraints()
if reuse:
self.gen.h1("Installed packages")
self.define_installed_packages(possible)
class SpecBuilder(object):
"""Class with actions to rebuild a spec from ASP results."""

View file

@ -529,29 +529,43 @@ node_platform_set(Package) :- node_platform_set(Package, _).
%-----------------------------------------------------------------------------
% OS semantics
%-----------------------------------------------------------------------------
% convert weighted OS declarations to simple one
os(OS) :- os(OS, _).
% one os per node
1 { node_os(Package, OS) : os(OS) } 1 :- node(Package), error("Each node must have exactly one OS").
% node_os_set implies that the node must have that os
node_os(Package, OS) :- node(Package), node_os_set(Package, OS).
node_os_set(Package) :- node_os_set(Package, _).
% can't have a non-buildable OS on a node we need to build
:- build(Package), node_os(Package, OS), not buildable_os(OS).
% inherit OS along dependencies
node_os_inherit(Package, OS) :- node_os_set(Package, OS).
node_os_inherit(Dependency, OS)
:- node_os_inherit(Package, OS), depends_on(Package, Dependency),
not node_os_set(Dependency).
node_os_inherit(Package) :- node_os_inherit(Package, _).
% can't have dependencies on incompatible OS's
:- depends_on(Package, Dependency),
node_os(Package, PackageOS),
node_os(Dependency, DependencyOS),
not os_compatible(PackageOS, DependencyOS),
build(Package).
node_os(Package, OS) :- node_os_inherit(Package, OS).
% fall back to default if not set or inherited
node_os(Package, OS)
% give OS choice weights according to os declarations
node_os_weight(Package, Weight)
:- node(Package),
not node_os_set(Package), not node_os_inherit(Package),
node_os_default(OS).
node_os(Package, OS),
os(OS, Weight).
% match semantics for OS's
node_os_match(Package, Dependency) :-
depends_on(Package, Dependency), node_os(Package, OS), node_os(Dependency, OS).
node_os_mismatch(Package, Dependency) :-
depends_on(Package, Dependency), not node_os_match(Package, Dependency).
% every OS is compatible with itself. We can use `os_compatible` to declare
os_compatible(OS, OS) :- os(OS).
% OS compatibility rules for reusing solves.
% catalina binaries can be used on bigsur. Direction is package -> dependency.
os_compatible("bigsur", "catalina").
#defined node_os_set/2.
#defined os_compatible/2.
%-----------------------------------------------------------------------------
% Target semantics
@ -770,54 +784,54 @@ build(Package) :- not hash(Package, _), node(Package).
% is displayed (clingo doesn't display sums over empty sets by default)
% Try hard to reuse installed packages (i.e., minimize the number built)
opt_criterion(17, "number of packages to build (vs. reuse)").
#minimize { 0@17 : #true }.
#minimize { 1@17,Package : build(Package) }.
opt_criterion(16, "number of packages to build (vs. reuse)").
#minimize { 0@16: #true }.
#minimize { 1@16,Package : build(Package) }.
% Minimize the number of deprecated versions being used
opt_criterion(16, "deprecated versions used").
#minimize{ 0@16 : #true }.
#minimize{ 1@16,Package : deprecated(Package, _)}.
opt_criterion(15, "deprecated versions used").
#minimize{ 0@15: #true }.
#minimize{ 1@15,Package : deprecated(Package, _)}.
% Minimize the:
% 1. Version weight
% 2. Number of variants with a non default value, if not set
% for the root(Package)
opt_criterion(15, "version weight").
#minimize{ 0@15 : #true }.
#minimize { Weight@15 : root(Package),version_weight(Package, Weight) }.
opt_criterion(14, "number of non-default variants (roots)").
opt_criterion(14, "version weight").
#minimize{ 0@14: #true }.
#minimize { Weight@14: root(Package),version_weight(Package, Weight) }.
opt_criterion(13, "number of non-default variants (roots)").
#minimize{ 0@13: #true }.
#minimize {
Weight@14,Package,Variant,Value
Weight@13,Package,Variant,Value
: variant_not_default(Package, Variant, Value, Weight), root(Package)
}.
opt_criterion(13, "preferred providers for roots").
#minimize{ 0@13 : #true }.
opt_criterion(12, "preferred providers for roots").
#minimize{ 0@12: #true }.
#minimize{
Weight@13,Provider,Virtual
Weight@12,Provider,Virtual
: provider_weight(Provider, Virtual, Weight), root(Provider)
}.
% If the value is a multivalued variant there could be multiple
% values set as default. Since a default value has a weight of 0 we
% need to maximize their number below to ensure they're all set
opt_criterion(12, "number of values in multi valued variants (root)").
#minimize{ 0@12 : #true }.
opt_criterion(11, "number of values in multi-valued variants (root)").
#minimize{ 0@11: #true }.
#maximize {
1@12,Package,Variant,Value
1@11,Package,Variant,Value
: variant_not_default(Package, Variant, Value, Weight),
not variant_single_value(Package, Variant),
root(Package)
}.
% Try to use default variants or variants that have been set
opt_criterion(11, "number of non-default variants (non-roots)").
#minimize{ 0@11 : #true }.
opt_criterion(10, "number of non-default variants (non-roots)").
#minimize{ 0@10: #true }.
#minimize {
Weight@11,Package,Variant,Value
Weight@10,Package,Variant,Value
: variant_not_default(Package, Variant, Value, Weight), not root(Package)
}.
@ -835,36 +849,46 @@ opt_criterion(8, "compiler mismatches").
#minimize{ 0@8: #true }.
#minimize{ 1@8,Package,Dependency : compiler_mismatch(Package, Dependency) }.
% Choose more recent versions for nodes
opt_criterion(7, "version badness").
% Try to minimize the number of compiler mismatches in the DAG.
opt_criterion(7, "OS mismatches").
#minimize{ 0@7: #true }.
#minimize{ 1@7,Package,Dependency : node_os_mismatch(Package, Dependency) }.
opt_criterion(6, "non-preferred OSes").
#minimize{ 0@6: #true }.
#minimize{ Weight@6,Package : node_os_weight(Package, Weight) }.
% Choose more recent versions for nodes
opt_criterion(5, "version badness").
#minimize{ 0@5: #true }.
#minimize{
Weight@7,Package : version_weight(Package, Weight)
Weight@5,Package : version_weight(Package, Weight)
}.
% If the value is a multivalued variant there could be multiple
% values set as default. Since a default value has a weight of 0 we
% need to maximize their number below to ensure they're all set
opt_criterion(6, "number of values in multi valued variants (non-root)").
#minimize{ 0@6 : #true }.
opt_criterion(4, "number of values in multi valued variants (non-root)").
#minimize{ 0@4: #true }.
#maximize {
1@6,Package,Variant,Value
1@4,Package,Variant,Value
: variant_not_default(Package, Variant, Value, _),
not variant_single_value(Package, Variant),
not root(Package)
}.
% Try to use preferred compilers
opt_criterion(5, "non-preferred compilers").
#minimize{ 0@5 : #true }.
#minimize{ Weight@5,Package : compiler_weight(Package, Weight) }.
opt_criterion(3, "non-preferred compilers").
#minimize{ 0@3: #true }.
#minimize{ Weight@3,Package : compiler_weight(Package, Weight) }.
% Minimize the number of mismatches for targets in the DAG, try
% to select the preferred target.
opt_criterion(4, "target mismatches").
#minimize{ 0@4 : #true }.
#minimize{ 1@4,Package,Dependency : node_target_mismatch(Package, Dependency) }.
opt_criterion(2, "target mismatches").
#minimize{ 0@2: #true }.
#minimize{ 1@2,Package,Dependency : node_target_mismatch(Package, Dependency) }.
opt_criterion(1, "non-preferred targets").
#minimize{ 0@1: #true }.
#minimize{ Weight@1,Package : node_target_weight(Package, Weight) }.
opt_criterion(3, "non-preferred targets").
#minimize{ 0@3 : #true }.
#minimize{ Weight@3,Package : node_target_weight(Package, Weight) }.