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
7 # Copyright (C) 2009, Thomas Leonard
8 # See the README file for details, or visit http://0install.net.
10 from zeroinstall
import _
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
25 """Chooses a set of implementations based on a policy.
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
44 @ivar stale_feeds: set of feeds which are present but haven't been checked for a long time
45 @type stale_feeds: set
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
)
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):
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)
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
87 requirements
.command
= command
88 self
.target_arch
= arch
.get_host_architecture()
90 assert root
== src
== None
92 self
.target_arch
= arch
.get_architecture(requirements
.os
, requirements
.cpu
)
93 self
.requirements
= requirements
95 self
.stale_feeds
= set()
98 self
.config
= load_config(handler
)
100 assert handler
is None, "can't pass a handler and a 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
))]
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} """
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
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()
156 def usable_feeds(self
, iface
):
157 """Generator for C{iface.feeds} that are valid for our architecture.
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}
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
:
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
)
183 if self
._warned
_offline
:
184 debug(_("Not downloading feed '%s' because we are off-line."), feed_url
)
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.
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
202 assert isinstance(interface
, Interface
)
205 return self
.implementation
[interface
]
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
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
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
))
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
)
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.
261 try_quick_exit
= not (force
or update_local
)
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
:
269 try_quick_exit
= False
271 if not self
.solver
.ready
:
274 for f
in self
.solver
.feeds_used
:
275 if f
in downloads_finished
or f
in downloads_in_progress
:
279 self
.config
.iface_cache
.get_feed(f
, force
= True)
280 downloads_in_progress
[f
] = tasks
.IdleBlocker('Refresh local feed')
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.
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"))
296 # Wait for at least one download to finish
297 blockers
= downloads_in_progress
.values()
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
]
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
319 refreshed
= self
.solve_with_downloads(refresh
)
322 tasks
.check(refreshed
)
324 if not self
.solver
.ready
:
325 raise self
.solver
.get_failure_reason()
328 downloaded
= self
.download_uncached_implementations()
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)
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():
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
)],
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
)
366 modification_time
= None
368 existing_icon
= self
.config
.iface_cache
.get_icon_path(interface
)
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"""
379 warnings
.warn("Policy.get_interface is deprecated!", DeprecationWarning, stacklevel
= 2)
380 return self
.config
.iface_cache
.get_interface(uri
)
384 return self
.requirements
.command
388 return self
.requirements
.interface_uri
391 def get_deprecated_singleton_config():
394 _config
= load_config()