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
, debug
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 test_selections(selections
, prog_args
, dry_run
, main
):
34 """Run the program in a child process, collecting stdout and stderr.
35 @return: the output produced by the process
39 output
= tempfile
.TemporaryFile(prefix
= '0launch-test')
46 os
.dup2(output
.fileno(), 1)
47 os
.dup2(output
.fileno(), 2)
48 execute_selections(selections
, prog_args
, dry_run
, main
)
57 info(_("Waiting for test process to finish..."))
59 pid
, status
= os
.waitpid(child
, 0)
63 results
= output
.read()
65 results
+= _("Error from child process: exit code = %d") % status
71 def _process_args(args
, element
):
72 """Append each <arg> under <element> to args, performing $-expansion."""
73 for child
in element
.childNodes
:
74 if child
.uri
== namespaces
.XMLNS_IFACE
and child
.name
== 'arg':
75 args
.append(Template(child
.content
).substitute(os
.environ
))
82 _checked_runenv
= False
84 def __init__(self
, stores
, selections
):
85 """@param stores: where to find cached implementations
86 @type stores: L{zerostore.Stores}"""
88 self
.selections
= selections
90 def build_command(self
, command_iface
, command_name
, user_command
= None):
91 """Create a list of strings to be passed to exec to run the <command>s in the selections.
92 @param command_iface: the interface of the program being run
93 @type command_iface: str
94 @param command_name: the name of the command being run
95 @type command_name: str
96 @param user_command: a custom command to use instead
97 @type user_command: L{model.Command}
98 @return: the argument list
101 assert command_name
or user_command
104 sels
= self
.selections
.selections
106 while command_name
or user_command
:
107 command_sel
= sels
[command_iface
]
109 if user_command
is None:
110 command
= command_sel
.get_command(command_name
)
112 command
= user_command
117 # Add extra arguments for runner
118 runner
= command
.get_runner()
120 command_iface
= runner
.interface
121 command_name
= runner
.command
122 _process_args(command_args
, runner
.qdom
)
127 # Add main program path
128 command_path
= command
.path
129 if command_path
is not None:
130 if command_sel
.id.startswith('package:'):
131 prog_path
= command_path
133 if command_path
.startswith('/'):
134 raise SafeException(_("Command path must be relative, but '%s' starts with '/'!") %
136 prog_path
= os
.path
.join(command_sel
.get_path(self
.stores
), command_path
)
138 assert prog_path
is not None
140 if not os
.path
.exists(prog_path
):
141 raise SafeException(_("File '%(program_path)s' does not exist.\n"
142 "(implementation '%(implementation_id)s' + program '%(main)s')") %
143 {'program_path': prog_path
, 'implementation_id': command_sel
.id,
144 'main': command_path
})
146 command_args
.append(prog_path
)
148 # Add extra arguments for program
149 _process_args(command_args
, command
.qdom
)
151 prog_args
= command_args
+ prog_args
153 # Each command is run by the next, but the last one is run by exec, and we
154 # need a path for that.
155 if command
.path
is None:
156 raise SafeException("Missing 'path' attribute on <command>")
160 def prepare_env(self
):
161 """Do all the environment bindings in the selections (setting os.environ)."""
162 self
._exec
_bindings
= []
164 def _do_bindings(impl
, bindings
, iface
):
166 self
.do_binding(impl
, b
, iface
)
170 dep_impl
= sels
.get(dep
.interface
, None)
172 assert dep
.importance
!= Dependency
.Essential
, dep
174 _do_bindings(dep_impl
, dep
.bindings
, dep
.interface
)
176 sels
= self
.selections
.selections
177 for selection
in sels
.values():
178 _do_bindings(selection
, selection
.bindings
, selection
.interface
)
179 _do_deps(selection
.dependencies
)
181 # Process commands' dependencies' bindings too
182 for command
in selection
.get_commands().values():
183 _do_bindings(selection
, command
.bindings
, selection
.interface
)
184 _do_deps(command
.requires
)
186 # Do these after <environment>s, because they may do $-expansion
187 for binding
, iface
in self
._exec
_bindings
:
188 self
.do_exec_binding(binding
, iface
)
189 self
._exec
_bindings
= None
191 def do_binding(self
, impl
, binding
, iface
):
192 """Called by L{prepare_env} for each binding.
193 Sub-classes may wish to override this.
194 @param impl: the selected implementation
195 @type impl: L{selections.Selection}
196 @param binding: the binding to be processed
197 @type binding: L{model.Binding}
198 @param iface: the interface containing impl
199 @type iface: L{model.Interface}
201 if isinstance(binding
, EnvironmentBinding
):
202 if impl
.id.startswith('package:'):
203 path
= None # (but still do the binding, e.g. for values)
205 path
= impl
.get_path(self
.stores
)
206 do_env_binding(binding
, path
)
207 elif isinstance(binding
, ExecutableBinding
):
208 if isinstance(iface
, Dependency
):
210 warnings
.warn("Pass an interface URI instead", DeprecationWarning, 2)
211 iface
= iface
.interface
212 self
._exec
_bindings
.append((binding
, iface
))
214 def do_exec_binding(self
, binding
, iface
):
215 assert iface
is not None
217 if '/' in name
or name
.startswith('.') or "'" in name
:
218 raise SafeException("Invalid <executable> name '%s'" % name
)
219 exec_dir
= basedir
.save_cache_path(namespaces
.config_site
, namespaces
.config_prog
, 'executables', name
)
220 exec_path
= os
.path
.join(exec_dir
, name
)
222 if not self
._checked
_runenv
:
225 if not os
.path
.exists(exec_path
):
226 # Symlink ~/.cache/0install.net/injector/executables/$name/$name to runenv.py
227 os
.symlink('../../runenv.py', exec_path
)
228 os
.chmod(exec_dir
, 0o500)
231 path
= os
.environ
["PATH"] = exec_dir
+ os
.pathsep
+ os
.environ
["PATH"]
232 info("PATH=%s", path
)
234 os
.environ
[name
] = exec_path
235 info("%s=%s", name
, exec_path
)
238 args
= self
.build_command(iface
, binding
.command
)
239 os
.environ
["0install-runenv-" + name
] = json
.dumps(args
)
241 def _check_runenv(self
):
242 # Create the runenv.py helper script under ~/.cache if missing or out-of-date
243 main_dir
= basedir
.save_cache_path(namespaces
.config_site
, namespaces
.config_prog
)
244 runenv
= os
.path
.join(main_dir
, 'runenv.py')
245 expected_contents
= "#!%s\nfrom zeroinstall.injector import _runenv; _runenv.main()\n" % sys
.executable
247 actual_contents
= None
248 if os
.path
.exists(runenv
):
249 with
open(runenv
) as s
:
250 actual_contents
= s
.read()
252 if actual_contents
!= expected_contents
:
254 tmp
= tempfile
.NamedTemporaryFile('w', dir = main_dir
, delete
= False)
255 info("Updating %s", runenv
)
256 tmp
.write(expected_contents
)
258 os
.chmod(tmp
.name
, 0o555)
259 os
.rename(tmp
.name
, runenv
)
261 self
._checked
_runenv
= True
263 def execute_selections(selections
, prog_args
, dry_run
= False, main
= None, wrapper
= None, stores
= None):
264 """Execute program. On success, doesn't return. On failure, raises an Exception.
265 Returns normally only for a successful dry run.
266 @param selections: the selected versions
267 @type selections: L{selections.Selections}
268 @param prog_args: arguments to pass to the program
269 @type prog_args: [str]
270 @param dry_run: if True, just print a message about what would have happened
272 @param main: the name of the binary to run, or None to use the default
274 @param wrapper: a command to use to actually run the binary, or None to run the binary directly
277 @precondition: All implementations are in the cache.
279 #assert stores is not None
281 from zeroinstall
import zerostore
282 stores
= zerostore
.Stores()
284 setup
= Setup(stores
, selections
)
286 commands
= selections
.commands
288 # Replace first command with user's input
289 if main
.startswith('/'):
290 main
= main
[1:] # User specified a path relative to the package root
292 old_path
= commands
[0].path
293 assert old_path
, "Can't use a relative replacement main when there is no original one!"
294 main
= os
.path
.join(os
.path
.dirname(old_path
), main
) # User main is relative to command's name
295 # Copy all child nodes (e.g. <runner>) except for the arguments
296 user_command_element
= qdom
.Element(namespaces
.XMLNS_IFACE
, 'command', {'path': main
})
298 for child
in commands
[0].qdom
.childNodes
:
299 if child
.uri
== namespaces
.XMLNS_IFACE
and child
.name
== 'arg':
301 user_command_element
.childNodes
.append(child
)
302 user_command
= Command(user_command_element
, None)
307 prog_args
= setup
.build_command(selections
.interface
, selections
.command
, user_command
) + prog_args
310 prog_args
= ['/bin/sh', '-c', wrapper
+ ' "$@"', '-'] + list(prog_args
)
313 print(_("Would execute: %s") % ' '.join(prog_args
))
315 info(_("Executing: %s"), prog_args
)
319 env
= os
.environ
.copy()
320 for x
in ['0install-runenv-ZEROINSTALL_GPG', 'ZEROINSTALL_GPG']:
324 os
.execve(prog_args
[0], prog_args
, env
)
325 except OSError as ex
:
326 raise SafeException(_("Failed to run '%(program_path)s': %(exception)s") % {'program_path': prog_args
[0], 'exception': str(ex
)})