Added <executable> binding
[zeroinstall.git] / zeroinstall / injector / run.py
blobeb71300318e14282550680a91b7ddaca8f2efb6d
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
94 def __init__(self, stores, selections):
95 """@param stores: where to find cached implementations
96 @type stores: L{zerostore.Stores}"""
97 self.stores = stores
98 self.selections = selections
100 def build_command(self, command_iface, command_name, user_command = None):
101 """Create a list of strings to be passed to exec to run the <command>s in the selections.
102 @param commands: the commands to be used (taken from selections is None)
103 @type commands: [L{model.Command}]
104 @return: the argument list
105 @rtype: [str]"""
107 assert command_name
109 prog_args = []
110 sels = self.selections.selections
112 while command_name:
113 command_sel = sels[command_iface]
115 if user_command is None:
116 command = command_sel.get_command(command_name)
117 else:
118 command = user_command
119 user_command = None
121 command_args = []
123 # Add extra arguments for runner
124 runner = command.get_runner()
125 if runner:
126 command_iface = runner.interface
127 command_name = runner.command
128 _process_args(command_args, runner.qdom)
129 else:
130 command_iface = None
131 command_name = None
133 # Add main program path
134 command_path = command.path
135 if command_path is not None:
136 if command_sel.id.startswith('package:'):
137 prog_path = command_path
138 else:
139 if command_path.startswith('/'):
140 raise SafeException(_("Command path must be relative, but '%s' starts with '/'!") %
141 command_path)
142 prog_path = os.path.join(self._get_implementation_path(command_sel), command_path)
144 assert prog_path is not None
146 if not os.path.exists(prog_path):
147 raise SafeException(_("File '%(program_path)s' does not exist.\n"
148 "(implementation '%(implementation_id)s' + program '%(main)s')") %
149 {'program_path': prog_path, 'implementation_id': command_sel.id,
150 'main': command_path})
152 command_args.append(prog_path)
154 # Add extra arguments for program
155 _process_args(command_args, command.qdom)
157 prog_args = command_args + prog_args
159 # Each command is run by the next, but the last one is run by exec, and we
160 # need a path for that.
161 if command.path is None:
162 raise SafeException("Missing 'path' attribute on <command>")
164 return prog_args
166 def _get_implementation_path(self, impl):
167 return impl.local_path or self.stores.lookup_any(impl.digests)
169 def prepare_env(self):
170 """Do all the environment bindings in the selections (setting os.environ)."""
171 self._exec_bindings = []
173 def _do_bindings(impl, bindings, dep):
174 for b in bindings:
175 self.do_binding(impl, b, dep)
177 def _do_deps(deps):
178 for dep in deps:
179 dep_impl = sels.get(dep.interface, None)
180 if dep_impl is None:
181 assert dep.importance != Dependency.Essential, dep
182 elif not dep_impl.id.startswith('package:'):
183 _do_bindings(dep_impl, dep.bindings, dep)
185 sels = self.selections.selections
186 for selection in sels.values():
187 _do_bindings(selection, selection.bindings, None)
188 _do_deps(selection.dependencies)
190 # Process commands' dependencies' bindings too
191 for command in selection.get_commands().values():
192 _do_deps(command.requires)
194 # Do these after <environment>s, because they may do $-expansion
195 for binding, dep in self._exec_bindings:
196 self.do_exec_binding(binding, dep)
197 self._exec_bindings = None
199 def do_binding(self, impl, binding, dep):
200 """Called by L{prepare_env} for each binding.
201 Sub-classes may wish to override this.
202 @param impl: the selected implementation
203 @type impl: L{selections.Selection}
204 @param binding: the binding to be processed
205 @type binding: L{model.Binding}
206 @param dep: the dependency containing the binding, or None for implementation bindings
207 @type dep: L{model.Dependency}
209 if isinstance(binding, EnvironmentBinding):
210 do_env_binding(binding, self._get_implementation_path(impl))
211 elif isinstance(binding, ExecutableBinding):
212 self._exec_bindings.append((binding, dep))
214 def do_exec_binding(self, binding, dep):
215 if dep is None:
216 raise SafeException("<executable> can only appear within a <requires>")
217 if dep.command is None:
218 raise SafeException("<executable> can only appear within a <requires> with a command attribute set")
219 name = binding.name
220 if '/' in name or name.startswith('.') or "'" in name:
221 raise SafeException("Invalid <executable> name '%s'" % name)
222 exec_dir = basedir.save_cache_path(namespaces.config_site, namespaces.config_prog, 'executables', name)
223 exec_path = os.path.join(exec_dir, name)
224 if not os.path.exists(exec_path):
225 import tempfile
227 # Create the runenv.py helper script under ~/.cache if missing
228 main_dir = basedir.save_cache_path(namespaces.config_site, namespaces.config_prog)
229 runenv = os.path.join(main_dir, 'runenv.py')
230 if not os.path.exists(runenv):
231 tmp = tempfile.NamedTemporaryFile('w', dir = main_dir, delete = False)
232 tmp.write("#!%s\nfrom zeroinstall.injector import _runenv; _runenv.main()" % sys.executable)
233 tmp.close()
234 os.chmod(tmp.name, 0555)
235 os.rename(tmp.name, runenv)
237 # Symlink ~/.cache/0install.net/injector/executables/$name/$name to runenv.py
238 os.symlink('../../runenv.py', exec_path)
240 path = os.environ["PATH"] = exec_dir + os.pathsep + os.environ["PATH"]
241 info("PATH=%s", path)
243 import json
244 args = self.build_command(dep.interface, dep.command)
245 os.environ["0install-runenv-" + name] = json.dumps(args)
247 def execute_selections(selections, prog_args, dry_run = False, main = None, wrapper = None, stores = None):
248 """Execute program. On success, doesn't return. On failure, raises an Exception.
249 Returns normally only for a successful dry run.
250 @param selections: the selected versions
251 @type selections: L{selections.Selections}
252 @param prog_args: arguments to pass to the program
253 @type prog_args: [str]
254 @param dry_run: if True, just print a message about what would have happened
255 @type dry_run: bool
256 @param main: the name of the binary to run, or None to use the default
257 @type main: str
258 @param wrapper: a command to use to actually run the binary, or None to run the binary directly
259 @type wrapper: str
260 @since: 0.27
261 @precondition: All implementations are in the cache.
263 #assert stores is not None
264 if stores is None:
265 from zeroinstall import zerostore
266 stores = zerostore.Stores()
268 setup = Setup(stores, selections)
270 commands = selections.commands
271 if main is not None:
272 # Replace first command with user's input
273 if main.startswith('/'):
274 main = main[1:] # User specified a path relative to the package root
275 else:
276 old_path = commands[0].path
277 assert old_path, "Can't use a relative replacement main when there is no original one!"
278 main = os.path.join(os.path.dirname(old_path), main) # User main is relative to command's name
279 # Copy all child nodes (e.g. <runner>) except for the arguments
280 user_command_element = qdom.Element(namespaces.XMLNS_IFACE, 'command', {'path': main})
281 if commands:
282 for child in commands[0].qdom.childNodes:
283 if child.uri == namespaces.XMLNS_IFACE and child.name == 'arg':
284 continue
285 user_command_element.childNodes.append(child)
286 user_command = Command(user_command_element, None)
287 else:
288 user_command = None
290 setup.prepare_env()
291 prog_args = setup.build_command(selections.interface, selections.command, user_command) + prog_args
293 if wrapper:
294 prog_args = ['/bin/sh', '-c', wrapper + ' "$@"', '-'] + list(prog_args)
296 if dry_run:
297 print _("Would execute: %s") % ' '.join(prog_args)
298 else:
299 info(_("Executing: %s"), prog_args)
300 sys.stdout.flush()
301 sys.stderr.flush()
302 try:
303 os.execv(prog_args[0], prog_args)
304 except OSError as ex:
305 raise SafeException(_("Failed to run '%(program_path)s': %(exception)s") % {'program_path': prog_args[0], 'exception': str(ex)})