76681ad
"""Module/script to byte-compile all .py files to .pyc files.
76681ad
76681ad
When called as a script with arguments, this compiles the directories
76681ad
given as arguments recursively; the -l option prevents it from
76681ad
recursing into directories.
76681ad
76681ad
Without arguments, if compiles all modules on sys.path, without
76681ad
recursing into subdirectories.  (Even though it should do so for
76681ad
packages -- for now, you'll have to deal with packages separately.)
76681ad
76681ad
See module py_compile for details of the actual byte-compilation.
bebf85d
bebf85d
License:
bebf85d
Compileall2 is an enhanced copy of Python's compileall module
bebf85d
and it follows Python licensing. For more info see: https://www.python.org/psf/license/
76681ad
"""
76681ad
import os
76681ad
import sys
76681ad
import importlib.util
76681ad
import py_compile
76681ad
import struct
456f3ec
import filecmp
76681ad
76681ad
from functools import partial
76681ad
from pathlib import Path
76681ad
76681ad
# Python 3.7 and higher
76681ad
PY37 = sys.version_info[0:2] >= (3, 7)
76681ad
# Python 3.6 and higher
76681ad
PY36 = sys.version_info[0:2] >= (3, 6)
76681ad
# Python 3.5 and higher
76681ad
PY35 = sys.version_info[0:2] >= (3, 5)
76681ad
76681ad
# Python 3.7 and above has a different structure and length
76681ad
# of pyc files header. Also, multiple ways how to invalidate pyc file was
76681ad
# introduced in Python 3.7. These cases are covered by variables here or by PY37
76681ad
# variable itself.
76681ad
if PY37:
76681ad
    pyc_struct_format = '<4sll'
76681ad
    pyc_header_lenght = 12
76681ad
    pyc_header_format = (pyc_struct_format, importlib.util.MAGIC_NUMBER, 0)
76681ad
else:
76681ad
    pyc_struct_format = '<4sl'
76681ad
    pyc_header_lenght = 8
76681ad
    pyc_header_format = (pyc_struct_format, importlib.util.MAGIC_NUMBER)
76681ad
76681ad
__all__ = ["compile_dir","compile_file","compile_path"]
76681ad
76681ad
def optimization_kwarg(opt):
76681ad
    """Returns opt as a dictionary {optimization: opt} for use as **kwarg
76681ad
    for Python >= 3.5 and empty dictionary for Python 3.4"""
76681ad
    if PY35:
76681ad
        return dict(optimization=opt)
76681ad
    else:
76681ad
        # `debug_override` is a way how to enable optimized byte-compiled files
76681ad
        # (.pyo) in Python <= 3.4
76681ad
        if opt:
76681ad
            return dict(debug_override=False)
76681ad
        else:
76681ad
            return dict()
76681ad
bebf85d
def _walk_dir(dir, maxlevels, quiet=0):
76681ad
    if PY36 and quiet < 2 and isinstance(dir, os.PathLike):
76681ad
        dir = os.fspath(dir)
76681ad
    else:
76681ad
        dir = str(dir)
76681ad
    if not quiet:
76681ad
        print('Listing {!r}...'.format(dir))
76681ad
    try:
76681ad
        names = os.listdir(dir)
76681ad
    except OSError:
76681ad
        if quiet < 2:
76681ad
            print("Can't list {!r}".format(dir))
76681ad
        names = []
76681ad
    names.sort()
76681ad
    for name in names:
76681ad
        if name == '__pycache__':
76681ad
            continue
76681ad
        fullname = os.path.join(dir, name)
76681ad
        if not os.path.isdir(fullname):
76681ad
            yield fullname
76681ad
        elif (maxlevels > 0 and name != os.curdir and name != os.pardir and
76681ad
              os.path.isdir(fullname) and not os.path.islink(fullname)):
76681ad
            yield from _walk_dir(fullname, maxlevels=maxlevels - 1,
76681ad
                                 quiet=quiet)
76681ad
bebf85d
def compile_dir(dir, maxlevels=None, ddir=None, force=False,
76681ad
                rx=None, quiet=0, legacy=False, optimize=-1, workers=1,
76681ad
                invalidation_mode=None, stripdir=None,
456f3ec
                prependdir=None, limit_sl_dest=None, hardlink_dupes=False):
