-
-
Notifications
You must be signed in to change notification settings - Fork 291
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Bytecode compile using the correct interpreter.
Previously a PEXBuilder with a custom interpeter would not use it to bytecode compile sources when building a pex. This could lead to errors when the current interpreter and the target interpreter spanned breaking language changes. Kill the CodeMarshaller which has only its marshalling half used in favor of using py_compile from the stdlib in the new interpreter-sensitive Compiler class. Additionally, actually compile all PEXBuilder source including any generated __init__.pys, the generated __main__.py and everything in the _pex/ bootstrap. Testing Done: CI went green here: https://travis-ci.org/pantsbuild/pex/builds/69010594 Bugs closed: 14, 126, 127 Reviewed at https://rbcommons.com/s/twitter/r/2350/
- Loading branch information
Showing
8 changed files
with
215 additions
and
109 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
# Copyright 2015 Pants project contributors (see CONTRIBUTORS.md). | ||
# Licensed under the Apache License, Version 2.0 (see LICENSE). | ||
|
||
from __future__ import absolute_import | ||
|
||
import subprocess | ||
import tempfile | ||
|
||
from .compatibility import to_bytes | ||
|
||
|
||
_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): | ||
"""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 tempfile.NamedTemporaryFile() as fp: | ||
fp.write(to_bytes(_COMPILER_MAIN % {'root': root, 'relpaths': relpaths}, encoding='utf-8')) | ||
fp.flush() | ||
process = subprocess.Popen([self._interpreter.binary, fp.name], | ||
stdout=subprocess.PIPE, | ||
stderr=subprocess.PIPE) | ||
out, err = process.communicate() | ||
if process.returncode != 0: | ||
raise self.Error(err) | ||
return [pyc_relpath.decode('utf-8') for pyc_relpath in out.splitlines()] |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
# Copyright 2015 Pants project contributors (see CONTRIBUTORS.md). | ||
# Licensed under the Apache License, Version 2.0 (see LICENSE). | ||
|
||
import contextlib | ||
import os | ||
|
||
import marshal | ||
import pytest | ||
from twitter.common.contextutil import temporary_dir | ||
|
||
from pex import compatibility | ||
from pex.common import safe_open | ||
from pex.compatibility import to_bytes | ||
from pex.compiler import Compiler | ||
from pex.interpreter import PythonInterpreter | ||
|
||
|
||
def write_source(path, valid=True): | ||
with safe_open(path, 'wb') as fp: | ||
fp.write(to_bytes('basename = %r\n' % os.path.basename(path))) | ||
if not valid: | ||
fp.write(to_bytes('invalid!\n')) | ||
|
||
|
||
@contextlib.contextmanager | ||
def compilation(valid_paths=None, invalid_paths=None, compile_paths=None): | ||
with temporary_dir() as root: | ||
for path in valid_paths: | ||
write_source(os.path.join(root, path)) | ||
for path in invalid_paths: | ||
write_source(os.path.join(root, path), valid=False) | ||
compiler = Compiler(PythonInterpreter.get()) | ||
yield root, compiler.compile(root, compile_paths) | ||
|
||
|
||
def test_compile_success(): | ||
with compilation(valid_paths=['a.py', 'c/c.py'], | ||
invalid_paths=['b.py', 'd/d.py'], | ||
compile_paths=['a.py', 'c/c.py']) as (root, compiled_relpaths): | ||
|
||
assert 2 == len(compiled_relpaths) | ||
|
||
results = {} | ||
for compiled in compiled_relpaths: | ||
compiled_abspath = os.path.join(root, compiled) | ||
with open(compiled_abspath, 'rb') as fp: | ||
fp.read(4) # Skip the magic header. | ||
fp.read(4) # Skip the timestamp. | ||
if compatibility.PY3: | ||
fp.read(4) # Skip the size. | ||
code = marshal.load(fp) | ||
local_symbols = {} | ||
exec(code, {}, local_symbols) | ||
results[compiled] = local_symbols | ||
|
||
assert {'basename': 'a.py'} == results.pop('a.pyc') | ||
assert {'basename': 'c.py'} == results.pop('c/c.pyc') | ||
assert 0 == len(results) | ||
|
||
|
||
def test_compile_failure(): | ||
with pytest.raises(Compiler.Error) as e: | ||
with compilation(valid_paths=['a.py', 'c/c.py'], | ||
invalid_paths=['b.py', 'd/d.py'], | ||
compile_paths=['a.py', 'b.py', 'c/c.py', 'd/d.py']): | ||
raise AssertionError('Should not reach here.') | ||
|
||
message = str(e.value) | ||
assert 'a.py' not in message | ||
assert 'b.py' in message | ||
assert 'c/c.py' not in message | ||
assert 'd/d.py' in message |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.