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