76681ad
    """Byte-compile all modules in the given directory tree.
76681ad
76681ad
    Arguments (only dir is required):
76681ad
76681ad
    dir:       the directory to byte-compile
76681ad
    maxlevels: maximum recursion level (default `sys.getrecursionlimit()`)
76681ad
    ddir:      the directory that will be prepended to the path to the
76681ad
               file as it is compiled into each byte-code file.
76681ad
    force:     if True, force compilation, even if timestamps are up-to-date
76681ad
    quiet:     full output with False or 0, errors only with 1,
76681ad
               no output with 2
76681ad
    legacy:    if True, produce legacy pyc paths instead of PEP 3147 paths
76681ad
    optimize:  int or list of optimization levels or -1 for level of
76681ad
               the interpreter. Multiple levels leads to multiple compiled
76681ad
               files each with one optimization level.
76681ad
    workers:   maximum number of parallel workers
76681ad
    invalidation_mode: how the up-to-dateness of the pyc will be checked
76681ad
    stripdir:  part of path to left-strip from source file path
76681ad
    prependdir: path to prepend to beggining of original file path, applied
76681ad
               after stripdir
76681ad
    limit_sl_dest: ignore symlinks if they are pointing outside of
76681ad
                   the defined path
456f3ec
    hardlink_dupes: hardlink duplicated pyc files
76681ad
    """
76681ad
    ProcessPoolExecutor = None
15e32a3
    if ddir is not None and (stripdir is not None or prependdir is not None):
15e32a3
        raise ValueError(("Destination dir (ddir) cannot be used "
15e32a3
                          "in combination with stripdir or prependdir"))
15e32a3
    if ddir is not None:
15e32a3
        stripdir = dir
15e32a3
        prependdir = ddir
15e32a3
        ddir = None
76681ad
    if workers is not None:
76681ad
        if workers < 0:
76681ad
            raise ValueError('workers must be greater or equal to 0')
76681ad
        elif workers != 1:
76681ad
            try:
76681ad
                # Only import when needed, as low resource platforms may
76681ad
                # fail to import it
76681ad
                from concurrent.futures import ProcessPoolExecutor
76681ad
            except ImportError:
76681ad
                workers = 1
bebf85d
    if maxlevels is None:
bebf85d
        maxlevels = sys.getrecursionlimit()
76681ad
    files = _walk_dir(dir, quiet=quiet, maxlevels=maxlevels)
76681ad
    success = True
76681ad
    if workers is not None and workers != 1 and ProcessPoolExecutor is not None:
76681ad
        workers = workers or None
76681ad
        with ProcessPoolExecutor(max_workers=workers) as executor:
76681ad
            results = executor.map(partial(compile_file,
76681ad
                                           ddir=ddir, force=force,
76681ad
                                           rx=rx, quiet=quiet,
76681ad
                                           legacy=legacy,
76681ad
                                           optimize=optimize,
76681ad
                                           invalidation_mode=invalidation_mode,
76681ad
                                           stripdir=stripdir,
76681ad
                                           prependdir=prependdir,
76681ad
                                           limit_sl_dest=limit_sl_dest),
76681ad
                                   files)
76681ad
            success = min(results, default=True)
76681ad
    else:
76681ad
        for file in files:
76681ad
            if not compile_file(file, ddir, force, rx, quiet,
76681ad
                                legacy, optimize, invalidation_mode,
76681ad
                                stripdir=stripdir, prependdir=prependdir,
456f3ec
                                limit_sl_dest=limit_sl_dest,
456f3ec
                                hardlink_dupes=hardlink_dupes):
76681ad
                success = False
76681ad
    return success
76681ad
76681ad
def compile_file(fullname, ddir=None, force=False, rx=None, quiet=0,
76681ad
                 legacy=False, optimize=-1,
76681ad
                 invalidation_mode=None, stripdir=None, prependdir=None,
456f3ec
                 limit_sl_dest=None, hardlink_dupes=False):
