Moved command attribute from <requires> to <executable-in-*> element
[zeroinstall.git] / zeroinstall / injector / run.py
blobe48d5c9fe09cc77196470db3c3edc1074458ef63
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("<%s> can only appear within a <requires>" % binding.qdom.name)
217 name = binding.name
218 if '/' in name or name.startswith('.') or "'" in name:
219 raise SafeException("Invalid <executable> name '%s'" % name)
220 exec_dir = basedir.save_cache_path(namespaces.config_site, namespaces.config_prog, 'executables', name)
221 exec_path = os.path.join(exec_dir, name)
222 if not os.path.exists(exec_path):
223 import tempfile
225 # Create the runenv.py helper script under ~/.cache if missing
226 main_dir = basedir.save_cache_path(namespaces.config_site, namespaces.config_prog)
227 runenv = os.path.join(main_dir, 'runenv.py')
228 if not os.path.exists(runenv):
229 tmp = tempfile.NamedTemporaryFile('w', dir = main_dir, delete = False)
230 tmp.write("#!%s\nfrom zeroinstall.injector import _runenv; _runenv.main()\n" % sys.executable)
231 tmp.close()
232 os.chmod(tmp.name, 0555)
233 os.rename(tmp.name, runenv)
235 # Symlink ~/.cache/0install.net/injector/executables/$name/$name to runenv.py
236 os.symlink('../../runenv.py', exec_path)
237 os.chmod(exec_dir, 0o500)
239 if binding.in_path:
240 path = os.environ["PATH"] = exec_dir + os.pathsep + os.environ["PATH"]
241 info("PATH=%s", path)
242 else:
243 os.environ[name] = exec_path
244 info("%s=%s", name, exec_path)
246 import json
247 args = self.build_command(dep.interface, binding.command)
248 os.environ["0install-runenv-" + name] = json.dumps(args)
250 def execute_selections(selections, prog_args, dry_run = False, main = None, wrapper = None, stores = None):
251 """Execute program. On success, doesn't return. On failure, raises an Exception.
252 Returns normally only for a successful dry run.
253 @param selections: the selected versions
254 @type selections: L{selections.Selections}
255 @param prog_args: arguments to pass to the program
256 @type prog_args: [str]
257 @param dry_run: if True, just print a message about what would have happened
258 @type dry_run: bool
259 @param main: the name of the binary to run, or None to use the default
260 @type main: str
261 @param wrapper: a command to use to actually run the binary, or None to run the binary directly
262 @type wrapper: str
263 @since: 0.27
264 @precondition: All implementations are in the cache.
266 #assert stores is not None
267 if stores is None:
268 from zeroinstall import zerostore
269 stores = zerostore.Stores()
271 setup = Setup(stores, selections)
273 commands = selections.commands
274 if main is not None:
275 # Replace first command with user's input
276 if main.startswith('/'):
277 main = main[1:] # User specified a path relative to the package root
278 else:
279 old_path = commands[0].path
280 assert old_path, "Can't use a relative replacement main when there is no original one!"
281 main = os.path.join(os.path.dirname(old_path), main) # User main is relative to command's name
282 # Copy all child nodes (e.g. <runner>) except for the arguments
283 user_command_element = qdom.Element(namespaces.XMLNS_IFACE, 'command', {'path': main})
284 if commands:
285 for child in commands[0].qdom.childNodes:
286 if child.uri == namespaces.XMLNS_IFACE and child.name == 'arg':
287 continue
288 user_command_element.childNodes.append(child)
289 user_command = Command(user_command_element, None)
290 else:
291 user_command = None
293 setup.prepare_env()
294 prog_args = setup.build_command(selections.interface, selections.command, user_command) + prog_args
296 if wrapper:
297 prog_args = ['/bin/sh', '-c', wrapper + ' "$@"', '-'] + list(prog_args)
299 if dry_run:
300 print _("Would execute: %s") % ' '.join(prog_args)
301 else:
302 info(_("Executing: %s"), prog_args)
303 sys.stdout.flush()
304 sys.stderr.flush()
305 try:
306 os.execv(prog_args[0], prog_args)
307 except OSError as ex:
308 raise SafeException(_("Failed to run '%(program_path)s': %(exception)s") % {'program_path': prog_args[0], 'exception': str(ex)})