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 __future__
import print_function
10 from zeroinstall
import _
12 from logging
import info
13 from string
import Template
15 from zeroinstall
.injector
.model
import SafeException
, EnvironmentBinding
, ExecutableBinding
, Command
, Dependency
16 from zeroinstall
.injector
import namespaces
, qdom
17 from zeroinstall
.support
import basedir
19 def do_env_binding(binding
, path
):
20 """Update this process's environment by applying the binding.
21 @param binding: the binding to apply
22 @type binding: L{model.EnvironmentBinding}
23 @param path: the selected implementation
25 os
.environ
[binding
.name
] = binding
.get_value(path
,
26 os
.environ
.get(binding
.name
, None))
27 info("%s=%s", binding
.name
, os
.environ
[binding
.name
])
29 def execute(policy
, prog_args
, dry_run
= False, main
= None, wrapper
= None):
30 """Execute program. On success, doesn't return. On failure, raises an Exception.
31 Returns normally only for a successful dry run.
32 @param policy: a policy with the selected versions
33 @type policy: L{policy.Policy}
34 @param prog_args: arguments to pass to the program
35 @type prog_args: [str]
36 @param dry_run: if True, just print a message about what would have happened
38 @param main: the name of the binary to run, or None to use the default
40 @param wrapper: a command to use to actually run the binary, or None to run the binary directly
42 @precondition: C{policy.ready and policy.get_uncached_implementations() == []}
44 execute_selections(policy
.solver
.selections
, prog_args
, dry_run
, main
, wrapper
)
46 def test_selections(selections
, prog_args
, dry_run
, main
, wrapper
= None):
47 """Run the program in a child process, collecting stdout and stderr.
48 @return: the output produced by the process
52 output
= tempfile
.TemporaryFile(prefix
= '0launch-test')
59 os
.dup2(output
.fileno(), 1)
60 os
.dup2(output
.fileno(), 2)
61 execute_selections(selections
, prog_args
, dry_run
, main
)
70 info(_("Waiting for test process to finish..."))
72 pid
, status
= os
.waitpid(child
, 0)
76 results
= output
.read()
78 results
+= _("Error from child process: exit code = %d") % status
84 def _process_args(args
, element
):
85 """Append each <arg> under <element> to args, performing $-expansion."""
86 for child
in element
.childNodes
:
87 if child
.uri
== namespaces
.XMLNS_IFACE
and child
.name
== 'arg':
88 args
.append(Template(child
.content
).substitute(os
.environ
))
95 _checked_runenv
= False
97 def __init__(self
, stores
, selections
):
98 """@param stores: where to find cached implementations
99 @type stores: L{zerostore.Stores}"""
101 self
.selections
= selections
103 def build_command(self
, command_iface
, command_name
, user_command
= None):
104 """Create a list of strings to be passed to exec to run the <command>s in the selections.
105 @param command_iface: the interface of the program being run
106 @type command_iface: str
107 @param command_name: the name of the command being run
108 @type command_name: str
109 @param user_command: a custom command to use instead
110 @type user_command: L{model.Command}
111 @return: the argument list
117 sels
= self
.selections
.selections
120 command_sel
= sels
[command_iface
]
122 if user_command
is None:
123 command
= command_sel
.get_command(command_name
)
125 command
= user_command
130 # Add extra arguments for runner
131 runner
= command
.get_runner()
133 command_iface
= runner
.interface
134 command_name
= runner
.command
135 _process_args(command_args
, runner
.qdom
)
140 # Add main program path
141 command_path
= command
.path
142 if command_path
is not None:
143 if command_sel
.id.startswith('package:'):
144 prog_path
= command_path
146 if command_path
.startswith('/'):
147 raise SafeException(_("Command path must be relative, but '%s' starts with '/'!") %
149 prog_path
= os
.path
.join(self
._get
_implementation
_path
(command_sel
), command_path
)
151 assert prog_path
is not None
153 if not os
.path
.exists(prog_path
):
154 raise SafeException(_("File '%(program_path)s' does not exist.\n"
155 "(implementation '%(implementation_id)s' + program '%(main)s')") %
156 {'program_path': prog_path
, 'implementation_id': command_sel
.id,
157 'main': command_path
})
159 command_args
.append(prog_path
)
161 # Add extra arguments for program
162 _process_args(command_args
, command
.qdom
)
164 prog_args
= command_args
+ prog_args
166 # Each command is run by the next, but the last one is run by exec, and we
167 # need a path for that.
168 if command
.path
is None:
169 raise SafeException("Missing 'path' attribute on <command>")
173 def _get_implementation_path(self
, impl
):
174 return impl
.local_path
or self
.stores
.lookup_any(impl
.digests
)
176 def prepare_env(self
):
177 """Do all the environment bindings in the selections (setting os.environ)."""
178 self
._exec
_bindings
= []
180 def _do_bindings(impl
, bindings
, iface
):
182 self
.do_binding(impl
, b
, iface
)
186 dep_impl
= sels
.get(dep
.interface
, None)
188 assert dep
.importance
!= Dependency
.Essential
, dep
189 elif not dep_impl
.id.startswith('package:'):
190 _do_bindings(dep_impl
, dep
.bindings
, dep
.interface
)
192 sels
= self
.selections
.selections
193 for selection
in sels
.values():
194 _do_bindings(selection
, selection
.bindings
, selection
.interface
)
195 _do_deps(selection
.dependencies
)
197 # Process commands' dependencies' bindings too
198 for command
in selection
.get_commands().values():
199 _do_bindings(selection
, command
.bindings
, selection
.interface
)
200 _do_deps(command
.requires
)
202 # Do these after <environment>s, because they may do $-expansion
203 for binding
, iface
in self
._exec
_bindings
:
204 self
.do_exec_binding(binding
, iface
)
205 self
._exec
_bindings
= None
207 def do_binding(self
, impl
, binding
, iface
):
208 """Called by L{prepare_env} for each binding.
209 Sub-classes may wish to override this.
210 @param impl: the selected implementation
211 @type impl: L{selections.Selection}
212 @param binding: the binding to be processed
213 @type binding: L{model.Binding}
214 @param iface: the interface containing impl
215 @type iface: L{model.Interface}
217 if isinstance(binding
, EnvironmentBinding
):
218 do_env_binding(binding
, self
._get
_implementation
_path
(impl
))
219 elif isinstance(binding
, ExecutableBinding
):
220 if isinstance(iface
, Dependency
):
222 warnings
.warn("Pass an interface URI instead", DeprecationWarning, 2)
223 iface
= iface
.interface
224 self
._exec
_bindings
.append((binding
, iface
))
226 def do_exec_binding(self
, binding
, iface
):
227 assert iface
is not None
229 if '/' in name
or name
.startswith('.') or "'" in name
:
230 raise SafeException("Invalid <executable> name '%s'" % name
)
231 exec_dir
= basedir
.save_cache_path(namespaces
.config_site
, namespaces
.config_prog
, 'executables', name
)
232 exec_path
= os
.path
.join(exec_dir
, name
)
234 if not self
._checked
_runenv
:
237 if not os
.path
.exists(exec_path
):
238 # Symlink ~/.cache/0install.net/injector/executables/$name/$name to runenv.py
239 os
.symlink('../../runenv.py', exec_path
)
240 os
.chmod(exec_dir
, 0o500)
243 path
= os
.environ
["PATH"] = exec_dir
+ os
.pathsep
+ os
.environ
["PATH"]
244 info("PATH=%s", path
)
246 os
.environ
[name
] = exec_path
247 info("%s=%s", name
, exec_path
)
250 args
= self
.build_command(iface
, binding
.command
)
251 os
.environ
["0install-runenv-" + name
] = json
.dumps(args
)
253 def _check_runenv(self
):
254 # Create the runenv.py helper script under ~/.cache if missing or out-of-date
255 main_dir
= basedir
.save_cache_path(namespaces
.config_site
, namespaces
.config_prog
)
256 runenv
= os
.path
.join(main_dir
, 'runenv.py')
257 expected_contents
= "#!%s\nfrom zeroinstall.injector import _runenv; _runenv.main()\n" % sys
.executable
259 actual_contents
= None
260 if os
.path
.exists(runenv
):
261 with
open(runenv
) as s
:
262 actual_contents
= s
.read()
264 if actual_contents
!= expected_contents
:
266 tmp
= tempfile
.NamedTemporaryFile('w', dir = main_dir
, delete
= False)
267 info("Updating %s", runenv
)
268 tmp
.write(expected_contents
)
270 os
.chmod(tmp
.name
, 0555)
271 os
.rename(tmp
.name
, runenv
)
273 self
._checked
_runenv
= True
275 def execute_selections(selections
, prog_args
, dry_run
= False, main
= None, wrapper
= None, stores
= None):
276 """Execute program. On success, doesn't return. On failure, raises an Exception.
277 Returns normally only for a successful dry run.
278 @param selections: the selected versions
279 @type selections: L{selections.Selections}
280 @param prog_args: arguments to pass to the program
281 @type prog_args: [str]
282 @param dry_run: if True, just print a message about what would have happened
284 @param main: the name of the binary to run, or None to use the default
286 @param wrapper: a command to use to actually run the binary, or None to run the binary directly
289 @precondition: All implementations are in the cache.
291 #assert stores is not None
293 from zeroinstall
import zerostore
294 stores
= zerostore
.Stores()
296 setup
= Setup(stores
, selections
)
298 commands
= selections
.commands
300 # Replace first command with user's input
301 if main
.startswith('/'):
302 main
= main
[1:] # User specified a path relative to the package root
304 old_path
= commands
[0].path
305 assert old_path
, "Can't use a relative replacement main when there is no original one!"
306 main
= os
.path
.join(os
.path
.dirname(old_path
), main
) # User main is relative to command's name
307 # Copy all child nodes (e.g. <runner>) except for the arguments
308 user_command_element
= qdom
.Element(namespaces
.XMLNS_IFACE
, 'command', {'path': main
})
310 for child
in commands
[0].qdom
.childNodes
:
311 if child
.uri
== namespaces
.XMLNS_IFACE
and child
.name
== 'arg':
313 user_command_element
.childNodes
.append(child
)
314 user_command
= Command(user_command_element
, None)
319 prog_args
= setup
.build_command(selections
.interface
, selections
.command
, user_command
) + prog_args
322 prog_args
= ['/bin/sh', '-c', wrapper
+ ' "$@"', '-'] + list(prog_args
)
325 print(_("Would execute: %s") % ' '.join(prog_args
))
327 info(_("Executing: %s"), prog_args
)
331 os
.execv(prog_args
[0], prog_args
)
332 except OSError as ex
:
333 raise SafeException(_("Failed to run '%(program_path)s': %(exception)s") % {'program_path': prog_args
[0], 'exception': str(ex
)})