76681ad
    """Byte-compile one file.
76681ad
76681ad
    Arguments (only fullname is required):
76681ad
76681ad
    fullname:  the file to byte-compile
76681ad
    ddir:      if given, the directory name compiled in to the
76681ad
               byte-code file.
76681ad
    force:     if True, force compilation, even if timestamps are up-to-date
76681ad
    quiet:     full output with False or 0, errors only with 1,
76681ad
               no output with 2
76681ad
    legacy:    if True, produce legacy pyc paths instead of PEP 3147 paths
76681ad
    optimize:  int or list of optimization levels or -1 for level of
76681ad
               the interpreter. Multiple levels leads to multiple compiled
76681ad
               files each with one optimization level.
76681ad
    invalidation_mode: how the up-to-dateness of the pyc will be checked
76681ad
    stripdir:  part of path to left-strip from source file path
76681ad
    prependdir: path to prepend to beggining of original file path, applied
76681ad
               after stripdir
76681ad
    limit_sl_dest: ignore symlinks if they are pointing outside of
76681ad
                   the defined path.
456f3ec
    hardlink_dupes: hardlink duplicated pyc files
76681ad
    """
bebf85d
bebf85d
    if ddir is not None and (stripdir is not None or prependdir is not None):
bebf85d
        raise ValueError(("Destination dir (ddir) cannot be used "
bebf85d
                          "in combination with stripdir or prependdir"))
bebf85d
76681ad
    success = True
76681ad
    if PY36 and quiet < 2 and isinstance(fullname, os.PathLike):
76681ad
        fullname = os.fspath(fullname)
76681ad
    else:
76681ad
        fullname = str(fullname)
76681ad
    name = os.path.basename(fullname)
76681ad
76681ad
    dfile = None
76681ad
76681ad
    if ddir is not None:
76681ad
        if not PY36:
76681ad
            ddir = str(ddir)
76681ad
        dfile = os.path.join(ddir, name)
76681ad
76681ad
    if stripdir is not None:
76681ad
        fullname_parts = fullname.split(os.path.sep)
76681ad
        stripdir_parts = stripdir.split(os.path.sep)
76681ad
        ddir_parts = list(fullname_parts)
76681ad
76681ad
        for spart, opart in zip(stripdir_parts, fullname_parts):
76681ad
            if spart == opart:
76681ad
                ddir_parts.remove(spart)
76681ad
76681ad
        dfile = os.path.join(*ddir_parts)
76681ad
76681ad
    if prependdir is not None:
76681ad
        if dfile is None:
76681ad
            dfile = os.path.join(prependdir, fullname)
76681ad
        else:
76681ad
            dfile = os.path.join(prependdir, dfile)
76681ad
76681ad
    if isinstance(optimize, int):
76681ad
        optimize = [optimize]
76681ad
456f3ec
        if hardlink_dupes:
456f3ec
            raise ValueError(("Hardlinking of duplicated bytecode makes sense "
456f3ec
                              "only for more than one optimization level."))
456f3ec
76681ad
    if rx is not None:
76681ad
        mo = rx.search(fullname)
76681ad
        if mo:
76681ad
            return success
76681ad
76681ad
    if limit_sl_dest is not None and os.path.islink(fullname):
76681ad
        if Path(limit_sl_dest).resolve() not in Path(fullname).resolve().parents:
76681ad
            return success
76681ad
76681ad
    opt_cfiles = {}
76681ad
76681ad
    if os.path.isfile(fullname):
76681ad
        for opt_level in optimize:
76681ad
            if legacy:
76681ad
                opt_cfiles[opt_level] = fullname + 'c'
76681ad
            else:
76681ad
                if opt_level >= 0:
76681ad
                    opt = opt_level if opt_level >= 1 else ''
76681ad
                    opt_kwarg = optimization_kwarg(opt)
76681ad
                    cfile = (importlib.util.cache_from_source(
76681ad
                             fullname, **opt_kwarg))
76681ad
                    opt_cfiles[opt_level] = cfile
76681ad
                else:
76681ad
                    cfile = importlib.util.cache_from_source(fullname)
76681ad
                    opt_cfiles[opt_level] = cfile
76681ad
76681ad
        head, tail = name[:-3], name[-3:]
76681ad
        if tail == '.py':
76681ad
            if not force:
76681ad
                try:
