Made temporary PATH directories read-only
[zeroinstall.git] / zeroinstall / injector / run.py
blob17d10eb774b70c024c0e171b9cd56066e48e05eb
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 if dep.command is None:
218 raise SafeException("<%s> can only appear within a <requires> with a command attribute set" % binding.qdom.name)
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()\n" % 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)
239 os.chmod(exec_dir, 0o500)
241 if binding.in_path:
242 path = os.environ["PATH"] = exec_dir + os.pathsep + os.environ["PATH"]
243 info("PATH=%s", path)
244 else:
245 os.environ[name] = exec_path
246 info("%s=%s", name, exec_path)
248 import json
249 args = self.build_command(dep.interface, dep.command)
250 os.environ["0install-runenv-" + name] = json.dumps(args)
252 def execute_selections(selections, prog_args, dry_run = False, main = None, wrapper = None, stores = None):
253 """Execute program. On success, doesn't return. On failure, raises an Exception.
254 Returns normally only for a successful dry run.
255 @param selections: the selected versions
256 @type selections: L{selections.Selections}
257 @param prog_args: arguments to pass to the program
258 @type prog_args: [str]
259 @param dry_run: if True, just print a message about what would have happened
260 @type dry_run: bool
261 @param main: the name of the binary to run, or None to use the default
262 @type main: str
263 @param wrapper: a command to use to actually run the binary, or None to run the binary directly
264 @type wrapper: str
265 @since: 0.27
266 @precondition: All implementations are in the cache.
268 #assert stores is not None
269 if stores is None:
270 from zeroinstall import zerostore
271 stores = zerostore.Stores()
273 setup = Setup(stores, selections)
275 commands = selections.commands
276 if main is not None:
277 # Replace first command with user's input
278 if main.startswith('/'):
279 main = main[1:] # User specified a path relative to the package root
280 else:
281 old_path = commands[0].path
282 assert old_path, "Can't use a relative replacement main when there is no original one!"
283 main = os.path.join(os.path.dirname(old_path), main) # User main is relative to command's name
284 # Copy all child nodes (e.g. <runner>) except for the arguments
285 user_command_element = qdom.Element(namespaces.XMLNS_IFACE, 'command', {'path': main})
286 if commands:
287 for child in commands[0].qdom.childNodes:
288 if child.uri == namespaces.XMLNS_IFACE and child.name == 'arg':
289 continue
290 user_command_element.childNodes.append(child)
291 user_command = Command(user_command_element, None)
292 else:
293 user_command = None
295 setup.prepare_env()
296 prog_args = setup.build_command(selections.interface, selections.command, user_command) + prog_args
298 if wrapper:
299 prog_args = ['/bin/sh', '-c', wrapper + ' "$@"', '-'] + list(prog_args)
301 if dry_run:
302 print _("Would execute: %s") % ' '.join(prog_args)
303 else:
304 info(_("Executing: %s"), prog_args)
305 sys.stdout.flush()
306 sys.stderr.flush()
307 try:
308 os.execv(prog_args[0], prog_args)
309 except OSError as ex:
310 raise SafeException(_("Failed to run '%(program_path)s': %(exception)s") % {'program_path': prog_args[0], 'exception': str(ex)})