Fixed error solving with optional dependencies
[zeroinstall/solver.git] / 0alias
blob681fa3d779db613c326e583ce15f4a31f5b0aafc
1 #!/usr/bin/env python
2 # Copyright (C) 2010, Thomas Leonard
3 # See the README file for details, or visit http://0install.net.
5 from __future__ import print_function
7 import locale
8 import logging
9 from logging import warn
10 try:
11 locale.setlocale(locale.LC_ALL, '')
12 except locale.Error:
13 warn('Error setting locale (eg. Invalid locale)')
15 import os, sys
17 ## PATH ##
19 from optparse import OptionParser
21 from zeroinstall.injector import reader, model
22 from zeroinstall.injector.config import load_config
23 from zeroinstall import support, alias, helpers, _
24 from zeroinstall.support import basedir
26 config = load_config()
28 def export(name, value):
29 """Try to guess the command to set an environment variable."""
30 shell = os.environ.get('SHELL', '?')
31 if 'csh' in shell:
32 return "setenv %s %s" % (name, value)
33 return "export %s=%s" % (name, value)
35 def find_path(paths):
36 """Find the first writable path in the list,
37 skipping /bin, /sbin and everything under /usr except /usr/local/bin"""
38 for path in paths:
39 if path.startswith('/usr/') and not path.startswith('/usr/local/bin'):
40 # (/usr/local/bin is OK if we're running as root)
41 pass
42 elif path.startswith('/bin') or path.startswith('/sbin'):
43 pass
44 elif os.path.realpath(path).startswith(basedir.xdg_cache_home):
45 pass # print "Skipping cache", first_path
46 elif not os.access(path, os.W_OK):
47 pass # print "No access", first_path
48 else:
49 break
50 else:
51 return None
53 return path
55 # Do this here so we can include it in the help message.
56 # But, don't abort if there isn't one because we might
57 # be doing something else (e.g. --manpage)
58 first_path = find_path(os.environ['PATH'].split(':'))
59 in_path = first_path is not None
60 if not in_path:
61 first_path = os.path.expanduser('~/bin/')
63 parser = OptionParser(usage="usage: %%prog [options] alias [interface [main]]\n\n"
64 "Creates a script to run 'interface' (will be created in\n"
65 "%s unless overridden by --dir).\n\n"
66 "If no interface is given, edits the policy for an existing alias.\n"
67 "For interfaces providing more than one executable, the desired\n"
68 "'main' binary or command may also be given." % first_path)
69 parser.add_option("-c", "--command", help="the command the alias will run (default 'run')", metavar='COMMNAD')
70 parser.add_option("-m", "--manpage", help="show the manual page for an existing alias", action='store_true')
71 parser.add_option("-r", "--resolve", help="show the URI for an alias", action='store_true')
72 parser.add_option("-v", "--verbose", help="more verbose output", action='count')
73 parser.add_option("-V", "--version", help="display version information", action='store_true')
74 parser.add_option("-d", "--dir", help="install in DIR", dest="user_path", metavar="DIR")
76 (options, args) = parser.parse_args()
78 if options.verbose:
79 logger = logging.getLogger()
80 if options.verbose == 1:
81 logger.setLevel(logging.INFO)
82 else:
83 logger.setLevel(logging.DEBUG)
84 hdlr = logging.StreamHandler()
85 fmt = logging.Formatter("%(levelname)s:%(message)s")
86 hdlr.setFormatter(fmt)
87 logger.addHandler(hdlr)
89 if options.version:
90 import zeroinstall
91 print("0alias (zero-install) " + zeroinstall.version)
92 print("Copyright (C) 2010 Thomas Leonard")
93 print("This program comes with ABSOLUTELY NO WARRANTY,")
94 print("to the extent permitted by law.")
95 print("You may redistribute copies of this program")
96 print("under the terms of the GNU Lesser General Public License.")
97 print("For more information about these matters, see the file named COPYING.")
98 sys.exit(0)
100 if options.manpage:
101 if len(args) != 1:
102 os.execlp('man', 'man', *args)
103 sys.exit(1)
105 if len(args) < 1 or len(args) > 3:
106 parser.print_help()
107 sys.exit(1)
108 alias_prog, interface_uri, main = (list(args) + [None, None])[:3]
109 command = options.command
111 if options.resolve or options.manpage:
112 if interface_uri is not None:
113 parser.print_help()
114 sys.exit(1)
116 if options.user_path:
117 first_path = options.user_path
119 if interface_uri is None:
120 if options.command:
121 print("Can't use --command when editing an existing alias", file=sys.stderr)
122 sys.exit(1)
123 try:
124 if not os.path.isabs(alias_prog):
125 full_path = support.find_in_path(alias_prog)
126 if not full_path:
127 raise alias.NotAnAliasScript("Not found in $PATH: " + alias_prog)
128 else:
129 full_path = alias_prog
131 alias_info = alias.parse_script(full_path)
132 interface_uri = alias_info.uri
133 main = alias_info.main
134 command = alias_info.command
135 except (alias.NotAnAliasScript, IOError) as ex:
136 # (we get IOError if e.g. the script isn't readable)
137 if options.manpage:
138 logging.debug("not a 0alias script '%s': %s", alias_prog, ex)
139 os.execlp('man', 'man', *args)
140 print(str(ex), file=sys.stderr)
141 sys.exit(1)
143 interface_uri = model.canonical_iface_uri(interface_uri)
145 if options.resolve:
146 print(interface_uri)
147 sys.exit(0)
149 if options.manpage:
150 sels = helpers.ensure_cached(interface_uri, command = command or 'run')
151 if not sels:
152 # Cancelled by user
153 sys.exit(1)
155 if sels.commands:
156 selected_command = sels.commands[0]
157 else:
158 print("No <command> in selections!", file=sys.stderr)
159 selected_impl = sels.selections[interface_uri]
161 impl_path = selected_impl.local_path or config.iface_cache.stores.lookup_any(selected_impl.digests)
163 if main is None:
164 main = selected_command.path
165 if main is None:
166 print("No main program for interface '%s'" % interface_uri, file=sys.stderr)
167 sys.exit(1)
169 prog_name = os.path.basename(main)
170 alias_name = os.path.basename(args[0])
172 assert impl_path
174 # TODO: the feed should say where the man-pages are, but for now we'll accept
175 # a directory called man in some common locations...
176 for mandir in ['man', 'share/man', 'usr/man', 'usr/share/man']:
177 manpath = os.path.join(impl_path, mandir)
178 if os.path.isdir(manpath):
179 # Note: unlike "man -M", this also copes with LANG settings...
180 os.environ['MANPATH'] = manpath
181 os.execlp('man', 'man', prog_name)
182 sys.exit(1)
184 # No man directory given or found, so try searching for man files
186 manpages = []
187 for root, dirs, files in os.walk(impl_path):
188 for f in files:
189 if f.endswith('.gz'):
190 manpage_file = f[:-3]
191 else:
192 manpage_file = f
193 if manpage_file.endswith('.1') or \
194 manpage_file.endswith('.6') or \
195 manpage_file.endswith('.8'):
196 manpage_prog = manpage_file[:-2]
197 if manpage_prog == prog_name or manpage_prog == alias_name:
198 os.execlp('man', 'man', os.path.join(root, f))
199 sys.exit(1)
200 else:
201 manpages.append((root, f))
203 print("No matching manpage was found for '%s' (%s)" % (alias_name, interface_uri))
204 if manpages:
205 print("These non-matching man-pages were found, however:")
206 for root, file in manpages:
207 print(os.path.join(root, file))
208 sys.exit(1)
210 if not os.path.isdir(first_path):
211 print("(creating directory %s)" % first_path)
212 os.makedirs(first_path)
214 if len(args) == 1:
215 os.execlp('0launch', '0launch', '-gd', '--', interface_uri)
216 sys.exit(1)
218 try:
219 if not options.user_path:
220 if alias_prog == '0launch':
221 raise model.SafeException(_('Refusing to create an alias named "0launch" (to avoid an infinite loop)'))
223 interface = model.Interface(interface_uri)
224 if not reader.update_from_cache(interface):
225 print("Interface '%s' not currently in cache. Fetching..." % interface_uri, file=sys.stderr)
226 if os.spawnlp(os.P_WAIT, '0launch', '0launch', '-d', interface_uri):
227 raise model.SafeException("0launch failed")
228 if not reader.update_from_cache(interface):
229 raise model.SafeException("Interface still not in cache. Aborting.")
231 script = os.path.join(first_path, alias_prog)
232 if os.path.exists(script):
233 raise model.SafeException("File '%s' already exists. Delete it first." % script)
234 sys.exit(1)
235 except model.SafeException as ex:
236 print(ex, file=sys.stderr)
237 sys.exit(1)
239 seen = set([interface_uri])
240 while True:
241 feed = config.iface_cache.get_feed(interface_uri)
242 replacement_uri = feed.get_replaced_by()
243 if replacement_uri is None:
244 break
245 print(_("(interface {old} has been replaced by {new}; using that instead)").format(old = interface_uri, new = replacement_uri))
246 assert replacement_uri not in seen, "loop detected!"
247 seen.add(replacement_uri)
248 interface_uri = replacement_uri
250 wrapper = open(script, 'w')
251 alias.write_script(wrapper, interface_uri, main, command = command)
253 # Make new script executable
254 os.chmod(script, 0o111 | os.fstat(wrapper.fileno()).st_mode)
255 wrapper.close()
257 #print "Created script '%s'." % script
258 #print "To edit policy: 0alias %s" % alias_prog
259 if options.user_path:
260 pass # Assume user knows what they're doing
261 elif not in_path:
262 print('Warning: %s is not in $PATH. Add it with:\n%s' % (first_path, export('PATH', first_path + ':$PATH')), file=sys.stderr)
263 else:
264 shell = os.environ.get('SHELL', '?')
265 if not (shell.endswith('/zsh') or shell.endswith('/bash')):
266 print("(note: some shells require you to type 'rehash' now)")