Implementation.id doesn't have to be path or digest
[zeroinstall.git] / zeroinstall / injector / solver.py
blob2d3d31ceaf282250d1362b77be868f9d4d554f64
1 """
2 Chooses a set of components to make a running program.
3 """
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
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
17 class Solver(object):
18 """Chooses a set of implementations to satisfy the requirements of a program and its user.
19 Typical use:
20 1. Create a Solver object and configure it
21 2. Call L{solve}.
22 3. If any of the returned feeds_used are stale or missing, you may like to start downloading them
23 4. If it is 'ready' then you can download and run the chosen versions.
24 @ivar selections: the chosen implementation of each interface
25 @type selections: {L{model.Interface}: Implementation}
26 @ivar requires: the selected dependencies for each chosen version
27 @type requires: {L{model.Interface}: [L{model.Dependency}]}
28 @ivar feeds_used: the feeds which contributed to the choice in L{selections}
29 @type feeds_used: set(str)
30 @ivar record_details: whether to record information about unselected implementations
31 @type record_details: {L{Interface}: [(L{Implementation}, str)]}
32 @ivar details: extra information, if record_details mode was used
33 @type details: {str: [(Implementation, comment)]}
34 """
35 __slots__ = ['selections', 'requires', 'feeds_used', 'details', 'record_details', 'ready']
37 def __init__(self):
38 self.selections = self.requires = self.feeds_used = self.details = None
39 self.record_details = False
40 self.ready = False
42 def solve(self, root_interface, arch):
43 """Get the best implementation of root_interface and all of its dependencies.
44 @param root_interface: the URI of the program to be solved
45 @type root_interface: str
46 @param arch: the desired target architecture
47 @type arch: L{arch.Architecture}
48 @postcondition: self.ready, self.selections and self.feeds_used are updated"""
49 raise NotImplementedError("Abstract")
51 class DefaultSolver(Solver):
52 """The standard (rather naive) Zero Install solver."""
53 def __init__(self, network_use, iface_cache, stores, extra_restrictions = None):
54 """
55 @param network_use: how much use to make of the network
56 @type network_use: L{model.network_levels}
57 @param iface_cache: a cache of feeds containing information about available versions
58 @type iface_cache: L{iface_cache.IfaceCache}
59 @param stores: a cached of implementations (affects choice when offline or when minimising network use)
60 @type stores: L{zerostore.Stores}
61 @param extra_restrictions: extra restrictions on the chosen implementations
62 @type extra_restrictions: {L{model.Interface}: [L{model.Restriction}]}
63 """
64 Solver.__init__(self)
65 self.network_use = network_use
66 self.iface_cache = iface_cache
67 self.stores = stores
68 self.help_with_testing = False
69 self.extra_restrictions = extra_restrictions or {}
71 def solve(self, root_interface, arch):
72 self.selections = {}
73 self.requires = {}
74 self.feeds_used = set()
75 self.details = self.record_details and {}
76 self._machine_group = None
78 restrictions = {}
79 debug(_("Solve! root = %s"), root_interface)
80 def process(dep, arch):
81 ready = True
82 iface = self.iface_cache.get_interface(dep.interface)
84 if iface in self.selections:
85 debug("Interface requested twice; skipping second %s", iface)
86 if dep.restrictions:
87 warn("Interface requested twice; I've already chosen an implementation "
88 "of '%s' but there are more restrictions! Ignoring the second set.", iface)
89 return ready
90 self.selections[iface] = None # Avoid cycles
91 self.requires[iface] = selected_requires = []
93 assert iface not in restrictions
94 restrictions[iface] = dep.restrictions
96 impl = get_best_implementation(iface, arch)
97 if impl:
98 debug(_("Will use implementation %(implementation)s (version %(version)s)"), {'implementation': impl, 'version': impl.get_version()})
99 self.selections[iface] = impl
100 if self._machine_group is None and impl.machine and impl.machine != 'src':
101 self._machine_group = machine_groups.get(impl.machine, 0)
102 debug(_("Now restricted to architecture group %s"), self._machine_group)
103 for d in impl.requires:
104 debug(_("Considering dependency %s"), d)
105 use = d.metadata.get("use", None)
106 if use not in arch.use:
107 info("Skipping dependency; use='%s' not in %s", use, arch.use)
108 continue
109 if not process(d, arch.child_arch):
110 ready = False
111 selected_requires.append(d)
112 else:
113 debug(_("No implementation chould be chosen yet"));
114 ready = False
116 return ready
118 def get_best_implementation(iface, arch):
119 debug(_("get_best_implementation(%(interface)s), with feeds: %(feeds)s"), {'interface': iface, 'feeds': iface.feeds})
121 iface_restrictions = restrictions.get(iface, [])
122 extra_restrictions = self.extra_restrictions.get(iface, None)
123 if extra_restrictions:
124 # Don't modify original
125 iface_restrictions = iface_restrictions + extra_restrictions
127 impls = []
128 for f in usable_feeds(iface, arch):
129 self.feeds_used.add(f)
130 debug(_("Processing feed %s"), f)
132 try:
133 feed = self.iface_cache.get_interface(f)._main_feed
134 if not feed.last_modified: continue # DummyFeed
135 if feed.name and iface.uri != feed.url and iface.uri not in feed.feed_for:
136 info(_("Missing <feed-for> for '%(uri)s' in '%(feed)s'"), {'uri': iface.uri, 'feed': f})
138 if feed.implementations:
139 impls.extend(feed.implementations.values())
140 except Exception, ex:
141 warn(_("Failed to load feed %(feed)s for %(interface)s: %(exception)s"), {'feed': f, 'interface': iface, 'exception': str(ex)})
143 if not impls:
144 info(_("Interface %s has no implementations!"), iface)
145 return None
147 if self.record_details:
148 # In details mode, rank all the implementations and then choose the best
149 impls.sort(lambda a, b: compare(iface, a, b, iface_restrictions, arch))
150 best = impls[0]
151 self.details[iface] = [(impl, get_unusable_reason(impl, iface_restrictions, arch)) for impl in impls]
152 else:
153 # Otherwise, just choose the best without sorting
154 best = impls[0]
155 for x in impls[1:]:
156 if compare(iface, x, best, iface_restrictions, arch) < 0:
157 best = x
158 unusable = get_unusable_reason(best, iface_restrictions, arch)
159 if unusable:
160 info(_("Best implementation of %(interface)s is %(best)s, but unusable (%(unusable)s)"), {'interface': iface, 'best': best, 'unusable': unusable})
161 return None
162 return best
164 def compare(interface, b, a, iface_restrictions, arch):
165 """Compare a and b to see which would be chosen first.
166 @param interface: The interface we are trying to resolve, which may
167 not be the interface of a or b if they are from feeds.
168 @rtype: int"""
169 a_stab = a.get_stability()
170 b_stab = b.get_stability()
172 # Usable ones come first
173 r = cmp(is_unusable(b, iface_restrictions, arch), is_unusable(a, iface_restrictions, arch))
174 if r: return r
176 # Preferred versions come first
177 r = cmp(a_stab == model.preferred, b_stab == model.preferred)
178 if r: return r
180 if self.network_use != model.network_full:
181 r = cmp(get_cached(a), get_cached(b))
182 if r: return r
184 # Stability
185 stab_policy = interface.stability_policy
186 if not stab_policy:
187 if self.help_with_testing: stab_policy = model.testing
188 else: stab_policy = model.stable
190 if a_stab >= stab_policy: a_stab = model.preferred
191 if b_stab >= stab_policy: b_stab = model.preferred
193 r = cmp(a_stab, b_stab)
194 if r: return r
196 # Newer versions come before older ones
197 r = cmp(a.version, b.version)
198 if r: return r
200 # Get best OS
201 r = cmp(arch.os_ranks.get(b.os, None),
202 arch.os_ranks.get(a.os, None))
203 if r: return r
205 # Get best machine
206 r = cmp(arch.machine_ranks.get(b.machine, None),
207 arch.machine_ranks.get(a.machine, None))
208 if r: return r
210 # Slightly prefer cached versions
211 if self.network_use == model.network_full:
212 r = cmp(get_cached(a), get_cached(b))
213 if r: return r
215 return cmp(a.id, b.id)
217 def usable_feeds(iface, arch):
218 """Return all feeds for iface that support arch.
219 @rtype: generator(ZeroInstallFeed)"""
220 yield iface.uri
222 for f in iface.feeds:
223 # Note: when searching for src, None is not in machine_ranks
224 if f.os in arch.os_ranks and \
225 (f.machine is None or f.machine in arch.machine_ranks):
226 yield f.uri
227 else:
228 debug(_("Skipping '%(feed)s'; unsupported architecture %(os)s-%(machine)s"),
229 {'feed': f, 'os': f.os, 'machine': f.machine})
231 def is_unusable(impl, restrictions, arch):
232 """@return: whether this implementation is unusable.
233 @rtype: bool"""
234 return get_unusable_reason(impl, restrictions, arch) != None
236 def get_unusable_reason(impl, restrictions, arch):
238 @param impl: Implementation to test.
239 @type restrictions: [L{model.Restriction}]
240 @return: The reason why this impl is unusable, or None if it's OK.
241 @rtype: str
242 @note: The restrictions are for the interface being requested, not the interface
243 of the implementation; they may be different when feeds are being used."""
244 machine = impl.machine
245 if machine and self._machine_group is not None:
246 if machine_groups.get(machine, 0) != self._machine_group:
247 return _("Incompatible with another selection from a different architecture group")
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 and not get_cached(impl):
256 return _("Not cached and we are off-line")
257 if impl.os not in arch.os_ranks:
258 return _("Unsupported OS")
259 # When looking for source code, we need to known if we're
260 # looking at an implementation of the root interface, even if
261 # it's from a feed, hence the sneaky restrictions identity check.
262 if machine not in arch.machine_ranks:
263 if machine == 'src':
264 return _("Source code")
265 return _("Unsupported machine type")
266 return None
268 def get_cached(impl):
269 """Check whether an implementation is available locally.
270 @type impl: model.Implementation
271 @rtype: bool
273 if isinstance(impl, model.DistributionImplementation):
274 return impl.installed
275 if impl.local_path:
276 return os.path.exists(impl.local_path)
277 else:
278 try:
279 path = self.stores.lookup_any(impl.digests)
280 assert path
281 return True
282 except BadDigest:
283 return False
284 except NotStored:
285 return False
287 self.ready = process(model.InterfaceDependency(root_interface), arch)