bootstrap.py 7.71 KB
Newer Older
Damien Naudet's avatar
Damien Naudet committed
1 2 3 4 5 6 7 8 9 10 11 12
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Bootstrap helps you to test scripts without installing them
by patching your PYTHONPATH on the fly

example: ./bootstrap.py ipython
"""

__authors__ = ["Frédéric-Emmanuel Picca", "Jérôme Kieffer"]
__contact__ = "jerome.kieffer@esrf.eu"
__license__ = "MIT"
13
__date__ = "02/03/2018"
Damien Naudet's avatar
Damien Naudet committed
14 15 16 17 18 19


import sys
import os
import distutils.util
import subprocess
20
import logging
Damien Naudet's avatar
Damien Naudet committed
21

22 23
logging.basicConfig()
logger = logging.getLogger("bootstrap")
Damien Naudet's avatar
Damien Naudet committed
24 25


26 27 28 29 30 31 32 33 34 35 36 37 38 39
def is_debug_python():
    """Returns true if the Python interpreter is in debug mode."""
    try:
        import sysconfig
    except ImportError:  # pragma nocover
        # Python < 2.7
        import distutils.sysconfig as sysconfig

    if sysconfig.get_config_var("Py_DEBUG"):
        return True

    return hasattr(sys, "gettotalrefcount")


Damien Naudet's avatar
Damien Naudet committed
40 41 42 43 44 45 46
def _distutils_dir_name(dname="lib"):
    """
    Returns the name of a distutils build directory
    """
    platform = distutils.util.get_platform()
    architecture = "%s.%s-%i.%i" % (dname, platform,
                                    sys.version_info[0], sys.version_info[1])
47 48
    if is_debug_python():
        architecture += "-pydebug"
Damien Naudet's avatar
Damien Naudet committed
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
    return architecture


def _distutils_scripts_name():
    """Return the name of the distrutils scripts sirectory"""
    f = "scripts-{version[0]}.{version[1]}"
    return f.format(version=sys.version_info)


def _get_available_scripts(path):
    res = []
    try:
        res = " ".join([s.rstrip('.py') for s in os.listdir(path)])
    except OSError:
        res = ["no script available, did you ran "
               "'python setup.py build' before bootstrapping ?"]
    return res


68 69 70 71 72 73 74 75 76 77 78 79 80
if sys.version_info[0] >= 3:  # Python3
    def execfile(fullpath, globals=None, locals=None):
        "Python3 implementation for execfile"
        with open(fullpath) as f:
            try:
                data = f.read()
            except UnicodeDecodeError:
                raise SyntaxError("Not a Python script")
            code = compile(data, fullpath, 'exec')
            exec(code, globals, locals)


def run_file(filename, argv):
Damien Naudet's avatar
Damien Naudet committed
81
    """
82
    Execute a script trying first to use execfile, then a subprocess
Damien Naudet's avatar
Damien Naudet committed
83

