1"""Validates classes in the deployed jar have a max java language level . 2 3Usage: 4 python validate-jar-language-level.py <jar-file> <max-java-language-level> 5""" 6 7import re 8import shutil 9import subprocess 10import sys 11import tempfile 12import zipfile 13 14 15_LANGUAGE_LEVEL_PATTERN = re.compile(r'major version: (\d+)') 16 17 18def main(argv): 19 if len(argv) > 3: 20 raise ValueError( 21 'Expected only two arguments but got {0}'.format(len(argv)) 22 ) 23 24 jar_file, expected_language_level = argv[-2:] 25 print( 26 'Processing {0} with expected language level {1}...'.format( 27 jar_file, 28 expected_language_level 29 ) 30 ) 31 if jar_file.endswith('.jar'): 32 invalid_entries = _invalid_language_level(jar_file, expected_language_level) 33 elif jar_file.endswith('.aar'): 34 dirpath = tempfile.mkdtemp() 35 with zipfile.ZipFile(jar_file, 'r') as zip_file: 36 class_file = zip_file.extract('classes.jar', dirpath) 37 invalid_entries = _invalid_language_level( 38 class_file, 39 expected_language_level 40 ) 41 shutil.rmtree(dirpath) 42 else: 43 raise ValueError('Invalid jar file: {0}'.format(jar_file)) 44 45 if invalid_entries: 46 raise ValueError( 47 'Found invalid entries in {0} that do not match the expected java' 48 ' language level ({1}):\n {2}'.format( 49 jar_file, expected_language_level, '\n '.join(invalid_entries) 50 ) 51 ) 52 53 54def _invalid_language_level(jar_file, expected_language_level): 55 """Returns a list of jar entries with invalid language levels.""" 56 invalid_entries = [] 57 with zipfile.ZipFile(jar_file, 'r') as zip_file: 58 class_infolist = [ 59 info for info in zip_file.infolist() 60 if ( 61 not info.is_dir() 62 and info.filename.endswith('.class') 63 and not is_shaded_class(info.filename) 64 ) 65 ] 66 num_classes = len(class_infolist) 67 for i, info in enumerate(class_infolist): 68 cmd = 'javap -cp {0} -v {1}'.format(jar_file, info.filename[:-6]) 69 output1 = subprocess.run( 70 cmd.split(), 71 stdout=subprocess.PIPE, 72 text=True, 73 check=True, 74 ) 75 matches = _LANGUAGE_LEVEL_PATTERN.findall(output1.stdout) 76 if len(matches) != 1: 77 raise ValueError('Expected exactly one match but found: %s' % matches) 78 class_language_level = matches[0] 79 if class_language_level != expected_language_level: 80 invalid_entries.append( 81 '{0}: {1}'.format(info.filename, class_language_level) 82 ) 83 # This can take a while so print an update. 84 print( 85 ' ({0} of {1}) Found language level {2}: {3}'.format( 86 i + 1, 87 num_classes, 88 class_language_level, 89 info.filename, 90 ) 91 ) 92 93 return invalid_entries 94 95 96def is_shaded_class(filename): 97 # Ignore the shaded deps because we don't really control these classes. 98 shaded_prefixes = [ 99 'dagger/spi/internal/shaded/', 100 'dagger/grpc/shaded/', 101 ] 102 for shaded_prefix in shaded_prefixes: 103 if filename.startswith(shaded_prefix): 104 return True 105 return False 106 107 108if __name__ == '__main__': 109 main(sys.argv) 110