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 if binding
.insert
is not None and path
is None:
26 # Skip insert bindings for package implementations
27 debug("not setting %s as we selected a package implementation", binding
.name
)
29 os
.environ
[binding
.name
] = binding
.get_value(path
,
30 os
.environ
.get(binding
.name
, None))
31 info("%s=%s", binding
.name
, os
.environ
[binding
.name
])
33 def execute(policy
, prog_args
, dry_run
= False, main
= None, wrapper
= None):
34 """Execute program. On success, doesn't return. On failure, raises an Exception.
35 Returns normally only for a successful dry run.
36 @param policy: a policy with the selected versions
37 @type policy: L{policy.Policy}
38 @param prog_args: arguments to pass to the program
39 @type prog_args: [str]
40 @param dry_run: if True, just print a message about what would have happened
42 @param main: the name of the binary to run, or None to use the default
44 @param wrapper: a command to use to actually run the binary, or None to run the binary directly
46 @precondition: C{policy.ready and policy.get_uncached_implementations() == []}
48 execute_selections(policy
.solver
.selections
, prog_args
, dry_run
, main
, wrapper
)
50 def test_selections(selections
, prog_args
, dry_run
, main
, wrapper
= None):
51 """Run the program in a child process, collecting stdout and stderr.
52 @return: the output produced by the process
56 output
= tempfile
.TemporaryFile(prefix
= '0launch-test')
63 os
.dup2(output
.fileno(), 1)
64 os
.dup2(output
.fileno(), 2)
65 execute_selections(selections
, prog_args
, dry_run
, main
)
74 info(_("Waiting for test process to finish..."))
76 pid
, status
= os
.waitpid(child
, 0)
80 results
= output
.read()
82 results
+= _("Error from child process: exit code = %d") % status
88 def _process_args(args
, element
):
89 """Append each <arg> under <element> to args, performing $-expansion."""
90 for child
in element
.childNodes
:
91 if child
.uri
== namespaces
.XMLNS_IFACE
and child
.name
== 'arg':
92 args
.append(Template(child
.content
).substitute(os
.environ
))
99 _checked_runenv
= False
101 def __init__(self
, stores
, selections
):
102 """@param stores: where to find cached implementations
103 @type stores: L{zerostore.Stores}"""
105 self
.selections
= selections
107 def build_command(self
, command_iface
, command_name
, user_command
= None):
108 """Create a list of strings to be passed to exec to run the <command>s in the selections.
109 @param command_iface: the interface of the program being run
110 @type command_iface: str
111 @param command_name: the name of the command being run
112 @type command_name: str
113 @param user_command: a custom command to use instead
114 @type user_command: L{model.Command}
115 @return: the argument list
121 sels
= self
.selections
.selections
124 command_sel
= sels
[command_iface
]
126 if user_command
is None:
127 command
= command_sel
.get_command(command_name
)
129 command
= user_command
134 # Add extra arguments for runner
135 runner
= command
.get_runner()
137 command_iface
= runner
.interface
138 command_name
= runner
.command
139 _process_args(command_args
, runner
.qdom
)
144 # Add main program path
145 command_path
= command
.path
146 if command_path
is not None:
147 if command_sel
.id.startswith('package:'):
148 prog_path
= command_path
150 if command_path
.startswith('/'):
151 raise SafeException(_("Command path must be relative, but '%s' starts with '/'!") %
153 prog_path
= os
.path
.join(self
._get
_implementation
_path
(command_sel
), command_path
)
155 assert prog_path
is not None
157 if not os
.path
.exists(prog_path
):
158 raise SafeException(_("File '%(program_path)s' does not exist.\n"
159 "(implementation '%(implementation_id)s' + program '%(main)s')") %
160 {'program_path': prog_path
, 'implementation_id': command_sel
.id,
161 'main': command_path
})
163 command_args
.append(prog_path
)
165 # Add extra arguments for program
166 _process_args(command_args
, command
.qdom
)
168 prog_args
= command_args
+ prog_args
170 # Each command is run by the next, but the last one is run by exec, and we
171 # need a path for that.
172 if command
.path
is None:
173 raise SafeException("Missing 'path' attribute on <command>")
177 def _get_implementation_path(self
, impl
):
178 if impl
.id.startswith('package:'): return None
179 return impl
.local_path
or self
.stores
.lookup_any(impl
.digests
)
181 def prepare_env(self
):
182 """Do all the environment bindings in the selections (setting os.environ)."""
183 self
._exec
_bindings
= []
185 def _do_bindings(impl
, bindings
, iface
):
187 self
.do_binding(impl
, b
, iface
)
191 dep_impl
= sels
.get(dep
.interface
, None)
193 assert dep
.importance
!= Dependency
.Essential
, dep
195 _do_bindings(dep_impl
, dep
.bindings
, dep
.interface
)
197 sels
= self
.selections
.selections
198 for selection
in sels
.values():
199 _do_bindings(selection
, selection
.bindings
, selection
.interface
)
200 _do_deps(selection
.dependencies
)
202 # Process commands' dependencies' bindings too
203 for command
in selection
.get_commands().values():
204 _do_bindings(selection
, command
.bindings
, selection
.interface
)
205 _do_deps(command
.requires
)
207 # Do these after <environment>s, because they may do $-expansion
208 for binding
, iface
in self
._exec
_bindings
:
209 self
.do_exec_binding(binding
, iface
)
210 self
._exec
_bindings
= None
212 def do_binding(self
, impl
, binding
, iface
):
213 """Called by L{prepare_env} for each binding.
214 Sub-classes may wish to override this.
215 @param impl: the selected implementation
216 @type impl: L{selections.Selection}
217 @param binding: the binding to be processed
218 @type binding: L{model.Binding}
219 @param iface: the interface containing impl
220 @type iface: L{model.Interface}
222 if isinstance(binding
, EnvironmentBinding
):
223 do_env_binding(binding
, self
._get
_implementation
_path
(impl
))
224 elif isinstance(binding
, ExecutableBinding
):
225 if isinstance(iface
, Dependency
):
227 warnings
.warn("Pass an interface URI instead", DeprecationWarning, 2)
228 iface
= iface
.interface
229 self
._exec
_bindings
.append((binding
, iface
))
231 def do_exec_binding(self
, binding
, iface
):
232 assert iface
is not None
234 if '/' in name
or name
.startswith('.') or "'" in name
:
235 raise SafeException("Invalid <executable> name '%s'" % name
)
236 exec_dir
= basedir
.save_cache_path(namespaces
.config_site
, namespaces
.config_prog
, 'executables', name
)
237 exec_path
= os
.path
.join(exec_dir
, name
)
239 if not self
._checked
_runenv
:
242 if not os
.path
.exists(exec_path
):
243 # Symlink ~/.cache/0install.net/injector/executables/$name/$name to runenv.py
244 os
.symlink('../../runenv.py', exec_path
)
245 os
.chmod(exec_dir
, 0o500)
248 path
= os
.environ
["PATH"] = exec_dir
+ os
.pathsep
+ os
.environ
["PATH"]
249 info("PATH=%s", path
)
251 os
.environ
[name
] = exec_path
252 info("%s=%s", name
, exec_path
)
255 args
= self
.build_command(iface
, binding
.command
)
256 os
.environ
["0install-runenv-" + name
] = json
.dumps(args
)
258 def _check_runenv(self
):
259 # Create the runenv.py helper script under ~/.cache if missing or out-of-date
260 main_dir
= basedir
.save_cache_path(namespaces
.config_site
, namespaces
.config_prog
)
261 runenv
= os
.path
.join(main_dir
, 'runenv.py')
262 expected_contents
= "#!%s\nfrom zeroinstall.injector import _runenv; _runenv.main()\n" % sys
.executable
264 actual_contents
= None
265 if os
.path
.exists(runenv
):
266 with
open(runenv
) as s
:
267 actual_contents
= s
.read()
269 if actual_contents
!= expected_contents
:
271 tmp
= tempfile
.NamedTemporaryFile('w', dir = main_dir
, delete
= False)
272 info("Updating %s", runenv
)
273 tmp
.write(expected_contents
)
275 os
.chmod(tmp
.name
, 0555)
276 os
.rename(tmp
.name
, runenv
)
278 self
._checked
_runenv
= True
280 def execute_selections(selections
, prog_args
, dry_run
= False, main
= None, wrapper
= None, stores
= None):
281 """Execute program. On success, doesn't return. On failure, raises an Exception.
282 Returns normally only for a successful dry run.
283 @param selections: the selected versions
284 @type selections: L{selections.Selections}
285 @param prog_args: arguments to pass to the program
286 @type prog_args: [str]
287 @param dry_run: if True, just print a message about what would have happened
289 @param main: the name of the binary to run, or None to use the default
291 @param wrapper: a command to use to actually run the binary, or None to run the binary directly
294 @precondition: All implementations are in the cache.
296 #assert stores is not None
298 from zeroinstall
import zerostore
299 stores
= zerostore
.Stores()
301 setup
= Setup(stores
, selections
)
303 commands
= selections
.commands
305 # Replace first command with user's input
306 if main
.startswith('/'):
307 main
= main
[1:] # User specified a path relative to the package root
309 old_path
= commands
[0].path
310 assert old_path
, "Can't use a relative replacement main when there is no original one!"
311 main
= os
.path
.join(os
.path
.dirname(old_path
), main
) # User main is relative to command's name
312 # Copy all child nodes (e.g. <runner>) except for the arguments
313 user_command_element
= qdom
.Element(namespaces
.XMLNS_IFACE
, 'command', {'path': main
})
315 for child
in commands
[0].qdom
.childNodes
:
316 if child
.uri
== namespaces
.XMLNS_IFACE
and child
.name
== 'arg':
318 user_command_element
.childNodes
.append(child
)
319 user_command
= Command(user_command_element
, None)
324 prog_args
= setup
.build_command(selections
.interface
, selections
.command
, user_command
) + prog_args
327 prog_args
= ['/bin/sh', '-c', wrapper
+ ' "$@"', '-'] + list(prog_args
)
330 print(_("Would execute: %s") % ' '.join(prog_args
))
332 info(_("Executing: %s"), prog_args
)
336 os
.execv(prog_args
[0], prog_args
)
337 except OSError as ex
:
338 raise SafeException(_("Failed to run '%(program_path)s': %(exception)s") % {'program_path': prog_args
[0], 'exception': str(ex
)})