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