1 """
2 Executes a set of implementations as a program.
3 """
4
5
6
7
8 from __future__ import print_function
9
10 from zeroinstall import _, logger
11 import os, sys
12 from string import Template
13
14 from zeroinstall import support
15 from zeroinstall.injector.model import SafeException, EnvironmentBinding, ExecutableBinding, Command, Dependency
16 from zeroinstall.injector import namespaces, qdom
17 from zeroinstall.support import basedir
18
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
24 @type path: str"""
25 if binding.insert is not None and path is None:
26
27 logger.debug("not setting %s as we selected a package implementation", binding.name)
28 return
29 os.environ[binding.name] = binding.get_value(path,
30 os.environ.get(binding.name, None))
31 logger.info("%s=%s", binding.name, os.environ[binding.name])
32
34 """Run the program in a child process, collecting stdout and stderr.
35 @return: the output produced by the process
36 @since: 0.27"""
37 import tempfile
38 output = tempfile.TemporaryFile(prefix = '0launch-test')
39 try:
40 child = os.fork()
41 if child == 0:
42
43 try:
44 try:
45 os.dup2(output.fileno(), 1)
46 os.dup2(output.fileno(), 2)
47 execute_selections(selections, prog_args, dry_run, main)
48 except:
49 import traceback
50 traceback.print_exc()
51 finally:
52 sys.stdout.flush()
53 sys.stderr.flush()
54 os._exit(1)
55
56 logger.info(_("Waiting for test process to finish..."))
57
58 pid, status = os.waitpid(child, 0)
59 assert pid == child
60
61 output.seek(0)
62 results = output.read()
63 if status != 0:
64 results += _("Error from child process: exit code = %d") % status
65 finally:
66 output.close()
67
68 return results
69
71 """Append each <arg> under <element> to args, performing $-expansion. Also, process <for-each> loops.
72 @type args: [str]
73 @type element: L{zeroinstall.injector.qdom.Element}
74 @type env: {str: str}"""
75 for child in element.childNodes:
76 if child.uri != namespaces.XMLNS_IFACE: continue
77
78 if child.name == 'arg':
79 args.append(Template(child.content).substitute(env))
80 elif child.name == 'for-each':
81 array_var = child.attrs['item-from']
82 separator = child.attrs.get('separator', os.pathsep)
83 env_copy = env.copy()
84 seq = env.get(array_var, None)
85 if seq:
86 for item in seq.split(separator):
87 env_copy['item'] = item
88 _process_args(args, child, env_copy)
89
91 """@since: 1.2"""
92 stores = None
93 selections = None
94 _exec_bindings = None
95 _checked_runenv = False
96
98 """@param stores: where to find cached implementations
99 @type stores: L{zerostore.Stores}
100 @type selections: L{zeroinstall.injector.selections.Selections}"""
101 self.stores = stores
102 self.selections = selections
103
104 - def build_command(self, command_iface, command_name, user_command = None, dry_run = False):
105 """Create a list of strings to be passed to exec to run the <command>s in the selections.
106 @param command_iface: the interface of the program being run
107 @type command_iface: str
108 @param command_name: the name of the command being run
109 @type command_name: str
110 @param user_command: a custom command to use instead
111 @type user_command: L{model.Command}
112 @type dry_run: bool
113 @return: the argument list
114 @rtype: [str]"""
115
116 if not (command_name or user_command):
117 raise SafeException(_("Can't run: no command specified!"))
118
119 prog_args = []
120 sels = self.selections.selections
121
122 while command_name or user_command:
123 command_sel = sels[command_iface]
124
125 if user_command is None:
126 command = command_sel.get_command(command_name)
127 else:
128 command = user_command
129 user_command = None
130
131 command_args = []
132
133
134 runner = command.get_runner()
135 if runner:
136 command_iface = runner.interface
137 command_name = runner.command
138 _process_args(command_args, runner.qdom)
139 else:
140 command_iface = None
141 command_name = None
142
143
144 command_path = command.path
145 if command_path is not None:
146 if command_sel.id.startswith('package:'):
147 prog_path = command_path
148 else:
149 if command_path.startswith('/'):
150 raise SafeException(_("Command path must be relative, but '%s' starts with '/'!") %
151 command_path)
152 prog_path = os.path.join(command_sel.get_path(self.stores), command_path)
153
154 assert prog_path is not None
155
156 if not os.path.exists(prog_path) and not dry_run:
157 raise SafeException(_("File '%(program_path)s' does not exist.\n"
158 "(implementation '%(implementation_id)s' + program '%(main)s')") %
159 {'program_path': prog_path, 'implementation_id': command_sel.id,
160 'main': command_path})
161
162 command_args.append(prog_path)
163
164
165 _process_args(command_args, command.qdom)
166
167 prog_args = command_args + prog_args
168
169
170
171 if command.path is None:
172 raise SafeException("Missing 'path' attribute on <command>")
173
174 return prog_args
175
177 """Do all the environment bindings in the selections (setting os.environ)."""
178 self._exec_bindings = []
179
180 def _do_bindings(impl, bindings, iface):
181 for b in bindings:
182 self.do_binding(impl, b, iface)
183
184 def _do_deps(deps):
185 for dep in deps:
186 dep_impl = sels.get(dep.interface, None)
187 if dep_impl is None:
188 assert dep.importance != Dependency.Essential, dep
189 else:
190 _do_bindings(dep_impl, dep.bindings, dep.interface)
191
192 sels = self.selections.selections
193 for selection in sels.values():
194 _do_bindings(selection, selection.bindings, selection.interface)
195 _do_deps(selection.dependencies)
196
197
198 for command in selection.get_commands().values():
199 _do_bindings(selection, command.bindings, selection.interface)
200 _do_deps(command.requires)
201
202
203 for binding, iface in self._exec_bindings:
204 self.do_exec_binding(binding, iface)
205 self._exec_bindings = None
206
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}"""
216 if isinstance(binding, EnvironmentBinding):
217 if impl.id.startswith('package:'):
218 path = None
219 else:
220 path = impl.get_path(self.stores)
221 do_env_binding(binding, path)
222 elif isinstance(binding, ExecutableBinding):
223 if isinstance(iface, Dependency):
224 import warnings
225 warnings.warn("Pass an interface URI instead", DeprecationWarning, 2)
226 iface = iface.interface
227 self._exec_bindings.append((binding, iface))
228
230 """@type binding: L{ExecutableBinding}
231 @type iface: str"""
232 assert iface is not None
233 name = binding.name
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 + ".exe" if os.name == "nt" else name)
238
239 if os.name != "nt" and not self._checked_runenv:
240 self._check_runenv()
241
242 if not os.path.exists(exec_path):
243 if os.name == "nt":
244
245 import shutil
246 shutil.copyfile(os.environ['ZEROINSTALL_CLI_TEMPLATE'], exec_path)
247 else:
248
249 os.symlink('../../runenv.py', exec_path)
250 os.chmod(exec_dir, 0o500)
251
252 if binding.in_path:
253 path = os.environ["PATH"] = exec_dir + os.pathsep + os.environ["PATH"]
254 logger.info("PATH=%s", path)
255 else:
256 os.environ[name] = exec_path
257 logger.info("%s=%s", name, exec_path)
258
259 args = self.build_command(iface, binding.command)
260 if os.name == "nt":
261 os.environ["0install-runenv-file-" + name] = args[0]
262 os.environ["0install-runenv-args-" + name] = support.windows_args_escape(args[1:])
263 os.environ["ZEROINSTALL_RUNENV_FILE_" + name] = args[0]
264 os.environ["ZEROINSTALL_RUNENV_ARGS_" + name] = support.windows_args_escape(args[1:])
265 else:
266 import json
267 os.environ["0install-runenv-" + name] = json.dumps(args)
268
270
271 main_dir = basedir.save_cache_path(namespaces.config_site, namespaces.config_prog)
272 runenv = os.path.join(main_dir, 'runenv.py')
273 expected_contents = "#!%s\nfrom zeroinstall.injector import _runenv; _runenv.main()\n" % sys.executable
274
275 actual_contents = None
276 if os.path.exists(runenv):
277 with open(runenv) as s:
278 actual_contents = s.read()
279
280 if actual_contents != expected_contents:
281 import tempfile
282 tmp = tempfile.NamedTemporaryFile('w', dir = main_dir, delete = False)
283 logger.info("Updating %s", runenv)
284 tmp.write(expected_contents)
285 tmp.close()
286 os.chmod(tmp.name, 0o555)
287 os.rename(tmp.name, runenv)
288
289 self._checked_runenv = True
290
291 -def execute_selections(selections, prog_args, dry_run = False, main = None, wrapper = None, stores = None):
292 """Execute program. On success, doesn't return. On failure, raises an Exception.
293 Returns normally only for a successful dry run.
294 @param selections: the selected versions
295 @type selections: L{selections.Selections}
296 @param prog_args: arguments to pass to the program
297 @type prog_args: [str]
298 @param dry_run: if True, just print a message about what would have happened
299 @type dry_run: bool
300 @param main: the name of the binary to run, or None to use the default
301 @type main: str
302 @param wrapper: a command to use to actually run the binary, or None to run the binary directly
303 @type wrapper: str
304 @type stores: L{zeroinstall.zerostore.Stores} | None
305 @since: 0.27
306 @precondition: All implementations are in the cache."""
307
308 if stores is None:
309 from zeroinstall import zerostore
310 stores = zerostore.Stores()
311
312 setup = Setup(stores, selections)
313
314 commands = selections.commands
315 if main is not None:
316
317 if main.startswith('/'):
318 main = main[1:]
319 else:
320 old_path = commands[0].path if commands else None
321 if not old_path:
322 raise SafeException(_("Can't use a relative replacement main when there is no original one!"))
323 main = os.path.join(os.path.dirname(old_path), main)
324
325 user_command_element = qdom.Element(namespaces.XMLNS_IFACE, 'command', {'path': main})
326 if commands:
327 for child in commands[0].qdom.childNodes:
328 if child.uri == namespaces.XMLNS_IFACE and child.name in ('arg', 'for-each'):
329 continue
330 user_command_element.childNodes.append(child)
331 user_command = Command(user_command_element, None)
332 else:
333 user_command = None
334
335 setup.prepare_env()
336 prog_args = setup.build_command(selections.interface, selections.command, user_command, dry_run = dry_run) + prog_args
337
338 if wrapper:
339 prog_args = ['/bin/sh', '-c', wrapper + ' "$@"', '-'] + list(prog_args)
340
341 if dry_run:
342 print(_("[dry-run] would execute: %s") % ' '.join(prog_args))
343 else:
344 logger.info(_("Executing: %s"), prog_args)
345 sys.stdout.flush()
346 sys.stderr.flush()
347 try:
348 env = os.environ.copy()
349 for x in ['0install-runenv-ZEROINSTALL_GPG', 'ZEROINSTALL_GPG']:
350 if x in env:
351 del env[x]
352
353 os.execve(prog_args[0], prog_args, env)
354 except OSError as ex:
355 raise SafeException(_("Failed to run '%(program_path)s': %(exception)s") % {'program_path': prog_args[0], 'exception': str(ex)})
356