Adder driver module
[zeroinstall.git] / zeroinstall / injector / driver.py
blobd8e27e1d8fe345a41bdb360dd2c4866b01f541cc
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 _
12 import os
13 from logging import info, debug
15 from zeroinstall import SafeException
16 from zeroinstall.injector import arch, model
17 from zeroinstall.injector.model import Interface, Implementation, network_levels, network_offline, network_full
18 from zeroinstall.injector.namespaces import config_site, config_prog
19 from zeroinstall.injector.config import load_config
20 from zeroinstall.support import tasks
22 class Driver(object):
23 """Chooses a set of implementations based on a policy.
24 Typical use:
25 1. Create a Driver object, giving it the requirements about the program to be run.
26 2. Call L{solve_with_downloads}. If more information is needed, a L{fetch.Fetcher} will be used to download it.
27 3. When all downloads are complete, the L{solver} contains the chosen versions.
28 4. Use L{get_uncached_implementations} to find where to get these versions and download them
29 using L{download_uncached_implementations}.
31 @ivar target_arch: target architecture for binaries
32 @type target_arch: L{arch.Architecture}
33 @ivar solver: solver used to choose a set of implementations
34 @type solver: L{solve.Solver}
35 @ivar watchers: callbacks to invoke after solving
36 """
37 __slots__ = ['watchers', 'requirements', 'config', '_warned_offline', 'target_arch', 'solver']
39 def __init__(self, config, requirements):
40 """
41 @param config: The configuration settings to use
42 @type config: L{config.Config}
43 @param requirements: Details about the program we want to run
44 @type requirements: L{requirements.Requirements}
45 @since: 0.53
46 """
47 self.watchers = []
49 assert config
50 self.config = config
52 assert requirements
53 self.requirements = requirements
55 self.target_arch = arch.get_architecture(requirements.os, requirements.cpu)
57 from zeroinstall.injector.solver import DefaultSolver
58 self.solver = DefaultSolver(self.config)
60 # If we need to download something but can't because we are offline,
61 # warn the user. But only the first time.
62 self._warned_offline = False
64 debug(_("Supported systems: '%s'"), arch.os_ranks)
65 debug(_("Supported processors: '%s'"), arch.machine_ranks)
67 if requirements.before or requirements.not_before:
68 self.solver.extra_restrictions[config.iface_cache.get_interface(requirements.interface_uri)] = [
69 model.VersionRangeRestriction(model.parse_version(requirements.before),
70 model.parse_version(requirements.not_before))]
72 def get_uncached_implementations(self):
73 """List all chosen implementations which aren't yet available locally.
74 @rtype: [(L{model.Interface}, L{model.Implementation})]"""
75 iface_cache = self.config.iface_cache
76 stores = self.config.stores
77 uncached = []
78 for uri, selection in self.solver.selections.selections.iteritems():
79 impl = selection.impl
80 assert impl, self.solver.selections
81 if not impl.is_available(stores):
82 uncached.append((iface_cache.get_interface(uri), impl))
83 return uncached
85 @tasks.async
86 def solve_with_downloads(self, force = False, update_local = False):
87 """Run the solver, then download any feeds that are missing or
88 that need to be updated. Each time a new feed is imported into
89 the cache, the solver is run again, possibly adding new downloads.
90 @param force: whether to download even if we're already ready to run.
91 @param update_local: fetch PackageKit feeds even if we're ready to run."""
93 downloads_finished = set() # Successful or otherwise
94 downloads_in_progress = {} # URL -> Download
96 host_arch = self.target_arch
97 if self.requirements.source:
98 host_arch = arch.SourceArchitecture(host_arch)
100 # There are three cases:
101 # 1. We want to run immediately if possible. If not, download all the information we can.
102 # (force = False, update_local = False)
103 # 2. We're in no hurry, but don't want to use the network unnecessarily.
104 # We should still update local information (from PackageKit).
105 # (force = False, update_local = True)
106 # 3. The user explicitly asked us to refresh everything.
107 # (force = True)
109 try_quick_exit = not (force or update_local)
111 while True:
112 self.solver.solve(self.requirements.interface_uri, host_arch, command_name = self.requirements.command)
113 for w in self.watchers: w()
115 if try_quick_exit and self.solver.ready:
116 break
117 try_quick_exit = False
119 if not self.solver.ready:
120 force = True
122 for f in self.solver.feeds_used:
123 if f in downloads_finished or f in downloads_in_progress:
124 continue
125 if os.path.isabs(f):
126 if force:
127 self.config.iface_cache.get_feed(f, force = True)
128 downloads_in_progress[f] = tasks.IdleBlocker('Refresh local feed')
129 continue
130 elif f.startswith('distribution:'):
131 if force or update_local:
132 downloads_in_progress[f] = self.config.fetcher.download_and_import_feed(f, self.config.iface_cache)
133 elif force and self.config.network_use != network_offline:
134 downloads_in_progress[f] = self.config.fetcher.download_and_import_feed(f, self.config.iface_cache)
135 # Once we've starting downloading some things,
136 # we might as well get them all.
137 force = True
139 if not downloads_in_progress:
140 if self.config.network_use == network_offline:
141 info(_("Can't choose versions and in off-line mode, so aborting"))
142 break
144 # Wait for at least one download to finish
145 blockers = downloads_in_progress.values()
146 yield blockers
147 tasks.check(blockers, self.config.handler.report_error)
149 for f in downloads_in_progress.keys():
150 if f in downloads_in_progress and downloads_in_progress[f].happened:
151 del downloads_in_progress[f]
152 downloads_finished.add(f)
154 # Need to refetch any "distribution" feed that
155 # depends on this one
156 distro_feed_url = 'distribution:' + f
157 if distro_feed_url in downloads_finished:
158 downloads_finished.remove(distro_feed_url)
159 if distro_feed_url in downloads_in_progress:
160 del downloads_in_progress[distro_feed_url]
162 @tasks.async
163 def solve_and_download_impls(self, refresh = False, select_only = False):
164 """Run L{solve_with_downloads} and then get the selected implementations too.
165 @raise SafeException: if we couldn't select a set of implementations
166 @since: 0.40"""
167 refreshed = self.solve_with_downloads(refresh)
168 if refreshed:
169 yield refreshed
170 tasks.check(refreshed)
172 if not self.solver.ready:
173 raise self.solver.get_failure_reason()
175 if not select_only:
176 downloaded = self.download_uncached_implementations()
177 if downloaded:
178 yield downloaded
179 tasks.check(downloaded)
181 def need_download(self):
182 """Decide whether we need to download anything (but don't do it!)
183 @return: true if we MUST download something (feeds or implementations)
184 @rtype: bool"""
185 host_arch = self.target_arch
186 if self.requirements.source:
187 host_arch = arch.SourceArchitecture(host_arch)
188 self.solver.solve(self.requirements.interface_uri, host_arch, command_name = self.requirements.command)
189 for w in self.watchers: w()
191 if not self.solver.ready:
192 return True # Maybe a newer version will work?
194 if self.get_uncached_implementations():
195 return True
197 return False
199 def download_uncached_implementations(self):
200 """Download all implementations chosen by the solver that are missing from the cache."""
201 assert self.solver.ready, "Solver is not ready!\n%s" % self.solver.selections
202 stores = self.config.stores
203 return self.config.fetcher.download_impls([impl for impl in self.solver.selections.values() if not impl.is_available(stores)],
204 stores)