--download-only option was ignored if used with --set-selections
[zeroinstall.git] / zeroinstall / injector / cli.py
blob8d90497610cbee5c07c56e10dd92e0483ea0463b
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.verbose:
230 gui_args.insert(0, '--verbose')
231 if options.verbose > 1:
232 gui_args.insert(0, '--verbose')
233 if options.cpu:
234 gui_args.insert(0, options.cpu)
235 gui_args.insert(0, '--cpu')
236 if options.os:
237 gui_args.insert(0, options.os)
238 gui_args.insert(0, '--os')
239 sels = _fork_gui(iface_uri, gui_args, prog_args, options)
240 if not sels:
241 sys.exit(1) # Aborted
242 if options.get_selections:
243 doc = sels.toDOM()
244 doc.writexml(sys.stdout)
245 sys.stdout.write('\n')
246 elif not options.download_only:
247 run.execute_selections(sels, prog_args, options.dry_run, options.main, options.wrapper)
248 else:
249 #program_log('download_and_execute ' + iface_uri)
250 policy.download_and_execute(prog_args, refresh = bool(options.refresh), main = options.main)
251 except NeedDownload, ex:
252 # This only happens for dry runs
253 print ex
255 def _fork_gui(iface_uri, gui_args, prog_args, options = None):
256 """Run the GUI to get the selections.
257 prog_args and options are used only if the GUI requests a test.
259 from zeroinstall import helpers
260 def test_callback(sels):
261 from zeroinstall.injector import run
262 return run.test_selections(sels, prog_args,
263 bool(options and options.dry_run),
264 options and options.main)
265 return helpers.get_selections_gui(iface_uri, gui_args, test_callback)
267 def _download_missing_selections(options, sels):
268 from zeroinstall.injector import fetch
269 from zeroinstall.injector.handler import Handler
270 handler = Handler(dry_run = options.dry_run)
271 fetcher = fetch.Fetcher(handler)
272 blocker = sels.download_missing(iface_cache, fetcher)
273 if blocker:
274 logging.info("Waiting for selected implementations to be downloaded...")
275 handler.wait_for_blocker(blocker)
277 def _get_selections(policy):
278 import selections
279 doc = selections.Selections(policy).toDOM()
280 doc.writexml(sys.stdout)
281 sys.stdout.write('\n')
283 class UsageError(Exception): pass
285 def main(command_args):
286 """Act as if 0launch was run with the given arguments.
287 @arg command_args: array of arguments (e.g. C{sys.argv[1:]})
288 @type command_args: [str]
290 # Ensure stdin, stdout and stderr FDs exist, to avoid confusion
291 for std in (0, 1, 2):
292 try:
293 os.fstat(std)
294 except OSError:
295 fd = os.open('/dev/null', os.O_RDONLY)
296 if fd != std:
297 os.dup2(fd, std)
298 os.close(fd)
300 parser = OptionParser(usage="usage: %prog [options] interface [args]\n"
301 " %prog --list [search-term]\n"
302 " %prog --import [signed-interface-files]\n"
303 " %prog --feed [interface]")
304 parser.add_option("", "--before", help="choose a version before this", metavar='VERSION')
305 parser.add_option("-c", "--console", help="never use GUI", action='store_false', dest='gui')
306 parser.add_option("", "--cpu", help="target CPU type", metavar='CPU')
307 parser.add_option("-d", "--download-only", help="fetch but don't run", action='store_true')
308 parser.add_option("-D", "--dry-run", help="just print actions", action='store_true')
309 parser.add_option("-f", "--feed", help="add or remove a feed", action='store_true')
310 parser.add_option("", "--get-selections", help="write selected versions as XML", action='store_true')
311 parser.add_option("-g", "--gui", help="show graphical policy editor", action='store_true')
312 parser.add_option("-i", "--import", help="import from files, not from the network", action='store_true')
313 parser.add_option("-l", "--list", help="list all known interfaces", action='store_true')
314 parser.add_option("-m", "--main", help="name of the file to execute")
315 parser.add_option("", "--not-before", help="minimum version to choose", metavar='VERSION')
316 parser.add_option("", "--os", help="target operation system type", metavar='OS')
317 parser.add_option("-o", "--offline", help="try to avoid using the network", action='store_true')
318 parser.add_option("-r", "--refresh", help="refresh all used interfaces", action='store_true')
319 parser.add_option("", "--set-selections", help="run versions specified in XML file", metavar='FILE')
320 parser.add_option("-s", "--source", help="select source code", action='store_true')
321 parser.add_option("-v", "--verbose", help="more verbose output", action='count')
322 parser.add_option("-V", "--version", help="display version information", action='store_true')
323 parser.add_option("-w", "--wrapper", help="execute program using a debugger, etc", metavar='COMMAND')
324 parser.disable_interspersed_args()
326 (options, args) = parser.parse_args(command_args)
328 if options.verbose:
329 logger = logging.getLogger()
330 if options.verbose == 1:
331 logger.setLevel(logging.INFO)
332 else:
333 logger.setLevel(logging.DEBUG)
334 import zeroinstall
335 logging.info("Running 0launch %s %s; Python %s", zeroinstall.version, repr(args), sys.version)
337 try:
338 if options.list:
339 _list_interfaces(args)
340 elif options.version:
341 import zeroinstall
342 print "0launch (zero-install) " + zeroinstall.version
343 print "Copyright (C) 2007 Thomas Leonard"
344 print "This program comes with ABSOLUTELY NO WARRANTY,"
345 print "to the extent permitted by law."
346 print "You may redistribute copies of this program"
347 print "under the terms of the GNU Lesser General Public License."
348 print "For more information about these matters, see the file named COPYING."
349 elif options.set_selections:
350 from zeroinstall.injector import selections, qdom, run
351 sels = selections.Selections(qdom.parse(file(options.set_selections)))
352 _download_missing_selections(options, sels)
353 if not options.download_only:
354 run.execute_selections(sels, args, options.dry_run, options.main, options.wrapper)
355 elif getattr(options, 'import'):
356 _import_feed(args)
357 elif options.feed:
358 _manage_feeds(options, args)
359 else:
360 _normal_mode(options, args)
361 except UsageError:
362 parser.print_help()
363 sys.exit(1)
364 except SafeException, ex:
365 if options.verbose: raise
366 print >>sys.stderr, ex
367 sys.exit(1)