2 Executes a set of implementations as a program.
5 # Copyright (C) 2009, Thomas Leonard
6 # See the README file for details, or visit http://0install.net.
8 from zeroinstall
import _
10 from logging
import info
11 from string
import Template
13 from zeroinstall
.injector
.model
import SafeException
, EnvironmentBinding
, Command
14 from zeroinstall
.injector
import namespaces
, qdom
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
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
35 @param main: the name of the binary to run, or None to use the default
37 @param wrapper: a command to use to actually run the binary, or None to run the binary directly
39 @precondition: C{policy.ready and policy.get_uncached_implementations() == []}
41 execute_selections(policy
.solver
.selections
, prog_args
, dry_run
, main
, wrapper
)
43 def test_selections(selections
, prog_args
, dry_run
, main
, wrapper
= None):
44 """Run the program in a child process, collecting stdout and stderr.
45 @return: the output produced by the process
49 output
= tempfile
.TemporaryFile(prefix
= '0launch-test')
56 os
.dup2(output
.fileno(), 1)
57 os
.dup2(output
.fileno(), 2)
58 execute_selections(selections
, prog_args
, dry_run
, main
)
67 info(_("Waiting for test process to finish..."))
69 pid
, status
= os
.waitpid(child
, 0)
73 results
= output
.read()
75 results
+= _("Error from child process: exit code = %d") % status
81 def _process_args(args
, element
):
82 """Append each <arg> under <element> to args, performing $-expansion."""
83 for child
in element
.childNodes
:
84 if child
.uri
== namespaces
.XMLNS_IFACE
and child
.name
== 'arg':
85 args
.append(Template(child
.content
).substitute(os
.environ
))
91 def __init__(self
, stores
):
92 """@param stores: where to find cached implementations
93 @type stores: L{zerostore.Stores}"""
96 def build_command_args(self
, selections
, commands
= None):
97 """Create a list of strings to be passed to exec to run the <command>s in the selections.
98 @param selections: the selections containing the commands
99 @type selections: L{selections.Selections}
100 @param commands: the commands to be used (taken from selections is None)
101 @type commands: [L{model.Command}]
102 @return: the argument list
106 commands
= commands
or selections
.commands
107 sels
= selections
.selections
109 # Each command is run by the next, but the last one is run by exec, and we
110 # need a path for that.
111 if commands
[-1].path
is None:
112 raise SafeException("Missing 'path' attribute on <command>")
114 command_iface
= selections
.interface
115 for command
in commands
:
116 command_sel
= sels
[command_iface
]
120 # Add extra arguments for runner
121 runner
= command
.get_runner()
123 command_iface
= runner
.interface
124 _process_args(command_args
, runner
.qdom
)
126 # Add main program path
127 command_path
= command
.path
128 if command_path
is not None:
129 if command_sel
.id.startswith('package:'):
130 prog_path
= command_path
132 if command_path
.startswith('/'):
133 raise SafeException(_("Command path must be relative, but '%s' starts with '/'!") %
135 prog_path
= os
.path
.join(self
._get
_implementation
_path
(command_sel
), command_path
)
137 assert prog_path
is not None
139 if not os
.path
.exists(prog_path
):
140 raise SafeException(_("File '%(program_path)s' does not exist.\n"
141 "(implementation '%(implementation_id)s' + program '%(main)s')") %
142 {'program_path': prog_path
, 'implementation_id': command_sel
.id,
143 'main': command_path
})
145 command_args
.append(prog_path
)
147 # Add extra arguments for program
148 _process_args(command_args
, command
.qdom
)
150 prog_args
= command_args
+ prog_args
154 def _get_implementation_path(self
, impl
):
155 return impl
.local_path
or self
.stores
.lookup_any(impl
.digests
)
157 def prepare_env(self
, selections
):
158 """Do all the environment bindings in selections (setting os.environ).
159 @param selections: the selections to be used
160 @type selections: L{selections.Selections}"""
162 def _do_bindings(impl
, bindings
):
164 self
.do_binding(impl
, b
)
166 commands
= selections
.commands
167 sels
= selections
.selections
168 for selection
in sels
.values():
169 _do_bindings(selection
, selection
.bindings
)
170 for dep
in selection
.dependencies
:
171 dep_impl
= sels
[dep
.interface
]
172 if not dep_impl
.id.startswith('package:'):
173 _do_bindings(dep_impl
, dep
.bindings
)
174 # Process commands' dependencies' bindings too
175 # (do this here because we still want the bindings, even with --main)
176 for command
in commands
:
177 for dep
in command
.requires
:
178 dep_impl
= sels
[dep
.interface
]
179 if not dep_impl
.id.startswith('package:'):
180 _do_bindings(dep_impl
, dep
.bindings
)
182 def do_binding(self
, impl
, binding
):
183 """Called by L{prepare_env} for each binding.
184 Sub-classes may wish to override this."""
185 if isinstance(binding
, EnvironmentBinding
):
186 do_env_binding(binding
, self
._get
_implementation
_path
(impl
))
188 def execute_selections(selections
, prog_args
, dry_run
= False, main
= None, wrapper
= None, stores
= None):
189 """Execute program. On success, doesn't return. On failure, raises an Exception.
190 Returns normally only for a successful dry run.
191 @param selections: the selected versions
192 @type selections: L{selections.Selections}
193 @param prog_args: arguments to pass to the program
194 @type prog_args: [str]
195 @param dry_run: if True, just print a message about what would have happened
197 @param main: the name of the binary to run, or None to use the default
199 @param wrapper: a command to use to actually run the binary, or None to run the binary directly
202 @precondition: All implementations are in the cache.
204 #assert stores is not None
206 from zeroinstall
import zerostore
207 stores
= zerostore
.Stores()
209 setup
= Setup(stores
)
211 commands
= selections
.commands
213 # Replace first command with user's input
214 if main
.startswith('/'):
215 main
= main
[1:] # User specified a path relative to the package root
217 old_path
= commands
[0].path
218 assert old_path
, "Can't use a relative replacement main when there is no original one!"
219 main
= os
.path
.join(os
.path
.dirname(old_path
), main
) # User main is relative to command's name
220 # Copy all child nodes (e.g. <runner>) except for the arguments
221 user_command_element
= qdom
.Element(namespaces
.XMLNS_IFACE
, 'command', {'path': main
})
223 for child
in commands
[0].qdom
.childNodes
:
224 if child
.uri
== namespaces
.XMLNS_IFACE
and child
.name
== 'arg':
226 user_command_element
.childNodes
.append(child
)
227 user_command
= Command(user_command_element
, None)
228 commands
= [user_command
] + commands
[1:]
230 setup
.prepare_env(selections
)
231 prog_args
= setup
.build_command_args(selections
, commands
) + prog_args
234 prog_args
= ['/bin/sh', '-c', wrapper
+ ' "$@"', '-'] + list(prog_args
)
237 print _("Would execute: %s") % ' '.join(prog_args
)
239 info(_("Executing: %s"), prog_args
)
243 os
.execv(prog_args
[0], prog_args
)
245 raise SafeException(_("Failed to run '%(program_path)s': %(exception)s") % {'program_path': prog_args
[0], 'exception': str(ex
)})