76681ad
                    mtime = int(os.stat(fullname).st_mtime)
76681ad
                    expect = struct.pack(*(pyc_header_format + (mtime,)))
76681ad
                    for cfile in opt_cfiles.values():
76681ad
                        with open(cfile, 'rb') as chandle:
76681ad
                            actual = chandle.read(pyc_header_lenght)
76681ad
                        if expect != actual:
76681ad
                            break
76681ad
                    else:
76681ad
                        return success
76681ad
                except OSError:
76681ad
                    pass
76681ad
            if not quiet:
76681ad
                print('Compiling {!r}...'.format(fullname))
76681ad
            try:
456f3ec
                for index, opt_level in enumerate(sorted(optimize)):
456f3ec
                    cfile = opt_cfiles[opt_level]
76681ad
                    if PY37:
76681ad
                        ok = py_compile.compile(fullname, cfile, dfile, True,
76681ad
                                                optimize=opt_level,
76681ad
                                                invalidation_mode=invalidation_mode)
76681ad
                    else:
76681ad
                        ok = py_compile.compile(fullname, cfile, dfile, True,
76681ad
                                                optimize=opt_level)
456f3ec
456f3ec
                    if index > 0 and hardlink_dupes:
456f3ec
                        previous_cfile = opt_cfiles[optimize[index - 1]]
456f3ec
                        if previous_cfile == cfile and optimize[0] not in (1, 2):
456f3ec
                            # Python 3.4 has only one .pyo file for -O and -OO so
456f3ec
                            # we hardlink it only if there is a .pyc file
456f3ec
                            # with the same content
456f3ec
                            previous_cfile = opt_cfiles[optimize[0]]
456f3ec
                        if  previous_cfile != cfile and filecmp.cmp(cfile, previous_cfile, shallow=False):
456f3ec
                            os.unlink(cfile)
456f3ec
                            os.link(previous_cfile, cfile)
456f3ec
76681ad
            except py_compile.PyCompileError as err:
76681ad
                success = False
76681ad
                if quiet >= 2:
76681ad
                    return success
76681ad
                elif quiet:
76681ad
                    print('*** Error compiling {!r}...'.format(fullname))
76681ad
                else:
76681ad
                    print('*** ', end='')
76681ad
                # escape non-printable characters in msg
76681ad
                msg = err.msg.encode(sys.stdout.encoding,
76681ad
                                     errors='backslashreplace')
76681ad
                msg = msg.decode(sys.stdout.encoding)
76681ad
                print(msg)
76681ad
            except (SyntaxError, UnicodeError, OSError) as e:
76681ad
                success = False
76681ad
                if quiet >= 2:
76681ad
                    return success
76681ad
                elif quiet:
76681ad
                    print('*** Error compiling {!r}...'.format(fullname))
76681ad
                else:
76681ad
                    print('*** ', end='')
76681ad
                print(e.__class__.__name__ + ':', e)
76681ad
            else:
76681ad
                if ok == 0:
76681ad
                    success = False
76681ad
    return success
76681ad
76681ad
def compile_path(skip_curdir=1, maxlevels=0, force=False, quiet=0,
76681ad
                 legacy=False, optimize=-1,
76681ad
                 invalidation_mode=None):
76681ad
    """Byte-compile all module on sys.path.
76681ad
76681ad
    Arguments (all optional):
76681ad
76681ad
    skip_curdir: if true, skip current directory (default True)
76681ad
    maxlevels:   max recursion level (default 0)
76681ad
    force: as for compile_dir() (default False)
76681ad
    quiet: as for compile_dir() (default 0)
76681ad
    legacy: as for compile_dir() (default False)
76681ad
    optimize: as for compile_dir() (default -1)
76681ad
    invalidation_mode: as for compiler_dir()
76681ad
    """
76681ad
    success = True
76681ad
    for dir in sys.path:
76681ad
        if (not dir or dir == os.curdir) and skip_curdir:
76681ad
            if quiet < 2:
76681ad
                print('Skipping current directory')
76681ad
        else:
76681ad
            success = success and compile_dir(
76681ad
                dir,
76681ad
                maxlevels,
76681ad
                None,
76681ad
                force,
76681ad
                quiet=quiet,
76681ad
                legacy=legacy,
76681ad
                optimize=optimize,
76681ad
                invalidation_mode=invalidation_mode,
76681ad
            )
