2 Chooses a set of components to make a running program.
5 # Copyright (C) 2009, Thomas Leonard
6 # See the README file for details, or visit http://0install.net.
8 from zeroinstall
import _
9 import os
, tempfile
, subprocess
, sys
10 from logging
import debug
, warn
, info
12 from zeroinstall
.zerostore
import BadDigest
, NotStored
14 from zeroinstall
.injector
.arch
import machine_groups
15 from zeroinstall
.injector
import model
, sat
17 def _get_cached(stores
, impl
):
18 """Check whether an implementation is available locally.
19 @type impl: model.Implementation
22 if isinstance(impl
, model
.DistributionImplementation
):
25 return os
.path
.exists(impl
.local_path
)
29 warn("No digests given for %s!", impl
)
31 path
= stores
.lookup_any(impl
.digests
)
40 def __init__(self
, iface
, impl
, arch
):
46 return str(self
.impl
.feed
.get_name() + "-" + self
.impl
.get_version())
49 """Chooses a set of implementations to satisfy the requirements of a program and its user.
51 1. Create a Solver object and configure it
53 3. If any of the returned feeds_used are stale or missing, you may like to start downloading them
54 4. If it is 'ready' then you can download and run the chosen versions.
55 @ivar selections: the chosen implementation of each interface
56 @type selections: {L{model.Interface}: Implementation}
57 @ivar requires: the selected dependencies for each chosen version
58 @type requires: {L{model.Interface}: [L{model.Dependency}]}
59 @ivar feeds_used: the feeds which contributed to the choice in L{selections}
60 @type feeds_used: set(str)
61 @ivar record_details: whether to record information about unselected implementations
62 @type record_details: {L{Interface}: [(L{Implementation}, str)]}
63 @ivar details: extra information, if record_details mode was used
64 @type details: {str: [(Implementation, comment)]}
66 __slots__
= ['selections', 'requires', 'feeds_used', 'details', 'record_details', 'ready']
69 self
.selections
= self
.requires
= self
.feeds_used
= self
.details
= None
70 self
.record_details
= False
73 def solve(self
, root_interface
, arch
):
74 """Get the best implementation of root_interface and all of its dependencies.
75 @param root_interface: the URI of the program to be solved
76 @type root_interface: str
77 @param arch: the desired target architecture
78 @type arch: L{arch.Architecture}
79 @postcondition: self.ready, self.selections and self.feeds_used are updated"""
80 raise NotImplementedError("Abstract")
82 class SATSolver(Solver
):
83 """Converts the problem to a set of pseudo-boolean constraints and uses a PB solver to solve them."""
84 def __init__(self
, network_use
, iface_cache
, stores
, extra_restrictions
= None):
86 @param network_use: how much use to make of the network
87 @type network_use: L{model.network_levels}
88 @param iface_cache: a cache of feeds containing information about available versions
89 @type iface_cache: L{iface_cache.IfaceCache}
90 @param stores: a cached of implementations (affects choice when offline or when minimising network use)
91 @type stores: L{zerostore.Stores}
92 @param extra_restrictions: extra restrictions on the chosen implementations
93 @type extra_restrictions: {L{model.Interface}: [L{model.Restriction}]}
96 self
.network_use
= network_use
97 self
.iface_cache
= iface_cache
99 self
.help_with_testing
= False
100 self
.extra_restrictions
= extra_restrictions
or {}
102 def compare(self
, interface
, b
, a
, arch
):
103 """Compare a and b to see which would be chosen first.
104 Does not consider whether the implementations are usable (check for that yourself first).
105 @param interface: The interface we are trying to resolve, which may
106 not be the interface of a or b if they are from feeds.
108 a_stab
= a
.get_stability()
109 b_stab
= b
.get_stability()
111 # Preferred versions come first
112 r
= cmp(a_stab
== model
.preferred
, b_stab
== model
.preferred
)
116 if self
.network_use
!= model
.network_full
:
117 r
= cmp(_get_cached(stores
, a
), _get_cached(stores
, b
))
121 stab_policy
= interface
.stability_policy
123 if self
.help_with_testing
: stab_policy
= model
.testing
124 else: stab_policy
= model
.stable
126 if a_stab
>= stab_policy
: a_stab
= model
.preferred
127 if b_stab
>= stab_policy
: b_stab
= model
.preferred
129 r
= cmp(a_stab
, b_stab
)
132 # Newer versions come before older ones
133 r
= cmp(a
.version
, b
.version
)
137 r
= cmp(arch
.os_ranks
.get(b
.os
, None),
138 arch
.os_ranks
.get(a
.os
, None))
142 r
= cmp(arch
.machine_ranks
.get(b
.machine
, None),
143 arch
.machine_ranks
.get(a
.machine
, None))
146 # Slightly prefer cached versions
147 if self
.network_use
== model
.network_full
:
148 r
= cmp(_get_cached(stores
, a
), _get_cached(stores
, b
))
151 return cmp(a
.id, b
.id)
153 def solve(self
, root_interface
, root_arch
):
154 # TODO: We need some way to figure out which feeds to include.
155 # Currently, we include any feed referenced from anywhere but
156 # this is probably too much. We could insert a dummy optimial
157 # implementation in stale/uncached feeds and see whether it
161 problem
= sat
.Solver()
163 feed_names
= {} # Feed -> "f1"
164 impl_names
= {} # Impl -> "f1_0"
165 self
.feeds_used
= set()
168 self
.details
= self
.record_details
and {}
170 self
.selections
= None
172 comment_problem
= False # debugging only
174 # TODO: simplify this; there's no need for two mappings!
175 var
= {} # our var names to sat var/literal numbers
177 if self
.help_with_testing
:
178 # Choose the newest, not the most stable.
179 # Preferred still affects the choice, though.
182 model
.packaged
: 1000,
184 model
.testing
: 1000,
185 model
.developer
: 1000,
188 # Strongly prefer packaged or stable versions.
191 model
.packaged
: 1000,
193 model
.testing
: 3000,
194 model
.developer
: 8000,
198 name
= feed_names
.get(feed
, None)
200 feed_names
[feed
] = name
= "f%d" % (len(feed_names
))
202 problem
.append("* feed %s is now known as %s" % (feed
, name
))
203 self
.feeds_used
.add(feed
.url
)
206 ifaces_processed
= set()
208 impls_for_machine_group
= {0 : []} # Machine group (e.g. "64") to [impl] in that group
209 for machine_group
in machine_groups
.values():
210 impls_for_machine_group
[machine_group
] = []
212 impls_for_iface
= {} # Iface -> [impl]
214 group_clause_for
= {} # Iface URI -> AtMostOneClause | bool
216 def find_dependency_candidates(requiring_impl
, dependency
):
217 dep_iface
= self
.iface_cache
.get_interface(dependency
.interface
)
218 dep_union
= [sat
.neg(var
[requiring_impl
])]
219 for candidate
in impls_for_iface
[dep_iface
]:
220 for r
in dependency
.restrictions
:
221 if not r
.meets_restriction(candidate
):
222 #warn("%s rejected due to %s", candidate.get_version(), r)
225 c_name
= impl_names
.get(candidate
, None)
227 dep_union
.append(var
[c_name
])
228 # else we filtered that version out, so ignore it
230 problem
.append("* %s requires %s" % (requiring_impl
, dependency
))
232 problem
.add_clause(dep_union
)
234 problem
.assign(requiring_impl
, 0)
236 def is_unusable(impl
, restrictions
, arch
):
237 """@return: whether this implementation is unusable.
239 return get_unusable_reason(impl
, restrictions
, arch
) != None
241 def get_unusable_reason(impl
, restrictions
, arch
):
243 @param impl: Implementation to test.
244 @type restrictions: [L{model.Restriction}]
245 @return: The reason why this impl is unusable, or None if it's OK.
247 @note: The restrictions are for the interface being requested, not the feed
248 of the implementation; they may be different when feeds are being used."""
249 for r
in restrictions
:
250 if not r
.meets_restriction(impl
):
251 return _("Incompatible with another selected implementation")
252 stability
= impl
.get_stability()
253 if stability
<= model
.buggy
:
254 return stability
.name
255 if (self
.network_use
== model
.network_offline
or not impl
.download_sources
) and not _get_cached(self
.stores
, impl
):
256 if not impl
.download_sources
:
257 return _("No retrieval methods")
258 return _("Not cached and we are off-line")
259 if impl
.os
not in arch
.os_ranks
:
260 return _("Unsupported OS")
261 if impl
.machine
not in arch
.machine_ranks
:
262 if impl
.machine
== 'src':
263 return _("Source code")
264 return _("Unsupported machine type")
267 def usable_feeds(iface
, arch
):
268 """Return all feeds for iface that support arch.
269 @rtype: generator(ZeroInstallFeed)"""
272 for f
in iface
.feeds
:
273 # Note: when searching for src, None is not in machine_ranks
274 if f
.os
in arch
.os_ranks
and \
275 (f
.machine
is None or f
.machine
in arch
.machine_ranks
):
278 debug(_("Skipping '%(feed)s'; unsupported architecture %(os)s-%(machine)s"),
279 {'feed': f
, 'os': f
.os
, 'machine': f
.machine
})
281 def add_iface(uri
, arch
):
282 """Name implementations from feed, assign costs and assert that one one can be selected."""
283 if uri
in ifaces_processed
: return
284 ifaces_processed
.add(uri
)
285 iface_name
= 'i%d' % len(ifaces_processed
)
287 iface
= self
.iface_cache
.get_interface(uri
)
290 for f
in usable_feeds(iface
, arch
):
291 self
.feeds_used
.add(f
)
292 debug(_("Processing feed %s"), f
)
295 feed
= self
.iface_cache
.get_interface(f
)._main
_feed
296 if not feed
.last_modified
: continue # DummyFeed
297 if feed
.name
and iface
.uri
!= feed
.url
and iface
.uri
not in feed
.feed_for
:
298 info(_("Missing <feed-for> for '%(uri)s' in '%(feed)s'"), {'uri': iface
.uri
, 'feed': f
})
300 if feed
.implementations
:
301 impls
.extend(feed
.implementations
.values())
302 except Exception, ex
:
303 warn(_("Failed to load feed %(feed)s for %(interface)s: %(exception)s"), {'feed': f
, 'interface': iface
, 'exception': str(ex
)})
305 impls
.sort(lambda a
, b
: self
.compare(iface
, a
, b
, arch
))
307 impls_for_iface
[iface
] = filtered_impls
= []
309 my_extra_restrictions
= self
.extra_restrictions
.get(iface
, [])
311 if self
.record_details
:
312 self
.details
[iface
] = [(impl
, get_unusable_reason(impl
, my_extra_restrictions
, arch
)) for impl
in impls
]
317 if is_unusable(impl
, my_extra_restrictions
, arch
):
320 filtered_impls
.append(impl
)
323 name
= feed_name(impl
.feed
) + "_" + str(rank
)
324 assert impl
not in impl_names
325 v
= problem
.add_variable(ImplInfo(iface
, impl
, arch
))
327 impl_names
[impl
] = name
329 var_names
.append(var
[name
])
331 if impl
.machine
and impl
.machine
!= 'src':
332 impls_for_machine_group
[machine_groups
.get(impl
.machine
, 0)].append(name
)
334 for d
in impl
.requires
:
335 debug(_("Considering dependency %s"), d
)
336 use
= d
.metadata
.get("use", None)
337 if use
not in arch
.use
:
338 info("Skipping dependency; use='%s' not in %s", use
, arch
.use
)
341 add_iface(d
.interface
, arch
.child_arch
)
343 # Must choose one version of d if impl is selected
344 find_dependency_candidates(name
, d
)
346 # Only one implementation of this interface can be selected
347 if uri
== root_interface
:
349 clause
= problem
.at_most_one(var_names
)
350 problem
.add_clause(var_names
) # at least one
355 clause
= problem
.at_most_one(var_names
)
357 # Don't need to add to group_clause_for because we should
358 # never get a possible selection involving this.
361 assert clause
is not True
362 assert clause
is not None
363 if clause
is not False:
364 group_clause_for
[uri
] = clause
366 add_iface(root_interface
, root_arch
)
368 # Require m<group> to be true if we select an implementation in that group
370 for machine_group
, impls
in impls_for_machine_group
.iteritems():
371 m_group
= 'm%d' % machine_group
372 var
[m_group
] = problem
.add_variable(m_group
)
375 problem
.append("* define machine group %d" % m_group
)
377 problem
.add_clause([var
[m_group
], sat
.neg(var
[impl
])])
378 m_groups
.append(var
[m_group
])
381 problem
.append("* select implementations from at most one machine group")
382 m_groups_clause
= problem
.at_most_one(m_groups
)
384 m_groups_clause
= None
387 """Recurse through the current selections until we get to an interface with
388 no chosen version, then tell the solver to try the best version from that."""
391 def find_undecided(uri
):
393 return # Break cycles
396 group
= group_clause_for
[uri
]
397 #print "Group for %s = %s" % (uri, group)
400 return group
.best_undecided()
401 # else there was only one choice anyway
403 # Check for undecided dependencies
404 lit_info
= problem
.get_varinfo_for_lit(lit
).obj
406 for dep
in lit_info
.impl
.requires
:
407 use
= dep
.metadata
.get("use", None)
408 if use
not in lit_info
.arch
.use
:
410 dep_lit
= find_undecided(dep
.interface
)
411 if dep_lit
is not None:
414 # This whole sub-tree is decided
417 best
= find_undecided(root_interface
)
421 # If we're chosen everything we need, we can probably
422 # set everything else to False.
423 for group
in group_clause_for
.values() + [m_groups_clause
]:
424 if group
.current
is None:
425 best
= group
.best_undecided()
429 return None # Failed to find any valid combination
431 self
.ready
= problem
.run_solver(decide
) is True
433 # Pointing the root at None by default is just
434 # to make the unit-tests happy :-/
435 self
.selections
= {self
.iface_cache
.get_interface(root_interface
): None}
437 for uri
, group
in group_clause_for
.iteritems():
438 if group
.current
is not None:
439 lit_info
= problem
.get_varinfo_for_lit(group
.current
).obj
440 self
.selections
[lit_info
.iface
] = lit_info
.impl
441 deps
= self
.requires
[lit_info
.iface
] = []
442 for dep
in lit_info
.impl
.requires
:
443 use
= dep
.metadata
.get("use", None)
444 if use
not in lit_info
.arch
.use
:
448 DefaultSolver
= SATSolver