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