blob: dd0302c708c31e27fcacc3fa03dfecfeb4e2d6fc [file] [log] [blame]
# Copyright 2015 Pants project contributors (see CONTRIBUTORS.md).
# Licensed under the Apache License, Version 2.0 (see LICENSE).
from __future__ import absolute_import
from .compatibility import to_bytes
from .executor import Executor
from .util import named_temporary_file
_COMPILER_MAIN = """
from __future__ import print_function
import os
import py_compile
import sys
def compile(root, relpaths):
compiled = []
errored = {}
for relpath in relpaths:
abspath = os.path.join(root, relpath)
# NB: We give the compiled bytecode file a `.pyc` extension, but if PYTHONOPTIMIZE is in play
# the generated bytecode will be optimized. Traditionally these optimized bytecode files would
# have a `.pyo` extension, but the extension only matters for location of the file to execute
# for a given module and not on the interpretation of its bytecode contents. As such we're
# safe to pick the `.pyc` extension for all bytecode file cases without a need to interpret the
# current optimization setting for the active python interpreter.
pyc_relpath = relpath + 'c'
pyc_abspath = os.path.join(root, pyc_relpath)
try:
py_compile.compile(abspath, cfile=pyc_abspath, dfile=relpath, doraise=True)
compiled.append(pyc_relpath)
except py_compile.PyCompileError as e:
errored[e.file] = e.msg
return compiled, errored
def main(root, relpaths):
compiled, errored = compile(root, relpaths)
if not errored:
for path in compiled:
print(path)
sys.exit(0)
print('Encountered %%d errors compiling %%d files:' %% (len(errored), len(relpaths)),
file=sys.stderr)
for file, msg in errored.items():
print(' %%s: %%s' %% (file, msg), file=sys.stderr)
sys.exit(1)
root = %(root)r
relpaths = %(relpaths)r
main(root, relpaths)
"""
class Compiler(object):
class Error(Exception): pass
class CompilationFailure(Error): # N.B. This subclasses `Error` only for backwards compatibility.
"""Indicates an error compiling one or more python source files."""
def __init__(self, interpreter):
"""Creates a bytecode compiler for the given `interpreter`.
:param interpreter: The interpreter to use to compile sources with.
:type interpreter: :class:`pex.interpreter.PythonInterpreter`
"""
self._interpreter = interpreter
def compile(self, root, relpaths):
"""Compiles the given python source files using this compiler's interpreter.
:param string root: The root path all the source files are found under.
:param list relpaths: The realtive paths from the `root` of the source files to compile.
:returns: A list of relative paths of the compiled bytecode files.
:raises: A :class:`Compiler.Error` if there was a problem bytecode compiling any of the files.
"""
with named_temporary_file() as fp:
fp.write(to_bytes(_COMPILER_MAIN % {'root': root, 'relpaths': relpaths}, encoding='utf-8'))
fp.flush()
try:
out, _ = Executor.execute([self._interpreter.binary, fp.name])
except Executor.NonZeroExit as e:
raise self.CompilationFailure(
'encountered %r during bytecode compilation.\nstderr was:\n%s\n' % (e, e.stderr)
)
return out.splitlines()