Fixed Python 3 whitespace error in 0alias
[zeroinstall/solver.git] / zeroinstall / injector / driver.py
blob12bf878f629a5653e569e0e247b07961a51845a1
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 @since: 0.53
6 """
8 # Copyright (C) 2011, Thomas Leonard
9 # See the README file for details, or visit http://0install.net.
11 from zeroinstall import _, logger
12 import os
14 from zeroinstall.injector import arch, model
15 from zeroinstall.injector.model import network_offline
16 from zeroinstall.support import tasks
18 class Driver(object):
19 """Chooses a set of implementations based on a policy.
20 Typical use:
21 1. Create a Driver object, giving it the requirements about the program to be run.
22 2. Call L{solve_with_downloads}. If more information is needed, a L{fetch.Fetcher} will be used to download it.
23 3. When all downloads are complete, the L{solver} contains the chosen versions.
24 4. Use L{get_uncached_implementations} to find where to get these versions and download them
25 using L{download_uncached_implementations}.
27 @ivar target_arch: target architecture for binaries (deprecated)
28 @type target_arch: L{arch.Architecture}
29 @ivar solver: solver used to choose a set of implementations
30 @type solver: L{solve.Solver}
31 @ivar watchers: callbacks to invoke after solving
32 """
33 __slots__ = ['watchers', 'requirements', 'config', 'target_arch', 'solver']
35 def __init__(self, config, requirements):
36 """
37 @param config: The configuration settings to use
38 @type config: L{config.Config}
39 @param requirements: Details about the program we want to run
40 @type requirements: L{requirements.Requirements}
41 @since: 0.53
42 """
43 self.watchers = []
45 assert config
46 self.config = config
48 assert requirements
49 self.requirements = requirements
51 self.target_arch = arch.get_architecture(requirements.os, requirements.cpu)
53 from zeroinstall.injector.solver import DefaultSolver
54 self.solver = DefaultSolver(self.config)
56 logger.debug(_("Supported systems: '%s'"), arch.os_ranks)
57 logger.debug(_("Supported processors: '%s'"), arch.machine_ranks)
59 if requirements.before or requirements.not_before:
60 self.solver.extra_restrictions[config.iface_cache.get_interface(requirements.interface_uri)] = [
61 model.VersionRangeRestriction(model.parse_version(requirements.before),
62 model.parse_version(requirements.not_before))]
64 def get_uncached_implementations(self):
65 """List all chosen implementations which aren't yet available locally.
66 @rtype: [(L{model.Interface}, L{model.Implementation})]"""
67 iface_cache = self.config.iface_cache
68 stores = self.config.stores
69 uncached = []
70 for uri, selection in self.solver.selections.selections.items():
71 impl = selection.impl
72 assert impl, self.solver.selections
73 if not impl.is_available(stores):
74 uncached.append((iface_cache.get_interface(uri), impl))
75 return uncached
77 @tasks.async
78 def solve_with_downloads(self, force = False, update_local = False):
79 """Run the solver, then download any feeds that are missing or
80 that need to be updated. Each time a new feed is imported into
81 the cache, the solver is run again, possibly adding new downloads.
82 @param force: whether to download even if we're already ready to run.
83 @param update_local: fetch PackageKit feeds even if we're ready to run."""
85 downloads_finished = set() # Successful or otherwise
86 downloads_in_progress = {} # URL -> Download
88 # There are three cases:
89 # 1. We want to run immediately if possible. If not, download all the information we can.
90 # (force = False, update_local = False)
91 # 2. We're in no hurry, but don't want to use the network unnecessarily.
92 # We should still update local information (from PackageKit).
93 # (force = False, update_local = True)
94 # 3. The user explicitly asked us to refresh everything.
95 # (force = True)
97 try_quick_exit = not (force or update_local)
99 while True:
100 self.solver.solve_for(self.requirements)
101 for w in self.watchers: w()
103 if try_quick_exit and self.solver.ready:
104 break
105 try_quick_exit = False
107 if not self.solver.ready:
108 force = True
110 for f in self.solver.feeds_used:
111 if f in downloads_finished or f in downloads_in_progress:
112 continue
113 if os.path.isabs(f):
114 if force:
115 self.config.iface_cache.get_feed(f, force = True)
116 downloads_in_progress[f] = tasks.IdleBlocker('Refresh local feed')
117 continue
118 elif f.startswith('distribution:'):
119 if force or update_local:
120 downloads_in_progress[f] = self.config.fetcher.download_and_import_feed(f, self.config.iface_cache)
121 elif force and self.config.network_use != network_offline:
122 downloads_in_progress[f] = self.config.fetcher.download_and_import_feed(f, self.config.iface_cache)
123 # Once we've starting downloading some things,
124 # we might as well get them all.
125 force = True
127 if not downloads_in_progress:
128 if self.config.network_use == network_offline:
129 logger.info(_("Can't choose versions and in off-line mode, so aborting"))
130 break
132 # Wait for at least one download to finish
133 blockers = downloads_in_progress.values()
134 yield blockers
135 tasks.check(blockers, self.config.handler.report_error)
137 for f in list(downloads_in_progress.keys()):
138 if f in downloads_in_progress and downloads_in_progress[f].happened:
139 del downloads_in_progress[f]
140 downloads_finished.add(f)
142 # Need to refetch any "distribution" feed that
143 # depends on this one
144 distro_feed_url = 'distribution:' + f
145 if distro_feed_url in downloads_finished:
146 downloads_finished.remove(distro_feed_url)
147 if distro_feed_url in downloads_in_progress:
148 del downloads_in_progress[distro_feed_url]
150 @tasks.async
151 def solve_and_download_impls(self, refresh = False, select_only = False):
152 """Run L{solve_with_downloads} and then get the selected implementations too.
153 @raise SafeException: if we couldn't select a set of implementations
154 @since: 0.40"""
155 refreshed = self.solve_with_downloads(refresh)
156 if refreshed:
157 yield refreshed
158 tasks.check(refreshed)
160 if not self.solver.ready:
161 raise self.solver.get_failure_reason()
163 if not select_only:
164 downloaded = self.download_uncached_implementations()
165 if downloaded:
166 yield downloaded
167 tasks.check(downloaded)
169 def need_download(self):
170 """Decide whether we need to download anything (but don't do it!)
171 @return: true if we MUST download something (feeds or implementations)
172 @rtype: bool"""
173 self.solver.solve_for(self.requirements)
174 for w in self.watchers: w()
176 if not self.solver.ready:
177 return True # Maybe a newer version will work?
179 if self.get_uncached_implementations():
180 return True
182 return False
184 def download_uncached_implementations(self):
185 """Download all implementations chosen by the solver that are missing from the cache."""
186 assert self.solver.ready, "Solver is not ready!\n%s" % self.solver.selections
187 stores = self.config.stores
188 return self.config.fetcher.download_impls([impl for impl in self.solver.selections.values() if not impl.is_available(stores)],
189 stores)