Fixed parsing of Ports package names
[zeroinstall/solver.git] / zeroinstall / injector / cli.py
blobb1b18182432160d648f3faaea87e691b5bfec27a
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 policy.is_stale(iface_cache.get_feed(feed))]
196 if options.download_only and stale_feeds:
197 can_run_immediately = False
199 if can_run_immediately:
200 if stale_feeds:
201 if policy.network_use == model.network_offline:
202 logging.debug(_("No doing background update because we are in off-line mode."))
203 else:
204 # There are feeds we should update, but we can run without them.
205 # Do the update in the background while the program is running.
206 import background
207 background.spawn_background_update(policy, options.verbose > 0)
208 if options.get_selections:
209 _get_selections(policy)
210 else:
211 if not options.download_only:
212 from zeroinstall.injector import run
213 run.execute(policy, args[1:], dry_run = options.dry_run, main = options.main, wrapper = options.wrapper)
214 else:
215 logging.info(_("Downloads done (download-only mode)"))
216 assert options.dry_run or options.download_only
217 return
219 # If the user didn't say whether to use the GUI, choose for them.
220 if options.gui is None and os.environ.get('DISPLAY', None):
221 options.gui = True
222 # If we need to download anything, we might as well
223 # refresh all the interfaces first. Also, this triggers
224 # the 'checking for updates' box, which is non-interactive
225 # when there are no changes to the selection.
226 options.refresh = True
227 logging.info(_("Switching to GUI mode... (use --console to disable)"))
229 prog_args = args[1:]
231 try:
232 from zeroinstall.injector import run
233 if options.gui:
234 gui_args = []
235 if options.download_only:
236 # Just changes the button's label
237 gui_args.append('--download-only')
238 if options.refresh:
239 gui_args.append('--refresh')
240 if options.systray:
241 gui_args.append('--systray')
242 if options.not_before:
243 gui_args.insert(0, options.not_before)
244 gui_args.insert(0, '--not-before')
245 if options.before:
246 gui_args.insert(0, options.before)
247 gui_args.insert(0, '--before')
248 if options.source:
249 gui_args.insert(0, '--source')
250 if options.message:
251 gui_args.insert(0, options.message)
252 gui_args.insert(0, '--message')
253 if options.verbose:
254 gui_args.insert(0, '--verbose')
255 if options.verbose > 1:
256 gui_args.insert(0, '--verbose')
257 if options.cpu:
258 gui_args.insert(0, options.cpu)
259 gui_args.insert(0, '--cpu')
260 if options.os:
261 gui_args.insert(0, options.os)
262 gui_args.insert(0, '--os')
263 if options.with_store:
264 for x in options.with_store:
265 gui_args += ['--with-store', x]
266 if options.select_only:
267 gui_args.append('--select-only')
268 sels = _fork_gui(iface_uri, gui_args, prog_args, options)
269 if not sels:
270 sys.exit(1) # Aborted
271 else:
272 # Note: --download-only also makes us stop and download stale feeds first.
273 downloaded = policy.solve_and_download_impls(refresh = options.refresh or options.download_only or False,
274 select_only = bool(options.select_only))
275 if downloaded:
276 policy.handler.wait_for_blocker(downloaded)
277 sels = selections.Selections(policy)
279 if options.get_selections:
280 doc = sels.toDOM()
281 doc.writexml(sys.stdout)
282 sys.stdout.write('\n')
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(policy):
313 doc = selections.Selections(policy).toDOM()
314 doc.writexml(sys.stdout)
315 sys.stdout.write('\n')
317 class UsageError(Exception): pass
319 def main(command_args):
320 """Act as if 0launch was run with the given arguments.
321 @arg command_args: array of arguments (e.g. C{sys.argv[1:]})
322 @type command_args: [str]
324 # Ensure stdin, stdout and stderr FDs exist, to avoid confusion
325 for std in (0, 1, 2):
326 try:
327 os.fstat(std)
328 except OSError:
329 fd = os.open('/dev/null', os.O_RDONLY)
330 if fd != std:
331 os.dup2(fd, std)
332 os.close(fd)
334 parser = OptionParser(usage=_("usage: %prog [options] interface [args]\n"
335 " %prog --list [search-term]\n"
336 " %prog --import [signed-interface-files]\n"
337 " %prog --feed [interface]"))
338 parser.add_option("", "--before", help=_("choose a version before this"), metavar='VERSION')
339 parser.add_option("-c", "--console", help=_("never use GUI"), action='store_false', dest='gui')
340 parser.add_option("", "--cpu", help=_("target CPU type"), metavar='CPU')
341 parser.add_option("-d", "--download-only", help=_("fetch but don't run"), action='store_true')
342 parser.add_option("-D", "--dry-run", help=_("just print actions"), action='store_true')
343 parser.add_option("-f", "--feed", help=_("add or remove a feed"), action='store_true')
344 parser.add_option("", "--get-selections", help=_("write selected versions as XML"), action='store_true')
345 parser.add_option("-g", "--gui", help=_("show graphical policy editor"), action='store_true')
346 parser.add_option("-i", "--import", help=_("import from files, not from the network"), action='store_true')
347 parser.add_option("-l", "--list", help=_("list all known interfaces"), action='store_true')
348 parser.add_option("-m", "--main", help=_("name of the file to execute"))
349 parser.add_option("", "--message", help=_("message to display when interacting with user"))
350 parser.add_option("", "--not-before", help=_("minimum version to choose"), metavar='VERSION')
351 parser.add_option("", "--os", help=_("target operation system type"), metavar='OS')
352 parser.add_option("-o", "--offline", help=_("try to avoid using the network"), action='store_true')
353 parser.add_option("-r", "--refresh", help=_("refresh all used interfaces"), action='store_true')
354 parser.add_option("", "--select-only", help=_("only download the feeds"), action='store_true')
355 parser.add_option("", "--set-selections", help=_("run versions specified in XML file"), metavar='FILE')
356 parser.add_option("-s", "--source", help=_("select source code"), action='store_true')
357 parser.add_option("", "--systray", help=_("download in the background"), action='store_true')
358 parser.add_option("-v", "--verbose", help=_("more verbose output"), action='count')
359 parser.add_option("-V", "--version", help=_("display version information"), action='store_true')
360 parser.add_option("", "--with-store", help=_("add an implementation cache"), action='append', metavar='DIR')
361 parser.add_option("-w", "--wrapper", help=_("execute program using a debugger, etc"), metavar='COMMAND')
362 parser.disable_interspersed_args()
364 (options, args) = parser.parse_args(command_args)
366 if options.verbose:
367 logger = logging.getLogger()
368 if options.verbose == 1:
369 logger.setLevel(logging.INFO)
370 else:
371 logger.setLevel(logging.DEBUG)
372 import zeroinstall
373 logging.info(_("Running 0launch %(version)s %(args)s; Python %(python_version)s"), {'version': zeroinstall.version, 'args': repr(args), 'python_version': sys.version})
375 if options.select_only:
376 options.download_only = True
378 if options.with_store:
379 from zeroinstall import zerostore
380 for x in options.with_store:
381 iface_cache.stores.stores.append(zerostore.Store(os.path.abspath(x)))
382 logging.info(_("Stores search path is now %s"), iface_cache.stores.stores)
384 try:
385 if options.list:
386 _list_interfaces(args)
387 elif options.version:
388 import zeroinstall
389 print "0launch (zero-install) " + zeroinstall.version
390 print "Copyright (C) 2009 Thomas Leonard"
391 print _("This program comes with ABSOLUTELY NO WARRANTY,"
392 "\nto the extent permitted by law."
393 "\nYou may redistribute copies of this program"
394 "\nunder the terms of the GNU Lesser General Public License."
395 "\nFor more information about these matters, see the file named COPYING.")
396 elif options.set_selections:
397 from zeroinstall.injector import qdom, run
398 sels = selections.Selections(qdom.parse(file(options.set_selections)))
399 _download_missing_selections(options, sels)
400 if not options.download_only:
401 run.execute_selections(sels, args, options.dry_run, options.main, options.wrapper)
402 elif getattr(options, 'import'):
403 _import_feed(args)
404 elif options.feed:
405 _manage_feeds(options, args)
406 else:
407 _normal_mode(options, args)
408 except UsageError:
409 parser.print_help()
410 sys.exit(1)
411 except SafeException, ex:
412 if options.verbose: raise
413 try:
414 print >>sys.stderr, unicode(ex)
415 except:
416 print >>sys.stderr, repr(ex)
417 sys.exit(1)