• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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