76681ad
    return success
76681ad
76681ad
76681ad
def main():
76681ad
    """Script main program."""
76681ad
    import argparse
76681ad
76681ad
    parser = argparse.ArgumentParser(
76681ad
        description='Utilities to support installing Python libraries.')
76681ad
    parser.add_argument('-l', action='store_const', const=0,
bebf85d
                        default=None, dest='maxlevels',
76681ad
                        help="don't recurse into subdirectories")
76681ad
    parser.add_argument('-r', type=int, dest='recursion',
76681ad
                        help=('control the maximum recursion level. '
76681ad
                              'if `-l` and `-r` options are specified, '
76681ad
                              'then `-r` takes precedence.'))
76681ad
    parser.add_argument('-f', action='store_true', dest='force',
76681ad
                        help='force rebuild even if timestamps are up to date')
76681ad
    parser.add_argument('-q', action='count', dest='quiet', default=0,
76681ad
                        help='output only error messages; -qq will suppress '
76681ad
                             'the error messages as well.')
76681ad
    parser.add_argument('-b', action='store_true', dest='legacy',
76681ad
                        help='use legacy (pre-PEP3147) compiled file locations')
76681ad
    parser.add_argument('-d', metavar='DESTDIR',  dest='ddir', default=None,
76681ad
                        help=('directory to prepend to file paths for use in '
76681ad
                              'compile-time tracebacks and in runtime '
76681ad
                              'tracebacks in cases where the source file is '
76681ad
                              'unavailable'))
76681ad
    parser.add_argument('-s', metavar='STRIPDIR',  dest='stripdir',
76681ad
                        default=None,
76681ad
                        help=('part of path to left-strip from path '
76681ad
                              'to source file - for example buildroot. '
bebf85d
                              '`-d` and `-s` options cannot be '
bebf85d
                              'specified together.'))
76681ad
    parser.add_argument('-p', metavar='PREPENDDIR',  dest='prependdir',
76681ad
                        default=None,
76681ad
                        help=('path to add as prefix to path '
76681ad
                              'to source file - for example / to make '
76681ad
                              'it absolute when some part is removed '
bebf85d
                              'by `-s` option. '
bebf85d
                              '`-d` and `-p` options cannot be '
bebf85d
                              'specified together.'))
76681ad
    parser.add_argument('-x', metavar='REGEXP', dest='rx', default=None,
76681ad
                        help=('skip files matching the regular expression; '
76681ad
                              'the regexp is searched for in the full path '
76681ad
                              'of each file considered for compilation'))
76681ad
    parser.add_argument('-i', metavar='FILE', dest='flist',
76681ad
                        help=('add all the files and directories listed in '
76681ad
                              'FILE to the list considered for compilation; '
76681ad
                              'if "-", names are read from stdin'))
76681ad
    parser.add_argument('compile_dest', metavar='FILE|DIR', nargs='*',
76681ad
                        help=('zero or more file and directory names '
76681ad
                              'to compile; if no arguments given, defaults '
76681ad
                              'to the equivalent of -l sys.path'))
76681ad
    parser.add_argument('-j', '--workers', default=1,
76681ad
                        type=int, help='Run compileall concurrently')
76681ad
    parser.add_argument('-o', action='append', type=int, dest='opt_levels',
456f3ec
                        help=('Optimization levels to run compilation with. '
456f3ec
                              'Default is -1 which uses optimization level of '
76681ad
                              'Python interpreter itself (specified by -O).'))
76681ad
    parser.add_argument('-e', metavar='DIR', dest='limit_sl_dest',
76681ad
                        help='Ignore symlinks pointing outsite of the DIR')
456f3ec
    parser.add_argument('--hardlink-dupes', action='store_true',
456f3ec
                        dest='hardlink_dupes',
456f3ec
                        help='Hardlink duplicated pyc files')
76681ad
76681ad
    if PY37:
76681ad
        invalidation_modes = [mode.name.lower().replace('_', '-')
76681ad
                              for mode in py_compile.PycInvalidationMode]
