def __init__(self, path): if not os.path.exists(path): raise PythonLauncher.NotFound("Could not find python environment in %s" % path) self._dir = PythonDirectoryWrapper.get(path) try: self._manifest = self._dir.read(PythonLauncher.MANIFEST) if Compatibility.PY3: self._manifest = str(self._manifest, encoding='utf8') self._manifest = json.loads(self._manifest) except ValueError as e: raise PythonLauncher.InvalidFormat("Could not parse manifest! %s" % e) self._cache = EggCache(self._dir) self._path = OrderedSet([os.path.abspath(path)])
class PythonLauncher(object): """ An execution wrapper around a serialized PythonEnvironment. """ class NotFound(Exception): pass class InvalidFormat(Exception): pass MANIFEST = 'PEX-INFO' def __init__(self, path): if not os.path.exists(path): raise PythonLauncher.NotFound("Could not find python environment in %s" % path) self._dir = PythonDirectoryWrapper.get(path) try: self._manifest = self._dir.read(PythonLauncher.MANIFEST) if Compatibility.PY3: self._manifest = str(self._manifest, encoding='utf8') self._manifest = json.loads(self._manifest) except ValueError as e: raise PythonLauncher.InvalidFormat("Could not parse manifest! %s" % e) self._cache = EggCache(self._dir) self._path = OrderedSet([os.path.abspath(path)]) @staticmethod def debug(msg): if 'PEX_VERBOSE' in os.environ: print('PEX: %s' % msg, file=sys.stderr) def entry(self): """ Return the module spec of the entry point of this PythonEnvironment. None if there is no binary for this environment. """ if 'PEX_MODULE' in os.environ: return os.environ['PEX_MODULE'] entry_point = self._manifest.get('entry', None) if entry_point: return str(entry_point) def binary(self): """ Translate the entry point module spec into its equivalent python script. """ entry_point = self.entry() if entry_point is None: return None entry_point = os.path.sep.join(entry_point.split('.')) return '%s.py' % entry_point def execute(self): entry_point = self.entry() saved_sys_path = sys.path[:] # TODO(John Sirois): plumb this through all the way to the BUILD # files so that "thin" targets may specify this by default. if 'PEX_INHERIT_PATH' in os.environ: sys.path.extend(self.path()) else: sys.path = self.path() if 'PEX_COVERAGE' in os.environ: start_coverage() PythonLauncher.debug('Initialized sys.path to %s' % os.path.pathsep.join(sys.path)) force_interpreter = 'PEX_INTERPRETER' in os.environ if entry_point and not force_interpreter: PythonLauncher.debug('Detected entry_point: %s' % entry_point) runpy.run_module(entry_point, run_name='__main__') else: PythonLauncher.debug('%s, dropping into interpreter' % ( 'PEX_INTERPRETER specified' if force_interpreter else 'No entry point specified.')) if sys.argv[1:]: self.run(args=sys.argv[1:]) else: import code code.interact() sys.path = saved_sys_path @staticmethod def minimum_path(): """ Return the emulated sys.path of a bare python installation, so that we can try to mimick python -S without actually calling python -S (which would be ideal but doesn't play well with virtualenvs with rely upon site manipulation.) """ import sys, site from distutils.sysconfig import get_python_lib save_sys_path = sys.path[:] try: site_packages_prefix = get_python_lib() site_packages = set() site.addsitepackages(site_packages) scrub_from_sys_path = [pkg for pkg in sys.path if pkg in site_packages or site_packages_prefix in pkg] for path in scrub_from_sys_path: PythonLauncher.debug('Scrubbing from sys.path: %s' % path) scrubbed_sys_path = list(OrderedSet(sys.path) - OrderedSet(scrub_from_sys_path)) finally: sys.path = save_sys_path return scrubbed_sys_path def path(self, extras=[]): """ Return the sys.path necessary to run this environment. """ p = OrderedSet(self._path) p.update(self._cache.paths()) p.update(extras) p.update(PythonLauncher.minimum_path()) return list(p) def cmdline(self, interpreter=None, binary=None, interpreter_args=[], args=[]): """ The commandline to run this environment. Optional arguments: interpreter: The interpreter to use [defaults to sys.executable] binary: The binary to run instead of the entry point in the environment interpreter_args: Arguments to be passed to the interpreter before, e.g. '-E' or ['-m', 'pylint.lint'] args: Arguments to be passed to the application being invoked by the environment. """ interpreter = interpreter or PythonInterpreter(sys.executable) cmds = [interpreter.binary()] cmds.extend(interpreter_args) if binary is None: binary = self.binary() if binary: cmds.append(os.path.join(self._dir.path(), binary)) cmds.extend(args) return cmds def run(self, interpreter=None, binary=None, interpreter_args=[], args=[], extra_deps=[], with_chroot=False, kill_orphans=False): """ Run the PythonEnvironment in an interpreter in a subprocess. """ cmdline = self.cmdline(interpreter, binary, interpreter_args, args) pythonpath = os.path.pathsep.join(p for p in self.path(extras=extra_deps)) with pushd(self._dir.path() if with_chroot else os.getcwd()): with environment_as(PYTHONPATH=pythonpath): PythonLauncher.debug('With PYTHONPATH=%s, executing %s' % (pythonpath, ' '.join(cmdline))) # Spawn in a new session so we can cleanup any orphans po = subprocess.Popen(cmdline, preexec_fn=os.setsid if kill_orphans else None) rv = -1 try: rv = po.wait() finally: if kill_orphans and rv: self._reap_orphans(po.pid) return rv def _reap_orphans(self, pid): try: os.killpg(pid, signal.SIGTERM) except OSError as e: # It is not unexpected that all children exited normally if e.errno == errno.EPERM: PythonLauncher.debug("Unable to kill process group: %d" % pid) return if e.errno != errno.ESRCH: raise