Only remove <command>'s <arg> elements when using --main
[zeroinstall.git] / zeroinstall / injector / run.py
blob22ffde45b21469aca01099bf1661310d508661c8
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
11 from string import Template
13 from zeroinstall.injector.model import SafeException, EnvironmentBinding, Command
14 from zeroinstall.injector import namespaces, qdom
15 from zeroinstall.injector.iface_cache import iface_cache
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 _do_bindings(impl, bindings):
45 for b in bindings:
46 if isinstance(b, EnvironmentBinding):
47 do_env_binding(b, _get_implementation_path(impl))
49 def _get_implementation_path(impl):
50 return impl.local_path or iface_cache.stores.lookup_any(impl.digests)
52 def test_selections(selections, prog_args, dry_run, main, wrapper = None):
53 """Run the program in a child process, collecting stdout and stderr.
54 @return: the output produced by the process
55 @since: 0.27
56 """
57 args = []
58 import tempfile
59 output = tempfile.TemporaryFile(prefix = '0launch-test')
60 try:
61 child = os.fork()
62 if child == 0:
63 # We are the child
64 try:
65 try:
66 os.dup2(output.fileno(), 1)
67 os.dup2(output.fileno(), 2)
68 execute_selections(selections, prog_args, dry_run, main)
69 except:
70 import traceback
71 traceback.print_exc()
72 finally:
73 sys.stdout.flush()
74 sys.stderr.flush()
75 os._exit(1)
77 info(_("Waiting for test process to finish..."))
79 pid, status = os.waitpid(child, 0)
80 assert pid == child
82 output.seek(0)
83 results = output.read()
84 if status != 0:
85 results += _("Error from child process: exit code = %d") % status
86 finally:
87 output.close()
89 return results
91 def _process_args(args, element):
92 """Append each <arg> under <element> to args, performing $-expansion."""
93 for child in element.childNodes:
94 if child.uri == namespaces.XMLNS_IFACE and child.name == 'arg':
95 args.append(Template(child.content).substitute(os.environ))
97 def execute_selections(selections, prog_args, dry_run = False, main = None, wrapper = None):
98 """Execute program. On success, doesn't return. On failure, raises an Exception.
99 Returns normally only for a successful dry run.
100 @param selections: the selected versions
101 @type selections: L{selections.Selections}
102 @param prog_args: arguments to pass to the program
103 @type prog_args: [str]
104 @param dry_run: if True, just print a message about what would have happened
105 @type dry_run: bool
106 @param main: the name of the binary to run, or None to use the default
107 @type main: str
108 @param wrapper: a command to use to actually run the binary, or None to run the binary directly
109 @type wrapper: str
110 @since: 0.27
111 @precondition: All implementations are in the cache.
113 commands = selections.commands
114 sels = selections.selections
115 for selection in sels.values():
116 _do_bindings(selection, selection.bindings)
117 for dep in selection.dependencies:
118 dep_impl = sels[dep.interface]
119 if not dep_impl.id.startswith('package:'):
120 _do_bindings(dep_impl, dep.bindings)
121 # Process commands' dependencies' bindings too
122 # (do this here because we still want the bindings, even with --main)
123 for command in commands:
124 for dep in command.requires:
125 dep_impl = sels[dep.interface]
126 if not dep_impl.id.startswith('package:'):
127 _do_bindings(dep_impl, dep.bindings)
129 root_sel = sels[selections.interface]
131 assert root_sel is not None
133 if main is not None:
134 # Replace first command with user's input
135 old_path = commands[0].path
136 if main.startswith('/'):
137 main = main[1:] # User specified a path relative to the package root
138 else:
139 assert old_path
140 main = os.path.join(os.path.dirname(old_path), main) # User main is relative to command's name
141 # Copy all child nodes (e.g. <runner>) except for the arguments
142 user_command_element = qdom.Element(namespaces.XMLNS_IFACE, 'command', {'path': main})
143 for child in commands[0].qdom.childNodes:
144 if child.uri == namespaces.XMLNS_IFACE and child.name == 'arg':
145 continue
146 user_command_element.childNodes.append(child)
147 user_command = Command(user_command_element, None)
148 commands = [user_command] + commands[1:]
150 if commands[-1].path is None:
151 raise SafeException("Missing 'path' attribute on <command>")
153 command_iface = selections.interface
154 for command in commands:
155 command_sel = sels[command_iface]
157 command_args = []
159 # Add extra arguments for runner
160 runner = command.get_runner()
161 if runner:
162 command_iface = runner.interface
163 _process_args(command_args, runner.qdom)
165 # Add main program path
166 command_path = command.path
167 if command_path is not None:
168 if command_sel.id.startswith('package:'):
169 prog_path = command_path
170 else:
171 if command_path.startswith('/'):
172 raise SafeException(_("Command path must be relative, but '%s' starts with '/'!") %
173 command_path)
174 prog_path = os.path.join(_get_implementation_path(command_sel), command_path)
176 assert prog_path is not None
177 command_args.append(prog_path)
179 # Add extra arguments for program
180 _process_args(command_args, command.qdom)
182 prog_args = command_args + prog_args
184 if not os.path.exists(prog_args[0]):
185 raise SafeException(_("File '%(program_path)s' does not exist.\n"
186 "(implementation '%(implementation_id)s' + program '%(main)s')") %
187 {'program_path': prog_args[0], 'implementation_id': command_sel.id,
188 'main': commands[-1].path})
189 if wrapper:
190 prog_args = ['/bin/sh', '-c', wrapper + ' "$@"', '-'] + list(prog_args)
192 if dry_run:
193 print _("Would execute: %s") % ' '.join(prog_args)
194 else:
195 info(_("Executing: %s"), prog_args)
196 sys.stdout.flush()
197 sys.stderr.flush()
198 try:
199 os.execv(prog_args[0], prog_args)
200 except OSError, ex:
201 raise SafeException(_("Failed to run '%(program_path)s': %(exception)s") % {'program_path': prog_args[0], 'exception': str(ex)})