Index: FilesCheck.py =================================================================== --- FilesCheck.py (revision 1667) +++ FilesCheck.py (working copy) @@ -200,6 +200,7 @@ normal_zero_length_regex = re.compile('^/etc/security/console\.apps/|/\.nosearch$|/__init__\.py$') perl_regex = re.compile('^/usr/lib/perl5/(?:vendor_perl/)?([0-9]+\.[0-9]+)\.([0-9]+)/') python_regex = re.compile('^/usr/lib/python([.0-9]+)/') +python_bytecode_regex = re.compile('^(.*)(\.py[oc])$') perl_version_trick = Config.getOption('PerlVersionTrick', True) log_regex = re.compile('^/var/log/[^/]+$') lib_path_regex = re.compile('^(/usr(/X11R6)?)?/lib(64)?') @@ -266,6 +267,51 @@ return False return True +def py_demarshal_long(b): + # Counterpart to Python's PyMarshal_ReadLongFromFile, operating on the + # bytes in a string: + return (ord(b[0]) + + (ord(b[1]) << 8) + + (ord(b[2]) << 16) + + (ord(b[3]) << 24)) + +# What is the expected version of the ABI that .pyc files in arbitrarily +# locations are for? +default_python_version = '2.6' + +def get_expected_pyc_magic(path): + # .pyc/.pyo files embed a 4-byte magic value identifying which version of + # the python bytecode ABI they are for. + # Given a path to a .pyc/.pyo file, return a (description, magic ABI value) + # pair. For example, '/usr/lib/python3.1/foo.pyc' should return + # ('3.1', 3151) + + # See Python/import.c (in the trunk and py3k branches) for a full list of + # the values here. + magic_values = {'2.2': 60717, + '2.3': 62011, + '2.4': 62061, + '2.5': 62131, + '2.6': 62161, + '3.0': 3130, + '3.1': 3150} + + m = python_regex.match(path) + if m: + py_version = m.group(1) + else: + py_version = default_python_version + + expected_magic_value = magic_values[py_version] + + # In Python 2, if Py_UnicodeFlag is set, Python's import code uses a value + # one higher, but this is off by default. + # In Python 3, it always uses the value one higher: + if py_version.startswith('3.'): + expected_magic_value += 1 + + return (py_version, expected_magic_value) + class FilesCheck(AbstractCheck.AbstractCheck): def __init__(self): @@ -547,6 +593,35 @@ printError(pkg, 'no-dependency-on', 'python-base', res.group(1)) python_dep_error = True + res = python_bytecode_regex.search(f) + if res: + source_file = res.group(1) + '.py' + if source_file in files: + # Extract header of .pyc file: + pyc_fobj = open(pkgfile.path, 'rb') + try: + pyc_bytes = pyc_fobj.read(8) + finally: + pyc_fobj.close() + pyc_magic = py_demarshal_long(pyc_bytes[:4]) + pyc_timestamp = py_demarshal_long(pyc_bytes[4:8]) + + # Verify that the magic ABI value embedded in the .pyc + # header is correct + magic_desc, exp_magic = get_expected_pyc_magic(f) + if pyc_magic & 0xffff != exp_magic: + printError(pkg, 'python-bytecode-wrong-magic-value', + f, pyc_magic & 0xffff, magic_desc, exp_magic) + + # Verify that the timestamp embedded in the .pyc header + # matches the mtime of the .py file: + if pyc_timestamp != files[source_file].mtime: + printError(pkg, 'python-bytecode-inconsistent-mtime', + f, pyc_timestamp, + source_file, files[source_file].mtime) + else: + printError(pkg, 'python-bytecode-without-source', f) + # normal executable check if mode & stat.S_IXUSR and perm != 0755: printError(pkg, 'non-standard-executable-perm', f, oct(perm)) @@ -1068,6 +1143,20 @@ 'rpath-in-buildconfig', '''This build configuration file contains rpaths which will be introduced into dependent packages.''', + +'python-bytecode-wrong-magic-value', +'''The "magic" ABI version embedded in this python bytecode file isn't equal +to that of the corresponding runtime, which will force the interpreter to +recompile the .py source every time, ignoring the saved bytecode.''', + +'python-bytecode-inconsistent-mtime', +'''The timestamp embedded in this python bytecode file isn't equal to the mtime +of the original source file, which will force the interpreter to recompile the +.py source every time, ignoring the saved bytecode.''', + +'python-bytecode-without-source', +'''This python bytecode file (.pyo/.pyc) is not accompanied by its original +source file (.py)''', ) # FilesCheck.py ends here