2 A driver manages the process of iteratively solving and downloading extra feeds, and
3 then downloading the implementations.
7 # Copyright (C) 2011, Thomas Leonard
8 # See the README file for details, or visit http://0install.net.
10 from zeroinstall
import _
13 from logging
import info
, debug
, warn
16 from zeroinstall
import zerostore
, SafeException
17 from zeroinstall
.injector
import arch
, model
18 from zeroinstall
.injector
.model
import Interface
, Implementation
, network_levels
, network_offline
, DistributionImplementation
, network_full
19 from zeroinstall
.injector
.handler
import Handler
20 from zeroinstall
.injector
.namespaces
import config_site
, config_prog
21 from zeroinstall
.support
import tasks
, basedir
23 # If we started a check within this period, don't start another one:
24 FAILED_CHECK_DELAY
= 60 * 60 # 1 Hour
27 """Manages the process of downloading feeds, solving, and downloading implementations.
29 1. Create a Driver object using a DriverFactory, giving it the Requirements about the program to be run.
30 2. Call L{solve_with_downloads}. If more information is needed, a L{fetch.Fetcher} will be used to download it.
31 3. When all downloads are complete, the L{solver} contains the chosen versions.
32 4. Use L{get_uncached_implementations} to find where to get these versions and download them
33 using L{download_uncached_implementations}.
34 @ivar solver: solver used to choose a set of implementations
35 @type solver: L{solve.Solver}
36 @ivar watchers: callbacks to invoke after recalculating
37 @ivar stale_feeds: set of feeds which are present but haven't been checked for a long time
38 @type stale_feeds: set
40 __slots__
= ['watchers', 'requirements', '_warned_offline', 'stale_feeds', 'solver']
42 def __init__(self
, requirements
= None, solver
= None):
44 @param requirements: Details about the program we want to run
45 @type requirements: L{requirements.Requirements}
48 self
.target_arch
= arch
.get_architecture(requirements
.os
, requirements
.cpu
)
49 self
.requirements
= requirements
52 self
.stale_feeds
= set()
54 # If we need to download something but can't because we are offline,
55 # warn the user. But only the first time.
56 self
._warned
_offline
= False
58 def download_and_import_feed_if_online(self
, feed_url
):
59 """If we're online, call L{fetch.Fetcher.download_and_import_feed}. Otherwise, log a suitable warning."""
60 if self
.network_use
!= network_offline
:
61 debug(_("Feed %s not cached and not off-line. Downloading..."), feed_url
)
62 return self
.fetcher
.download_and_import_feed(feed_url
, self
.iface_cache
)
64 if self
._warned
_offline
:
65 debug(_("Not downloading feed '%s' because we are off-line."), feed_url
)
67 warn(_("Not downloading feed '%s' because we are in off-line mode."), feed_url
)
68 self
._warned
_offline
= True
70 def get_uncached_implementations(self
):
71 """List all chosen implementations which aren't yet available locally.
72 @rtype: [(L{model.Interface}, L{model.Implementation})]"""
73 iface_cache
= self
.iface_cache
75 for uri
, selection
in self
.solver
.selections
.selections
.iteritems():
77 assert impl
, self
.solver
.selections
78 if not self
.stores
.is_available(impl
):
79 uncached
.append((iface_cache
.get_interface(uri
), impl
))
83 def solve_with_downloads(self
, force
= False, update_local
= False):
84 """Run the solver, then download any feeds that are missing or
85 that need to be updated. Each time a new feed is imported into
86 the cache, the solver is run again, possibly adding new downloads.
87 @param force: whether to download even if we're already ready to run.
88 @param update_local: fetch PackageKit feeds even if we're ready to run."""
90 downloads_finished
= set() # Successful or otherwise
91 downloads_in_progress
= {} # URL -> Download
93 host_arch
= self
.target_arch
94 if self
.requirements
.source
:
95 host_arch
= arch
.SourceArchitecture(host_arch
)
97 # There are three cases:
98 # 1. We want to run immediately if possible. If not, download all the information we can.
99 # (force = False, update_local = False)
100 # 2. We're in no hurry, but don't want to use the network unnecessarily.
101 # We should still update local information (from PackageKit).
102 # (force = False, update_local = True)
103 # 3. The user explicitly asked us to refresh everything.
106 try_quick_exit
= not (force
or update_local
)
109 self
.solver
.solve(self
.root
, host_arch
, command_name
= self
.command
)
110 for w
in self
.watchers
: w()
112 if try_quick_exit
and self
.solver
.ready
:
114 try_quick_exit
= False
116 if not self
.solver
.ready
:
119 for f
in self
.solver
.feeds_used
:
120 if f
in downloads_finished
or f
in downloads_in_progress
:
124 self
.iface_cache
.get_feed(f
, force
= True)
125 downloads_in_progress
[f
] = tasks
.IdleBlocker('Refresh local feed')
127 elif f
.startswith('distribution:'):
128 if force
or update_local
:
129 downloads_in_progress
[f
] = self
.fetcher
.download_and_import_feed(f
, self
.iface_cache
)
130 elif force
and self
.network_use
!= network_offline
:
131 downloads_in_progress
[f
] = self
.fetcher
.download_and_import_feed(f
, self
.iface_cache
)
132 # Once we've starting downloading some things,
133 # we might as well get them all.
136 if not downloads_in_progress
:
137 if self
.network_use
== network_offline
:
138 info(_("Can't choose versions and in off-line mode, so aborting"))
141 # Wait for at least one download to finish
142 blockers
= downloads_in_progress
.values()
144 tasks
.check(blockers
, self
.handler
.report_error
)
146 for f
in downloads_in_progress
.keys():
147 if f
in downloads_in_progress
and downloads_in_progress
[f
].happened
:
148 del downloads_in_progress
[f
]
149 downloads_finished
.add(f
)
151 # Need to refetch any "distribution" feed that
152 # depends on this one
153 distro_feed_url
= 'distribution:' + f
154 if distro_feed_url
in downloads_finished
:
155 downloads_finished
.remove(distro_feed_url
)
156 if distro_feed_url
in downloads_in_progress
:
157 del downloads_in_progress
[distro_feed_url
]
160 def solve_and_download_impls(self
, refresh
= False, select_only
= False):
161 """Run L{solve_with_downloads} and then get the selected implementations too.
162 @raise SafeException: if we couldn't select a set of implementations
164 refreshed
= self
.solve_with_downloads(refresh
)
167 tasks
.check(refreshed
)
169 if not self
.solver
.ready
:
170 raise self
.solver
.get_failure_reason()
173 downloaded
= self
.download_uncached_implementations()
176 tasks
.check(downloaded
)
178 def need_download(self
):
179 """Decide whether we need to download anything (but don't do it!)
180 @return: true if we MUST download something (feeds or implementations)
182 host_arch
= self
.target_arch
183 if self
.requirements
.source
:
184 host_arch
= arch
.SourceArchitecture(host_arch
)
185 self
.solver
.solve(self
.root
, host_arch
, command_name
= self
.command
)
186 for w
in self
.watchers
: w()
188 if not self
.solver
.ready
:
189 return True # Maybe a newer version will work?
191 if self
.get_uncached_implementations():
196 def download_uncached_implementations(self
):
197 """Download all implementations chosen by the solver that are missing from the cache."""
198 assert self
.solver
.ready
, "Solver is not ready!\n%s" % self
.solver
.selections
199 return self
.fetcher
.download_impls([impl
for impl
in self
.solver
.selections
.values() if not self
.stores
.is_available(impl
)],
203 def __init__(self
, settings
, iface_cache
, stores
, user_interface
):
204 self
.settings
= settings
205 self
.iface_cache
= iface_cache
207 self
.user_interface
= user_interface
209 def make_driver(self
, requirements
):
210 from zeroinstall
.injector
.solver
import DefaultSolver
211 solver
= DefaultSolver(self
.settings
, self
.stores
, self
.iface_cache
)
213 if requirements
.before
or requirements
.not_before
:
214 solver
.extra_restrictions
[self
.iface_cache
.get_interface(requirements
.interface_uri
)] = [
215 model
.VersionRangeRestriction(model
.parse_version(requirements
.before
),
216 model
.parse_version(requirements
.not_before
))]
218 return Driver(requirements
, solver
= solver
)