Added support for arguments in a <command>
[zeroinstall/zeroinstall-afb.git] / zeroinstall / injector / run.py
blobbfc490fa3c10bcb906bebd4455b308670e4e1fc8
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 debug, info
12 from zeroinstall.injector.model import SafeException, EnvironmentBinding
13 from zeroinstall.injector import namespaces
14 from zeroinstall.injector.iface_cache import iface_cache
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 _do_bindings(impl, bindings):
44 for b in bindings:
45 if isinstance(b, EnvironmentBinding):
46 do_env_binding(b, _get_implementation_path(impl))
48 def _get_implementation_path(impl):
49 return impl.local_path or iface_cache.stores.lookup_any(impl.digests)
51 def test_selections(selections, prog_args, dry_run, main, wrapper = None):
52 """Run the program in a child process, collecting stdout and stderr.
53 @return: the output produced by the process
54 @since: 0.27
55 """
56 args = []
57 import tempfile
58 output = tempfile.TemporaryFile(prefix = '0launch-test')
59 try:
60 child = os.fork()
61 if child == 0:
62 # We are the child
63 try:
64 try:
65 os.dup2(output.fileno(), 1)
66 os.dup2(output.fileno(), 2)
67 execute_selections(selections, prog_args, dry_run, main)
68 except:
69 import traceback
70 traceback.print_exc()
71 finally:
72 sys.stdout.flush()
73 sys.stderr.flush()
74 os._exit(1)
76 info(_("Waiting for test process to finish..."))
78 pid, status = os.waitpid(child, 0)
79 assert pid == child
81 output.seek(0)
82 results = output.read()
83 if status != 0:
84 results += _("Error from child process: exit code = %d") % status
85 finally:
86 output.close()
88 return results
90 def execute_selections(selections, prog_args, dry_run = False, main = None, wrapper = None):
91 """Execute program. On success, doesn't return. On failure, raises an Exception.
92 Returns normally only for a successful dry run.
93 @param selections: the selected versions
94 @type selections: L{selections.Selections}
95 @param prog_args: arguments to pass to the program
96 @type prog_args: [str]
97 @param dry_run: if True, just print a message about what would have happened
98 @type dry_run: bool
99 @param main: the name of the binary to run, or None to use the default
100 @type main: str
101 @param wrapper: a command to use to actually run the binary, or None to run the binary directly
102 @type wrapper: str
103 @since: 0.27
104 @precondition: All implementations are in the cache.
106 sels = selections.selections
107 for selection in sels.values():
108 _do_bindings(selection, selection.bindings)
109 for dep in selection.dependencies:
110 dep_impl = sels[dep.interface]
111 if not dep_impl.id.startswith('package:'):
112 _do_bindings(dep_impl, dep.bindings)
114 root_sel = sels[selections.interface]
116 assert root_sel is not None
118 command = selections.command
120 # Process command's dependencies' bindings
121 for dep in command.requires:
122 dep_impl = sels[dep.interface]
123 if not dep_impl.id.startswith('package:'):
124 _do_bindings(dep_impl, dep.bindings)
126 command_path = command.path
128 if main is None:
129 command_args = []
130 for child in command.qdom.childNodes:
131 if child.uri == namespaces.XMLNS_IFACE and child.name == 'arg':
132 command_args.append(child.content)
133 prog_args = command_args + prog_args
135 if root_sel.id.startswith('package:'):
136 main = main or command_path
137 prog_path = main
138 else:
139 if command_path and command_path.startswith('/'):
140 raise SafeException(_("Command path must be relative, but '%s' starts with '/'!") %
141 command_path)
142 if main is None:
143 main = command_path # User didn't specify a main path
144 elif main.startswith('/'):
145 main = main[1:] # User specified a path relative to the package root
146 elif command_path:
147 main = os.path.join(os.path.dirname(command_path), main) # User main is relative to command's name
149 if main is not None:
150 prog_path = os.path.join(_get_implementation_path(root_sel), main)
152 if main is None:
153 # This shouldn't happen, because the solve would fail first.
154 raise Exception(_("Implementation '%s' cannot be executed directly; it is just a library "
155 "to be used by other programs (or missing 'main' attribute)") %
156 root_sel)
158 if not os.path.exists(prog_path):
159 raise SafeException(_("File '%(program_path)s' does not exist.\n"
160 "(implementation '%(implementation_id)s' + program '%(main)s')") %
161 {'program_path': prog_path, 'implementation_id': root_sel.id,
162 'main': main})
163 if wrapper:
164 prog_args = ['-c', wrapper + ' "$@"', '-', prog_path] + list(prog_args)
165 prog_path = '/bin/sh'
167 if dry_run:
168 print _("Would execute: %s") % ' '.join([prog_path] + prog_args)
169 else:
170 info(_("Executing: %s"), prog_path)
171 sys.stdout.flush()
172 sys.stderr.flush()
173 try:
174 os.execl(prog_path, prog_path, *prog_args)
175 except OSError, ex:
176 raise SafeException(_("Failed to run '%(program_path)s': %(exception)s") % {'program_path': prog_path, 'exception': str(ex)})