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