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 _
13 from logging
import info
, debug
, warn
16 from zeroinstall
import SafeException
17 from zeroinstall
.injector
import arch
18 from zeroinstall
.injector
.model
import Interface
, Implementation
, network_levels
, network_offline
, DistributionImplementation
, network_full
19 from zeroinstall
.injector
.namespaces
import config_site
, config_prog
20 from zeroinstall
.support
import tasks
, basedir
21 from zeroinstall
.injector
.iface_cache
import iface_cache
23 # If we started a check within this period, don't start another one:
24 FAILED_CHECK_DELAY
= 60 * 60 # 1 Hour
27 """Chooses a set of implementations based on a policy.
29 1. Create a Policy object, giving it the URI of the program to be run and a handler.
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}.
35 @ivar target_arch: target architecture for binaries
36 @type target_arch: L{arch.Architecture}
37 @ivar root: URI of the root interface
38 @ivar solver: solver used to choose a set of implementations
39 @type solver: L{solve.Solver}
40 @ivar watchers: callbacks to invoke after recalculating
41 @ivar help_with_testing: default stability policy
42 @type help_with_testing: bool
43 @ivar network_use: one of the model.network_* values
44 @ivar freshness: seconds allowed since last update
46 @ivar handler: handler for main-loop integration
47 @type handler: L{handler.Handler}
48 @ivar src: whether we are looking for source code
50 @ivar stale_feeds: set of feeds which are present but haven't been checked for a long time
51 @type stale_feeds: set
53 __slots__
= ['root', 'watchers', 'command',
54 'freshness', 'handler', '_warned_offline',
55 'target_arch', 'src', 'stale_feeds', 'solver', '_fetcher']
57 help_with_testing
= property(lambda self
: self
.solver
.help_with_testing
,
58 lambda self
, value
: setattr(self
.solver
, 'help_with_testing', value
))
60 network_use
= property(lambda self
: self
.solver
.network_use
,
61 lambda self
, value
: setattr(self
.solver
, 'network_use', value
))
63 implementation
= property(lambda self
: self
.solver
.selections
)
65 ready
= property(lambda self
: self
.solver
.ready
)
67 def __init__(self
, root
, handler
= None, src
= False, command
= -1):
69 @param root: The URI of the root interface (the program we want to run).
70 @param handler: A handler for main-loop integration.
71 @type handler: L{zeroinstall.injector.handler.Handler}
72 @param src: Whether we are looking for source code.
74 @param command: The name of the command to run (e.g. 'run', 'test', 'compile', etc)
78 self
.src
= src
# Root impl must be a "src" machine type
79 self
.stale_feeds
= set()
85 self
.command
= command
87 from zeroinstall
.injector
.solver
import DefaultSolver
88 self
.solver
= DefaultSolver(network_full
, iface_cache
, iface_cache
.stores
)
90 # If we need to download something but can't because we are offline,
91 # warn the user. But only the first time.
92 self
._warned
_offline
= False
95 # (allow self for backwards compat)
96 self
.handler
= handler
or self
98 debug(_("Supported systems: '%s'"), arch
.os_ranks
)
99 debug(_("Supported processors: '%s'"), arch
.machine_ranks
)
101 config
= ConfigParser
.ConfigParser()
102 config
.add_section('global')
103 config
.set('global', 'help_with_testing', 'False')
104 config
.set('global', 'freshness', str(60 * 60 * 24 * 30)) # One month
105 config
.set('global', 'network_use', 'full')
107 path
= basedir
.load_first_config(config_site
, config_prog
, 'global')
109 info("Loading configuration from %s", path
)
112 except Exception, ex
:
113 warn(_("Error loading config: %s"), str(ex
) or repr(ex
))
115 self
.solver
.help_with_testing
= config
.getboolean('global', 'help_with_testing')
116 self
.solver
.network_use
= config
.get('global', 'network_use')
117 self
.freshness
= int(config
.get('global', 'freshness'))
118 assert self
.solver
.network_use
in network_levels
, self
.solver
.network_use
122 self
.target_arch
= arch
.get_host_architecture()
126 if not self
._fetcher
:
128 self
._fetcher
= fetch
.Fetcher(self
.handler
)
131 def set_root(self
, root
):
132 """Change the root interface URI."""
133 assert isinstance(root
, (str, unicode))
135 for w
in self
.watchers
: w()
137 def save_config(self
):
138 """Write global settings."""
139 config
= ConfigParser
.ConfigParser()
140 config
.add_section('global')
142 config
.set('global', 'help_with_testing', self
.help_with_testing
)
143 config
.set('global', 'network_use', self
.network_use
)
144 config
.set('global', 'freshness', self
.freshness
)
146 path
= basedir
.save_config_path(config_site
, config_prog
)
147 path
= os
.path
.join(path
, 'global')
148 config
.write(file(path
+ '.new', 'w'))
149 os
.rename(path
+ '.new', path
)
151 def recalculate(self
, fetch_stale_interfaces
= True):
152 """@deprecated: see L{solve_with_downloads} """
154 warnings
.warn("Policy.recalculate is deprecated!", DeprecationWarning, stacklevel
= 2)
156 self
.stale_feeds
= set()
158 host_arch
= self
.target_arch
160 host_arch
= arch
.SourceArchitecture(host_arch
)
161 self
.solver
.solve(self
.root
, host_arch
, command_name
= self
.command
)
163 if self
.network_use
== network_offline
:
164 fetch_stale_interfaces
= False
167 for f
in self
.solver
.feeds_used
:
168 if os
.path
.isabs(f
): continue
169 feed
= iface_cache
.get_feed(f
)
170 if feed
is None or feed
.last_modified
is None:
171 self
.download_and_import_feed_if_online(f
) # Will start a download
172 elif self
.is_stale(feed
):
173 debug(_("Adding %s to stale set"), f
)
174 self
.stale_feeds
.add(iface_cache
.get_interface(f
)) # Legacy API
175 if fetch_stale_interfaces
:
176 self
.download_and_import_feed_if_online(f
) # Will start a download
178 for w
in self
.watchers
: w()
182 def usable_feeds(self
, iface
):
183 """Generator for C{iface.feeds} that are valid for our architecture.
186 if self
.src
and iface
.uri
== self
.root
:
187 # Note: when feeds are recursive, we'll need a better test for root here
188 machine_ranks
= {'src': 1}
190 machine_ranks
= arch
.machine_ranks
192 for f
in iface_cache
.get_feed_imports(iface
):
193 if f
.os
in arch
.os_ranks
and f
.machine
in machine_ranks
:
196 debug(_("Skipping '%(feed)s'; unsupported architecture %(os)s-%(machine)s"),
197 {'feed': f
, 'os': f
.os
, 'machine': f
.machine
})
199 def is_stale(self
, feed
):
200 """Check whether feed needs updating, based on the configured L{freshness}.
201 None is considered to be stale.
202 @return: true if feed is stale or missing."""
205 if os
.path
.isabs(feed
.url
):
206 return False # Local feeds are never stale
207 if feed
.last_modified
is None:
208 return True # Don't even have it yet
210 staleness
= now
- (feed
.last_checked
or 0)
211 debug(_("Staleness for %(feed)s is %(staleness).2f hours"), {'feed': feed
, 'staleness': staleness
/ 3600.0})
213 if self
.freshness
<= 0 or staleness
< self
.freshness
:
214 return False # Fresh enough for us
216 last_check_attempt
= iface_cache
.get_last_check_attempt(feed
.url
)
217 if last_check_attempt
and last_check_attempt
> now
- FAILED_CHECK_DELAY
:
218 debug(_("Stale, but tried to check recently (%s) so not rechecking now."), time
.ctime(last_check_attempt
))
223 def download_and_import_feed_if_online(self
, feed_url
):
224 """If we're online, call L{fetch.Fetcher.download_and_import_feed}. Otherwise, log a suitable warning."""
225 if self
.network_use
!= network_offline
:
226 debug(_("Feed %s not cached and not off-line. Downloading..."), feed_url
)
227 return self
.fetcher
.download_and_import_feed(feed_url
, iface_cache
)
229 if self
._warned
_offline
:
230 debug(_("Not downloading feed '%s' because we are off-line."), feed_url
)
232 warn(_("Not downloading feed '%s' because we are in off-line mode."), feed_url
)
233 self
._warned
_offline
= True
235 def get_implementation_path(self
, impl
):
236 """Return the local path of impl.
238 @raise zeroinstall.zerostore.NotStored: if it needs to be added to the cache first."""
239 assert isinstance(impl
, Implementation
)
240 return impl
.local_path
or iface_cache
.stores
.lookup_any(impl
.digests
)
242 def get_implementation(self
, interface
):
243 """Get the chosen implementation.
244 @type interface: Interface
245 @rtype: L{model.Implementation}
246 @raise SafeException: if interface has not been fetched or no implementation could be
248 assert isinstance(interface
, Interface
)
251 return self
.implementation
[interface
]
253 raise SafeException(_("No usable implementation found for '%s'.") % interface
.uri
)
255 def get_cached(self
, impl
):
256 """Check whether an implementation is available locally.
257 @type impl: model.Implementation
260 if isinstance(impl
, DistributionImplementation
):
261 return impl
.installed
263 return os
.path
.exists(impl
.local_path
)
266 path
= self
.get_implementation_path(impl
)
273 def get_uncached_implementations(self
):
274 """List all chosen implementations which aren't yet available locally.
275 @rtype: [(L{model.Interface}, L{model.Implementation})]"""
277 for iface
in self
.solver
.selections
:
278 impl
= self
.solver
.selections
[iface
]
279 assert impl
, self
.solver
.selections
280 if not self
.get_cached(impl
):
281 uncached
.append((iface
, impl
))
284 def refresh_all(self
, force
= True):
285 """Start downloading all feeds for all selected interfaces.
286 @param force: Whether to restart existing downloads."""
287 return self
.solve_with_downloads(force
= True)
289 def get_feed_targets(self
, feed_iface_uri
):
290 """Return a list of Interfaces for which feed_iface can be a feed.
291 This is used by B{0launch --feed}.
292 @rtype: [model.Interface]
293 @raise SafeException: If there are no known feeds."""
294 # TODO: what if it isn't cached yet?
295 feed_iface
= iface_cache
.get_interface(feed_iface_uri
)
296 if not feed_iface
.feed_for
:
297 if not feed_iface
.name
:
298 raise SafeException(_("Can't get feed targets for '%s'; failed to load interface.") %
300 raise SafeException(_("Missing <feed-for> element in '%s'; "
301 "this interface can't be used as a feed.") % feed_iface_uri
)
302 feed_targets
= feed_iface
.feed_for
303 debug(_("Feed targets: %s"), feed_targets
)
304 if not feed_iface
.name
:
305 warn(_("Warning: unknown interface '%s'") % feed_iface_uri
)
306 return [iface_cache
.get_interface(uri
) for uri
in feed_targets
]
309 def solve_with_downloads(self
, force
= False, update_local
= False):
310 """Run the solver, then download any feeds that are missing or
311 that need to be updated. Each time a new feed is imported into
312 the cache, the solver is run again, possibly adding new downloads.
313 @param force: whether to download even if we're already ready to run.
314 @param update_local: fetch PackageKit feeds even if we're ready to run."""
316 downloads_finished
= set() # Successful or otherwise
317 downloads_in_progress
= {} # URL -> Download
319 host_arch
= self
.target_arch
321 host_arch
= arch
.SourceArchitecture(host_arch
)
323 # There are three cases:
324 # 1. We want to run immediately if possible. If not, download all the information we can.
325 # (force = False, update_local = False)
326 # 2. We're in no hurry, but don't want to use the network unnecessarily.
327 # We should still update local information (from PackageKit).
328 # (force = False, update_local = True)
329 # 3. The user explicitly asked us to refresh everything.
332 try_quick_exit
= not (force
or update_local
)
335 self
.solver
.solve(self
.root
, host_arch
, command_name
= self
.command
)
336 for w
in self
.watchers
: w()
338 if try_quick_exit
and self
.solver
.ready
:
340 try_quick_exit
= False
342 if not self
.solver
.ready
:
345 for f
in self
.solver
.feeds_used
:
346 if f
in downloads_finished
or f
in downloads_in_progress
:
350 elif f
.startswith('distribution:'):
351 if force
or update_local
:
352 downloads_in_progress
[f
] = self
.fetcher
.download_and_import_feed(f
, iface_cache
)
353 elif force
and self
.network_use
!= network_offline
:
354 downloads_in_progress
[f
] = self
.fetcher
.download_and_import_feed(f
, iface_cache
)
355 # Once we've starting downloading some things,
356 # we might as well get them all.
359 if not downloads_in_progress
:
360 if self
.network_use
== network_offline
:
361 info(_("Can't choose versions and in off-line mode, so aborting"))
364 # Wait for at least one download to finish
365 blockers
= downloads_in_progress
.values()
367 tasks
.check(blockers
, self
.handler
.report_error
)
369 for f
in downloads_in_progress
.keys():
370 if f
in downloads_in_progress
and downloads_in_progress
[f
].happened
:
371 del downloads_in_progress
[f
]
372 downloads_finished
.add(f
)
374 # Need to refetch any "distribution" feed that
375 # depends on this one
376 distro_feed_url
= 'distribution:' + f
377 if distro_feed_url
in downloads_finished
:
378 downloads_finished
.remove(distro_feed_url
)
379 if distro_feed_url
in downloads_in_progress
:
380 del downloads_in_progress
[distro_feed_url
]
383 def solve_and_download_impls(self
, refresh
= False, select_only
= False):
384 """Run L{solve_with_downloads} and then get the selected implementations too.
385 @raise SafeException: if we couldn't select a set of implementations
387 refreshed
= self
.solve_with_downloads(refresh
)
390 tasks
.check(refreshed
)
392 if not self
.solver
.ready
:
393 raise self
.solver
.get_failure_reason()
396 downloaded
= self
.download_uncached_implementations()
399 tasks
.check(downloaded
)
401 def need_download(self
):
402 """Decide whether we need to download anything (but don't do it!)
403 @return: true if we MUST download something (feeds or implementations)
405 host_arch
= self
.target_arch
407 host_arch
= arch
.SourceArchitecture(host_arch
)
408 self
.solver
.solve(self
.root
, host_arch
, command_name
= self
.command
)
409 for w
in self
.watchers
: w()
411 if not self
.solver
.ready
:
412 return True # Maybe a newer version will work?
414 if self
.get_uncached_implementations():
419 def download_uncached_implementations(self
):
420 """Download all implementations chosen by the solver that are missing from the cache."""
421 assert self
.solver
.ready
, "Solver is not ready!\n%s" % self
.solver
.selections
422 return self
.fetcher
.download_impls([impl
for impl
in self
.solver
.selections
.values() if not self
.get_cached(impl
)],
425 def download_icon(self
, interface
, force
= False):
426 """Download an icon for this interface and add it to the
427 icon cache. If the interface has no icon or we are offline, do nothing.
428 @return: the task doing the import, or None
429 @rtype: L{tasks.Task}"""
430 if self
.network_use
== network_offline
:
431 info("Not downloading icon for %s as we are off-line", interface
)
434 modification_time
= None
436 existing_icon
= iface_cache
.get_icon_path(interface
)
438 file_mtime
= os
.stat(existing_icon
).st_mtime
439 from email
.utils
import formatdate
440 modification_time
= formatdate(timeval
= file_mtime
, localtime
= False, usegmt
= True)
442 return self
.fetcher
.download_icon(interface
, force
, modification_time
)
444 def get_interface(self
, uri
):
445 """@deprecated: use L{iface_cache.IfaceCache.get_interface} instead"""
447 warnings
.warn("Policy.get_interface is deprecated!", DeprecationWarning, stacklevel
= 2)
448 return iface_cache
.get_interface(uri
)