Import _ into each module rather than using a builtin
[zeroinstall.git] / zeroinstall / injector / cli.py
blobfc2fd210706971670cfee2e83fbb2344c6a09c3a
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, namespaces, 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 iface = iface_cache.get_interface(uri)
53 logging.info(_("Importing information about interface %s"), iface)
54 signed_data.seek(0)
56 pending = PendingFeed(uri, signed_data)
58 def run():
59 keys_downloaded = tasks.Task(pending.download_keys(handler), "download keys")
60 yield keys_downloaded.finished
61 tasks.check(keys_downloaded.finished)
62 if not iface_cache.update_interface_if_trusted(iface, pending.sigs, pending.new_xml):
63 blocker = handler.confirm_trust_keys(iface, pending.sigs, pending.new_xml)
64 if blocker:
65 yield blocker
66 tasks.check(blocker)
67 if not iface_cache.update_interface_if_trusted(iface, pending.sigs, pending.new_xml):
68 raise SafeException(_("No signing keys trusted; not importing"))
70 task = tasks.Task(run(), "import feed")
72 errors = handler.wait_for_blocker(task.finished)
73 if errors:
74 raise SafeException(_("Errors during download: ") + '\n'.join(errors))
76 def _manage_feeds(options, args):
77 from zeroinstall.injector import writer
78 from zeroinstall.injector.handler import Handler
79 from zeroinstall.injector.policy import Policy
80 handler = Handler(dry_run = options.dry_run)
81 if not args: raise UsageError()
82 for x in args:
83 print _("Feed '%s':\n") % x
84 x = model.canonical_iface_uri(x)
85 policy = Policy(x, handler)
86 if options.offline:
87 policy.network_use = model.network_offline
89 feed = iface_cache.get_feed(x)
90 if policy.network_use != model.network_offline and policy.is_stale(feed):
91 blocker = policy.fetcher.download_and_import_feed(x, iface_cache.iface_cache)
92 print _("Downloading feed; please wait...")
93 handler.wait_for_blocker(blocker)
94 print _("Done")
96 interfaces = policy.get_feed_targets(x)
97 for i in range(len(interfaces)):
98 feed = interfaces[i].get_feed(x)
99 if feed:
100 print _("%(index)d) Remove as feed for '%(uri)s'") % {'index': i + 1, 'uri': interfaces[i].uri}
101 else:
102 print _("%(index)d) Add as feed for '%(uri)s'") % {'index': i + 1, 'uri': interfaces[i].uri}
103 print
104 while True:
105 try:
106 i = raw_input(_('Enter a number, or CTRL-C to cancel [1]: ')).strip()
107 except KeyboardInterrupt:
108 print
109 raise SafeException(_("Aborted at user request."))
110 if i == '':
111 i = 1
112 else:
113 try:
114 i = int(i)
115 except ValueError:
116 i = 0
117 if i > 0 and i <= len(interfaces):
118 break
119 print _("Invalid number. Try again. (1 to %d)") % len(interfaces)
120 iface = interfaces[i - 1]
121 feed = iface.get_feed(x)
122 if feed:
123 iface.extra_feeds.remove(feed)
124 else:
125 iface.extra_feeds.append(model.Feed(x, arch = None, user_override = True))
126 writer.save_interface(iface)
127 print '\n' + _("Feed list for interface '%s' is now:") % iface.get_name()
128 if iface.feeds:
129 for f in iface.feeds:
130 print "- " + f.uri
131 else:
132 print _("(no feeds)")
134 def _normal_mode(options, args):
135 if len(args) < 1:
136 # You can use -g on its own to edit the GUI's own policy
137 # Otherwise, failing to give an interface is an error
138 if options.gui:
139 args = [namespaces.injector_gui_uri]
140 options.download_only = True
141 else:
142 raise UsageError()
144 iface_uri = model.canonical_iface_uri(args[0])
145 root_iface = iface_cache.get_interface(iface_uri)
147 policy = autopolicy.AutoPolicy(iface_uri,
148 download_only = bool(options.download_only),
149 dry_run = options.dry_run,
150 src = options.source)
152 if options.before or options.not_before:
153 policy.solver.extra_restrictions[root_iface] = [model.VersionRangeRestriction(model.parse_version(options.before),
154 model.parse_version(options.not_before))]
156 if options.os or options.cpu:
157 from zeroinstall.injector import arch
158 policy.target_arch = arch.get_architecture(options.os, options.cpu)
160 if options.offline:
161 policy.network_use = model.network_offline
163 if options.get_selections:
164 if len(args) > 1:
165 raise SafeException(_("Can't use arguments with --get-selections"))
166 if options.main:
167 raise SafeException(_("Can't use --main with --get-selections"))
169 # Note that need_download() triggers a solve
170 if options.refresh or options.gui:
171 # We could run immediately, but the user asked us not to
172 can_run_immediately = False
173 else:
174 can_run_immediately = (not policy.need_download()) and policy.ready
176 stale_feeds = [feed for feed in policy.solver.feeds_used if policy.is_stale(iface_cache.get_feed(feed))]
178 if options.download_only and stale_feeds:
179 can_run_immediately = False
181 if can_run_immediately:
182 if stale_feeds:
183 if policy.network_use == model.network_offline:
184 logging.debug(_("No doing background update because we are in off-line mode."))
185 else:
186 # There are feeds we should update, but we can run without them.
187 # Do the update in the background while the program is running.
188 import background
189 background.spawn_background_update(policy, options.verbose > 0)
190 if options.get_selections:
191 _get_selections(policy)
192 else:
193 if not options.download_only:
194 from zeroinstall.injector import run
195 run.execute(policy, args[1:], dry_run = options.dry_run, main = options.main, wrapper = options.wrapper)
196 else:
197 logging.info(_("Downloads done (download-only mode)"))
198 assert options.dry_run or options.download_only
199 return
201 # If the user didn't say whether to use the GUI, choose for them.
202 if options.gui is None and os.environ.get('DISPLAY', None):
203 options.gui = True
204 # If we need to download anything, we might as well
205 # refresh all the interfaces first. Also, this triggers
206 # the 'checking for updates' box, which is non-interactive
207 # when there are no changes to the selection.
208 options.refresh = True
209 logging.info(_("Switching to GUI mode... (use --console to disable)"))
211 prog_args = args[1:]
213 try:
214 from zeroinstall.injector import run
215 if options.gui:
216 gui_args = []
217 if options.download_only:
218 # Just changes the button's label
219 gui_args.append('--download-only')
220 if options.refresh:
221 gui_args.append('--refresh')
222 if options.systray:
223 gui_args.append('--systray')
224 if options.not_before:
225 gui_args.insert(0, options.not_before)
226 gui_args.insert(0, '--not-before')
227 if options.before:
228 gui_args.insert(0, options.before)
229 gui_args.insert(0, '--before')
230 if options.source:
231 gui_args.insert(0, '--source')
232 if options.message:
233 gui_args.insert(0, options.message)
234 gui_args.insert(0, '--message')
235 if options.verbose:
236 gui_args.insert(0, '--verbose')
237 if options.verbose > 1:
238 gui_args.insert(0, '--verbose')
239 if options.cpu:
240 gui_args.insert(0, options.cpu)
241 gui_args.insert(0, '--cpu')
242 if options.os:
243 gui_args.insert(0, options.os)
244 gui_args.insert(0, '--os')
245 if options.with_store:
246 for x in options.with_store:
247 gui_args += ['--with-store', x]
248 sels = _fork_gui(iface_uri, gui_args, prog_args, options)
249 if not sels:
250 sys.exit(1) # Aborted
251 else:
252 #program_log('download_and_execute ' + iface_uri)
253 downloaded = policy.solve_and_download_impls(refresh = bool(options.refresh))
254 if downloaded:
255 policy.handler.wait_for_blocker(downloaded)
256 sels = selections.Selections(policy)
258 if options.get_selections:
259 doc = sels.toDOM()
260 doc.writexml(sys.stdout)
261 sys.stdout.write('\n')
262 elif not options.download_only:
263 run.execute_selections(sels, prog_args, options.dry_run, options.main, options.wrapper)
265 except NeedDownload, ex:
266 # This only happens for dry runs
267 print ex
269 def _fork_gui(iface_uri, gui_args, prog_args, options = None):
270 """Run the GUI to get the selections.
271 prog_args and options are used only if the GUI requests a test.
273 from zeroinstall import helpers
274 def test_callback(sels):
275 from zeroinstall.injector import run
276 return run.test_selections(sels, prog_args,
277 bool(options and options.dry_run),
278 options and options.main)
279 return helpers.get_selections_gui(iface_uri, gui_args, test_callback)
281 def _download_missing_selections(options, sels):
282 from zeroinstall.injector import fetch
283 from zeroinstall.injector.handler import Handler
284 handler = Handler(dry_run = options.dry_run)
285 fetcher = fetch.Fetcher(handler)
286 blocker = sels.download_missing(iface_cache, fetcher)
287 if blocker:
288 logging.info(_("Waiting for selected implementations to be downloaded..."))
289 handler.wait_for_blocker(blocker)
291 def _get_selections(policy):
292 doc = selections.Selections(policy).toDOM()
293 doc.writexml(sys.stdout)
294 sys.stdout.write('\n')
296 class UsageError(Exception): pass
298 def main(command_args):
299 """Act as if 0launch was run with the given arguments.
300 @arg command_args: array of arguments (e.g. C{sys.argv[1:]})
301 @type command_args: [str]
303 # Ensure stdin, stdout and stderr FDs exist, to avoid confusion
304 for std in (0, 1, 2):
305 try:
306 os.fstat(std)
307 except OSError:
308 fd = os.open('/dev/null', os.O_RDONLY)
309 if fd != std:
310 os.dup2(fd, std)
311 os.close(fd)
313 parser = OptionParser(usage=_("usage: %prog [options] interface [args]\n"
314 " %prog --list [search-term]\n"
315 " %prog --import [signed-interface-files]\n"
316 " %prog --feed [interface]"))
317 parser.add_option("", "--before", help=_("choose a version before this"), metavar='VERSION')
318 parser.add_option("-c", "--console", help=_("never use GUI"), action='store_false', dest='gui')
319 parser.add_option("", "--cpu", help=_("target CPU type"), metavar='CPU')
320 parser.add_option("-d", "--download-only", help=_("fetch but don't run"), action='store_true')
321 parser.add_option("-D", "--dry-run", help=_("just print actions"), action='store_true')
322 parser.add_option("-f", "--feed", help=_("add or remove a feed"), action='store_true')
323 parser.add_option("", "--get-selections", help=_("write selected versions as XML"), action='store_true')
324 parser.add_option("-g", "--gui", help=_("show graphical policy editor"), action='store_true')
325 parser.add_option("-i", "--import", help=_("import from files, not from the network"), action='store_true')
326 parser.add_option("-l", "--list", help=_("list all known interfaces"), action='store_true')
327 parser.add_option("-m", "--main", help=_("name of the file to execute"))
328 parser.add_option("", "--message", help=_("message to display when interacting with user"))
329 parser.add_option("", "--not-before", help=_("minimum version to choose"), metavar='VERSION')
330 parser.add_option("", "--os", help=_("target operation system type"), metavar='OS')
331 parser.add_option("-o", "--offline", help=_("try to avoid using the network"), action='store_true')
332 parser.add_option("-r", "--refresh", help=_("refresh all used interfaces"), action='store_true')
333 parser.add_option("", "--set-selections", help=_("run versions specified in XML file"), metavar='FILE')
334 parser.add_option("-s", "--source", help=_("select source code"), action='store_true')
335 parser.add_option("", "--systray", help=_("download in the background"), action='store_true')
336 parser.add_option("-v", "--verbose", help=_("more verbose output"), action='count')
337 parser.add_option("-V", "--version", help=_("display version information"), action='store_true')
338 parser.add_option("", "--with-store", help=_("add an implementation cache"), action='append', metavar='DIR')
339 parser.add_option("-w", "--wrapper", help=_("execute program using a debugger, etc"), metavar='COMMAND')
340 parser.disable_interspersed_args()
342 (options, args) = parser.parse_args(command_args)
344 if options.verbose:
345 logger = logging.getLogger()
346 if options.verbose == 1:
347 logger.setLevel(logging.INFO)
348 else:
349 logger.setLevel(logging.DEBUG)
350 import zeroinstall
351 logging.info(_("Running 0launch %(version)s %(args)s; Python %(python_version)s"), {'version': zeroinstall.version, 'args': repr(args), 'python_version': sys.version})
353 if options.with_store:
354 from zeroinstall import zerostore
355 for x in options.with_store:
356 iface_cache.stores.stores.append(zerostore.Store(os.path.abspath(x)))
357 logging.info(_("Stores search path is now %s"), iface_cache.stores.stores)
359 try:
360 if options.list:
361 _list_interfaces(args)
362 elif options.version:
363 import zeroinstall
364 print "0launch (zero-install) " + zeroinstall.version
365 print "Copyright (C) 2009 Thomas Leonard"
366 print _("This program comes with ABSOLUTELY NO WARRANTY,"
367 "\nto the extent permitted by law."
368 "\nYou may redistribute copies of this program"
369 "\nunder the terms of the GNU Lesser General Public License."
370 "\nFor more information about these matters, see the file named COPYING.")
371 elif options.set_selections:
372 from zeroinstall.injector import qdom, run
373 sels = selections.Selections(qdom.parse(file(options.set_selections)))
374 _download_missing_selections(options, sels)
375 if not options.download_only:
376 run.execute_selections(sels, args, options.dry_run, options.main, options.wrapper)
377 elif getattr(options, 'import'):
378 _import_feed(args)
379 elif options.feed:
380 _manage_feeds(options, args)
381 else:
382 _normal_mode(options, args)
383 except UsageError:
384 parser.print_help()
385 sys.exit(1)
386 except SafeException, ex:
387 if options.verbose: raise
388 print >>sys.stderr, ex
389 sys.exit(1)