Update year to 2009 in various places
[zeroinstall/zeroinstall-rsl.git] / zeroinstall / injector / cli.py
blob67c0298b470b52060667fa3cf928e86b0f65e7f6
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 import SafeException, NeedDownload
13 from zeroinstall.injector import model, autopolicy, namespaces
14 from zeroinstall.injector.iface_cache import iface_cache
16 #def program_log(msg): os.access('MARK: 0launch: ' + msg, os.F_OK)
17 #import __main__
18 #__main__.__builtins__.program_log = program_log
19 #program_log('0launch ' + ' '.join((sys.argv[1:])))
21 def _list_interfaces(args):
22 if len(args) == 0:
23 matches = iface_cache.list_all_interfaces()
24 elif len(args) == 1:
25 match = args[0].lower()
26 matches = [i for i in iface_cache.list_all_interfaces() if match in i.lower()]
27 else:
28 raise UsageError()
30 matches.sort()
31 for i in matches:
32 print i
34 def _import_feed(args):
35 from zeroinstall.support import tasks
36 from zeroinstall.injector import gpg, handler
37 from zeroinstall.injector.iface_cache import PendingFeed
38 from xml.dom import minidom
39 handler = handler.Handler()
41 for x in args:
42 if not os.path.isfile(x):
43 raise SafeException("File '%s' does not exist" % x)
44 logging.info("Importing from file '%s'", x)
45 signed_data = file(x)
46 data, sigs = gpg.check_stream(signed_data)
47 doc = minidom.parseString(data.read())
48 uri = doc.documentElement.getAttribute('uri')
49 if not uri:
50 raise SafeException("Missing 'uri' attribute on root element in '%s'" % x)
51 iface = iface_cache.get_interface(uri)
52 logging.info("Importing information about interface %s", iface)
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_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 SafeException("Errors during download: " + '\n'.join(errors))
75 def _manage_feeds(options, args):
76 from zeroinstall.injector import writer
77 from zeroinstall.injector.handler import Handler
78 from zeroinstall.injector.policy import Policy
79 handler = Handler(dry_run = options.dry_run)
80 if not args: raise UsageError()
81 for x in args:
82 print "Feed '%s':\n" % x
83 x = model.canonical_iface_uri(x)
84 policy = Policy(x, handler)
85 if options.offline:
86 policy.network_use = model.network_offline
88 feed = iface_cache.get_feed(x)
89 if policy.network_use != model.network_offline and policy.is_stale(feed):
90 blocker = policy.fetcher.download_and_import_feed(x, iface_cache.iface_cache)
91 print "Downloading feed; please wait..."
92 handler.wait_for_blocker(blocker)
93 print "Done"
95 interfaces = policy.get_feed_targets(x)
96 for i in range(len(interfaces)):
97 feed = interfaces[i].get_feed(x)
98 if feed:
99 print "%d) Remove as feed for '%s'" % (i + 1, interfaces[i].uri)
100 else:
101 print "%d) Add as feed for '%s'" % (i + 1, interfaces[i].uri)
102 print
103 while True:
104 try:
105 i = raw_input('Enter a number, or CTRL-C to cancel [1]: ').strip()
106 except KeyboardInterrupt:
107 print
108 raise SafeException("Aborted at user request.")
109 if i == '':
110 i = 1
111 else:
112 try:
113 i = int(i)
114 except ValueError:
115 i = 0
116 if i > 0 and i <= len(interfaces):
117 break
118 print "Invalid number. Try again. (1 to %d)" % len(interfaces)
119 iface = interfaces[i - 1]
120 feed = iface.get_feed(x)
121 if feed:
122 iface.extra_feeds.remove(feed)
123 else:
124 iface.extra_feeds.append(model.Feed(x, arch = None, user_override = True))
125 writer.save_interface(iface)
126 print "\nFeed list for interface '%s' is now:" % iface.get_name()
127 if iface.feeds:
128 for f in iface.feeds:
129 print "- " + f.uri
130 else:
131 print "(no feeds)"
133 def _normal_mode(options, args):
134 if len(args) < 1:
135 # You can use -g on its own to edit the GUI's own policy
136 # Otherwise, failing to give an interface is an error
137 if options.gui:
138 args = [namespaces.injector_gui_uri]
139 options.download_only = True
140 else:
141 raise UsageError()
143 iface_uri = model.canonical_iface_uri(args[0])
144 root_iface = iface_cache.get_interface(iface_uri)
146 policy = autopolicy.AutoPolicy(iface_uri,
147 download_only = bool(options.download_only),
148 dry_run = options.dry_run,
149 src = options.source)
151 if options.before or options.not_before:
152 policy.solver.extra_restrictions[root_iface] = [model.VersionRangeRestriction(model.parse_version(options.before),
153 model.parse_version(options.not_before))]
155 if options.os or options.cpu:
156 from zeroinstall.injector import arch
157 policy.target_arch = arch.get_architecture(options.os, options.cpu)
159 if options.offline:
160 policy.network_use = model.network_offline
162 if options.get_selections:
163 if len(args) > 1:
164 raise SafeException("Can't use arguments with --get-selections")
165 if options.main:
166 raise SafeException("Can't use --main with --get-selections")
168 # Note that need_download() triggers a solve
169 if options.refresh or options.gui:
170 # We could run immediately, but the user asked us not to
171 can_run_immediately = False
172 else:
173 can_run_immediately = (not policy.need_download()) and policy.ready
175 stale_feeds = [feed for feed in policy.solver.feeds_used if policy.is_stale(iface_cache.get_feed(feed))]
177 if options.download_only and stale_feeds:
178 can_run_immediately = False
180 if can_run_immediately:
181 if stale_feeds:
182 if policy.network_use == model.network_offline:
183 logging.debug("No doing background update because we are in off-line mode.")
184 else:
185 # There are feeds we should update, but we can run without them.
186 # Do the update in the background while the program is running.
187 import background
188 background.spawn_background_update(policy, options.verbose > 0)
189 if options.get_selections:
190 _get_selections(policy)
191 else:
192 if not options.download_only:
193 from zeroinstall.injector import run
194 run.execute(policy, args[1:], dry_run = options.dry_run, main = options.main, wrapper = options.wrapper)
195 else:
196 logging.info("Downloads done (download-only mode)")
197 assert options.dry_run or options.download_only
198 return
200 # If the user didn't say whether to use the GUI, choose for them.
201 if options.gui is None and os.environ.get('DISPLAY', None):
202 options.gui = True
203 # If we need to download anything, we might as well
204 # refresh all the interfaces first. Also, this triggers
205 # the 'checking for updates' box, which is non-interactive
206 # when there are no changes to the selection.
207 options.refresh = True
208 logging.info("Switching to GUI mode... (use --console to disable)")
210 prog_args = args[1:]
212 try:
213 if options.gui:
214 from zeroinstall.injector import run
215 gui_args = []
216 if options.download_only:
217 # Just changes the button's label
218 gui_args.append('--download-only')
219 if options.refresh:
220 gui_args.append('--refresh')
221 if options.not_before:
222 gui_args.insert(0, options.not_before)
223 gui_args.insert(0, '--not-before')
224 if options.before:
225 gui_args.insert(0, options.before)
226 gui_args.insert(0, '--before')
227 if options.source:
228 gui_args.insert(0, '--source')
229 if options.message:
230 gui_args.insert(0, options.message)
231 gui_args.insert(0, '--message')
232 if options.verbose:
233 gui_args.insert(0, '--verbose')
234 if options.verbose > 1:
235 gui_args.insert(0, '--verbose')
236 if options.cpu:
237 gui_args.insert(0, options.cpu)
238 gui_args.insert(0, '--cpu')
239 if options.os:
240 gui_args.insert(0, options.os)
241 gui_args.insert(0, '--os')
242 sels = _fork_gui(iface_uri, gui_args, prog_args, options)
243 if not sels:
244 sys.exit(1) # Aborted
245 if options.get_selections:
246 doc = sels.toDOM()
247 doc.writexml(sys.stdout)
248 sys.stdout.write('\n')
249 elif not options.download_only:
250 run.execute_selections(sels, prog_args, options.dry_run, options.main, options.wrapper)
251 else:
252 #program_log('download_and_execute ' + iface_uri)
253 policy.download_and_execute(prog_args, refresh = bool(options.refresh), main = options.main)
254 except NeedDownload, ex:
255 # This only happens for dry runs
256 print ex
258 def _fork_gui(iface_uri, gui_args, prog_args, options = None):
259 """Run the GUI to get the selections.
260 prog_args and options are used only if the GUI requests a test.
262 from zeroinstall import helpers
263 def test_callback(sels):
264 from zeroinstall.injector import run
265 return run.test_selections(sels, prog_args,
266 bool(options and options.dry_run),
267 options and options.main)
268 return helpers.get_selections_gui(iface_uri, gui_args, test_callback)
270 def _download_missing_selections(options, sels):
271 from zeroinstall.injector import fetch
272 from zeroinstall.injector.handler import Handler
273 handler = Handler(dry_run = options.dry_run)
274 fetcher = fetch.Fetcher(handler)
275 blocker = sels.download_missing(iface_cache, fetcher)
276 if blocker:
277 logging.info("Waiting for selected implementations to be downloaded...")
278 handler.wait_for_blocker(blocker)
280 def _get_selections(policy):
281 import selections
282 doc = selections.Selections(policy).toDOM()
283 doc.writexml(sys.stdout)
284 sys.stdout.write('\n')
286 class UsageError(Exception): pass
288 def main(command_args):
289 """Act as if 0launch was run with the given arguments.
290 @arg command_args: array of arguments (e.g. C{sys.argv[1:]})
291 @type command_args: [str]
293 # Ensure stdin, stdout and stderr FDs exist, to avoid confusion
294 for std in (0, 1, 2):
295 try:
296 os.fstat(std)
297 except OSError:
298 fd = os.open('/dev/null', os.O_RDONLY)
299 if fd != std:
300 os.dup2(fd, std)
301 os.close(fd)
303 parser = OptionParser(usage="usage: %prog [options] interface [args]\n"
304 " %prog --list [search-term]\n"
305 " %prog --import [signed-interface-files]\n"
306 " %prog --feed [interface]")
307 parser.add_option("", "--before", help="choose a version before this", metavar='VERSION')
308 parser.add_option("-c", "--console", help="never use GUI", action='store_false', dest='gui')
309 parser.add_option("", "--cpu", help="target CPU type", metavar='CPU')
310 parser.add_option("-d", "--download-only", help="fetch but don't run", action='store_true')
311 parser.add_option("-D", "--dry-run", help="just print actions", action='store_true')
312 parser.add_option("-f", "--feed", help="add or remove a feed", action='store_true')
313 parser.add_option("", "--get-selections", help="write selected versions as XML", action='store_true')
314 parser.add_option("-g", "--gui", help="show graphical policy editor", action='store_true')
315 parser.add_option("-i", "--import", help="import from files, not from the network", action='store_true')
316 parser.add_option("-l", "--list", help="list all known interfaces", action='store_true')
317 parser.add_option("-m", "--main", help="name of the file to execute")
318 parser.add_option("", "--message", help="message to display when interacting with user")
319 parser.add_option("", "--not-before", help="minimum version to choose", metavar='VERSION')
320 parser.add_option("", "--os", help="target operation system type", metavar='OS')
321 parser.add_option("-o", "--offline", help="try to avoid using the network", action='store_true')
322 parser.add_option("-r", "--refresh", help="refresh all used interfaces", action='store_true')
323 parser.add_option("", "--set-selections", help="run versions specified in XML file", metavar='FILE')
324 parser.add_option("-s", "--source", help="select source code", action='store_true')
325 parser.add_option("-v", "--verbose", help="more verbose output", action='count')
326 parser.add_option("-V", "--version", help="display version information", action='store_true')
327 parser.add_option("-w", "--wrapper", help="execute program using a debugger, etc", metavar='COMMAND')
328 parser.disable_interspersed_args()
330 (options, args) = parser.parse_args(command_args)
332 if options.verbose:
333 logger = logging.getLogger()
334 if options.verbose == 1:
335 logger.setLevel(logging.INFO)
336 else:
337 logger.setLevel(logging.DEBUG)
338 import zeroinstall
339 logging.info("Running 0launch %s %s; Python %s", zeroinstall.version, repr(args), sys.version)
341 try:
342 if options.list:
343 _list_interfaces(args)
344 elif options.version:
345 import zeroinstall
346 print "0launch (zero-install) " + zeroinstall.version
347 print "Copyright (C) 2009 Thomas Leonard"
348 print "This program comes with ABSOLUTELY NO WARRANTY,"
349 print "to the extent permitted by law."
350 print "You may redistribute copies of this program"
351 print "under the terms of the GNU Lesser General Public License."
352 print "For more information about these matters, see the file named COPYING."
353 elif options.set_selections:
354 from zeroinstall.injector import selections, qdom, run
355 sels = selections.Selections(qdom.parse(file(options.set_selections)))
356 _download_missing_selections(options, sels)
357 if not options.download_only:
358 run.execute_selections(sels, args, options.dry_run, options.main, options.wrapper)
359 elif getattr(options, 'import'):
360 _import_feed(args)
361 elif options.feed:
362 _manage_feeds(options, args)
363 else:
364 _normal_mode(options, args)
365 except UsageError:
366 parser.print_help()
367 sys.exit(1)
368 except SafeException, ex:
369 if options.verbose: raise
370 print >>sys.stderr, ex
371 sys.exit(1)