Catch cyclic dependency graphs and give a nice error instead of running out of stack
[zeroinstall/zeroinstall-mseaborn.git] / zeroinstall / injector / run.py
blob3485123c598d883e04ea6aaab94e63d2807c6b0f
1 """
2 Executes a set of implementations as a program.
3 """
5 # Copyright (C) 2006, Thomas Leonard
6 # See the README file for details, or visit http://0install.net.
8 import errno
9 import hashlib
10 import os, sys
11 from logging import debug, info
13 from zeroinstall.injector import selections
14 from zeroinstall.injector.model import SafeException, EnvironmentBinding, ZeroInstallImplementation
15 from zeroinstall.injector.iface_cache import iface_cache
16 from zeroinstall.support import basedir
18 class CyclicDependencyError(Exception):
19 pass
21 def do_env_binding(environ, binding, path):
22 """Update this process's environment by applying the binding.
23 @param binding: the binding to apply
24 @type binding: L{model.EnvironmentBinding}
25 @param path: the selected implementation
26 @type path: str"""
27 environ[binding.name] = binding.get_value(path,
28 environ.get(binding.name, None))
29 info("%s=%s", binding.name, environ[binding.name])
31 def add_env_bindings(environ, sels):
32 for selection in sels.selections.values():
33 _do_bindings(sels, environ, selection, selection.bindings)
34 for dep in selection.dependencies:
35 dep_impl = sels.selections[dep.interface]
36 if not dep_impl.id.startswith('package:'):
37 _do_bindings(sels, environ, dep_impl, dep.bindings)
38 else:
39 debug("Implementation %s is native; no bindings needed", dep_impl)
41 def execute(policy, prog_args, dry_run = False, main = None, wrapper = None):
42 """Execute program. On success, doesn't return. On failure, raises an Exception.
43 Returns normally only for a successful dry run.
44 @param policy: a policy with the selected versions
45 @type policy: L{policy.Policy}
46 @param prog_args: arguments to pass to the program
47 @type prog_args: [str]
48 @param dry_run: if True, just print a message about what would have happened
49 @type dry_run: bool
50 @param main: the name of the binary to run, or None to use the default
51 @type main: str
52 @param wrapper: a command to use to actually run the binary, or None to run the binary directly
53 @type wrapper: str
54 @precondition: C{policy.ready and policy.get_uncached_implementations() == []}
55 """
56 selns = selections.Selections(policy)
57 execute_selections(selns, prog_args, dry_run=dry_run, main=main,
58 wrapper=wrapper)
60 def _do_bindings(selns, environ, impl, bindings):
61 for b in bindings:
62 if isinstance(b, EnvironmentBinding):
63 do_env_binding(environ, b, _get_implementation_path(selns, impl))
65 def read_file(filename):
66 fh = open(filename, "r")
67 try:
68 return fh.read()
69 finally:
70 fh.close()
72 def write_file(filename, data):
73 fh = open(filename, "w")
74 try:
75 fh.write(data)
76 finally:
77 fh.close()
79 def copy_tree(src_path, dest_path, copy_file):
80 if os.path.islink(src_path):
81 if os.path.islink(dest_path):
82 os.unlink(dest_path)
83 os.symlink(os.readlink(src_path), dest_path)
84 elif os.path.isdir(src_path):
85 if not os.path.exists(dest_path):
86 os.mkdir(dest_path)
87 for leafname in os.listdir(src_path):
88 copy_tree(os.path.join(src_path, leafname),
89 os.path.join(dest_path, leafname),
90 copy_file)
91 else:
92 copy_file(src_path, dest_path)
94 def rewrite_dir(impl_dir, dep_rewrites, self_rewrites):
95 cache_dir = os.path.join(basedir.xdg_cache_home, "0install.net", "rewrites")
96 if not os.path.exists(cache_dir):
97 os.makedirs(cache_dir)
98 digest = hashlib.sha1(str(dep_rewrites + self_rewrites)).hexdigest()
99 # We don't need the full path. Rewrites are local.
100 leafname = digest[:8]
101 dest_dir = os.path.join(cache_dir, leafname)
102 # TODO: fix race condition
103 if os.path.exists(dest_dir):
104 return dest_dir
105 rewrites = dep_rewrites[:]
106 for rewrite in self_rewrites:
107 dest_dir2 = _make_fixed_length_link(dest_dir, len(rewrite))
108 rewrites.append((rewrite, dest_dir2))
110 def copy_file(src_path, dest_path):
111 data = read_file(src_path)
112 # This is not an optimal way to do several
113 # substitutions, but when I tried to use the "array"
114 # module to do it in-place, it was slower.
115 for from_str, to_str in rewrites:
116 data = data.replace(from_str, to_str)
117 write_file(dest_path, data)
118 os.chmod(dest_path, os.stat(src_path).st_mode)
120 copy_tree(impl_dir, dest_dir, copy_file)
121 return dest_dir
123 def _make_fixed_length_link(pathname, length):
124 link_dir = os.path.join(basedir.xdg_cache_home, "0install.net", "ln")
125 if not os.path.exists(link_dir):
126 os.makedirs(link_dir)
127 digest = hashlib.sha1(pathname).hexdigest()[:8]
128 dest_base = os.path.join(link_dir, digest)
129 assert len(dest_base) <= length, (dest_base, len(dest_base), length)
130 dest_path = dest_base + "X" * (length - len(dest_base))
131 try:
132 os.symlink(pathname, dest_path)
133 except OSError, exn:
134 if exn.errno != errno.EEXIST:
135 raise
136 return dest_path
138 CYCLIC_REF = object()
140 def memoize(func):
141 # TODO: use weak references?
142 cache = {}
143 def wrapper(*args):
144 try:
145 value = cache[args]
146 except KeyError:
147 cache[args] = CYCLIC_REF
148 value = func(*args)
149 cache[args] = value
150 else:
151 if value is CYCLIC_REF:
152 raise CyclicDependencyError()
153 return value
154 return wrapper
156 @memoize
157 def _get_implementation_path(selns, impl):
158 if impl.id.startswith('/'):
159 return impl.id
160 rewrites = []
161 for dependency in impl.dependencies:
162 for rewrite_string in dependency.rewrites:
163 dep_impl = selns.selections[dependency.interface]
164 to_path = _get_implementation_path(selns, dep_impl)
165 to_path = _make_fixed_length_link(to_path, len(rewrite_string))
166 rewrites.append((rewrite_string, to_path))
167 impl_dir = iface_cache.stores.lookup(impl.id)
168 if len(rewrites) == 0 and len(impl.self_rewrites) == 0:
169 return impl_dir
170 return rewrite_dir(impl_dir, rewrites, impl.self_rewrites)
172 def execute_selections(selections, prog_args, dry_run = False, main = None, wrapper = None):
173 """Execute program. On success, doesn't return. On failure, raises an Exception.
174 Returns normally only for a successful dry run.
175 @param selections: the selected versions
176 @type selections: L{selections.Selections}
177 @param prog_args: arguments to pass to the program
178 @type prog_args: [str]
179 @param dry_run: if True, just print a message about what would have happened
180 @type dry_run: bool
181 @param main: the name of the binary to run, or None to use the default
182 @type main: str
183 @param wrapper: a command to use to actually run the binary, or None to run the binary directly
184 @type wrapper: str
185 @since: 0.27
186 @precondition: All implementations are in the cache.
188 add_env_bindings(os.environ, selections)
189 _execute(selections, prog_args, dry_run, main, wrapper)
191 def test_selections(selections, prog_args, dry_run, main, wrapper = None):
192 """Run the program in a child process, collecting stdout and stderr.
193 @return: the output produced by the process
194 @since: 0.27
196 args = []
197 import tempfile
198 output = tempfile.TemporaryFile(prefix = '0launch-test')
199 try:
200 child = os.fork()
201 if child == 0:
202 # We are the child
203 try:
204 try:
205 os.dup2(output.fileno(), 1)
206 os.dup2(output.fileno(), 2)
207 execute_selections(selections, prog_args, dry_run, main)
208 except:
209 import traceback
210 traceback.print_exc()
211 finally:
212 sys.stdout.flush()
213 sys.stderr.flush()
214 os._exit(1)
216 info("Waiting for test process to finish...")
218 pid, status = os.waitpid(child, 0)
219 assert pid == child
221 output.seek(0)
222 results = output.read()
223 if status != 0:
224 results += "Error from child process: exit code = %d" % status
225 finally:
226 output.close()
228 return results
230 def get_executable_for_selections(selns, main=None):
231 root_impl = selns.selections[selns.interface]
232 assert root_impl is not None
234 if root_impl.id.startswith('package:'):
235 main = main or root_impl.main
236 prog_path = main
237 else:
238 if main is None:
239 main = root_impl.main
240 elif main.startswith('/'):
241 main = main[1:]
242 elif root_impl.main:
243 main = os.path.join(os.path.dirname(root_impl.main), main)
244 if main:
245 prog_path = os.path.join(_get_implementation_path(selns, root_impl), main)
247 if main is None:
248 raise SafeException("Implementation '%s' cannot be executed directly; it is just a library "
249 "to be used by other programs (or missing 'main' attribute)" %
250 root_impl)
252 if not os.path.exists(prog_path):
253 raise SafeException("File '%s' does not exist.\n"
254 "(implementation '%s' + program '%s')" %
255 (prog_path, root_impl.id, main))
256 return prog_path
258 def _execute(selns, prog_args, dry_run, main, wrapper):
259 prog_path = get_executable_for_selections(selns, main)
260 if wrapper:
261 prog_args = ['-c', wrapper + ' "$@"', '-', prog_path] + list(prog_args)
262 prog_path = '/bin/sh'
264 if dry_run:
265 print "Would execute:", prog_path, ' '.join(prog_args)
266 else:
267 info("Executing: %s", prog_path)
268 sys.stdout.flush()
269 sys.stderr.flush()
270 try:
271 os.execl(prog_path, prog_path, *prog_args)
272 except OSError, ex:
273 raise SafeException("Failed to run '%s': %s" % (prog_path, str(ex)))