Added support for optional dependencies
[zeroinstall.git] / zeroinstall / injector / run.py
blob02d169cf9bcad53443866ec255d718a6badf15b7
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, Command, Dependency
14 from zeroinstall.injector import namespaces, qdom
16 def do_env_binding(binding, path):
17 """Update this process's environment by applying the binding.
18 @param binding: the binding to apply
19 @type binding: L{model.EnvironmentBinding}
20 @param path: the selected implementation
21 @type path: str"""
22 os.environ[binding.name] = binding.get_value(path,
23 os.environ.get(binding.name, None))
24 info("%s=%s", binding.name, os.environ[binding.name])
26 def execute(policy, prog_args, dry_run = False, main = None, wrapper = None):
27 """Execute program. On success, doesn't return. On failure, raises an Exception.
28 Returns normally only for a successful dry run.
29 @param policy: a policy with the selected versions
30 @type policy: L{policy.Policy}
31 @param prog_args: arguments to pass to the program
32 @type prog_args: [str]
33 @param dry_run: if True, just print a message about what would have happened
34 @type dry_run: bool
35 @param main: the name of the binary to run, or None to use the default
36 @type main: str
37 @param wrapper: a command to use to actually run the binary, or None to run the binary directly
38 @type wrapper: str
39 @precondition: C{policy.ready and policy.get_uncached_implementations() == []}
40 """
41 execute_selections(policy.solver.selections, prog_args, dry_run, main, wrapper)
43 def test_selections(selections, prog_args, dry_run, main, wrapper = None):
44 """Run the program in a child process, collecting stdout and stderr.
45 @return: the output produced by the process
46 @since: 0.27
47 """
48 import tempfile
49 output = tempfile.TemporaryFile(prefix = '0launch-test')
50 try:
51 child = os.fork()
52 if child == 0:
53 # We are the child
54 try:
55 try:
56 os.dup2(output.fileno(), 1)
57 os.dup2(output.fileno(), 2)
58 execute_selections(selections, prog_args, dry_run, main)
59 except:
60 import traceback
61 traceback.print_exc()
62 finally:
63 sys.stdout.flush()
64 sys.stderr.flush()
65 os._exit(1)
67 info(_("Waiting for test process to finish..."))
69 pid, status = os.waitpid(child, 0)
70 assert pid == child
72 output.seek(0)
73 results = output.read()
74 if status != 0:
75 results += _("Error from child process: exit code = %d") % status
76 finally:
77 output.close()
79 return results
81 def _process_args(args, element):
82 """Append each <arg> under <element> to args, performing $-expansion."""
83 for child in element.childNodes:
84 if child.uri == namespaces.XMLNS_IFACE and child.name == 'arg':
85 args.append(Template(child.content).substitute(os.environ))
87 class Setup(object):
88 """@since: 1.1"""
89 stores = None
91 def __init__(self, stores):
92 """@param stores: where to find cached implementations
93 @type stores: L{zerostore.Stores}"""
94 self.stores = stores
96 def build_command_args(self, selections, commands = None):
97 """Create a list of strings to be passed to exec to run the <command>s in the selections.
98 @param selections: the selections containing the commands
99 @type selections: L{selections.Selections}
100 @param commands: the commands to be used (taken from selections is None)
101 @type commands: [L{model.Command}]
102 @return: the argument list
103 @rtype: [str]"""
105 prog_args = []
106 commands = commands or selections.commands
107 sels = selections.selections
109 # Each command is run by the next, but the last one is run by exec, and we
110 # need a path for that.
111 if commands[-1].path is None:
112 raise SafeException("Missing 'path' attribute on <command>")
114 command_iface = selections.interface
115 for command in commands:
116 command_sel = sels[command_iface]
118 command_args = []
120 # Add extra arguments for runner
121 runner = command.get_runner()
122 if runner:
123 command_iface = runner.interface
124 _process_args(command_args, runner.qdom)
126 # Add main program path
127 command_path = command.path
128 if command_path is not None:
129 if command_sel.id.startswith('package:'):
130 prog_path = command_path
131 else:
132 if command_path.startswith('/'):
133 raise SafeException(_("Command path must be relative, but '%s' starts with '/'!") %
134 command_path)
135 prog_path = os.path.join(self._get_implementation_path(command_sel), command_path)
137 assert prog_path is not None
139 if not os.path.exists(prog_path):
140 raise SafeException(_("File '%(program_path)s' does not exist.\n"
141 "(implementation '%(implementation_id)s' + program '%(main)s')") %
142 {'program_path': prog_path, 'implementation_id': command_sel.id,
143 'main': command_path})
145 command_args.append(prog_path)
147 # Add extra arguments for program
148 _process_args(command_args, command.qdom)
150 prog_args = command_args + prog_args
152 return prog_args
154 def _get_implementation_path(self, impl):
155 return impl.local_path or self.stores.lookup_any(impl.digests)
157 def prepare_env(self, selections):
158 """Do all the environment bindings in selections (setting os.environ).
159 @param selections: the selections to be used
160 @type selections: L{selections.Selections}"""
162 def _do_bindings(impl, bindings):
163 for b in bindings:
164 self.do_binding(impl, b)
166 commands = selections.commands
167 sels = selections.selections
168 for selection in sels.values():
169 _do_bindings(selection, selection.bindings)
170 for dep in selection.dependencies:
171 dep_impl = sels.get(dep.interface, None)
172 if dep_impl is None:
173 assert dep.importance != Dependency.Essential, dep
174 elif not dep_impl.id.startswith('package:'):
175 _do_bindings(dep_impl, dep.bindings)
176 # Process commands' dependencies' bindings too
177 # (do this here because we still want the bindings, even with --main)
178 for command in commands:
179 for dep in command.requires:
180 dep_impl = sels.get(dep.interface, None)
181 if dep_impl is None:
182 assert dep.importance != Dependency.Essential, dep
183 elif not dep_impl.id.startswith('package:'):
184 _do_bindings(dep_impl, dep.bindings)
186 def do_binding(self, impl, binding):
187 """Called by L{prepare_env} for each binding.
188 Sub-classes may wish to override this."""
189 if isinstance(binding, EnvironmentBinding):
190 do_env_binding(binding, self._get_implementation_path(impl))
192 def execute_selections(selections, prog_args, dry_run = False, main = None, wrapper = None, stores = None):
193 """Execute program. On success, doesn't return. On failure, raises an Exception.
194 Returns normally only for a successful dry run.
195 @param selections: the selected versions
196 @type selections: L{selections.Selections}
197 @param prog_args: arguments to pass to the program
198 @type prog_args: [str]
199 @param dry_run: if True, just print a message about what would have happened
200 @type dry_run: bool
201 @param main: the name of the binary to run, or None to use the default
202 @type main: str
203 @param wrapper: a command to use to actually run the binary, or None to run the binary directly
204 @type wrapper: str
205 @since: 0.27
206 @precondition: All implementations are in the cache.
208 #assert stores is not None
209 if stores is None:
210 from zeroinstall import zerostore
211 stores = zerostore.Stores()
213 setup = Setup(stores)
215 commands = selections.commands
216 if main is not None:
217 # Replace first command with user's input
218 if main.startswith('/'):
219 main = main[1:] # User specified a path relative to the package root
220 else:
221 old_path = commands[0].path
222 assert old_path, "Can't use a relative replacement main when there is no original one!"
223 main = os.path.join(os.path.dirname(old_path), main) # User main is relative to command's name
224 # Copy all child nodes (e.g. <runner>) except for the arguments
225 user_command_element = qdom.Element(namespaces.XMLNS_IFACE, 'command', {'path': main})
226 if commands:
227 for child in commands[0].qdom.childNodes:
228 if child.uri == namespaces.XMLNS_IFACE and child.name == 'arg':
229 continue
230 user_command_element.childNodes.append(child)
231 user_command = Command(user_command_element, None)
232 commands = [user_command] + commands[1:]
234 setup.prepare_env(selections)
235 prog_args = setup.build_command_args(selections, commands) + prog_args
237 if wrapper:
238 prog_args = ['/bin/sh', '-c', wrapper + ' "$@"', '-'] + list(prog_args)
240 if dry_run:
241 print _("Would execute: %s") % ' '.join(prog_args)
242 else:
243 info(_("Executing: %s"), prog_args)
244 sys.stdout.flush()
245 sys.stderr.flush()
246 try:
247 os.execv(prog_args[0], prog_args)
248 except OSError as ex:
249 raise SafeException(_("Failed to run '%(program_path)s': %(exception)s") % {'program_path': prog_args[0], 'exception': str(ex)})