Updated epydoc
[zeroinstall.git] / zeroinstall / injector / run.py
blobd7943ff9f7c8f8df0ec0519a9863403ffd8f2d54
1 """
2 Executes a set of implementations as a program.
3 """
5 # Copyright (C) 2009, Thomas Leonard
6 # See the README file for details, or visit http://0install.net.
8 from zeroinstall import _
9 import os, sys
10 from logging import info
11 from string import Template
13 from zeroinstall.injector.model import SafeException, EnvironmentBinding, ExecutableBinding, Command, Dependency
14 from zeroinstall.injector import namespaces, qdom
15 from zeroinstall.support import basedir
17 def do_env_binding(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 os.environ[binding.name] = binding.get_value(path,
24 os.environ.get(binding.name, None))
25 info("%s=%s", binding.name, os.environ[binding.name])
27 def execute(policy, prog_args, dry_run = False, main = None, wrapper = None):
28 """Execute program. On success, doesn't return. On failure, raises an Exception.
29 Returns normally only for a successful dry run.
30 @param policy: a policy with the selected versions
31 @type policy: L{policy.Policy}
32 @param prog_args: arguments to pass to the program
33 @type prog_args: [str]
34 @param dry_run: if True, just print a message about what would have happened
35 @type dry_run: bool
36 @param main: the name of the binary to run, or None to use the default
37 @type main: str
38 @param wrapper: a command to use to actually run the binary, or None to run the binary directly
39 @type wrapper: str
40 @precondition: C{policy.ready and policy.get_uncached_implementations() == []}
41 """
42 execute_selections(policy.solver.selections, prog_args, dry_run, main, wrapper)
44 def test_selections(selections, prog_args, dry_run, main, wrapper = None):
45 """Run the program in a child process, collecting stdout and stderr.
46 @return: the output produced by the process
47 @since: 0.27
48 """
49 import tempfile
50 output = tempfile.TemporaryFile(prefix = '0launch-test')
51 try:
52 child = os.fork()
53 if child == 0:
54 # We are the child
55 try:
56 try:
57 os.dup2(output.fileno(), 1)
58 os.dup2(output.fileno(), 2)
59 execute_selections(selections, prog_args, dry_run, main)
60 except:
61 import traceback
62 traceback.print_exc()
63 finally:
64 sys.stdout.flush()
65 sys.stderr.flush()
66 os._exit(1)
68 info(_("Waiting for test process to finish..."))
70 pid, status = os.waitpid(child, 0)
71 assert pid == child
73 output.seek(0)
74 results = output.read()
75 if status != 0:
76 results += _("Error from child process: exit code = %d") % status
77 finally:
78 output.close()
80 return results
82 def _process_args(args, element):
83 """Append each <arg> under <element> to args, performing $-expansion."""
84 for child in element.childNodes:
85 if child.uri == namespaces.XMLNS_IFACE and child.name == 'arg':
86 args.append(Template(child.content).substitute(os.environ))
88 class Setup(object):
89 """@since: 1.2"""
90 stores = None
91 selections = None
92 _exec_bindings = None
93 _checked_runenv = False
95 def __init__(self, stores, selections):
96 """@param stores: where to find cached implementations
97 @type stores: L{zerostore.Stores}"""
98 self.stores = stores
99 self.selections = selections
101 def build_command(self, command_iface, command_name, user_command = None):
102 """Create a list of strings to be passed to exec to run the <command>s in the selections.
103 @param command_iface: the interface of the program being run
104 @type command_iface: str
105 @param command_name: the name of the command being run
106 @type command_name: str
107 @param user_command: a custom command to use instead
108 @type user_command: L{model.Command}
109 @return: the argument list
110 @rtype: [str]"""
112 assert command_name
114 prog_args = []
115 sels = self.selections.selections
117 while command_name:
118 command_sel = sels[command_iface]
120 if user_command is None:
121 command = command_sel.get_command(command_name)
122 else:
123 command = user_command
124 user_command = None
126 command_args = []
128 # Add extra arguments for runner
129 runner = command.get_runner()
130 if runner:
131 command_iface = runner.interface
132 command_name = runner.command
133 _process_args(command_args, runner.qdom)
134 else:
135 command_iface = None
136 command_name = None
138 # Add main program path
139 command_path = command.path
140 if command_path is not None:
141 if command_sel.id.startswith('package:'):
142 prog_path = command_path
143 else:
144 if command_path.startswith('/'):
145 raise SafeException(_("Command path must be relative, but '%s' starts with '/'!") %
146 command_path)
147 prog_path = os.path.join(self._get_implementation_path(command_sel), command_path)
149 assert prog_path is not None
151 if not os.path.exists(prog_path):
152 raise SafeException(_("File '%(program_path)s' does not exist.\n"
153 "(implementation '%(implementation_id)s' + program '%(main)s')") %
154 {'program_path': prog_path, 'implementation_id': command_sel.id,
155 'main': command_path})
157 command_args.append(prog_path)
159 # Add extra arguments for program
160 _process_args(command_args, command.qdom)
162 prog_args = command_args + prog_args
164 # Each command is run by the next, but the last one is run by exec, and we
165 # need a path for that.
166 if command.path is None:
167 raise SafeException("Missing 'path' attribute on <command>")
169 return prog_args
171 def _get_implementation_path(self, impl):
172 return impl.local_path or self.stores.lookup_any(impl.digests)
174 def prepare_env(self):
175 """Do all the environment bindings in the selections (setting os.environ)."""
176 self._exec_bindings = []
178 def _do_bindings(impl, bindings, dep):
179 for b in bindings:
180 self.do_binding(impl, b, dep)
182 def _do_deps(deps):
183 for dep in deps:
184 dep_impl = sels.get(dep.interface, None)
185 if dep_impl is None:
186 assert dep.importance != Dependency.Essential, dep
187 elif not dep_impl.id.startswith('package:'):
188 _do_bindings(dep_impl, dep.bindings, dep)
190 sels = self.selections.selections
191 for selection in sels.values():
192 _do_bindings(selection, selection.bindings, None)
193 _do_deps(selection.dependencies)
195 # Process commands' dependencies' bindings too
196 for command in selection.get_commands().values():
197 _do_deps(command.requires)
199 # Do these after <environment>s, because they may do $-expansion
200 for binding, dep in self._exec_bindings:
201 self.do_exec_binding(binding, dep)
202 self._exec_bindings = None
204 def do_binding(self, impl, binding, dep):
205 """Called by L{prepare_env} for each binding.
206 Sub-classes may wish to override this.
207 @param impl: the selected implementation
208 @type impl: L{selections.Selection}
209 @param binding: the binding to be processed
210 @type binding: L{model.Binding}
211 @param dep: the dependency containing the binding, or None for implementation bindings
212 @type dep: L{model.Dependency}
214 if isinstance(binding, EnvironmentBinding):
215 do_env_binding(binding, self._get_implementation_path(impl))
216 elif isinstance(binding, ExecutableBinding):
217 self._exec_bindings.append((binding, dep))
219 def do_exec_binding(self, binding, dep):
220 if dep is None:
221 raise SafeException("<%s> can only appear within a <requires>" % binding.qdom.name)
222 name = binding.name
223 if '/' in name or name.startswith('.') or "'" in name:
224 raise SafeException("Invalid <executable> name '%s'" % name)
225 exec_dir = basedir.save_cache_path(namespaces.config_site, namespaces.config_prog, 'executables', name)
226 exec_path = os.path.join(exec_dir, name)
228 if not self._checked_runenv:
229 self._check_runenv()
231 if not os.path.exists(exec_path):
232 # Symlink ~/.cache/0install.net/injector/executables/$name/$name to runenv.py
233 os.symlink('../../runenv.py', exec_path)
234 os.chmod(exec_dir, 0o500)
236 if binding.in_path:
237 path = os.environ["PATH"] = exec_dir + os.pathsep + os.environ["PATH"]
238 info("PATH=%s", path)
239 else:
240 os.environ[name] = exec_path
241 info("%s=%s", name, exec_path)
243 import json
244 args = self.build_command(dep.interface, binding.command)
245 os.environ["0install-runenv-" + name] = json.dumps(args)
247 def _check_runenv(self):
248 # Create the runenv.py helper script under ~/.cache if missing or out-of-date
249 main_dir = basedir.save_cache_path(namespaces.config_site, namespaces.config_prog)
250 runenv = os.path.join(main_dir, 'runenv.py')
251 expected_contents = "#!%s\nfrom zeroinstall.injector import _runenv; _runenv.main()\n" % sys.executable
253 actual_contents = None
254 if os.path.exists(runenv):
255 with open(runenv) as s:
256 actual_contents = s.read()
258 if actual_contents != expected_contents:
259 import tempfile
260 tmp = tempfile.NamedTemporaryFile('w', dir = main_dir, delete = False)
261 info("Updating %s", runenv)
262 tmp.write(expected_contents)
263 tmp.close()
264 os.chmod(tmp.name, 0555)
265 os.rename(tmp.name, runenv)
267 self._checked_runenv = True
269 def execute_selections(selections, prog_args, dry_run = False, main = None, wrapper = None, stores = None):
270 """Execute program. On success, doesn't return. On failure, raises an Exception.
271 Returns normally only for a successful dry run.
272 @param selections: the selected versions
273 @type selections: L{selections.Selections}
274 @param prog_args: arguments to pass to the program
275 @type prog_args: [str]
276 @param dry_run: if True, just print a message about what would have happened
277 @type dry_run: bool
278 @param main: the name of the binary to run, or None to use the default
279 @type main: str
280 @param wrapper: a command to use to actually run the binary, or None to run the binary directly
281 @type wrapper: str
282 @since: 0.27
283 @precondition: All implementations are in the cache.
285 #assert stores is not None
286 if stores is None:
287 from zeroinstall import zerostore
288 stores = zerostore.Stores()
290 setup = Setup(stores, selections)
292 commands = selections.commands
293 if main is not None:
294 # Replace first command with user's input
295 if main.startswith('/'):
296 main = main[1:] # User specified a path relative to the package root
297 else:
298 old_path = commands[0].path
299 assert old_path, "Can't use a relative replacement main when there is no original one!"
300 main = os.path.join(os.path.dirname(old_path), main) # User main is relative to command's name
301 # Copy all child nodes (e.g. <runner>) except for the arguments
302 user_command_element = qdom.Element(namespaces.XMLNS_IFACE, 'command', {'path': main})
303 if commands:
304 for child in commands[0].qdom.childNodes:
305 if child.uri == namespaces.XMLNS_IFACE and child.name == 'arg':
306 continue
307 user_command_element.childNodes.append(child)
308 user_command = Command(user_command_element, None)
309 else:
310 user_command = None
312 setup.prepare_env()
313 prog_args = setup.build_command(selections.interface, selections.command, user_command) + prog_args
315 if wrapper:
316 prog_args = ['/bin/sh', '-c', wrapper + ' "$@"', '-'] + list(prog_args)
318 if dry_run:
319 print _("Would execute: %s") % ' '.join(prog_args)
320 else:
321 info(_("Executing: %s"), prog_args)
322 sys.stdout.flush()
323 sys.stderr.flush()
324 try:
325 os.execv(prog_args[0], prog_args)
326 except OSError as ex:
327 raise SafeException(_("Failed to run '%(program_path)s': %(exception)s") % {'program_path': prog_args[0], 'exception': str(ex)})