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