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