When switching to GUI mode, forward any --verbose options.
[zeroinstall/zeroinstall-mseaborn.git] / zeroinstall / injector / cli.py
blob9bd003dd0d471b7bbe0502efb763051c95d8434d
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 import os, sys
9 from optparse import OptionParser
10 import logging
12 from zeroinstall.injector import model, download, autopolicy, namespaces
14 #def program_log(msg): os.access('MARK: 0launch: ' + msg, os.F_OK)
15 #import __main__
16 #__main__.__builtins__.program_log = program_log
17 #program_log('0launch ' + ' '.join((sys.argv[1:])))
19 def _list_interfaces(args):
20 from zeroinstall.injector.iface_cache import iface_cache
21 if len(args) == 0:
22 matches = iface_cache.list_all_interfaces()
23 elif len(args) == 1:
24 match = args[0].lower()
25 matches = [i for i in iface_cache.list_all_interfaces() if match in i.lower()]
26 else:
27 raise UsageError()
29 matches.sort()
30 for i in matches:
31 print i
33 def _import_interface(args):
34 from zeroinstall.support import tasks
35 from zeroinstall.injector import gpg, handler, trust
36 from zeroinstall.injector.iface_cache import iface_cache, PendingFeed
37 from xml.dom import minidom
38 for x in args:
39 if not os.path.isfile(x):
40 raise model.SafeException("File '%s' does not exist" % x)
41 logging.info("Importing from file '%s'", x)
42 signed_data = file(x)
43 data, sigs = gpg.check_stream(signed_data)
44 doc = minidom.parseString(data.read())
45 uri = doc.documentElement.getAttribute('uri')
46 if not uri:
47 raise model.SafeException("Missing 'uri' attribute on root element in '%s'" % x)
48 iface = iface_cache.get_interface(uri)
49 logging.info("Importing information about interface %s", iface)
50 signed_data.seek(0)
52 pending = PendingFeed(uri, signed_data)
53 iface_cache.add_pending(pending)
55 handler = handler.Handler()
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_interface_if_trusted(iface, pending.sigs, pending.new_xml):
62 blocker = handler.confirm_trust_keys(iface, pending.sigs, pending.new_xml)
63 if blocker:
64 yield blocker
65 tasks.check(blocker)
66 if not iface_cache.update_interface_if_trusted(iface, pending.sigs, pending.new_xml):
67 raise SafeException("No signing keys trusted; not importing")
69 task = tasks.Task(run(), "import feed")
71 errors = handler.wait_for_blocker(task.finished)
72 if errors:
73 raise model.SafeException("Errors during download: " + '\n'.join(errors))
75 def _manage_feeds(options, args):
76 from zeroinstall.injector import iface_cache, writer
77 from xml.dom import minidom
78 if not args: raise UsageError()
79 for x in args:
80 print "Feed '%s':\n" % x
81 x = model.canonical_iface_uri(x)
82 policy = autopolicy.AutoPolicy(x, download_only = True, dry_run = options.dry_run)
83 if options.offline:
84 policy.network_use = model.network_offline
85 policy.recalculate_with_dl() # XXX
86 interfaces = policy.get_feed_targets(policy.root)
87 for i in range(len(interfaces)):
88 feed = interfaces[i].get_feed(x)
89 if feed:
90 print "%d) Remove as feed for '%s'" % (i + 1, interfaces[i].uri)
91 else:
92 print "%d) Add as feed for '%s'" % (i + 1, interfaces[i].uri)
93 print
94 while True:
95 try:
96 i = raw_input('Enter a number, or CTRL-C to cancel [1]: ').strip()
97 except KeyboardInterrupt:
98 print
99 raise model.SafeException("Aborted at user request.")
100 if i == '':
101 i = 1
102 else:
103 try:
104 i = int(i)
105 except ValueError:
106 i = 0
107 if i > 0 and i <= len(interfaces):
108 break
109 print "Invalid number. Try again. (1 to %d)" % len(interfaces)
110 iface = interfaces[i - 1]
111 feed = iface.get_feed(x)
112 if feed:
113 iface.extra_feeds.remove(feed)
114 else:
115 iface.extra_feeds.append(model.Feed(x, arch = None, user_override = True))
116 writer.save_interface(iface)
117 print "\nFeed list for interface '%s' is now:" % iface.get_name()
118 if iface.feeds:
119 for f in iface.feeds:
120 print "- " + f.uri
121 else:
122 print "(no feeds)"
124 def _normal_mode(options, args):
125 if len(args) < 1:
126 # You can use -g on its own to edit the GUI's own policy
127 # Otherwise, failing to give an interface is an error
128 if options.gui:
129 args = [namespaces.injector_gui_uri]
130 options.download_only = True
131 else:
132 raise UsageError()
134 iface_uri = model.canonical_iface_uri(args[0])
136 policy = autopolicy.AutoPolicy(iface_uri,
137 download_only = bool(options.download_only),
138 dry_run = options.dry_run,
139 src = options.source)
141 if options.before or options.not_before:
142 policy.root_restrictions.append(model.Restriction(model.parse_version(options.before),
143 model.parse_version(options.not_before)))
145 if options.offline:
146 policy.network_use = model.network_offline
148 if options.get_selections:
149 if len(args) > 1:
150 raise model.SafeException("Can't use arguments with --get-selections")
151 if options.main:
152 raise model.SafeException("Can't use --main with --get-selections")
154 # Note that need_download() triggers a solve
155 if options.refresh or options.gui:
156 # We could run immediately, but the user asked us not to
157 can_run_immediately = False
158 else:
159 can_run_immediately = (not policy.need_download()) and policy.ready
161 from zeroinstall.injector.iface_cache import iface_cache
162 stale_feeds = [feed for feed in policy.solver.feeds_used if policy.is_stale(iface_cache.get_feed(feed))]
164 if options.download_only and stale_feeds:
165 can_run_immediately = False
167 if can_run_immediately:
168 if stale_feeds:
169 if policy.network_use == model.network_offline:
170 logging.debug("No doing background update because we are in off-line mode.")
171 else:
172 # There are feeds we should update, but we can run without them.
173 # Do the update in the background while the program is running.
174 import background
175 background.spawn_background_update(policy, options.verbose > 0)
176 if options.get_selections:
177 _get_selections(policy)
178 else:
179 if not options.download_only:
180 from zeroinstall.injector import run
181 run.execute(policy, args[1:], dry_run = options.dry_run, main = options.main, wrapper = options.wrapper)
182 else:
183 logging.info("Downloads done (download-only mode)")
184 assert options.dry_run or options.download_only
185 return
187 # If the user didn't say whether to use the GUI, choose for them.
188 if options.gui is None and os.environ.get('DISPLAY', None):
189 options.gui = True
190 # If we need to download anything, we might as well
191 # refresh all the interfaces first. Also, this triggers
192 # the 'checking for updates' box, which is non-interactive
193 # when there are no changes to the selection.
194 options.refresh = True
195 logging.info("Switching to GUI mode... (use --console to disable)")
197 prog_args = args[1:]
199 try:
200 if options.gui:
201 from zeroinstall.injector import run
202 gui_args = []
203 if options.download_only:
204 # Just changes the button's label
205 gui_args.append('--download-only')
206 if options.refresh:
207 gui_args.append('--refresh')
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.verbose:
217 gui_args.insert(0, '--verbose')
218 if options.verbose > 1:
219 gui_args.insert(0, '--verbose')
220 sels = _fork_gui(iface_uri, gui_args, prog_args, options)
221 if not sels:
222 sys.exit(1) # Aborted
223 if options.get_selections:
224 doc = sels.toDOM()
225 doc.writexml(sys.stdout)
226 sys.stdout.write('\n')
227 elif not options.download_only:
228 run.execute_selections(sels, prog_args, options.dry_run, options.main, options.wrapper)
229 else:
230 #program_log('download_and_execute ' + iface_uri)
231 policy.download_and_execute(prog_args, refresh = bool(options.refresh), main = options.main)
232 except autopolicy.NeedDownload, ex:
233 # This only happens for dry runs
234 print ex
236 def _fork_gui(iface_uri, gui_args, prog_args, options = None):
237 """Run the GUI to get the selections.
238 prog_args and options are used only if the GUI requests a test.
240 from zeroinstall import helpers
241 def test_callback(sels):
242 from zeroinstall.injector import run
243 return run.test_selections(sels, prog_args,
244 bool(options and options.dry_run),
245 options and options.main)
246 return helpers.get_selections_gui(iface_uri, gui_args, test_callback)
248 def _get_selections(policy):
249 import selections
250 doc = selections.Selections(policy).toDOM()
251 doc.writexml(sys.stdout)
252 sys.stdout.write('\n')
254 class UsageError(Exception): pass
256 def main(command_args):
257 """Act as if 0launch was run with the given arguments.
258 @arg command_args: array of arguments (e.g. C{sys.argv[1:]})
259 @type command_args: [str]
261 # Ensure stdin, stdout and stderr FDs exist, to avoid confusion
262 for std in (0, 1, 2):
263 try:
264 os.fstat(std)
265 except OSError:
266 fd = os.open('/dev/null', os.O_RDONLY)
267 if fd != std:
268 os.dup2(fd, std)
269 os.close(fd)
271 parser = OptionParser(usage="usage: %prog [options] interface [args]\n"
272 " %prog --list [search-term]\n"
273 " %prog --import [signed-interface-files]\n"
274 " %prog --feed [interface]")
275 parser.add_option("", "--before", help="choose a version before this", metavar='VERSION')
276 parser.add_option("-c", "--console", help="never use GUI", action='store_false', dest='gui')
277 parser.add_option("-d", "--download-only", help="fetch but don't run", action='store_true')
278 parser.add_option("-D", "--dry-run", help="just print actions", action='store_true')
279 parser.add_option("-f", "--feed", help="add or remove a feed", action='store_true')
280 parser.add_option("", "--get-selections", help="write selected versions as XML", action='store_true')
281 parser.add_option("-g", "--gui", help="show graphical policy editor", action='store_true')
282 parser.add_option("-i", "--import", help="import from files, not from the network", action='store_true')
283 parser.add_option("-l", "--list", help="list all known interfaces", action='store_true')
284 parser.add_option("-m", "--main", help="name of the file to execute")
285 parser.add_option("", "--not-before", help="minimum version to choose", metavar='VERSION')
286 parser.add_option("-o", "--offline", help="try to avoid using the network", action='store_true')
287 parser.add_option("-r", "--refresh", help="refresh all used interfaces", action='store_true')
288 parser.add_option("", "--set-selections", help="run versions specified in XML file", metavar='FILE')
289 parser.add_option("-s", "--source", help="select source code", action='store_true')
290 parser.add_option("-v", "--verbose", help="more verbose output", action='count')
291 parser.add_option("-V", "--version", help="display version information", action='store_true')
292 parser.add_option("-w", "--wrapper", help="execute program using a debugger, etc", metavar='COMMAND')
293 parser.disable_interspersed_args()
295 (options, args) = parser.parse_args(command_args)
297 if options.verbose:
298 logger = logging.getLogger()
299 if options.verbose == 1:
300 logger.setLevel(logging.INFO)
301 else:
302 logger.setLevel(logging.DEBUG)
303 import zeroinstall
304 logging.info("Running 0launch %s %s; Python %s", zeroinstall.version, repr(args), sys.version)
306 try:
307 if options.list:
308 _list_interfaces(args)
309 elif options.version:
310 import zeroinstall
311 print "0launch (zero-install) " + zeroinstall.version
312 print "Copyright (C) 2007 Thomas Leonard"
313 print "This program comes with ABSOLUTELY NO WARRANTY,"
314 print "to the extent permitted by law."
315 print "You may redistribute copies of this program"
316 print "under the terms of the GNU Lesser General Public License."
317 print "For more information about these matters, see the file named COPYING."
318 elif options.set_selections:
319 from zeroinstall.injector import selections, qdom, run
320 sels = selections.Selections(qdom.parse(file(options.set_selections)))
321 run.execute_selections(sels, args, options.dry_run, options.main, options.wrapper)
322 elif getattr(options, 'import'):
323 _import_interface(args)
324 elif options.feed:
325 _manage_feeds(options, args)
326 else:
327 _normal_mode(options, args)
328 except UsageError:
329 parser.print_help()
330 sys.exit(1)
331 except model.SafeException, ex:
332 if options.verbose: raise
333 print >>sys.stderr, ex
334 sys.exit(1)