Make --show (and --get-selections) work with --set-selections
[zeroinstall/zeroinstall-limyreth.git] / zeroinstall / injector / cli.py
blob7fa76b88135a79da358d19925978135e8f52fdf0
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(selections.Selections(policy), options)
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 _get_selections(sels, options)
283 elif not options.download_only:
284 run.execute_selections(sels, prog_args, options.dry_run, options.main, options.wrapper)
286 except NeedDownload, ex:
287 # This only happens for dry runs
288 print ex
290 def _fork_gui(iface_uri, gui_args, prog_args, options = None):
291 """Run the GUI to get the selections.
292 prog_args and options are used only if the GUI requests a test.
294 from zeroinstall import helpers
295 def test_callback(sels):
296 from zeroinstall.injector import run
297 return run.test_selections(sels, prog_args,
298 bool(options and options.dry_run),
299 options and options.main)
300 return helpers.get_selections_gui(iface_uri, gui_args, test_callback)
302 def _download_missing_selections(options, sels):
303 from zeroinstall.injector import fetch
304 from zeroinstall.injector.handler import Handler
305 handler = Handler(dry_run = options.dry_run)
306 fetcher = fetch.Fetcher(handler)
307 blocker = sels.download_missing(iface_cache, fetcher)
308 if blocker:
309 logging.info(_("Waiting for selected implementations to be downloaded..."))
310 handler.wait_for_blocker(blocker)
312 def _get_selections(sels, options):
313 if options.show:
314 from zeroinstall import zerostore
315 done = set() # detect cycles
316 def print_node(uri, indent):
317 if uri in done: return
318 done.add(uri)
319 impl = sels.selections.get(uri, None)
320 print indent + "- URI:", uri
321 if impl:
322 print indent + " Version:", impl.version
323 try:
324 if impl.id.startswith('package:'):
325 path = "(" + impl.id + ")"
326 else:
327 path = impl.local_path or iface_cache.stores.lookup_any(impl.digests)
328 except zerostore.NotStored:
329 path = "(not cached)"
330 print indent + " Path:", path
331 indent += " "
332 for child in impl.dependencies:
333 if isinstance(child, model.InterfaceDependency):
334 print_node(child.interface, indent)
335 else:
336 print indent + " No selected version"
339 print_node(sels.interface, "")
340 else:
341 doc = sels.toDOM()
342 doc.writexml(sys.stdout)
343 sys.stdout.write('\n')
345 class UsageError(Exception): pass
347 def main(command_args):
348 """Act as if 0launch was run with the given arguments.
349 @arg command_args: array of arguments (e.g. C{sys.argv[1:]})
350 @type command_args: [str]
352 # Ensure stdin, stdout and stderr FDs exist, to avoid confusion
353 for std in (0, 1, 2):
354 try:
355 os.fstat(std)
356 except OSError:
357 fd = os.open('/dev/null', os.O_RDONLY)
358 if fd != std:
359 os.dup2(fd, std)
360 os.close(fd)
362 parser = OptionParser(usage=_("usage: %prog [options] interface [args]\n"
363 " %prog --list [search-term]\n"
364 " %prog --import [signed-interface-files]\n"
365 " %prog --feed [interface]"))
366 parser.add_option("", "--before", help=_("choose a version before this"), metavar='VERSION')
367 parser.add_option("-c", "--console", help=_("never use GUI"), action='store_false', dest='gui')
368 parser.add_option("", "--cpu", help=_("target CPU type"), metavar='CPU')
369 parser.add_option("-d", "--download-only", help=_("fetch but don't run"), action='store_true')
370 parser.add_option("-D", "--dry-run", help=_("just print actions"), action='store_true')
371 parser.add_option("-f", "--feed", help=_("add or remove a feed"), action='store_true')
372 parser.add_option("", "--get-selections", help=_("write selected versions as XML"), action='store_true')
373 parser.add_option("-g", "--gui", help=_("show graphical policy editor"), action='store_true')
374 parser.add_option("-i", "--import", help=_("import from files, not from the network"), action='store_true')
375 parser.add_option("-l", "--list", help=_("list all known interfaces"), action='store_true')
376 parser.add_option("-m", "--main", help=_("name of the file to execute"))
377 parser.add_option("", "--message", help=_("message to display when interacting with user"))
378 parser.add_option("", "--not-before", help=_("minimum version to choose"), metavar='VERSION')
379 parser.add_option("", "--os", help=_("target operation system type"), metavar='OS')
380 parser.add_option("-o", "--offline", help=_("try to avoid using the network"), action='store_true')
381 parser.add_option("-r", "--refresh", help=_("refresh all used interfaces"), action='store_true')
382 parser.add_option("", "--select-only", help=_("only download the feeds"), action='store_true')
383 parser.add_option("", "--set-selections", help=_("run versions specified in XML file"), metavar='FILE')
384 parser.add_option("", "--show", help=_("show where components are installed"), action='store_true')
385 parser.add_option("-s", "--source", help=_("select source code"), action='store_true')
386 parser.add_option("", "--systray", help=_("download in the background"), action='store_true')
387 parser.add_option("-v", "--verbose", help=_("more verbose output"), action='count')
388 parser.add_option("-V", "--version", help=_("display version information"), action='store_true')
389 parser.add_option("", "--with-store", help=_("add an implementation cache"), action='append', metavar='DIR')
390 parser.add_option("-w", "--wrapper", help=_("execute program using a debugger, etc"), metavar='COMMAND')
391 parser.disable_interspersed_args()
393 (options, args) = parser.parse_args(command_args)
395 if options.verbose:
396 logger = logging.getLogger()
397 if options.verbose == 1:
398 logger.setLevel(logging.INFO)
399 else:
400 logger.setLevel(logging.DEBUG)
401 import zeroinstall
402 logging.info(_("Running 0launch %(version)s %(args)s; Python %(python_version)s"), {'version': zeroinstall.version, 'args': repr(args), 'python_version': sys.version})
404 if options.select_only or options.show:
405 options.download_only = True
407 if options.show:
408 options.get_selections = True
410 if options.with_store:
411 from zeroinstall import zerostore
412 for x in options.with_store:
413 iface_cache.stores.stores.append(zerostore.Store(os.path.abspath(x)))
414 logging.info(_("Stores search path is now %s"), iface_cache.stores.stores)
416 try:
417 if options.list:
418 _list_interfaces(args)
419 elif options.version:
420 import zeroinstall
421 print "0launch (zero-install) " + zeroinstall.version
422 print "Copyright (C) 2010 Thomas Leonard"
423 print _("This program comes with ABSOLUTELY NO WARRANTY,"
424 "\nto the extent permitted by law."
425 "\nYou may redistribute copies of this program"
426 "\nunder the terms of the GNU Lesser General Public License."
427 "\nFor more information about these matters, see the file named COPYING.")
428 elif options.set_selections:
429 from zeroinstall.injector import qdom, run
430 sels = selections.Selections(qdom.parse(file(options.set_selections)))
431 _download_missing_selections(options, sels)
432 if options.get_selections:
433 _get_selections(sels, options)
434 elif not options.download_only:
435 run.execute_selections(sels, args, options.dry_run, options.main, options.wrapper)
436 elif getattr(options, 'import'):
437 _import_feed(args)
438 elif options.feed:
439 _manage_feeds(options, args)
440 else:
441 _normal_mode(options, args)
442 except UsageError:
443 parser.print_help()
444 sys.exit(1)
445 except SafeException, ex:
446 if options.verbose: raise
447 try:
448 print >>sys.stderr, unicode(ex)
449 except:
450 print >>sys.stderr, repr(ex)
451 sys.exit(1)