Fixed --show to include recursive runners
[zeroinstall/zeroinstall-afb.git] / zeroinstall / injector / cli.py
blob074118e5c4f3475c2dd66339250c14569987824d
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 command_name = options.command
161 if command_name is None:
162 command_name = 'run'
163 elif command_name == '':
164 command_name = None
165 policy = autopolicy.AutoPolicy(iface_uri,
166 handler = h,
167 download_only = bool(options.download_only),
168 src = options.source,
169 command = command_name)
171 if options.before or options.not_before:
172 policy.solver.extra_restrictions[root_iface] = [model.VersionRangeRestriction(model.parse_version(options.before),
173 model.parse_version(options.not_before))]
175 if options.os or options.cpu:
176 from zeroinstall.injector import arch
177 policy.target_arch = arch.get_architecture(options.os, options.cpu)
179 if options.offline:
180 policy.network_use = model.network_offline
182 if options.get_selections:
183 if len(args) > 1:
184 raise SafeException(_("Can't use arguments with --get-selections"))
185 if options.main:
186 raise SafeException(_("Can't use --main with --get-selections"))
188 # Note that need_download() triggers a solve
189 if options.refresh or options.gui:
190 # We could run immediately, but the user asked us not to
191 can_run_immediately = False
192 else:
193 if options.select_only:
194 # --select-only: we only care that we've made a selection, not that we've cached the implementations
195 policy.need_download()
196 can_run_immediately = policy.ready
197 else:
198 can_run_immediately = not policy.need_download()
200 stale_feeds = [feed for feed in policy.solver.feeds_used if
201 not feed.startswith('distribution:') and # Ignore (memory-only) PackageKit feeds
202 policy.is_stale(iface_cache.get_feed(feed))]
204 if options.download_only and stale_feeds:
205 can_run_immediately = False
207 if can_run_immediately:
208 if stale_feeds:
209 if policy.network_use == model.network_offline:
210 logging.debug(_("No doing background update because we are in off-line mode."))
211 else:
212 # There are feeds we should update, but we can run without them.
213 # Do the update in the background while the program is running.
214 import background
215 background.spawn_background_update(policy, options.verbose > 0)
216 if options.get_selections:
217 _get_selections(selections.Selections(policy), options)
218 else:
219 if not options.download_only:
220 from zeroinstall.injector import run
221 run.execute(policy, args[1:], dry_run = options.dry_run, main = options.main, wrapper = options.wrapper)
222 else:
223 logging.info(_("Downloads done (download-only mode)"))
224 assert options.dry_run or options.download_only
225 return
227 # If the user didn't say whether to use the GUI, choose for them.
228 if options.gui is None and os.environ.get('DISPLAY', None):
229 options.gui = True
230 # If we need to download anything, we might as well
231 # refresh all the interfaces first. Also, this triggers
232 # the 'checking for updates' box, which is non-interactive
233 # when there are no changes to the selection.
234 options.refresh = True
235 logging.info(_("Switching to GUI mode... (use --console to disable)"))
237 prog_args = args[1:]
239 try:
240 from zeroinstall.injector import run
241 if options.gui:
242 gui_args = []
243 if options.download_only:
244 # Just changes the button's label
245 gui_args.append('--download-only')
246 if options.refresh:
247 gui_args.append('--refresh')
248 if options.systray:
249 gui_args.append('--systray')
250 if options.not_before:
251 gui_args.insert(0, options.not_before)
252 gui_args.insert(0, '--not-before')
253 if options.before:
254 gui_args.insert(0, options.before)
255 gui_args.insert(0, '--before')
256 if options.source:
257 gui_args.insert(0, '--source')
258 if options.message:
259 gui_args.insert(0, options.message)
260 gui_args.insert(0, '--message')
261 if options.verbose:
262 gui_args.insert(0, '--verbose')
263 if options.verbose > 1:
264 gui_args.insert(0, '--verbose')
265 if options.cpu:
266 gui_args.insert(0, options.cpu)
267 gui_args.insert(0, '--cpu')
268 if options.os:
269 gui_args.insert(0, options.os)
270 gui_args.insert(0, '--os')
271 if options.with_store:
272 for x in options.with_store:
273 gui_args += ['--with-store', x]
274 if options.select_only:
275 gui_args.append('--select-only')
276 if command_name is not None:
277 gui_args.append('--command')
278 gui_args.append(command_name)
279 sels = _fork_gui(iface_uri, gui_args, prog_args, options)
280 if not sels:
281 sys.exit(1) # Aborted
282 else:
283 # Note: --download-only also makes us stop and download stale feeds first.
284 downloaded = policy.solve_and_download_impls(refresh = options.refresh or options.download_only or False,
285 select_only = bool(options.select_only))
286 if downloaded:
287 policy.handler.wait_for_blocker(downloaded)
288 sels = selections.Selections(policy)
290 if options.get_selections:
291 _get_selections(sels, options)
292 elif not options.download_only:
293 run.execute_selections(sels, prog_args, options.dry_run, options.main, options.wrapper)
295 except NeedDownload, ex:
296 # This only happens for dry runs
297 print ex
299 def _fork_gui(iface_uri, gui_args, prog_args, options = None):
300 """Run the GUI to get the selections.
301 prog_args and options are used only if the GUI requests a test.
303 from zeroinstall import helpers
304 def test_callback(sels):
305 from zeroinstall.injector import run
306 return run.test_selections(sels, prog_args,
307 bool(options and options.dry_run),
308 options and options.main)
309 return helpers.get_selections_gui(iface_uri, gui_args, test_callback)
311 def _download_missing_selections(options, sels):
312 from zeroinstall.injector import fetch
313 from zeroinstall.injector.handler import Handler
314 handler = Handler(dry_run = options.dry_run)
315 fetcher = fetch.Fetcher(handler)
316 blocker = sels.download_missing(iface_cache, fetcher)
317 if blocker:
318 logging.info(_("Waiting for selected implementations to be downloaded..."))
319 handler.wait_for_blocker(blocker)
321 def _get_selections(sels, options):
322 if options.show:
323 from zeroinstall import zerostore
324 done = set() # detect cycles
325 def print_node(uri, command, indent):
326 if uri in done: return
327 done.add(uri)
328 impl = sels.selections.get(uri, None)
329 print indent + "- URI:", uri
330 if impl:
331 print indent + " Version:", impl.version
332 try:
333 if impl.id.startswith('package:'):
334 path = "(" + impl.id + ")"
335 else:
336 path = impl.local_path or iface_cache.stores.lookup_any(impl.digests)
337 except zerostore.NotStored:
338 path = "(not cached)"
339 print indent + " Path:", path
340 indent += " "
341 deps = impl.dependencies
342 if command is not None:
343 deps += sels.commands[command].requires
344 for child in deps:
345 if isinstance(child, model.InterfaceDependency):
346 if child.qdom.name == 'runner':
347 child_command = command + 1
348 else:
349 child_command = None
350 print_node(child.interface, child_command, indent)
351 else:
352 print indent + " No selected version"
355 if sels.commands:
356 print_node(sels.interface, 0, "")
357 else:
358 print_node(sels.interface, None, "")
360 else:
361 doc = sels.toDOM()
362 doc.writexml(sys.stdout)
363 sys.stdout.write('\n')
365 class UsageError(Exception): pass
367 def main(command_args):
368 """Act as if 0launch was run with the given arguments.
369 @arg command_args: array of arguments (e.g. C{sys.argv[1:]})
370 @type command_args: [str]
372 # Ensure stdin, stdout and stderr FDs exist, to avoid confusion
373 for std in (0, 1, 2):
374 try:
375 os.fstat(std)
376 except OSError:
377 fd = os.open('/dev/null', os.O_RDONLY)
378 if fd != std:
379 os.dup2(fd, std)
380 os.close(fd)
382 parser = OptionParser(usage=_("usage: %prog [options] interface [args]\n"
383 " %prog --list [search-term]\n"
384 " %prog --import [signed-interface-files]\n"
385 " %prog --feed [interface]"))
386 parser.add_option("", "--before", help=_("choose a version before this"), metavar='VERSION')
387 parser.add_option("", "--command", help=_("command to select"), metavar='COMMAND')
388 parser.add_option("-c", "--console", help=_("never use GUI"), action='store_false', dest='gui')
389 parser.add_option("", "--cpu", help=_("target CPU type"), metavar='CPU')
390 parser.add_option("-d", "--download-only", help=_("fetch but don't run"), action='store_true')
391 parser.add_option("-D", "--dry-run", help=_("just print actions"), action='store_true')
392 parser.add_option("-f", "--feed", help=_("add or remove a feed"), action='store_true')
393 parser.add_option("", "--get-selections", help=_("write selected versions as XML"), action='store_true')
394 parser.add_option("-g", "--gui", help=_("show graphical policy editor"), action='store_true')
395 parser.add_option("-i", "--import", help=_("import from files, not from the network"), action='store_true')
396 parser.add_option("-l", "--list", help=_("list all known interfaces"), action='store_true')
397 parser.add_option("-m", "--main", help=_("name of the file to execute"))
398 parser.add_option("", "--message", help=_("message to display when interacting with user"))
399 parser.add_option("", "--not-before", help=_("minimum version to choose"), metavar='VERSION')
400 parser.add_option("", "--os", help=_("target operation system type"), metavar='OS')
401 parser.add_option("-o", "--offline", help=_("try to avoid using the network"), action='store_true')
402 parser.add_option("-r", "--refresh", help=_("refresh all used interfaces"), action='store_true')
403 parser.add_option("", "--select-only", help=_("only download the feeds"), action='store_true')
404 parser.add_option("", "--set-selections", help=_("run versions specified in XML file"), metavar='FILE')
405 parser.add_option("", "--show", help=_("show where components are installed"), action='store_true')
406 parser.add_option("-s", "--source", help=_("select source code"), action='store_true')
407 parser.add_option("", "--systray", help=_("download in the background"), action='store_true')
408 parser.add_option("-v", "--verbose", help=_("more verbose output"), action='count')
409 parser.add_option("-V", "--version", help=_("display version information"), action='store_true')
410 parser.add_option("", "--with-store", help=_("add an implementation cache"), action='append', metavar='DIR')
411 parser.add_option("-w", "--wrapper", help=_("execute program using a debugger, etc"), metavar='COMMAND')
412 parser.disable_interspersed_args()
414 (options, args) = parser.parse_args(command_args)
416 if options.verbose:
417 logger = logging.getLogger()
418 if options.verbose == 1:
419 logger.setLevel(logging.INFO)
420 else:
421 logger.setLevel(logging.DEBUG)
422 import zeroinstall
423 logging.info(_("Running 0launch %(version)s %(args)s; Python %(python_version)s"), {'version': zeroinstall.version, 'args': repr(args), 'python_version': sys.version})
425 if options.select_only or options.show:
426 options.download_only = True
428 if options.show:
429 options.get_selections = True
431 if options.with_store:
432 from zeroinstall import zerostore
433 for x in options.with_store:
434 iface_cache.stores.stores.append(zerostore.Store(os.path.abspath(x)))
435 logging.info(_("Stores search path is now %s"), iface_cache.stores.stores)
437 try:
438 if options.list:
439 _list_interfaces(args)
440 elif options.version:
441 import zeroinstall
442 print "0launch (zero-install) " + zeroinstall.version
443 print "Copyright (C) 2010 Thomas Leonard"
444 print _("This program comes with ABSOLUTELY NO WARRANTY,"
445 "\nto the extent permitted by law."
446 "\nYou may redistribute copies of this program"
447 "\nunder the terms of the GNU Lesser General Public License."
448 "\nFor more information about these matters, see the file named COPYING.")
449 elif options.set_selections:
450 from zeroinstall.injector import qdom, run
451 sels = selections.Selections(qdom.parse(file(options.set_selections)))
452 _download_missing_selections(options, sels)
453 if options.get_selections:
454 _get_selections(sels, options)
455 elif not options.download_only:
456 run.execute_selections(sels, args, options.dry_run, options.main, options.wrapper)
457 elif getattr(options, 'import'):
458 _import_feed(args)
459 elif options.feed:
460 _manage_feeds(options, args)
461 else:
462 _normal_mode(options, args)
463 except UsageError:
464 parser.print_help()
465 sys.exit(1)
466 except SafeException, ex:
467 if options.verbose: raise
468 try:
469 print >>sys.stderr, unicode(ex)
470 except:
471 print >>sys.stderr, repr(ex)
472 sys.exit(1)