Package zeroinstall :: Package injector :: Module run
[frames] | no frames]

Source Code for Module zeroinstall.injector.run

  1  """ 
  2  Executes a set of implementations as a program. 
  3  """ 
  4   
  5  # Copyright (C) 2009, Thomas Leonard 
  6  # See the README file for details, or visit http://0install.net. 
  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   
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 24 @type path: str""" 25 if binding.insert is not None and path is None: 26 # Skip insert bindings for package implementations 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
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 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 # We are the child 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
70 -def _process_args(args, element, env = os.environ):
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
90 -class Setup(object):
91 """@since: 1.2""" 92 stores = None 93 selections = None 94 _exec_bindings = None 95 _checked_runenv = False 96
97 - def __init__(self, stores, selections):
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 # Add extra arguments for runner 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 # Add main program path 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 # Add extra arguments for program 165 _process_args(command_args, command.qdom) 166 167 prog_args = command_args + prog_args 168 169 # Each command is run by the next, but the last one is run by exec, and we 170 # need a path for that. 171 if command.path is None: 172 raise SafeException("Missing 'path' attribute on <command>") 173 174 return prog_args
175
176 - def prepare_env(self):
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 # 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) 201 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 206
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}""" 216 if isinstance(binding, EnvironmentBinding): 217 if impl.id.startswith('package:'): 218 path = None # (but still do the binding, e.g. for values) 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
229 - def do_exec_binding(self, binding, iface):
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 # Copy runenv.cli.template to ~/.cache/0install.net/injector/executables/$name/$name 245 import shutil 246 shutil.copyfile(os.environ['ZEROINSTALL_CLI_TEMPLATE'], exec_path) 247 else: 248 # Symlink ~/.cache/0install.net/injector/executables/$name/$name to runenv.py 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
269 - def _check_runenv(self):
270 # Create the runenv.py helper script under ~/.cache if missing or out-of-date 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 #assert stores is not None 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 # Replace first command with user's input 317 if main.startswith('/'): 318 main = main[1:] # User specified a path relative to the package root 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) # User main is relative to command's name 324 # Copy all child nodes (e.g. <runner>) except for the arguments 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