84 85 86 87 88
    :param str filename: Script to execute
    :param list[str] argv: Arguments passed to the filename
    """
    full_args = [filename]
    full_args.extend(argv)
Damien Naudet's avatar
Damien Naudet committed
89 90

    try:
91 92 93 94 95 96 97 98 99 100 101
        logger.info("Execute target using exec")
        # execfile is considered as a local call.
        # Providing globals() as locals will force to feed the file into
        # globals() (for examples imports).
        # Without this any function call from the executed file loses imports
        try:
            old_argv = sys.argv
            sys.argv = full_args
            logger.info("Patch the sys.argv: %s", sys.argv)
            logger.info("Executing %s.main()", filename)
            print("########### EXECFILE ###########")
102 103 104
            module_globals = globals().copy()
            module_globals['__file__'] = filename
            execfile(filename, module_globals, module_globals)
105 106 107 108 109
        finally:
            sys.argv = old_argv
    except SyntaxError as error:
        logger.error(error)
        logger.info("Execute target using subprocess")
Damien Naudet's avatar
Damien Naudet committed
110 111
        env = os.environ.copy()
        env.update({"PYTHONPATH": LIBPATH + os.pathsep + os.environ.get("PYTHONPATH", ""),
112
                    "PATH": os.environ.get("PATH", "")})
113 114
        print("########### SUBPROCESS ###########")
        run = subprocess.Popen(full_args, shell=False, env=env)
Damien Naudet's avatar
Damien Naudet committed
115 116
        run.wait()

117

118 119 120 121 122 123 124 125 126 127 128 129 130
def run_entry_point(entry_point, argv):
    """
    Execute an entry_point using the current python context
    (http://setuptools.readthedocs.io/en/latest/setuptools.html#automatic-script-creation)

    :param str entry_point: A string identifying a function from a module
        (NAME = PACKAGE.MODULE:FUNCTION)
    """
    import importlib
    elements = entry_point.split("=")
    target_name = elements[0].strip()
    elements = elements[1].split(":")
    module_name = elements[0].strip()
Thomas Vincent's avatar
Thomas Vincent committed
131 132
    # Take care of entry_point optional "extra" requirements declaration
    function_name = elements[1].split()[0].strip()
133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151

    logger.info("Execute target %s (function %s from module %s) using importlib", target_name, function_name, module_name)
    full_args = [target_name]
    full_args.extend(argv)
    try:
        old_argv = sys.argv
        sys.argv = full_args
        print("########### IMPORTLIB ###########")
        module = importlib.import_module(module_name)
        if hasattr(module, function_name):
            func = getattr(module, function_name)
            func()
        else:
            logger.info("Function %s not found", function_name)
    finally:
        sys.argv = old_argv


def find_executable(target):
152 153
    """Find a filename from a script name.

154 155 156
    - Check the script name as file path,
    - Then checks if the name is a target of the setup.py
    - Then search the script from the PATH environment variable.
157

158 159
    :param str target: Name of the script
    :returns: Returns a tuple: kind, name.
160
    """
161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179
    if os.path.isfile(target):
        return ("path", os.path.abspath(target))

    # search the file from setup.py
    import setup
    config = setup.get_project_configuration(dry_run=True)
    # scripts from project configuration
    if "scripts" in config:
        for script_name in config["scripts"]:
            if os.path.basename(script) == target:
                return ("path", os.path.abspath(script_name))
    # entry-points from project configuration
    if "entry_points" in config:
        for kind in config["entry_points"]:
            for entry_point in config["entry_points"][kind]:
                elements = entry_point.split("=")
                name = elements[0].strip()
                if name == target:
                    return ("entry_point", entry_point)
180 181 182

    # search the file from env PATH
    for dirname in os.environ.get("PATH", "").split(os.pathsep):
183
        path = os.path.join(dirname, target)
184
        if os.path.isfile(path):
185
            return ("path", path)
186

187
    return None, None
188 189


Damien Naudet's avatar
Damien Naudet committed
190
if __name__ == "__main__":
191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208
    home = os.path.dirname(os.path.abspath(__file__))
    LIBPATH = os.path.join(home, 'build', _distutils_dir_name('lib'))
    cwd = os.getcwd()
    os.chdir(home)
    build = subprocess.Popen([sys.executable, "setup.py", "build"],
                             shell=False, cwd=os.path.dirname(os.path.abspath(__file__)))
    build_rc = build.wait()
    if not os.path.exists(LIBPATH):
        logger.warning("`lib` directory does not exist, trying common Python3 lib")
        LIBPATH = os.path.join(os.path.split(LIBPATH)[0], "lib")
    os.chdir(cwd)

    if build_rc == 0:
        logger.info("Build process ended.")
    else:
        logger.error("Build process ended with rc=%s", build_rc)
        sys.exit(-1)

Damien Naudet's avatar
Damien Naudet committed
209
    if len(sys.argv) < 2:
210 211 212 213
        logger.warning("usage: ./bootstrap.py <script>\n")
        script = None
    else:
        script = sys.argv[1]
Damien Naudet's avatar
Damien Naudet committed
214

215 216 217 218
    if script:
        logger.info("Executing %s from source checkout", script)
    else:
        logging.info("Running iPython by default")
Damien Naudet's avatar
Damien Naudet committed
219
    sys.path.insert(0, LIBPATH)
220
    logger.info("Patched sys.path with %s", LIBPATH)
Damien Naudet's avatar
Damien Naudet committed
221

222 223
    if script:
        argv = sys.argv[2:]
224 225 226 227 228
        kind, target = find_executable(script)
        if kind == "path":
            run_file(target, argv)
        elif kind == "entry_point":
            run_entry_point(target, argv)
229 230
        else:
            logger.error("Script %s not found", script)
Damien Naudet's avatar
Damien Naudet committed
231
    else:
232 233 234 235 236 237 238 239 240
        logger.info("Patch the sys.argv: %s", sys.argv)
        sys.path.insert(2, "")
        try:
            from IPython import embed
        except Exception as err:
            logger.error("Unable to execute iPython, using normal Python")
            logger.error(err)
            import code
            code.interact()
Damien Naudet's avatar
Damien Naudet committed
241
        else:
242
            embed()