Add support for self-rewrites: <rewrite> not inside a <requires>
[zeroinstall/zeroinstall-mseaborn.git] / zeroinstall / injector / run.py
bloba45cf867394cd0dc7115698b65d27873c093e2a1
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 hashlib
9 import os, sys
10 from logging import debug, info
12 from zeroinstall.injector import selections
13 from zeroinstall.injector.model import SafeException, EnvironmentBinding, ZeroInstallImplementation
14 from zeroinstall.injector.iface_cache import iface_cache
15 from zeroinstall.support import basedir
17 def do_env_binding(environ, binding, path):
18 """Update this process's environment by applying the binding.
19 @param binding: the binding to apply
20 @type binding: L{model.EnvironmentBinding}
21 @param path: the selected implementation
22 @type path: str"""
23 environ[binding.name] = binding.get_value(path,
24 environ.get(binding.name, None))
25 info("%s=%s", binding.name, environ[binding.name])
27 def add_env_bindings(environ, sels):
28 for selection in sels.values():
29 _do_bindings(sels, environ, selection, selection.bindings)
30 for dep in selection.dependencies:
31 dep_impl = sels[dep.interface]
32 if not dep_impl.id.startswith('package:'):
33 _do_bindings(sels, environ, dep_impl, dep.bindings)
34 else:
35 debug("Implementation %s is native; no bindings needed", dep_impl)
37 def execute(policy, prog_args, dry_run = False, main = None, wrapper = None):
38 """Execute program. On success, doesn't return. On failure, raises an Exception.
39 Returns normally only for a successful dry run.
40 @param policy: a policy with the selected versions
41 @type policy: L{policy.Policy}
42 @param prog_args: arguments to pass to the program
43 @type prog_args: [str]
44 @param dry_run: if True, just print a message about what would have happened
45 @type dry_run: bool
46 @param main: the name of the binary to run, or None to use the default
47 @type main: str
48 @param wrapper: a command to use to actually run the binary, or None to run the binary directly
49 @type wrapper: str
50 @precondition: C{policy.ready and policy.get_uncached_implementations() == []}
51 """
52 selns = selections.Selections(policy)
53 execute_selections(selns, prog_args, dry_run=dry_run, main=main,
54 wrapper=wrapper)
56 def _do_bindings(selns, environ, impl, bindings):
57 for b in bindings:
58 if isinstance(b, EnvironmentBinding):
59 do_env_binding(environ, b, _get_implementation_path(selns, impl))
61 def read_file(filename):
62 fh = open(filename, "r")
63 try:
64 return fh.read()
65 finally:
66 fh.close()
68 def write_file(filename, data):
69 fh = open(filename, "w")
70 try:
71 fh.write(data)
72 finally:
73 fh.close()
75 def copy_tree(src_path, dest_path, copy_file):
76 if os.path.islink(src_path):
77 if os.path.islink(dest_path):
78 os.unlink(dest_path)
79 os.symlink(os.readlink(src_path), dest_path)
80 elif os.path.isdir(src_path):
81 if not os.path.exists(dest_path):
82 os.mkdir(dest_path)
83 for leafname in os.listdir(src_path):
84 copy_tree(os.path.join(src_path, leafname),
85 os.path.join(dest_path, leafname),
86 copy_file)
87 else:
88 copy_file(src_path, dest_path)
90 def rewrite_dir(impl_dir, dep_rewrites, self_rewrites):
91 cache_dir = os.path.join(basedir.xdg_cache_home, "0install.net", "rewrites")
92 if not os.path.exists(cache_dir):
93 os.makedirs(cache_dir)
94 digest = hashlib.sha1(str(dep_rewrites + self_rewrites)).hexdigest()
95 # We don't need the full path. Rewrites are local.
96 leafname = digest[:8]
97 dest_dir = os.path.join(cache_dir, leafname)
98 # TODO: fix race condition
99 if os.path.exists(dest_dir):
100 return dest_dir
101 rewrites = dep_rewrites[:]
102 for rewrite in self_rewrites:
103 rewrites.append((rewrite, dest_dir))
105 def copy_file(src_path, dest_path):
106 data = read_file(src_path)
107 # This is not an optimal way to do several
108 # substitutions, but when I tried to use the "array"
109 # module to do it in-place, it was slower.
110 for from_str, to_str in rewrites:
111 data = data.replace(from_str, to_str)
112 write_file(dest_path, data)
113 os.chmod(dest_path, os.stat(src_path).st_mode)
115 copy_tree(impl_dir, dest_dir, copy_file)
116 return dest_dir
118 def _get_implementation_path(selns, impl):
119 if impl.id.startswith('/'):
120 return impl.id
121 rewrites = []
122 for dependency in impl.dependencies:
123 for rewrite_string in dependency.rewrites:
124 dep_impl = selns.selections[dependency.interface]
125 to_path = _get_implementation_path(selns, dep_impl)
126 rewrites.append((rewrite_string, to_path))
127 impl_dir = iface_cache.stores.lookup(impl.id)
128 if len(rewrites) == 0 and len(impl.self_rewrites) == 0:
129 return impl_dir
130 return rewrite_dir(impl_dir, rewrites, impl.self_rewrites)
132 def execute_selections(selections, prog_args, dry_run = False, main = None, wrapper = None):
133 """Execute program. On success, doesn't return. On failure, raises an Exception.
134 Returns normally only for a successful dry run.
135 @param selections: the selected versions
136 @type selections: L{selections.Selections}
137 @param prog_args: arguments to pass to the program
138 @type prog_args: [str]
139 @param dry_run: if True, just print a message about what would have happened
140 @type dry_run: bool
141 @param main: the name of the binary to run, or None to use the default
142 @type main: str
143 @param wrapper: a command to use to actually run the binary, or None to run the binary directly
144 @type wrapper: str
145 @since: 0.27
146 @precondition: All implementations are in the cache.
148 add_env_bindings(os.environ, selections.selections)
149 _execute(selections, prog_args, dry_run, main, wrapper)
151 def test_selections(selections, prog_args, dry_run, main, wrapper = None):
152 """Run the program in a child process, collecting stdout and stderr.
153 @return: the output produced by the process
154 @since: 0.27
156 args = []
157 import tempfile
158 output = tempfile.TemporaryFile(prefix = '0launch-test')
159 try:
160 child = os.fork()
161 if child == 0:
162 # We are the child
163 try:
164 try:
165 os.dup2(output.fileno(), 1)
166 os.dup2(output.fileno(), 2)
167 execute_selections(selections, prog_args, dry_run, main)
168 except:
169 import traceback
170 traceback.print_exc()
171 finally:
172 sys.stdout.flush()
173 sys.stderr.flush()
174 os._exit(1)
176 info("Waiting for test process to finish...")
178 pid, status = os.waitpid(child, 0)
179 assert pid == child
181 output.seek(0)
182 results = output.read()
183 if status != 0:
184 results += "Error from child process: exit code = %d" % status
185 finally:
186 output.close()
188 return results
190 def get_executable_for_selections(selns, main=None):
191 root_impl = selns.selections[selns.interface]
192 assert root_impl is not None
194 if root_impl.id.startswith('package:'):
195 main = main or root_impl.main
196 prog_path = main
197 else:
198 if main is None:
199 main = root_impl.main
200 elif main.startswith('/'):
201 main = main[1:]
202 elif root_impl.main:
203 main = os.path.join(os.path.dirname(root_impl.main), main)
204 if main:
205 prog_path = os.path.join(_get_implementation_path(selns, root_impl), main)
207 if main is None:
208 raise SafeException("Implementation '%s' cannot be executed directly; it is just a library "
209 "to be used by other programs (or missing 'main' attribute)" %
210 root_impl)
212 if not os.path.exists(prog_path):
213 raise SafeException("File '%s' does not exist.\n"
214 "(implementation '%s' + program '%s')" %
215 (prog_path, root_impl.id, main))
216 return prog_path
218 def _execute(selns, prog_args, dry_run, main, wrapper):
219 prog_path = get_executable_for_selections(selns, main)
220 if wrapper:
221 prog_args = ['-c', wrapper + ' "$@"', '-', prog_path] + list(prog_args)
222 prog_path = '/bin/sh'
224 if dry_run:
225 print "Would execute:", prog_path, ' '.join(prog_args)
226 else:
227 info("Executing: %s", prog_path)
228 sys.stdout.flush()
229 sys.stderr.flush()
230 try:
231 os.execl(prog_path, prog_path, *prog_args)
232 except OSError, ex:
233 raise SafeException("Failed to run '%s': %s" % (prog_path, str(ex)))