76681ad
        parser.add_argument('--invalidation-mode',
76681ad
                            choices=sorted(invalidation_modes),
76681ad
                            help=('set .pyc invalidation mode; defaults to '
76681ad
                                  '"checked-hash" if the SOURCE_DATE_EPOCH '
76681ad
                                  'environment variable is set, and '
76681ad
                                  '"timestamp" otherwise.'))
76681ad
76681ad
    args = parser.parse_args()
76681ad
    compile_dests = args.compile_dest
76681ad
76681ad
    if args.rx:
76681ad
        import re
76681ad
        args.rx = re.compile(args.rx)
76681ad
76681ad
    if args.limit_sl_dest == "":
76681ad
        args.limit_sl_dest = None
76681ad
76681ad
    if args.recursion is not None:
76681ad
        maxlevels = args.recursion
76681ad
    else:
76681ad
        maxlevels = args.maxlevels
76681ad
76681ad
    if args.opt_levels is None:
76681ad
        args.opt_levels = [-1]
76681ad
456f3ec
    if len(args.opt_levels) == 1 and args.hardlink_dupes:
456f3ec
        parser.error(("Hardlinking of duplicated bytecode makes sense "
456f3ec
                      "only for more than one optimization level."))
456f3ec
bebf85d
    if args.ddir is not None and (
bebf85d
        args.stripdir is not None or args.prependdir is not None
bebf85d
    ):
bebf85d
        parser.error("-d cannot be used in combination with -s or -p")
bebf85d
76681ad
    # if flist is provided then load it
76681ad
    if args.flist:
76681ad
        try:
76681ad
            with (sys.stdin if args.flist=='-' else open(args.flist)) as f:
76681ad
                for line in f:
76681ad
                    compile_dests.append(line.strip())
76681ad
        except OSError:
76681ad
            if args.quiet < 2:
76681ad
                print("Error reading file list {}".format(args.flist))
76681ad
            return False
76681ad
76681ad
    if args.workers is not None:
76681ad
        args.workers = args.workers or None
76681ad
76681ad
    if PY37 and args.invalidation_mode:
76681ad
        ivl_mode = args.invalidation_mode.replace('-', '_').upper()
76681ad
        invalidation_mode = py_compile.PycInvalidationMode[ivl_mode]
76681ad
    else:
76681ad
        invalidation_mode = None
76681ad
76681ad
    success = True
76681ad
    try:
76681ad
        if compile_dests:
76681ad
            for dest in compile_dests:
76681ad
                if os.path.isfile(dest):
76681ad
                    if not compile_file(dest, args.ddir, args.force, args.rx,
76681ad
                                        args.quiet, args.legacy,
76681ad
                                        invalidation_mode=invalidation_mode,
76681ad
                                        stripdir=args.stripdir,
76681ad
                                        prependdir=args.prependdir,
76681ad
                                        optimize=args.opt_levels,
456f3ec
                                        limit_sl_dest=args.limit_sl_dest,
456f3ec
                                        hardlink_dupes=args.hardlink_dupes):
76681ad
                        success = False
76681ad
                else:
76681ad
                    if not compile_dir(dest, maxlevels, args.ddir,
76681ad
                                       args.force, args.rx, args.quiet,
76681ad
                                       args.legacy, workers=args.workers,
76681ad
                                       invalidation_mode=invalidation_mode,
76681ad
                                       stripdir=args.stripdir,
76681ad
                                       prependdir=args.prependdir,
76681ad
                                       optimize=args.opt_levels,
456f3ec
                                       limit_sl_dest=args.limit_sl_dest,
456f3ec
                                       hardlink_dupes=args.hardlink_dupes):
76681ad
                        success = False
76681ad
            return success
76681ad
        else:
76681ad
            return compile_path(legacy=args.legacy, force=args.force,
76681ad
                                quiet=args.quiet,
76681ad
                                invalidation_mode=invalidation_mode)
76681ad
    except KeyboardInterrupt:
76681ad
        if args.quiet < 2:
76681ad
            print("\n[interrupted]")
76681ad
        return False
76681ad
    return True
76681ad
76681ad
76681ad
if __name__ == '__main__':
76681ad
    exit_status = int(not main())
76681ad
    sys.exit(exit_status)