Added update_local attribute to solve_with_downloads
[zeroinstall/zeroinstall-afb.git] / zeroinstall / injector / cli.py
bloba4d4bd14ea012e05587fdf17db6d0724e1b936d8
1 """
2 The B{0launch} command-line interface.
4 This code is here, rather than in B{0launch} itself, simply so that it gets byte-compiled at
5 install time.
6 """
8 from zeroinstall import _
9 import os, sys
10 from optparse import OptionParser
11 import logging
13 from zeroinstall import SafeException, NeedDownload
14 from zeroinstall.injector import model, autopolicy, selections
15 from zeroinstall.injector.iface_cache import iface_cache
17 #def program_log(msg): os.access('MARK: 0launch: ' + msg, os.F_OK)
18 #import __main__
19 #__main__.__builtins__.program_log = program_log
20 #program_log('0launch ' + ' '.join((sys.argv[1:])))
22 def _list_interfaces(args):
23 if len(args) == 0:
24 matches = iface_cache.list_all_interfaces()
25 elif len(args) == 1:
26 match = args[0].lower()
27 matches = [i for i in iface_cache.list_all_interfaces() if match in i.lower()]
28 else:
29 raise UsageError()
31 matches.sort()
32 for i in matches:
33 print i
35 def _import_feed(args):
36 from zeroinstall.support import tasks
37 from zeroinstall.injector import gpg, handler
38 from zeroinstall.injector.iface_cache import PendingFeed
39 from xml.dom import minidom
40 handler = handler.Handler()
42 for x in args:
43 if not os.path.isfile(x):
44 raise SafeException(_("File '%s' does not exist") % x)
45 logging.info(_("Importing from file '%s'"), x)
46 signed_data = file(x)
47 data, sigs = gpg.check_stream(signed_data)
48 doc = minidom.parseString(data.read())
49 uri = doc.documentElement.getAttribute('uri')
50 if not uri:
51 raise SafeException(_("Missing 'uri' attribute on root element in '%s'") % x)
52 logging.info(_("Importing information about interface %s"), uri)
53 signed_data.seek(0)
55 pending = PendingFeed(uri, signed_data)
57 def run():
58 keys_downloaded = tasks.Task(pending.download_keys(handler), "download keys")
59 yield keys_downloaded.finished
60 tasks.check(keys_downloaded.finished)
61 if not iface_cache.update_feed_if_trusted(uri, pending.sigs, pending.new_xml):
62 from zeroinstall.injector import fetch
63 fetcher = fetch.Fetcher(handler)
64 blocker = handler.confirm_keys(pending, fetcher.fetch_key_info)
65 if blocker:
66 yield blocker
67 tasks.check(blocker)
68 if not iface_cache.update_feed_if_trusted(uri, pending.sigs, pending.new_xml):
69 raise SafeException(_("No signing keys trusted; not importing"))
71 task = tasks.Task(run(), "import feed")
73 errors = handler.wait_for_blocker(task.finished)
74 if errors:
75 raise SafeException(_("Errors during download: ") + '\n'.join(errors))
77 def _manage_feeds(options, args):
78 from zeroinstall.injector import writer
79 from zeroinstall.injector.handler import Handler
80 from zeroinstall.injector.policy import Policy
82 def find_feed_import(iface, feed_url):
83 for f in iface.extra_feeds:
84 if f.uri == feed_url:
85 return f
86 return None
88 handler = Handler(dry_run = options.dry_run)
89 if not args: raise UsageError()
90 for x in args:
91 print _("Feed '%s':") % x + '\n'
92 x = model.canonical_iface_uri(x)
93 policy = Policy(x, handler)
94 if options.offline:
95 policy.network_use = model.network_offline
97 feed = iface_cache.get_feed(x)
98 if policy.network_use != model.network_offline and policy.is_stale(feed):
99 blocker = policy.fetcher.download_and_import_feed(x, iface_cache.iface_cache)
100 print _("Downloading feed; please wait...")
101 handler.wait_for_blocker(blocker)
102 print _("Done")
104 interfaces = policy.get_feed_targets(x)
105 for i in range(len(interfaces)):
106 if find_feed_import(interfaces[i], x):
107 print _("%(index)d) Remove as feed for '%(uri)s'") % {'index': i + 1, 'uri': interfaces[i].uri}
108 else:
109 print _("%(index)d) Add as feed for '%(uri)s'") % {'index': i + 1, 'uri': interfaces[i].uri}
110 print
111 while True:
112 try:
113 i = raw_input(_('Enter a number, or CTRL-C to cancel [1]: ')).strip()
114 except KeyboardInterrupt:
115 print
116 raise SafeException(_("Aborted at user request."))
117 if i == '':
118 i = 1
119 else:
120 try:
121 i = int(i)
122 except ValueError:
123 i = 0
124 if i > 0 and i <= len(interfaces):
125 break
126 print _("Invalid number. Try again. (1 to %d)") % len(interfaces)
127 iface = interfaces[i - 1]
128 feed_import = find_feed_import(iface, x)
129 if feed_import:
130 iface.extra_feeds.remove(feed_import)
131 else:
132 iface.extra_feeds.append(model.Feed(x, arch = None, user_override = True))
133 writer.save_interface(iface)
134 print '\n' + _("Feed list for interface '%s' is now:") % iface.get_name()
135 if iface.extra_feeds:
136 for f in iface.extra_feeds:
137 print "- " + f.uri
138 else:
139 print _("(no feeds)")
141 def _normal_mode(options, args):
142 from zeroinstall.injector import handler
144 if len(args) < 1:
145 if options.gui:
146 from zeroinstall import helpers
147 return helpers.get_selections_gui(None, [])
148 else:
149 raise UsageError()
151 iface_uri = model.canonical_iface_uri(args[0])
152 root_iface = iface_cache.get_interface(iface_uri)
154 if os.isatty(1):
155 h = handler.ConsoleHandler()
156 else:
157 h = handler.Handler()
158 h.dry_run = bool(options.dry_run)
160 policy = autopolicy.AutoPolicy(iface_uri,
161 handler = h,
162 download_only = bool(options.download_only),
163 src = options.source)
165 if options.before or options.not_before:
166 policy.solver.extra_restrictions[root_iface] = [model.VersionRangeRestriction(model.parse_version(options.before),
167 model.parse_version(options.not_before))]
169 if options.os or options.cpu:
170 from zeroinstall.injector import arch
171 policy.target_arch = arch.get_architecture(options.os, options.cpu)
173 if options.offline:
174 policy.network_use = model.network_offline
176 if options.get_selections:
177 if len(args) > 1:
178 raise SafeException(_("Can't use arguments with --get-selections"))
179 if options.main:
180 raise SafeException(_("Can't use --main with --get-selections"))
182 # Note that need_download() triggers a solve
183 if options.refresh or options.gui:
184 # We could run immediately, but the user asked us not to
185 can_run_immediately = False
186 else:
187 if options.select_only:
188 # --select-only: we only care that we've made a selection, not that we've cached the implementations
189 policy.need_download()
190 can_run_immediately = policy.ready
191 else:
192 can_run_immediately = not policy.need_download()
194 stale_feeds = [feed for feed in policy.solver.feeds_used if
195 not feed.startswith('distribution:') and # Ignore (memory-only) PackageKit feeds
196 policy.is_stale(iface_cache.get_feed(feed))]
198 if options.download_only and stale_feeds:
199 can_run_immediately = False
201 if can_run_immediately:
202 if stale_feeds:
203 if policy.network_use == model.network_offline:
204 logging.debug(_("No doing background update because we are in off-line mode."))
205 else:
206 # There are feeds we should update, but we can run without them.
207 # Do the update in the background while the program is running.
208 import background
209 background.spawn_background_update(policy, options.verbose > 0)
210 if options.get_selections:
211 _get_selections(policy)
212 else:
213 if not options.download_only:
214 from zeroinstall.injector import run
215 run.execute(policy, args[1:], dry_run = options.dry_run, main = options.main, wrapper = options.wrapper)
216 else:
217 logging.info(_("Downloads done (download-only mode)"))
218 assert options.dry_run or options.download_only
219 return
221 # If the user didn't say whether to use the GUI, choose for them.
222 if options.gui is None and os.environ.get('DISPLAY', None):
223 options.gui = True
224 # If we need to download anything, we might as well
225 # refresh all the interfaces first. Also, this triggers
226 # the 'checking for updates' box, which is non-interactive
227 # when there are no changes to the selection.
228 options.refresh = True
229 logging.info(_("Switching to GUI mode... (use --console to disable)"))
231 prog_args = args[1:]
233 try:
234 from zeroinstall.injector import run
235 if options.gui:
236 gui_args = []
237 if options.download_only:
238 # Just changes the button's label
239 gui_args.append('--download-only')
240 if options.refresh:
241 gui_args.append('--refresh')
242 if options.systray:
243 gui_args.append('--systray')
244 if options.not_before:
245 gui_args.insert(0, options.not_before)
246 gui_args.insert(0, '--not-before')
247 if options.before:
248 gui_args.insert(0, options.before)
249 gui_args.insert(0, '--before')
250 if options.source:
251 gui_args.insert(0, '--source')
252 if options.message:
253 gui_args.insert(0, options.message)
254 gui_args.insert(0, '--message')
255 if options.verbose:
256 gui_args.insert(0, '--verbose')
257 if options.verbose > 1:
258 gui_args.insert(0, '--verbose')
259 if options.cpu:
260 gui_args.insert(0, options.cpu)
261 gui_args.insert(0, '--cpu')
262 if options.os:
263 gui_args.insert(0, options.os)
264 gui_args.insert(0, '--os')
265 if options.with_store:
266 for x in options.with_store:
267 gui_args += ['--with-store', x]
268 if options.select_only:
269 gui_args.append('--select-only')
270 sels = _fork_gui(iface_uri, gui_args, prog_args, options)
271 if not sels:
272 sys.exit(1) # Aborted
273 else:
274 # Note: --download-only also makes us stop and download stale feeds first.
275 downloaded = policy.solve_and_download_impls(refresh = options.refresh or options.download_only or False,
276 select_only = bool(options.select_only))
277 if downloaded:
278 policy.handler.wait_for_blocker(downloaded)
279 sels = selections.Selections(policy)
281 if options.get_selections:
282 doc = sels.toDOM()
283 doc.writexml(sys.stdout)
284 sys.stdout.write('\n')
285 elif not options.download_only:
286 run.execute_selections(sels, prog_args, options.dry_run, options.main, options.wrapper)
288 except NeedDownload, ex:
289 # This only happens for dry runs
290 print ex
292 def _fork_gui(iface_uri, gui_args, prog_args, options = None):
293 """Run the GUI to get the selections.
294 prog_args and options are used only if the GUI requests a test.
296 from zeroinstall import helpers
297 def test_callback(sels):
298 from zeroinstall.injector import run
299 return run.test_selections(sels, prog_args,
300 bool(options and options.dry_run),
301 options and options.main)
302 return helpers.get_selections_gui(iface_uri, gui_args, test_callback)
304 def _download_missing_selections(options, sels):
305 from zeroinstall.injector import fetch
306 from zeroinstall.injector.handler import Handler
307 handler = Handler(dry_run = options.dry_run)
308 fetcher = fetch.Fetcher(handler)
309 blocker = sels.download_missing(iface_cache, fetcher)
310 if blocker:
311 logging.info(_("Waiting for selected implementations to be downloaded..."))
312 handler.wait_for_blocker(blocker)
314 def _get_selections(policy):
315 doc = selections.Selections(policy).toDOM()
316 doc.writexml(sys.stdout)
317 sys.stdout.write('\n')
319 class UsageError(Exception): pass
321 def main(command_args):
322 """Act as if 0launch was run with the given arguments.
323 @arg command_args: array of arguments (e.g. C{sys.argv[1:]})
324 @type command_args: [str]
326 # Ensure stdin, stdout and stderr FDs exist, to avoid confusion
327 for std in (0, 1, 2):
328 try:
329 os.fstat(std)
330 except OSError:
331 fd = os.open('/dev/null', os.O_RDONLY)
332 if fd != std:
333 os.dup2(fd, std)
334 os.close(fd)
336 parser = OptionParser(usage=_("usage: %prog [options] interface [args]\n"
337 " %prog --list [search-term]\n"
338 " %prog --import [signed-interface-files]\n"
339 " %prog --feed [interface]"))
340 parser.add_option("", "--before", help=_("choose a version before this"), metavar='VERSION')
341 parser.add_option("-c", "--console", help=_("never use GUI"), action='store_false', dest='gui')
342 parser.add_option("", "--cpu", help=_("target CPU type"), metavar='CPU')
343 parser.add_option("-d", "--download-only", help=_("fetch but don't run"), action='store_true')
344 parser.add_option("-D", "--dry-run", help=_("just print actions"), action='store_true')
345 parser.add_option("-f", "--feed", help=_("add or remove a feed"), action='store_true')
346 parser.add_option("", "--get-selections", help=_("write selected versions as XML"), action='store_true')
347 parser.add_option("-g", "--gui", help=_("show graphical policy editor"), action='store_true')
348 parser.add_option("-i", "--import", help=_("import from files, not from the network"), action='store_true')
349 parser.add_option("-l", "--list", help=_("list all known interfaces"), action='store_true')
350 parser.add_option("-m", "--main", help=_("name of the file to execute"))
351 parser.add_option("", "--message", help=_("message to display when interacting with user"))
352 parser.add_option("", "--not-before", help=_("minimum version to choose"), metavar='VERSION')
353 parser.add_option("", "--os", help=_("target operation system type"), metavar='OS')
354 parser.add_option("-o", "--offline", help=_("try to avoid using the network"), action='store_true')
355 parser.add_option("-r", "--refresh", help=_("refresh all used interfaces"), action='store_true')
356 parser.add_option("", "--select-only", help=_("only download the feeds"), action='store_true')
357 parser.add_option("", "--set-selections", help=_("run versions specified in XML file"), metavar='FILE')
358 parser.add_option("-s", "--source", help=_("select source code"), action='store_true')
359 parser.add_option("", "--systray", help=_("download in the background"), action='store_true')
360 parser.add_option("-v", "--verbose", help=_("more verbose output"), action='count')
361 parser.add_option("-V", "--version", help=_("display version information"), action='store_true')
362 parser.add_option("", "--with-store", help=_("add an implementation cache"), action='append', metavar='DIR')
363 parser.add_option("-w", "--wrapper", help=_("execute program using a debugger, etc"), metavar='COMMAND')
364 parser.disable_interspersed_args()
366 (options, args) = parser.parse_args(command_args)
368 if options.verbose:
369 logger = logging.getLogger()
370 if options.verbose == 1:
371 logger.setLevel(logging.INFO)
372 else:
373 logger.setLevel(logging.DEBUG)
374 import zeroinstall
375 logging.info(_("Running 0launch %(version)s %(args)s; Python %(python_version)s"), {'version': zeroinstall.version, 'args': repr(args), 'python_version': sys.version})
377 if options.select_only:
378 options.download_only = True
380 if options.with_store:
381 from zeroinstall import zerostore
382 for x in options.with_store:
383 iface_cache.stores.stores.append(zerostore.Store(os.path.abspath(x)))
384 logging.info(_("Stores search path is now %s"), iface_cache.stores.stores)
386 try:
387 if options.list:
388 _list_interfaces(args)
389 elif options.version:
390 import zeroinstall
391 print "0launch (zero-install) " + zeroinstall.version
392 print "Copyright (C) 2009 Thomas Leonard"
393 print _("This program comes with ABSOLUTELY NO WARRANTY,"
394 "\nto the extent permitted by law."
395 "\nYou may redistribute copies of this program"
396 "\nunder the terms of the GNU Lesser General Public License."
397 "\nFor more information about these matters, see the file named COPYING.")
398 elif options.set_selections:
399 from zeroinstall.injector import qdom, run
400 sels = selections.Selections(qdom.parse(file(options.set_selections)))
401 _download_missing_selections(options, sels)
402 if not options.download_only:
403 run.execute_selections(sels, args, options.dry_run, options.main, options.wrapper)
404 elif getattr(options, 'import'):
405 _import_feed(args)
406 elif options.feed:
407 _manage_feeds(options, args)
408 else:
409 _normal_mode(options, args)
410 except UsageError:
411 parser.print_help()
412 sys.exit(1)
413 except SafeException, ex:
414 if options.verbose: raise
415 try:
416 print >>sys.stderr, unicode(ex)
417 except:
418 print >>sys.stderr, repr(ex)
419 sys.exit(1)