Added VersionRangeRestriction subclass of Restriction.
[zeroinstall/zeroinstall-rsl.git] / zeroinstall / injector / cli.py
blob800a4a4a3a0c013f0bb5af9eba4cd90003744442
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_feed(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 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.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 model.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])
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.root_restrictions.append(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 from zeroinstall.injector.iface_cache import iface_cache
171 stale_feeds = [feed for feed in policy.solver.feeds_used if policy.is_stale(iface_cache.get_feed(feed))]
173 if options.download_only and stale_feeds:
174 can_run_immediately = False
176 if can_run_immediately:
177 if stale_feeds:
178 if policy.network_use == model.network_offline:
179 logging.debug("No doing background update because we are in off-line mode.")
180 else:
181 # There are feeds we should update, but we can run without them.
182 # Do the update in the background while the program is running.
183 import background
184 background.spawn_background_update(policy, options.verbose > 0)
185 if options.get_selections:
186 _get_selections(policy)
187 else:
188 if not options.download_only:
189 from zeroinstall.injector import run
190 run.execute(policy, args[1:], dry_run = options.dry_run, main = options.main, wrapper = options.wrapper)
191 else:
192 logging.info("Downloads done (download-only mode)")
193 assert options.dry_run or options.download_only
194 return
196 # If the user didn't say whether to use the GUI, choose for them.
197 if options.gui is None and os.environ.get('DISPLAY', None):
198 options.gui = True
199 # If we need to download anything, we might as well
200 # refresh all the interfaces first. Also, this triggers
201 # the 'checking for updates' box, which is non-interactive
202 # when there are no changes to the selection.
203 options.refresh = True
204 logging.info("Switching to GUI mode... (use --console to disable)")
206 prog_args = args[1:]
208 try:
209 if options.gui:
210 from zeroinstall.injector import run
211 gui_args = []
212 if options.download_only:
213 # Just changes the button's label
214 gui_args.append('--download-only')
215 if options.refresh:
216 gui_args.append('--refresh')
217 if options.not_before:
218 gui_args.insert(0, options.not_before)
219 gui_args.insert(0, '--not-before')
220 if options.before:
221 gui_args.insert(0, options.before)
222 gui_args.insert(0, '--before')
223 if options.source:
224 gui_args.insert(0, '--source')
225 if options.verbose:
226 gui_args.insert(0, '--verbose')
227 if options.verbose > 1:
228 gui_args.insert(0, '--verbose')
229 sels = _fork_gui(iface_uri, gui_args, prog_args, options)
230 if not sels:
231 sys.exit(1) # Aborted
232 if options.get_selections:
233 doc = sels.toDOM()
234 doc.writexml(sys.stdout)
235 sys.stdout.write('\n')
236 elif not options.download_only:
237 run.execute_selections(sels, prog_args, options.dry_run, options.main, options.wrapper)
238 else:
239 #program_log('download_and_execute ' + iface_uri)
240 policy.download_and_execute(prog_args, refresh = bool(options.refresh), main = options.main)
241 except autopolicy.NeedDownload, ex:
242 # This only happens for dry runs
243 print ex
245 def _fork_gui(iface_uri, gui_args, prog_args, options = None):
246 """Run the GUI to get the selections.
247 prog_args and options are used only if the GUI requests a test.
249 from zeroinstall import helpers
250 def test_callback(sels):
251 from zeroinstall.injector import run
252 return run.test_selections(sels, prog_args,
253 bool(options and options.dry_run),
254 options and options.main)
255 return helpers.get_selections_gui(iface_uri, gui_args, test_callback)
257 def _get_selections(policy):
258 import selections
259 doc = selections.Selections(policy).toDOM()
260 doc.writexml(sys.stdout)
261 sys.stdout.write('\n')
263 class UsageError(Exception): pass
265 def main(command_args):
266 """Act as if 0launch was run with the given arguments.
267 @arg command_args: array of arguments (e.g. C{sys.argv[1:]})
268 @type command_args: [str]
270 # Ensure stdin, stdout and stderr FDs exist, to avoid confusion
271 for std in (0, 1, 2):
272 try:
273 os.fstat(std)
274 except OSError:
275 fd = os.open('/dev/null', os.O_RDONLY)
276 if fd != std:
277 os.dup2(fd, std)
278 os.close(fd)
280 parser = OptionParser(usage="usage: %prog [options] interface [args]\n"
281 " %prog --list [search-term]\n"
282 " %prog --import [signed-interface-files]\n"
283 " %prog --feed [interface]")
284 parser.add_option("", "--before", help="choose a version before this", metavar='VERSION')
285 parser.add_option("-c", "--console", help="never use GUI", action='store_false', dest='gui')
286 parser.add_option("-d", "--download-only", help="fetch but don't run", action='store_true')
287 parser.add_option("-D", "--dry-run", help="just print actions", action='store_true')
288 parser.add_option("-f", "--feed", help="add or remove a feed", action='store_true')
289 parser.add_option("", "--get-selections", help="write selected versions as XML", action='store_true')
290 parser.add_option("-g", "--gui", help="show graphical policy editor", action='store_true')
291 parser.add_option("-i", "--import", help="import from files, not from the network", action='store_true')
292 parser.add_option("-l", "--list", help="list all known interfaces", action='store_true')
293 parser.add_option("-m", "--main", help="name of the file to execute")
294 parser.add_option("", "--not-before", help="minimum version to choose", metavar='VERSION')
295 parser.add_option("-o", "--offline", help="try to avoid using the network", action='store_true')
296 parser.add_option("-r", "--refresh", help="refresh all used interfaces", action='store_true')
297 parser.add_option("", "--set-selections", help="run versions specified in XML file", metavar='FILE')
298 parser.add_option("-s", "--source", help="select source code", action='store_true')
299 parser.add_option("-v", "--verbose", help="more verbose output", action='count')
300 parser.add_option("-V", "--version", help="display version information", action='store_true')
301 parser.add_option("-w", "--wrapper", help="execute program using a debugger, etc", metavar='COMMAND')
302 parser.disable_interspersed_args()
304 (options, args) = parser.parse_args(command_args)
306 if options.verbose:
307 logger = logging.getLogger()
308 if options.verbose == 1:
309 logger.setLevel(logging.INFO)
310 else:
311 logger.setLevel(logging.DEBUG)
312 import zeroinstall
313 logging.info("Running 0launch %s %s; Python %s", zeroinstall.version, repr(args), sys.version)
315 try:
316 if options.list:
317 _list_interfaces(args)
318 elif options.version:
319 import zeroinstall
320 print "0launch (zero-install) " + zeroinstall.version
321 print "Copyright (C) 2007 Thomas Leonard"
322 print "This program comes with ABSOLUTELY NO WARRANTY,"
323 print "to the extent permitted by law."
324 print "You may redistribute copies of this program"
325 print "under the terms of the GNU Lesser General Public License."
326 print "For more information about these matters, see the file named COPYING."
327 elif options.set_selections:
328 from zeroinstall.injector import selections, qdom, run
329 sels = selections.Selections(qdom.parse(file(options.set_selections)))
330 run.execute_selections(sels, args, options.dry_run, options.main, options.wrapper)
331 elif getattr(options, 'import'):
332 _import_feed(args)
333 elif options.feed:
334 _manage_feeds(options, args)
335 else:
336 _normal_mode(options, args)
337 except UsageError:
338 parser.print_help()
339 sys.exit(1)
340 except model.SafeException, ex:
341 if options.verbose: raise
342 print >>sys.stderr, ex
343 sys.exit(1)