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
, ExecutableBinding
, Command
, Dependency
14 from zeroinstall
.injector
import namespaces
, qdom
15 from zeroinstall
.support
import basedir
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
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
36 @param main: the name of the binary to run, or None to use the default
38 @param wrapper: a command to use to actually run the binary, or None to run the binary directly
40 @precondition: C{policy.ready and policy.get_uncached_implementations() == []}
42 execute_selections(policy
.solver
.selections
, prog_args
, dry_run
, main
, wrapper
)
44 def test_selections(selections
, prog_args
, dry_run
, main
, wrapper
= None):
45 """Run the program in a child process, collecting stdout and stderr.
46 @return: the output produced by the process
50 output
= tempfile
.TemporaryFile(prefix
= '0launch-test')
57 os
.dup2(output
.fileno(), 1)
58 os
.dup2(output
.fileno(), 2)
59 execute_selections(selections
, prog_args
, dry_run
, main
)
68 info(_("Waiting for test process to finish..."))
70 pid
, status
= os
.waitpid(child
, 0)
74 results
= output
.read()
76 results
+= _("Error from child process: exit code = %d") % status
82 def _process_args(args
, element
):
83 """Append each <arg> under <element> to args, performing $-expansion."""
84 for child
in element
.childNodes
:
85 if child
.uri
== namespaces
.XMLNS_IFACE
and child
.name
== 'arg':
86 args
.append(Template(child
.content
).substitute(os
.environ
))
94 def __init__(self
, stores
, selections
):
95 """@param stores: where to find cached implementations
96 @type stores: L{zerostore.Stores}"""
98 self
.selections
= selections
100 def build_command(self
, command_iface
, command_name
, user_command
= None):
101 """Create a list of strings to be passed to exec to run the <command>s in the selections.
102 @param commands: the commands to be used (taken from selections is None)
103 @type commands: [L{model.Command}]
104 @return: the argument list
110 sels
= self
.selections
.selections
113 command_sel
= sels
[command_iface
]
115 if user_command
is None:
116 command
= command_sel
.get_command(command_name
)
118 command
= user_command
123 # Add extra arguments for runner
124 runner
= command
.get_runner()
126 command_iface
= runner
.interface
127 command_name
= runner
.command
128 _process_args(command_args
, runner
.qdom
)
133 # Add main program path
134 command_path
= command
.path
135 if command_path
is not None:
136 if command_sel
.id.startswith('package:'):
137 prog_path
= command_path
139 if command_path
.startswith('/'):
140 raise SafeException(_("Command path must be relative, but '%s' starts with '/'!") %
142 prog_path
= os
.path
.join(self
._get
_implementation
_path
(command_sel
), command_path
)
144 assert prog_path
is not None
146 if not os
.path
.exists(prog_path
):
147 raise SafeException(_("File '%(program_path)s' does not exist.\n"
148 "(implementation '%(implementation_id)s' + program '%(main)s')") %
149 {'program_path': prog_path
, 'implementation_id': command_sel
.id,
150 'main': command_path
})
152 command_args
.append(prog_path
)
154 # Add extra arguments for program
155 _process_args(command_args
, command
.qdom
)
157 prog_args
= command_args
+ prog_args
159 # Each command is run by the next, but the last one is run by exec, and we
160 # need a path for that.
161 if command
.path
is None:
162 raise SafeException("Missing 'path' attribute on <command>")
166 def _get_implementation_path(self
, impl
):
167 return impl
.local_path
or self
.stores
.lookup_any(impl
.digests
)
169 def prepare_env(self
):
170 """Do all the environment bindings in the selections (setting os.environ)."""
171 self
._exec
_bindings
= []
173 def _do_bindings(impl
, bindings
, dep
):
175 self
.do_binding(impl
, b
, dep
)
179 dep_impl
= sels
.get(dep
.interface
, None)
181 assert dep
.importance
!= Dependency
.Essential
, dep
182 elif not dep_impl
.id.startswith('package:'):
183 _do_bindings(dep_impl
, dep
.bindings
, dep
)
185 sels
= self
.selections
.selections
186 for selection
in sels
.values():
187 _do_bindings(selection
, selection
.bindings
, None)
188 _do_deps(selection
.dependencies
)
190 # Process commands' dependencies' bindings too
191 for command
in selection
.get_commands().values():
192 _do_deps(command
.requires
)
194 # Do these after <environment>s, because they may do $-expansion
195 for binding
, dep
in self
._exec
_bindings
:
196 self
.do_exec_binding(binding
, dep
)
197 self
._exec
_bindings
= None
199 def do_binding(self
, impl
, binding
, dep
):
200 """Called by L{prepare_env} for each binding.
201 Sub-classes may wish to override this.
202 @param impl: the selected implementation
203 @type impl: L{selections.Selection}
204 @param binding: the binding to be processed
205 @type binding: L{model.Binding}
206 @param dep: the dependency containing the binding, or None for implementation bindings
207 @type dep: L{model.Dependency}
209 if isinstance(binding
, EnvironmentBinding
):
210 do_env_binding(binding
, self
._get
_implementation
_path
(impl
))
211 elif isinstance(binding
, ExecutableBinding
):
212 self
._exec
_bindings
.append((binding
, dep
))
214 def do_exec_binding(self
, binding
, dep
):
216 raise SafeException("<%s> can only appear within a <requires>" % binding
.qdom
.name
)
218 if '/' in name
or name
.startswith('.') or "'" in name
:
219 raise SafeException("Invalid <executable> name '%s'" % name
)
220 exec_dir
= basedir
.save_cache_path(namespaces
.config_site
, namespaces
.config_prog
, 'executables', name
)
221 exec_path
= os
.path
.join(exec_dir
, name
)
222 if not os
.path
.exists(exec_path
):
225 # Create the runenv.py helper script under ~/.cache if missing
226 main_dir
= basedir
.save_cache_path(namespaces
.config_site
, namespaces
.config_prog
)
227 runenv
= os
.path
.join(main_dir
, 'runenv.py')
228 if not os
.path
.exists(runenv
):
229 tmp
= tempfile
.NamedTemporaryFile('w', dir = main_dir
, delete
= False)
230 tmp
.write("#!%s\nfrom zeroinstall.injector import _runenv; _runenv.main()\n" % sys
.executable
)
232 os
.chmod(tmp
.name
, 0555)
233 os
.rename(tmp
.name
, runenv
)
235 # Symlink ~/.cache/0install.net/injector/executables/$name/$name to runenv.py
236 os
.symlink('../../runenv.py', exec_path
)
237 os
.chmod(exec_dir
, 0o500)
240 path
= os
.environ
["PATH"] = exec_dir
+ os
.pathsep
+ os
.environ
["PATH"]
241 info("PATH=%s", path
)
243 os
.environ
[name
] = exec_path
244 info("%s=%s", name
, exec_path
)
247 args
= self
.build_command(dep
.interface
, binding
.command
)
248 os
.environ
["0install-runenv-" + name
] = json
.dumps(args
)
250 def execute_selections(selections
, prog_args
, dry_run
= False, main
= None, wrapper
= None, stores
= None):
251 """Execute program. On success, doesn't return. On failure, raises an Exception.
252 Returns normally only for a successful dry run.
253 @param selections: the selected versions
254 @type selections: L{selections.Selections}
255 @param prog_args: arguments to pass to the program
256 @type prog_args: [str]
257 @param dry_run: if True, just print a message about what would have happened
259 @param main: the name of the binary to run, or None to use the default
261 @param wrapper: a command to use to actually run the binary, or None to run the binary directly
264 @precondition: All implementations are in the cache.
266 #assert stores is not None
268 from zeroinstall
import zerostore
269 stores
= zerostore
.Stores()
271 setup
= Setup(stores
, selections
)
273 commands
= selections
.commands
275 # Replace first command with user's input
276 if main
.startswith('/'):
277 main
= main
[1:] # User specified a path relative to the package root
279 old_path
= commands
[0].path
280 assert old_path
, "Can't use a relative replacement main when there is no original one!"
281 main
= os
.path
.join(os
.path
.dirname(old_path
), main
) # User main is relative to command's name
282 # Copy all child nodes (e.g. <runner>) except for the arguments
283 user_command_element
= qdom
.Element(namespaces
.XMLNS_IFACE
, 'command', {'path': main
})
285 for child
in commands
[0].qdom
.childNodes
:
286 if child
.uri
== namespaces
.XMLNS_IFACE
and child
.name
== 'arg':
288 user_command_element
.childNodes
.append(child
)
289 user_command
= Command(user_command_element
, None)
294 prog_args
= setup
.build_command(selections
.interface
, selections
.command
, user_command
) + prog_args
297 prog_args
= ['/bin/sh', '-c', wrapper
+ ' "$@"', '-'] + list(prog_args
)
300 print _("Would execute: %s") % ' '.join(prog_args
)
302 info(_("Executing: %s"), prog_args
)
306 os
.execv(prog_args
[0], prog_args
)
307 except OSError as ex
:
308 raise SafeException(_("Failed to run '%(program_path)s': %(exception)s") % {'program_path': prog_args
[0], 'exception': str(ex
)})