Added '0install import' sub-command
[zeroinstall.git] / zeroinstall / injector / cli.py
blob515a2726aa9b78c9ba3adc72a137832898ce1ea8
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 _manage_feeds(options, args):
36 from zeroinstall.injector import writer
37 from zeroinstall.injector.handler import Handler
38 from zeroinstall.injector.policy import Policy
40 def find_feed_import(iface, feed_url):
41 for f in iface.extra_feeds:
42 if f.uri == feed_url:
43 return f
44 return None
46 handler = Handler(dry_run = options.dry_run)
47 if not args: raise UsageError()
48 for x in args:
49 print _("Feed '%s':") % x + '\n'
50 x = model.canonical_iface_uri(x)
51 policy = Policy(x, handler)
52 if options.offline:
53 policy.network_use = model.network_offline
55 feed = iface_cache.get_feed(x)
56 if policy.network_use != model.network_offline and policy.is_stale(feed):
57 blocker = policy.fetcher.download_and_import_feed(x, iface_cache.iface_cache)
58 print _("Downloading feed; please wait...")
59 handler.wait_for_blocker(blocker)
60 print _("Done")
62 interfaces = policy.get_feed_targets(x)
63 for i in range(len(interfaces)):
64 if find_feed_import(interfaces[i], x):
65 print _("%(index)d) Remove as feed for '%(uri)s'") % {'index': i + 1, 'uri': interfaces[i].uri}
66 else:
67 print _("%(index)d) Add as feed for '%(uri)s'") % {'index': i + 1, 'uri': interfaces[i].uri}
68 print
69 while True:
70 try:
71 i = raw_input(_('Enter a number, or CTRL-C to cancel [1]: ')).strip()
72 except KeyboardInterrupt:
73 print
74 raise SafeException(_("Aborted at user request."))
75 if i == '':
76 i = 1
77 else:
78 try:
79 i = int(i)
80 except ValueError:
81 i = 0
82 if i > 0 and i <= len(interfaces):
83 break
84 print _("Invalid number. Try again. (1 to %d)") % len(interfaces)
85 iface = interfaces[i - 1]
86 feed_import = find_feed_import(iface, x)
87 if feed_import:
88 iface.extra_feeds.remove(feed_import)
89 else:
90 iface.extra_feeds.append(model.Feed(x, arch = None, user_override = True))
91 writer.save_interface(iface)
92 print '\n' + _("Feed list for interface '%s' is now:") % iface.get_name()
93 if iface.extra_feeds:
94 for f in iface.extra_feeds:
95 print "- " + f.uri
96 else:
97 print _("(no feeds)")
99 def _normal_mode(options, args):
100 from zeroinstall.injector import handler
102 if len(args) < 1:
103 if options.gui:
104 from zeroinstall import helpers
105 return helpers.get_selections_gui(None, [])
106 else:
107 raise UsageError()
109 iface_uri = model.canonical_iface_uri(args[0])
110 root_iface = iface_cache.get_interface(iface_uri)
112 if os.isatty(1):
113 h = handler.ConsoleHandler()
114 else:
115 h = handler.Handler()
116 h.dry_run = bool(options.dry_run)
118 command_name = options.command
119 if command_name is None:
120 command_name = 'run'
121 elif command_name == '':
122 command_name = None
123 policy = autopolicy.AutoPolicy(iface_uri,
124 handler = h,
125 download_only = bool(options.download_only),
126 src = options.source,
127 command = command_name)
129 if options.before or options.not_before:
130 policy.solver.extra_restrictions[root_iface] = [model.VersionRangeRestriction(model.parse_version(options.before),
131 model.parse_version(options.not_before))]
133 if options.os or options.cpu:
134 from zeroinstall.injector import arch
135 policy.target_arch = arch.get_architecture(options.os, options.cpu)
137 if options.offline:
138 policy.network_use = model.network_offline
140 if options.get_selections:
141 if len(args) > 1:
142 raise SafeException(_("Can't use arguments with --get-selections"))
143 if options.main:
144 raise SafeException(_("Can't use --main with --get-selections"))
146 # Note that need_download() triggers a solve
147 if options.refresh or options.gui:
148 # We could run immediately, but the user asked us not to
149 can_run_immediately = False
150 else:
151 if options.select_only:
152 # --select-only: we only care that we've made a selection, not that we've cached the implementations
153 policy.need_download()
154 can_run_immediately = policy.ready
155 else:
156 can_run_immediately = not policy.need_download()
158 stale_feeds = [feed for feed in policy.solver.feeds_used if
159 not feed.startswith('distribution:') and # Ignore (memory-only) PackageKit feeds
160 policy.is_stale(iface_cache.get_feed(feed))]
162 if options.download_only and stale_feeds:
163 can_run_immediately = False
165 if can_run_immediately:
166 if stale_feeds:
167 if policy.network_use == model.network_offline:
168 logging.debug(_("No doing background update because we are in off-line mode."))
169 else:
170 # There are feeds we should update, but we can run without them.
171 # Do the update in the background while the program is running.
172 import background
173 background.spawn_background_update(policy, options.verbose > 0)
174 if options.get_selections:
175 _get_selections(selections.Selections(policy), options)
176 else:
177 if not options.download_only:
178 from zeroinstall.injector import run
179 run.execute(policy, args[1:], dry_run = options.dry_run, main = options.main, wrapper = options.wrapper)
180 else:
181 logging.info(_("Downloads done (download-only mode)"))
182 assert options.dry_run or options.download_only
183 return
185 # If the user didn't say whether to use the GUI, choose for them.
186 if options.gui is None and os.environ.get('DISPLAY', None):
187 options.gui = True
188 # If we need to download anything, we might as well
189 # refresh all the interfaces first. Also, this triggers
190 # the 'checking for updates' box, which is non-interactive
191 # when there are no changes to the selection.
192 options.refresh = True
193 logging.info(_("Switching to GUI mode... (use --console to disable)"))
195 prog_args = args[1:]
197 try:
198 from zeroinstall.injector import run
199 if options.gui:
200 gui_args = []
201 if options.download_only:
202 # Just changes the button's label
203 gui_args.append('--download-only')
204 if options.refresh:
205 gui_args.append('--refresh')
206 if options.systray:
207 gui_args.append('--systray')
208 if options.not_before:
209 gui_args.insert(0, options.not_before)
210 gui_args.insert(0, '--not-before')
211 if options.before:
212 gui_args.insert(0, options.before)
213 gui_args.insert(0, '--before')
214 if options.source:
215 gui_args.insert(0, '--source')
216 if options.message:
217 gui_args.insert(0, options.message)
218 gui_args.insert(0, '--message')
219 if options.verbose:
220 gui_args.insert(0, '--verbose')
221 if options.verbose > 1:
222 gui_args.insert(0, '--verbose')
223 if options.cpu:
224 gui_args.insert(0, options.cpu)
225 gui_args.insert(0, '--cpu')
226 if options.os:
227 gui_args.insert(0, options.os)
228 gui_args.insert(0, '--os')
229 if options.with_store:
230 for x in options.with_store:
231 gui_args += ['--with-store', x]
232 if options.select_only:
233 gui_args.append('--select-only')
234 if command_name is not None:
235 gui_args.append('--command')
236 gui_args.append(command_name)
237 sels = _fork_gui(iface_uri, gui_args, prog_args, options)
238 if not sels:
239 sys.exit(1) # Aborted
240 else:
241 # Note: --download-only also makes us stop and download stale feeds first.
242 downloaded = policy.solve_and_download_impls(refresh = options.refresh or options.download_only or False,
243 select_only = bool(options.select_only))
244 if downloaded:
245 policy.handler.wait_for_blocker(downloaded)
246 sels = selections.Selections(policy)
248 if options.get_selections:
249 _get_selections(sels, options)
250 elif not options.download_only:
251 run.execute_selections(sels, prog_args, options.dry_run, options.main, options.wrapper)
253 except NeedDownload, ex:
254 # This only happens for dry runs
255 print ex
257 def _fork_gui(iface_uri, gui_args, prog_args, options = None):
258 """Run the GUI to get the selections.
259 prog_args and options are used only if the GUI requests a test.
261 from zeroinstall import helpers
262 def test_callback(sels):
263 from zeroinstall.injector import run
264 return run.test_selections(sels, prog_args,
265 bool(options and options.dry_run),
266 options and options.main)
267 return helpers.get_selections_gui(iface_uri, gui_args, test_callback)
269 def _download_missing_selections(options, sels):
270 from zeroinstall.injector import fetch
271 from zeroinstall.injector.handler import Handler
272 handler = Handler(dry_run = options.dry_run)
273 fetcher = fetch.Fetcher(handler)
274 blocker = sels.download_missing(iface_cache, fetcher)
275 if blocker:
276 logging.info(_("Waiting for selected implementations to be downloaded..."))
277 handler.wait_for_blocker(blocker)
279 def _get_selections(sels, options):
280 if options.show:
281 from zeroinstall import zerostore
282 done = set() # detect cycles
283 def print_node(uri, command, indent):
284 if uri in done: return
285 done.add(uri)
286 impl = sels.selections.get(uri, None)
287 print indent + "- URI:", uri
288 if impl:
289 print indent + " Version:", impl.version
290 try:
291 if impl.id.startswith('package:'):
292 path = "(" + impl.id + ")"
293 else:
294 path = impl.local_path or iface_cache.stores.lookup_any(impl.digests)
295 except zerostore.NotStored:
296 path = "(not cached)"
297 print indent + " Path:", path
298 indent += " "
299 deps = impl.dependencies
300 if command is not None:
301 deps += sels.commands[command].requires
302 for child in deps:
303 if isinstance(child, model.InterfaceDependency):
304 if child.qdom.name == 'runner':
305 child_command = command + 1
306 else:
307 child_command = None
308 print_node(child.interface, child_command, indent)
309 else:
310 print indent + " No selected version"
313 if sels.commands:
314 print_node(sels.interface, 0, "")
315 else:
316 print_node(sels.interface, None, "")
318 else:
319 doc = sels.toDOM()
320 doc.writexml(sys.stdout)
321 sys.stdout.write('\n')
323 class UsageError(Exception): pass
325 def main(command_args):
326 """Act as if 0launch was run with the given arguments.
327 @arg command_args: array of arguments (e.g. C{sys.argv[1:]})
328 @type command_args: [str]
330 # Ensure stdin, stdout and stderr FDs exist, to avoid confusion
331 for std in (0, 1, 2):
332 try:
333 os.fstat(std)
334 except OSError:
335 fd = os.open('/dev/null', os.O_RDONLY)
336 if fd != std:
337 os.dup2(fd, std)
338 os.close(fd)
340 parser = OptionParser(usage=_("usage: %prog [options] interface [args]\n"
341 " %prog --list [search-term]\n"
342 " %prog --import [signed-interface-files]\n"
343 " %prog --feed [interface]"))
344 parser.add_option("", "--before", help=_("choose a version before this"), metavar='VERSION')
345 parser.add_option("", "--command", help=_("command to select"), metavar='COMMAND')
346 parser.add_option("-c", "--console", help=_("never use GUI"), action='store_false', dest='gui')
347 parser.add_option("", "--cpu", help=_("target CPU type"), metavar='CPU')
348 parser.add_option("-d", "--download-only", help=_("fetch but don't run"), action='store_true')
349 parser.add_option("-D", "--dry-run", help=_("just print actions"), action='store_true')
350 parser.add_option("-f", "--feed", help=_("add or remove a feed"), action='store_true')
351 parser.add_option("", "--get-selections", help=_("write selected versions as XML"), action='store_true')
352 parser.add_option("-g", "--gui", help=_("show graphical policy editor"), action='store_true')
353 parser.add_option("-i", "--import", help=_("import from files, not from the network"), action='store_true')
354 parser.add_option("-l", "--list", help=_("list all known interfaces"), action='store_true')
355 parser.add_option("-m", "--main", help=_("name of the file to execute"))
356 parser.add_option("", "--message", help=_("message to display when interacting with user"))
357 parser.add_option("", "--not-before", help=_("minimum version to choose"), metavar='VERSION')
358 parser.add_option("", "--os", help=_("target operation system type"), metavar='OS')
359 parser.add_option("-o", "--offline", help=_("try to avoid using the network"), action='store_true')
360 parser.add_option("-r", "--refresh", help=_("refresh all used interfaces"), action='store_true')
361 parser.add_option("", "--select-only", help=_("only download the feeds"), action='store_true')
362 parser.add_option("", "--set-selections", help=_("run versions specified in XML file"), metavar='FILE')
363 parser.add_option("", "--show", help=_("show where components are installed"), action='store_true')
364 parser.add_option("-s", "--source", help=_("select source code"), action='store_true')
365 parser.add_option("", "--systray", help=_("download in the background"), action='store_true')
366 parser.add_option("-v", "--verbose", help=_("more verbose output"), action='count')
367 parser.add_option("-V", "--version", help=_("display version information"), action='store_true')
368 parser.add_option("", "--with-store", help=_("add an implementation cache"), action='append', metavar='DIR')
369 parser.add_option("-w", "--wrapper", help=_("execute program using a debugger, etc"), metavar='COMMAND')
370 parser.disable_interspersed_args()
372 (options, args) = parser.parse_args(command_args)
374 if options.verbose:
375 logger = logging.getLogger()
376 if options.verbose == 1:
377 logger.setLevel(logging.INFO)
378 else:
379 logger.setLevel(logging.DEBUG)
380 import zeroinstall
381 logging.info(_("Running 0launch %(version)s %(args)s; Python %(python_version)s"), {'version': zeroinstall.version, 'args': repr(args), 'python_version': sys.version})
383 if options.select_only or options.show:
384 options.download_only = True
386 if options.show:
387 options.get_selections = True
389 if options.with_store:
390 from zeroinstall import zerostore
391 for x in options.with_store:
392 iface_cache.stores.stores.append(zerostore.Store(os.path.abspath(x)))
393 logging.info(_("Stores search path is now %s"), iface_cache.stores.stores)
395 try:
396 if options.list:
397 _list_interfaces(args)
398 elif options.version:
399 import zeroinstall
400 print "0launch (zero-install) " + zeroinstall.version
401 print "Copyright (C) 2010 Thomas Leonard"
402 print _("This program comes with ABSOLUTELY NO WARRANTY,"
403 "\nto the extent permitted by law."
404 "\nYou may redistribute copies of this program"
405 "\nunder the terms of the GNU Lesser General Public License."
406 "\nFor more information about these matters, see the file named COPYING.")
407 elif options.set_selections:
408 from zeroinstall.injector import qdom, run
409 sels = selections.Selections(qdom.parse(file(options.set_selections)))
410 _download_missing_selections(options, sels)
411 if options.get_selections:
412 _get_selections(sels, options)
413 elif not options.download_only:
414 run.execute_selections(sels, args, options.dry_run, options.main, options.wrapper)
415 elif getattr(options, 'import'):
416 # (import is a keyword)
417 cmd = __import__('zeroinstall.cmd.import', globals(), locals(), ["import"], 0)
418 cmd.handle(options, args)
419 elif options.feed:
420 _manage_feeds(options, args)
421 else:
422 _normal_mode(options, args)
423 except UsageError:
424 parser.print_help()
425 sys.exit(1)
426 except SafeException, ex:
427 if options.verbose: raise
428 try:
429 print >>sys.stderr, unicode(ex)
430 except:
431 print >>sys.stderr, repr(ex)
432 sys.exit(1)