Moved get_feed_targets and is_stale to IfaceCache
[zeroinstall/zeroinstall-afb.git] / zeroinstall / injector / policy.py
blob7c8c9c00974b5a83cd2b97edf9066ea07ceb9f73
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, warn
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 # If we started a check within this period, don't start another one:
22 FAILED_CHECK_DELAY = 60 * 60 # 1 Hour
24 class Policy(object):
25 """Chooses a set of implementations based on a policy.
26 Typical use:
27 1. Create a Policy object, giving it the URI of the program to be run and a handler.
28 2. Call L{solve_with_downloads}. If more information is needed, a L{fetch.Fetcher} will be used to download it.
29 3. When all downloads are complete, the L{solver} contains the chosen versions.
30 4. Use L{get_uncached_implementations} to find where to get these versions and download them
31 using L{download_uncached_implementations}.
33 @ivar target_arch: target architecture for binaries
34 @type target_arch: L{arch.Architecture}
35 @ivar root: URI of the root interface
36 @ivar solver: solver used to choose a set of implementations
37 @type solver: L{solve.Solver}
38 @ivar watchers: callbacks to invoke after recalculating
39 @ivar help_with_testing: default stability policy
40 @type help_with_testing: bool
41 @ivar network_use: one of the model.network_* values
42 @ivar freshness: seconds allowed since last update
43 @type freshness: int
44 @ivar stale_feeds: set of feeds which are present but haven't been checked for a long time
45 @type stale_feeds: set
46 """
47 __slots__ = ['root', 'watchers', 'requirements', 'config', '_warned_offline',
48 'command', 'target_arch',
49 'stale_feeds', 'solver']
51 help_with_testing = property(lambda self: self.config.help_with_testing,
52 lambda self, value: setattr(self.config, 'help_with_testing', bool(value)))
54 network_use = property(lambda self: self.config.network_use,
55 lambda self, value: setattr(self.config, 'network_use', value))
57 freshness = property(lambda self: self.config.freshness,
58 lambda self, value: setattr(self.config, 'freshness', str(value)))
60 implementation = property(lambda self: self.solver.selections)
62 ready = property(lambda self: self.solver.ready)
64 # (was used by 0test)
65 handler = property(lambda self: self.config.handler,
66 lambda self, value: setattr(self.config, 'handler', value))
69 def __init__(self, root = None, handler = None, src = None, command = -1, config = None, requirements = None):
70 """
71 @param requirements: Details about the program we want to run
72 @type requirements: L{requirements.Requirements}
73 @param config: The configuration settings to use, or None to load from disk.
74 @type config: L{config.Config}
75 Note: all other arguments are deprecated (since 0launch 0.52)
76 """
77 self.watchers = []
78 if requirements is None:
79 from zeroinstall.injector.requirements import Requirements
80 requirements = Requirements(root)
81 requirements.source = bool(src) # Root impl must be a "src" machine type
82 if command == -1:
83 if src:
84 command = 'compile'
85 else:
86 command = 'run'
87 requirements.command = command
88 self.target_arch = arch.get_host_architecture()
89 else:
90 assert root == src == None
91 assert command == -1
92 self.target_arch = arch.get_architecture(requirements.os, requirements.cpu)
93 self.requirements = requirements
95 self.stale_feeds = set()
97 if config is None:
98 self.config = load_config(handler)
99 else:
100 assert handler is None, "can't pass a handler and a config"
101 self.config = config
103 from zeroinstall.injector.solver import DefaultSolver
104 self.solver = DefaultSolver(self.config)
106 # If we need to download something but can't because we are offline,
107 # warn the user. But only the first time.
108 self._warned_offline = False
110 debug(_("Supported systems: '%s'"), arch.os_ranks)
111 debug(_("Supported processors: '%s'"), arch.machine_ranks)
113 if requirements.before or requirements.not_before:
114 self.solver.extra_restrictions[config.iface_cache.get_interface(requirements.interface_uri)] = [
115 model.VersionRangeRestriction(model.parse_version(requirements.before),
116 model.parse_version(requirements.not_before))]
118 @property
119 def fetcher(self):
120 return self.config.fetcher
122 def save_config(self):
123 self.config.save_globals()
125 def recalculate(self, fetch_stale_interfaces = True):
126 """@deprecated: see L{solve_with_downloads} """
127 import warnings
128 warnings.warn("Policy.recalculate is deprecated!", DeprecationWarning, stacklevel = 2)
130 self.stale_feeds = set()
132 host_arch = self.target_arch
133 if self.requirements.source:
134 host_arch = arch.SourceArchitecture(host_arch)
135 self.solver.solve(self.root, host_arch, command_name = self.command)
137 if self.network_use == network_offline:
138 fetch_stale_interfaces = False
140 blockers = []
141 for f in self.solver.feeds_used:
142 if os.path.isabs(f): continue
143 feed = self.config.iface_cache.get_feed(f)
144 if feed is None or feed.last_modified is None:
145 self.download_and_import_feed_if_online(f) # Will start a download
146 elif self.is_stale(feed):
147 debug(_("Adding %s to stale set"), f)
148 self.stale_feeds.add(self.config.iface_cache.get_interface(f)) # Legacy API
149 if fetch_stale_interfaces:
150 self.download_and_import_feed_if_online(f) # Will start a download
152 for w in self.watchers: w()
154 return blockers
156 def usable_feeds(self, iface):
157 """Generator for C{iface.feeds} that are valid for our architecture.
158 @rtype: generator
159 @see: L{arch}"""
160 if self.requirements.source and iface.uri == self.root:
161 # Note: when feeds are recursive, we'll need a better test for root here
162 machine_ranks = {'src': 1}
163 else:
164 machine_ranks = arch.machine_ranks
166 for f in self.config.iface_cache.get_feed_imports(iface):
167 if f.os in arch.os_ranks and f.machine in machine_ranks:
168 yield f
169 else:
170 debug(_("Skipping '%(feed)s'; unsupported architecture %(os)s-%(machine)s"),
171 {'feed': f, 'os': f.os, 'machine': f.machine})
173 def is_stale(self, feed):
174 """@deprecated: use IfaceCache.is_stale"""
175 return self.config.iface_cache.is_stale(feed, self.config.freshness)
177 def download_and_import_feed_if_online(self, feed_url):
178 """If we're online, call L{fetch.Fetcher.download_and_import_feed}. Otherwise, log a suitable warning."""
179 if self.network_use != network_offline:
180 debug(_("Feed %s not cached and not off-line. Downloading..."), feed_url)
181 return self.fetcher.download_and_import_feed(feed_url, self.config.iface_cache)
182 else:
183 if self._warned_offline:
184 debug(_("Not downloading feed '%s' because we are off-line."), feed_url)
185 else:
186 warn(_("Not downloading feed '%s' because we are in off-line mode."), feed_url)
187 self._warned_offline = True
189 def get_implementation_path(self, impl):
190 """Return the local path of impl.
191 @rtype: str
192 @raise zeroinstall.zerostore.NotStored: if it needs to be added to the cache first."""
193 assert isinstance(impl, Implementation)
194 return impl.local_path or self.config.stores.lookup_any(impl.digests)
196 def get_implementation(self, interface):
197 """Get the chosen implementation.
198 @type interface: Interface
199 @rtype: L{model.Implementation}
200 @raise SafeException: if interface has not been fetched or no implementation could be
201 chosen."""
202 assert isinstance(interface, Interface)
204 try:
205 return self.implementation[interface]
206 except KeyError:
207 raise SafeException(_("No usable implementation found for '%s'.") % interface.uri)
209 def get_cached(self, impl):
210 """Check whether an implementation is available locally.
211 @type impl: model.Implementation
212 @rtype: bool
214 return impl.is_available(self.config.stores)
216 def get_uncached_implementations(self):
217 """List all chosen implementations which aren't yet available locally.
218 @rtype: [(L{model.Interface}, L{model.Implementation})]"""
219 iface_cache = self.config.iface_cache
220 uncached = []
221 for uri, selection in self.solver.selections.selections.iteritems():
222 impl = selection.impl
223 assert impl, self.solver.selections
224 if not self.get_cached(impl):
225 uncached.append((iface_cache.get_interface(uri), impl))
226 return uncached
228 def refresh_all(self, force = True):
229 """Start downloading all feeds for all selected interfaces.
230 @param force: Whether to restart existing downloads."""
231 return self.solve_with_downloads(force = True)
233 def get_feed_targets(self, feed):
234 """@deprecated: use IfaceCache.get_feed_targets"""
235 return self.config.iface_cache.get_feed_targets(feed)
237 @tasks.async
238 def solve_with_downloads(self, force = False, update_local = False):
239 """Run the solver, then download any feeds that are missing or
240 that need to be updated. Each time a new feed is imported into
241 the cache, the solver is run again, possibly adding new downloads.
242 @param force: whether to download even if we're already ready to run.
243 @param update_local: fetch PackageKit feeds even if we're ready to run."""
245 downloads_finished = set() # Successful or otherwise
246 downloads_in_progress = {} # URL -> Download
248 host_arch = self.target_arch
249 if self.requirements.source:
250 host_arch = arch.SourceArchitecture(host_arch)
252 # There are three cases:
253 # 1. We want to run immediately if possible. If not, download all the information we can.
254 # (force = False, update_local = False)
255 # 2. We're in no hurry, but don't want to use the network unnecessarily.
256 # We should still update local information (from PackageKit).
257 # (force = False, update_local = True)
258 # 3. The user explicitly asked us to refresh everything.
259 # (force = True)
261 try_quick_exit = not (force or update_local)
263 while True:
264 self.solver.solve(self.root, host_arch, command_name = self.command)
265 for w in self.watchers: w()
267 if try_quick_exit and self.solver.ready:
268 break
269 try_quick_exit = False
271 if not self.solver.ready:
272 force = True
274 for f in self.solver.feeds_used:
275 if f in downloads_finished or f in downloads_in_progress:
276 continue
277 if os.path.isabs(f):
278 if force:
279 self.config.iface_cache.get_feed(f, force = True)
280 downloads_in_progress[f] = tasks.IdleBlocker('Refresh local feed')
281 continue
282 elif f.startswith('distribution:'):
283 if force or update_local:
284 downloads_in_progress[f] = self.fetcher.download_and_import_feed(f, self.config.iface_cache)
285 elif force and self.network_use != network_offline:
286 downloads_in_progress[f] = self.fetcher.download_and_import_feed(f, self.config.iface_cache)
287 # Once we've starting downloading some things,
288 # we might as well get them all.
289 force = True
291 if not downloads_in_progress:
292 if self.network_use == network_offline:
293 info(_("Can't choose versions and in off-line mode, so aborting"))
294 break
296 # Wait for at least one download to finish
297 blockers = downloads_in_progress.values()
298 yield blockers
299 tasks.check(blockers, self.handler.report_error)
301 for f in downloads_in_progress.keys():
302 if f in downloads_in_progress and downloads_in_progress[f].happened:
303 del downloads_in_progress[f]
304 downloads_finished.add(f)
306 # Need to refetch any "distribution" feed that
307 # depends on this one
308 distro_feed_url = 'distribution:' + f
309 if distro_feed_url in downloads_finished:
310 downloads_finished.remove(distro_feed_url)
311 if distro_feed_url in downloads_in_progress:
312 del downloads_in_progress[distro_feed_url]
314 @tasks.async
315 def solve_and_download_impls(self, refresh = False, select_only = False):
316 """Run L{solve_with_downloads} and then get the selected implementations too.
317 @raise SafeException: if we couldn't select a set of implementations
318 @since: 0.40"""
319 refreshed = self.solve_with_downloads(refresh)
320 if refreshed:
321 yield refreshed
322 tasks.check(refreshed)
324 if not self.solver.ready:
325 raise self.solver.get_failure_reason()
327 if not select_only:
328 downloaded = self.download_uncached_implementations()
329 if downloaded:
330 yield downloaded
331 tasks.check(downloaded)
333 def need_download(self):
334 """Decide whether we need to download anything (but don't do it!)
335 @return: true if we MUST download something (feeds or implementations)
336 @rtype: bool"""
337 host_arch = self.target_arch
338 if self.requirements.source:
339 host_arch = arch.SourceArchitecture(host_arch)
340 self.solver.solve(self.root, host_arch, command_name = self.command)
341 for w in self.watchers: w()
343 if not self.solver.ready:
344 return True # Maybe a newer version will work?
346 if self.get_uncached_implementations():
347 return True
349 return False
351 def download_uncached_implementations(self):
352 """Download all implementations chosen by the solver that are missing from the cache."""
353 assert self.solver.ready, "Solver is not ready!\n%s" % self.solver.selections
354 return self.fetcher.download_impls([impl for impl in self.solver.selections.values() if not self.get_cached(impl)],
355 self.config.stores)
357 def download_icon(self, interface, force = False):
358 """Download an icon for this interface and add it to the
359 icon cache. If the interface has no icon or we are offline, do nothing.
360 @return: the task doing the import, or None
361 @rtype: L{tasks.Task}"""
362 if self.network_use == network_offline:
363 info("Not downloading icon for %s as we are off-line", interface)
364 return
366 modification_time = None
368 existing_icon = self.config.iface_cache.get_icon_path(interface)
369 if existing_icon:
370 file_mtime = os.stat(existing_icon).st_mtime
371 from email.utils import formatdate
372 modification_time = formatdate(timeval = file_mtime, localtime = False, usegmt = True)
374 return self.fetcher.download_icon(interface, force, modification_time)
376 def get_interface(self, uri):
377 """@deprecated: use L{iface_cache.IfaceCache.get_interface} instead"""
378 import warnings
379 warnings.warn("Policy.get_interface is deprecated!", DeprecationWarning, stacklevel = 2)
380 return self.config.iface_cache.get_interface(uri)
382 @property
383 def command(self):
384 return self.requirements.command
386 @property
387 def root(self):
388 return self.requirements.interface_uri
390 _config = None
391 def get_deprecated_singleton_config():
392 global _config
393 if _config is None:
394 _config = load_config()
395 return _config