"""Validates classes in the deployed jar have a max java language level . Usage: python validate-jar-language-level.py """ import re import shutil import subprocess import sys import tempfile import zipfile _LANGUAGE_LEVEL_PATTERN = re.compile(r'major version: (\d+)') def main(argv): if len(argv) > 3: raise ValueError( 'Expected only two arguments but got {0}'.format(len(argv)) ) jar_file, expected_language_level = argv[-2:] print( 'Processing {0} with expected language level {1}...'.format( jar_file, expected_language_level ) ) if jar_file.endswith('.jar'): invalid_entries = _invalid_language_level(jar_file, expected_language_level) elif jar_file.endswith('.aar'): dirpath = tempfile.mkdtemp() with zipfile.ZipFile(jar_file, 'r') as zip_file: class_file = zip_file.extract('classes.jar', dirpath) invalid_entries = _invalid_language_level( class_file, expected_language_level ) shutil.rmtree(dirpath) else: raise ValueError('Invalid jar file: {0}'.format(jar_file)) if invalid_entries: raise ValueError( 'Found invalid entries in {0} that do not match the expected java' ' language level ({1}):\n {2}'.format( jar_file, expected_language_level, '\n '.join(invalid_entries) ) ) def _invalid_language_level(jar_file, expected_language_level): """Returns a list of jar entries with invalid language levels.""" invalid_entries = [] with zipfile.ZipFile(jar_file, 'r') as zip_file: class_infolist = [ info for info in zip_file.infolist() if ( not info.is_dir() and info.filename.endswith('.class') and not is_shaded_class(info.filename) ) ] num_classes = len(class_infolist) for i, info in enumerate(class_infolist): cmd = 'javap -cp {0} -v {1}'.format(jar_file, info.filename[:-6]) output1 = subprocess.run( cmd.split(), stdout=subprocess.PIPE, text=True, check=True, ) matches = _LANGUAGE_LEVEL_PATTERN.findall(output1.stdout) if len(matches) != 1: raise ValueError('Expected exactly one match but found: %s' % matches) class_language_level = matches[0] if class_language_level != expected_language_level: invalid_entries.append( '{0}: {1}'.format(info.filename, class_language_level) ) # This can take a while so print an update. print( ' ({0} of {1}) Found language level {2}: {3}'.format( i + 1, num_classes, class_language_level, info.filename, ) ) return invalid_entries def is_shaded_class(filename): # Ignore the shaded deps because we don't really control these classes. shaded_prefixes = [ 'dagger/spi/internal/shaded/', 'dagger/grpc/shaded/', ] for shaded_prefix in shaded_prefixes: if filename.startswith(shaded_prefix): return True return False if __name__ == '__main__': main(sys.argv)