Moved modification time logic from Policy.download_icon to Fetcher.download_icon
[zeroinstall/zeroinstall-limyreth.git] / zeroinstall / injector / policy.py
blob16fd9aadc7073101be742688a367c846b62c272a
1 """
2 This class brings together a L{solve.Solver} to choose a set of implmentations, a
3 L{fetch.Fetcher} to download additional components, and the user's configuration
4 settings.
5 """
7 # Copyright (C) 2009, Thomas Leonard
8 # See the README file for details, or visit http://0install.net.
10 from zeroinstall import _
11 import os
12 from logging import info, debug
14 from zeroinstall import SafeException
15 from zeroinstall.injector import arch, model
16 from zeroinstall.injector.model import Interface, Implementation, network_levels, network_offline, network_full
17 from zeroinstall.injector.namespaces import config_site, config_prog
18 from zeroinstall.injector.config import load_config
19 from zeroinstall.support import tasks
21 class Policy(object):
22 """Chooses a set of implementations based on a policy.
23 Typical use:
24 1. Create a Policy object, giving it the URI of the program to be run and a handler.
25 2. Call L{solve_with_downloads}. If more information is needed, a L{fetch.Fetcher} will be used to download it.
26 3. When all downloads are complete, the L{solver} contains the chosen versions.
27 4. Use L{get_uncached_implementations} to find where to get these versions and download them
28 using L{download_uncached_implementations}.
30 @ivar target_arch: target architecture for binaries
31 @type target_arch: L{arch.Architecture}
32 @ivar root: URI of the root interface
33 @ivar solver: solver used to choose a set of implementations
34 @type solver: L{solve.Solver}
35 @ivar watchers: callbacks to invoke after recalculating
36 @ivar help_with_testing: default stability policy
37 @type help_with_testing: bool
38 @ivar network_use: one of the model.network_* values
39 @ivar freshness: seconds allowed since last update
40 @type freshness: int
41 @ivar stale_feeds: set of feeds which are present but haven't been checked for a long time
42 @type stale_feeds: set
43 """
44 __slots__ = ['root', 'watchers', 'requirements', 'config', '_warned_offline',
45 'command', 'target_arch',
46 'stale_feeds', 'solver']
48 help_with_testing = property(lambda self: self.config.help_with_testing,
49 lambda self, value: setattr(self.config, 'help_with_testing', bool(value)))
51 network_use = property(lambda self: self.config.network_use,
52 lambda self, value: setattr(self.config, 'network_use', value))
54 freshness = property(lambda self: self.config.freshness,
55 lambda self, value: setattr(self.config, 'freshness', str(value)))
57 implementation = property(lambda self: self.solver.selections)
59 ready = property(lambda self: self.solver.ready)
61 # (was used by 0test)
62 handler = property(lambda self: self.config.handler,
63 lambda self, value: setattr(self.config, 'handler', value))
66 def __init__(self, root = None, handler = None, src = None, command = -1, config = None, requirements = None):
67 """
68 @param requirements: Details about the program we want to run
69 @type requirements: L{requirements.Requirements}
70 @param config: The configuration settings to use, or None to load from disk.
71 @type config: L{config.Config}
72 Note: all other arguments are deprecated (since 0launch 0.52)
73 """
74 self.watchers = []
75 if requirements is None:
76 from zeroinstall.injector.requirements import Requirements
77 requirements = Requirements(root)
78 requirements.source = bool(src) # Root impl must be a "src" machine type
79 if command == -1:
80 if src:
81 command = 'compile'
82 else:
83 command = 'run'
84 requirements.command = command
85 self.target_arch = arch.get_host_architecture()
86 else:
87 assert root == src == None
88 assert command == -1
89 self.target_arch = arch.get_architecture(requirements.os, requirements.cpu)
90 self.requirements = requirements
92 self.stale_feeds = set()
94 if config is None:
95 self.config = load_config(handler)
96 else:
97 assert handler is None, "can't pass a handler and a config"
98 self.config = config
100 from zeroinstall.injector.solver import DefaultSolver
101 self.solver = DefaultSolver(self.config)
103 # If we need to download something but can't because we are offline,
104 # warn the user. But only the first time.
105 self._warned_offline = False
107 debug(_("Supported systems: '%s'"), arch.os_ranks)
108 debug(_("Supported processors: '%s'"), arch.machine_ranks)
110 if requirements.before or requirements.not_before:
111 self.solver.extra_restrictions[config.iface_cache.get_interface(requirements.interface_uri)] = [
112 model.VersionRangeRestriction(model.parse_version(requirements.before),
113 model.parse_version(requirements.not_before))]
115 @property
116 def fetcher(self):
117 return self.config.fetcher
119 def save_config(self):
120 self.config.save_globals()
122 def usable_feeds(self, iface):
123 """Generator for C{iface.feeds} that are valid for our architecture.
124 @rtype: generator
125 @see: L{arch}"""
126 if self.requirements.source and iface.uri == self.root:
127 # Note: when feeds are recursive, we'll need a better test for root here
128 machine_ranks = {'src': 1}
129 else:
130 machine_ranks = arch.machine_ranks
132 for f in self.config.iface_cache.get_feed_imports(iface):
133 if f.os in arch.os_ranks and f.machine in machine_ranks:
134 yield f
135 else:
136 debug(_("Skipping '%(feed)s'; unsupported architecture %(os)s-%(machine)s"),
137 {'feed': f, 'os': f.os, 'machine': f.machine})
139 def is_stale(self, feed):
140 """@deprecated: use IfaceCache.is_stale"""
141 return self.config.iface_cache.is_stale(feed, self.config.freshness)
143 def get_implementation_path(self, impl):
144 """Return the local path of impl.
145 @rtype: str
146 @raise zeroinstall.zerostore.NotStored: if it needs to be added to the cache first."""
147 assert isinstance(impl, Implementation)
148 return impl.local_path or self.config.stores.lookup_any(impl.digests)
150 def get_implementation(self, interface):
151 """Get the chosen implementation.
152 @type interface: Interface
153 @rtype: L{model.Implementation}
154 @raise SafeException: if interface has not been fetched or no implementation could be
155 chosen."""
156 assert isinstance(interface, Interface)
158 try:
159 return self.implementation[interface]
160 except KeyError:
161 raise SafeException(_("No usable implementation found for '%s'.") % interface.uri)
163 def get_cached(self, impl):
164 """Check whether an implementation is available locally.
165 @type impl: model.Implementation
166 @rtype: bool
168 return impl.is_available(self.config.stores)
170 def get_uncached_implementations(self):
171 """List all chosen implementations which aren't yet available locally.
172 @rtype: [(L{model.Interface}, L{model.Implementation})]"""
173 iface_cache = self.config.iface_cache
174 uncached = []
175 for uri, selection in self.solver.selections.selections.iteritems():
176 impl = selection.impl
177 assert impl, self.solver.selections
178 if not self.get_cached(impl):
179 uncached.append((iface_cache.get_interface(uri), impl))
180 return uncached
182 def refresh_all(self, force = True):
183 """Start downloading all feeds for all selected interfaces.
184 @param force: Whether to restart existing downloads."""
185 return self.solve_with_downloads(force = True)
187 def get_feed_targets(self, feed):
188 """@deprecated: use IfaceCache.get_feed_targets"""
189 return self.config.iface_cache.get_feed_targets(feed)
191 @tasks.async
192 def solve_with_downloads(self, force = False, update_local = False):
193 """Run the solver, then download any feeds that are missing or
194 that need to be updated. Each time a new feed is imported into
195 the cache, the solver is run again, possibly adding new downloads.
196 @param force: whether to download even if we're already ready to run.
197 @param update_local: fetch PackageKit feeds even if we're ready to run."""
199 downloads_finished = set() # Successful or otherwise
200 downloads_in_progress = {} # URL -> Download
202 host_arch = self.target_arch
203 if self.requirements.source:
204 host_arch = arch.SourceArchitecture(host_arch)
206 # There are three cases:
207 # 1. We want to run immediately if possible. If not, download all the information we can.
208 # (force = False, update_local = False)
209 # 2. We're in no hurry, but don't want to use the network unnecessarily.
210 # We should still update local information (from PackageKit).
211 # (force = False, update_local = True)
212 # 3. The user explicitly asked us to refresh everything.
213 # (force = True)
215 try_quick_exit = not (force or update_local)
217 while True:
218 self.solver.solve(self.root, host_arch, command_name = self.command)
219 for w in self.watchers: w()
221 if try_quick_exit and self.solver.ready:
222 break
223 try_quick_exit = False
225 if not self.solver.ready:
226 force = True
228 for f in self.solver.feeds_used:
229 if f in downloads_finished or f in downloads_in_progress:
230 continue
231 if os.path.isabs(f):
232 if force:
233 self.config.iface_cache.get_feed(f, force = True)
234 downloads_in_progress[f] = tasks.IdleBlocker('Refresh local feed')
235 continue
236 elif f.startswith('distribution:'):
237 if force or update_local:
238 downloads_in_progress[f] = self.fetcher.download_and_import_feed(f, self.config.iface_cache)
239 elif force and self.network_use != network_offline:
240 downloads_in_progress[f] = self.fetcher.download_and_import_feed(f, self.config.iface_cache)
241 # Once we've starting downloading some things,
242 # we might as well get them all.
243 force = True
245 if not downloads_in_progress:
246 if self.network_use == network_offline:
247 info(_("Can't choose versions and in off-line mode, so aborting"))
248 break
250 # Wait for at least one download to finish
251 blockers = downloads_in_progress.values()
252 yield blockers
253 tasks.check(blockers, self.handler.report_error)
255 for f in downloads_in_progress.keys():
256 if f in downloads_in_progress and downloads_in_progress[f].happened:
257 del downloads_in_progress[f]
258 downloads_finished.add(f)
260 # Need to refetch any "distribution" feed that
261 # depends on this one
262 distro_feed_url = 'distribution:' + f
263 if distro_feed_url in downloads_finished:
264 downloads_finished.remove(distro_feed_url)
265 if distro_feed_url in downloads_in_progress:
266 del downloads_in_progress[distro_feed_url]
268 @tasks.async
269 def solve_and_download_impls(self, refresh = False, select_only = False):
270 """Run L{solve_with_downloads} and then get the selected implementations too.
271 @raise SafeException: if we couldn't select a set of implementations
272 @since: 0.40"""
273 refreshed = self.solve_with_downloads(refresh)
274 if refreshed:
275 yield refreshed
276 tasks.check(refreshed)
278 if not self.solver.ready:
279 raise self.solver.get_failure_reason()
281 if not select_only:
282 downloaded = self.download_uncached_implementations()
283 if downloaded:
284 yield downloaded
285 tasks.check(downloaded)
287 def need_download(self):
288 """Decide whether we need to download anything (but don't do it!)
289 @return: true if we MUST download something (feeds or implementations)
290 @rtype: bool"""
291 host_arch = self.target_arch
292 if self.requirements.source:
293 host_arch = arch.SourceArchitecture(host_arch)
294 self.solver.solve(self.root, host_arch, command_name = self.command)
295 for w in self.watchers: w()
297 if not self.solver.ready:
298 return True # Maybe a newer version will work?
300 if self.get_uncached_implementations():
301 return True
303 return False
305 def download_uncached_implementations(self):
306 """Download all implementations chosen by the solver that are missing from the cache."""
307 assert self.solver.ready, "Solver is not ready!\n%s" % self.solver.selections
308 return self.fetcher.download_impls([impl for impl in self.solver.selections.values() if not self.get_cached(impl)],
309 self.config.stores)
311 def download_icon(self, interface, force = False):
312 """Download an icon for this interface and add it to the
313 icon cache. If the interface has no icon or we are offline, do nothing.
314 @return: the task doing the import, or None
315 @rtype: L{tasks.Task}"""
316 if self.network_use == network_offline:
317 info("Not downloading icon for %s as we are off-line", interface)
318 return
320 return self.fetcher.download_icon(interface, force)
322 def get_interface(self, uri):
323 """@deprecated: use L{iface_cache.IfaceCache.get_interface} instead"""
324 import warnings
325 warnings.warn("Policy.get_interface is deprecated!", DeprecationWarning, stacklevel = 2)
326 return self.config.iface_cache.get_interface(uri)
328 @property
329 def command(self):
330 return self.requirements.command
332 @property
333 def root(self):
334 return self.requirements.interface_uri
336 _config = None
337 def get_deprecated_singleton_config():
338 global _config
339 if _config is None:
340 _config = load_config()
341 return _config