• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2#
3# Copyright 2013 The Chromium Authors
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6"""Runs Android's lint tool."""
7
8import argparse
9import logging
10import os
11import shutil
12import sys
13import time
14from xml.dom import minidom
15from xml.etree import ElementTree
16
17from util import build_utils
18from util import manifest_utils
19from util import server_utils
20import action_helpers  # build_utils adds //build to sys.path.
21
22_LINT_MD_URL = 'https://chromium.googlesource.com/chromium/src/+/main/build/android/docs/lint.md'  # pylint: disable=line-too-long
23
24# These checks are not useful for chromium.
25_DISABLED_ALWAYS = [
26    "AppCompatResource",  # Lint does not correctly detect our appcompat lib.
27    "AppLinkUrlError",  # As a browser, we have intent filters without a host.
28    "Assert",  # R8 --force-enable-assertions is used to enable java asserts.
29    "InflateParams",  # Null is ok when inflating views for dialogs.
30    "InlinedApi",  # Constants are copied so they are always available.
31    "LintBaseline",  # Don't warn about using baseline.xml files.
32    "LintBaselineFixed",  # We dont care if baseline.xml has unused entries.
33    "MissingInflatedId",  # False positives https://crbug.com/1394222
34    "MissingApplicationIcon",  # False positive for non-production targets.
35    "MissingVersion",  # We set versions via aapt2, which runs after lint.
36    "NetworkSecurityConfig",  # Breaks on library certificates b/269783280.
37    "ObsoleteLintCustomCheck",  # We have no control over custom lint checks.
38    "OldTargetApi",  # We sometimes need targetSdkVersion to not be latest.
39    "StringFormatCount",  # Has false-positives.
40    "SwitchIntDef",  # Many C++ enums are not used at all in java.
41    "Typos",  # Strings are committed in English first and later translated.
42    "VisibleForTests",  # Does not recognize "ForTesting" methods.
43    "UniqueConstants",  # Chromium enums allow aliases.
44    "UnusedAttribute",  # Chromium apks have various minSdkVersion values.
45]
46
47_RES_ZIP_DIR = 'RESZIPS'
48_SRCJAR_DIR = 'SRCJARS'
49_AAR_DIR = 'AARS'
50
51
52def _SrcRelative(path):
53  """Returns relative path to top-level src dir."""
54  return os.path.relpath(path, build_utils.DIR_SOURCE_ROOT)
55
56
57def _GenerateProjectFile(android_manifest,
58                         android_sdk_root,
59                         cache_dir,
60                         partials_dir,
61                         sources=None,
62                         classpath=None,
63                         srcjar_sources=None,
64                         resource_sources=None,
65                         custom_lint_jars=None,
66                         custom_annotation_zips=None,
67                         android_sdk_version=None,
68                         baseline_path=None):
69  project = ElementTree.Element('project')
70  root = ElementTree.SubElement(project, 'root')
71  # Run lint from output directory: crbug.com/1115594
72  root.set('dir', os.getcwd())
73  sdk = ElementTree.SubElement(project, 'sdk')
74  # Lint requires that the sdk path be an absolute path.
75  sdk.set('dir', os.path.abspath(android_sdk_root))
76  if baseline_path is not None:
77    baseline = ElementTree.SubElement(project, 'baseline')
78    baseline.set('file', baseline_path)
79  cache = ElementTree.SubElement(project, 'cache')
80  cache.set('dir', cache_dir)
81  main_module = ElementTree.SubElement(project, 'module')
82  main_module.set('name', 'main')
83  main_module.set('android', 'true')
84  main_module.set('library', 'false')
85  # Required to make lint-resources.xml be written to a per-target path.
86  # https://crbug.com/1515070 and b/324598620
87  main_module.set('partial-results-dir', partials_dir)
88  if android_sdk_version:
89    main_module.set('compile_sdk_version', android_sdk_version)
90  manifest = ElementTree.SubElement(main_module, 'manifest')
91  manifest.set('file', android_manifest)
92  if srcjar_sources:
93    for srcjar_file in srcjar_sources:
94      src = ElementTree.SubElement(main_module, 'src')
95      src.set('file', srcjar_file)
96      # Cannot add generated="true" since then lint does not scan them, and
97      # we get "UnusedResources" lint errors when resources are used only by
98      # generated files.
99  if sources:
100    for source in sources:
101      src = ElementTree.SubElement(main_module, 'src')
102      src.set('file', source)
103      # Cannot set test="true" since we sometimes put Test.java files beside
104      # non-test files, which lint does not allow:
105      # "Test sources cannot be in the same source root as production files"
106  if classpath:
107    for file_path in classpath:
108      classpath_element = ElementTree.SubElement(main_module, 'classpath')
109      classpath_element.set('file', file_path)
110  if resource_sources:
111    for resource_file in resource_sources:
112      resource = ElementTree.SubElement(main_module, 'resource')
113      resource.set('file', resource_file)
114  if custom_lint_jars:
115    for lint_jar in custom_lint_jars:
116      lint = ElementTree.SubElement(main_module, 'lint-checks')
117      lint.set('file', lint_jar)
118  if custom_annotation_zips:
119    for annotation_zip in custom_annotation_zips:
120      annotation = ElementTree.SubElement(main_module, 'annotations')
121      annotation.set('file', annotation_zip)
122  return project
123
124
125def _RetrieveBackportedMethods(backported_methods_path):
126  with open(backported_methods_path) as f:
127    methods = f.read().splitlines()
128  # Methods look like:
129  #   java/util/Set#of(Ljava/lang/Object;)Ljava/util/Set;
130  # But error message looks like:
131  #   Call requires API level R (current min is 21): java.util.Set#of [NewApi]
132  methods = (m.replace('/', '\\.') for m in methods)
133  methods = (m[:m.index('(')] for m in methods)
134  return sorted(set(methods))
135
136
137def _GenerateConfigXmlTree(orig_config_path, backported_methods):
138  if orig_config_path:
139    root_node = ElementTree.parse(orig_config_path).getroot()
140  else:
141    root_node = ElementTree.fromstring('<lint/>')
142
143  issue_node = ElementTree.SubElement(root_node, 'issue')
144  issue_node.attrib['id'] = 'NewApi'
145  ignore_node = ElementTree.SubElement(issue_node, 'ignore')
146  ignore_node.attrib['regexp'] = '|'.join(backported_methods)
147  return root_node
148
149
150def _GenerateAndroidManifest(original_manifest_path, extra_manifest_paths,
151                             min_sdk_version, android_sdk_version):
152  # Set minSdkVersion in the manifest to the correct value.
153  doc, manifest, app_node = manifest_utils.ParseManifest(original_manifest_path)
154
155  # TODO(crbug.com/40148088): Should this be done using manifest merging?
156  # Add anything in the application node of the extra manifests to the main
157  # manifest to prevent unused resource errors.
158  for path in extra_manifest_paths:
159    _, _, extra_app_node = manifest_utils.ParseManifest(path)
160    for node in extra_app_node:
161      app_node.append(node)
162
163  uses_sdk = manifest.find('./uses-sdk')
164  if uses_sdk is None:
165    uses_sdk = ElementTree.Element('uses-sdk')
166    manifest.insert(0, uses_sdk)
167  uses_sdk.set('{%s}minSdkVersion' % manifest_utils.ANDROID_NAMESPACE,
168               min_sdk_version)
169  uses_sdk.set('{%s}targetSdkVersion' % manifest_utils.ANDROID_NAMESPACE,
170               android_sdk_version)
171  return doc
172
173
174def _WriteXmlFile(root, path):
175  logging.info('Writing xml file %s', path)
176  build_utils.MakeDirectory(os.path.dirname(path))
177  with action_helpers.atomic_output(path) as f:
178    # Although we can write it just with ElementTree.tostring, using minidom
179    # makes it a lot easier to read as a human (also on code search).
180    f.write(
181        minidom.parseString(ElementTree.tostring(
182            root, encoding='utf-8')).toprettyxml(indent='  ').encode('utf-8'))
183
184
185def _RunLint(custom_lint_jar_path,
186             lint_jar_path,
187             backported_methods_path,
188             config_path,
189             manifest_path,
190             extra_manifest_paths,
191             sources,
192             classpath,
193             cache_dir,
194             android_sdk_version,
195             aars,
196             srcjars,
197             min_sdk_version,
198             resource_sources,
199             resource_zips,
200             android_sdk_root,
201             lint_gen_dir,
202             baseline,
203             create_cache,
204             warnings_as_errors=False):
205  logging.info('Lint starting')
206  if not cache_dir:
207    # Use per-target cache directory when --cache-dir is not used.
208    cache_dir = os.path.join(lint_gen_dir, 'cache')
209    # Lint complains if the directory does not exist.
210    # When --create-cache is used, ninja will create this directory because the
211    # stamp file is created within it.
212    os.makedirs(cache_dir, exist_ok=True)
213
214  if baseline and not os.path.exists(baseline):
215    # Generating new baselines is only done locally, and requires more memory to
216    # avoid OOMs.
217    creating_baseline = True
218    lint_xmx = '4G'
219  else:
220    creating_baseline = False
221    lint_xmx = '3G'
222
223  # Lint requires this directory to exist and be cleared.
224  # See b/324598620
225  partials_dir = os.path.join(lint_gen_dir, 'partials')
226  shutil.rmtree(partials_dir, ignore_errors=True)
227  os.makedirs(partials_dir)
228
229  # All paths in lint are based off of relative paths from root with root as the
230  # prefix. Path variable substitution is based off of prefix matching so custom
231  # path variables need to match exactly in order to show up in baseline files.
232  # e.g. lint_path=path/to/output/dir/../../file/in/src
233  root_path = os.getcwd()  # This is usually the output directory.
234  pathvar_src = os.path.join(
235      root_path, os.path.relpath(build_utils.DIR_SOURCE_ROOT, start=root_path))
236
237  cmd = build_utils.JavaCmd(xmx=lint_xmx) + [
238      '-cp',
239      '{}:{}'.format(lint_jar_path, custom_lint_jar_path),
240      'org.chromium.build.CustomLint',
241      '--sdk-home',
242      android_sdk_root,
243      '--jdk-home',
244      build_utils.JAVA_HOME,
245      '--path-variables',
246      f'SRC={pathvar_src}',
247      '--offline',
248      '--quiet',  # Silences lint's "." progress updates.
249      '--stacktrace',  # Prints full stacktraces for internal lint errors.
250  ]
251
252  # Only disable for real runs since otherwise you get UnknownIssueId warnings
253  # when disabling custom lint checks since they are not passed during cache
254  # creation.
255  if not create_cache:
256    cmd += [
257        '--disable',
258        ','.join(_DISABLED_ALWAYS),
259    ]
260
261  if not manifest_path:
262    manifest_path = os.path.join(build_utils.DIR_SOURCE_ROOT, 'build',
263                                 'android', 'AndroidManifest.xml')
264
265  logging.info('Generating config.xml')
266  backported_methods = _RetrieveBackportedMethods(backported_methods_path)
267  config_xml_node = _GenerateConfigXmlTree(config_path, backported_methods)
268  generated_config_path = os.path.join(lint_gen_dir, 'config.xml')
269  _WriteXmlFile(config_xml_node, generated_config_path)
270  cmd.extend(['--config', generated_config_path])
271
272  logging.info('Generating Android manifest file')
273  android_manifest_tree = _GenerateAndroidManifest(manifest_path,
274                                                   extra_manifest_paths,
275                                                   min_sdk_version,
276                                                   android_sdk_version)
277  # Just use a hardcoded name, since we may have different target names (and
278  # thus different manifest_paths) using the same lint baseline. Eg.
279  # trichrome_chrome_bundle and trichrome_chrome_32_64_bundle.
280  lint_android_manifest_path = os.path.join(lint_gen_dir, 'AndroidManifest.xml')
281  _WriteXmlFile(android_manifest_tree.getroot(), lint_android_manifest_path)
282
283  resource_root_dir = os.path.join(lint_gen_dir, _RES_ZIP_DIR)
284  shutil.rmtree(resource_root_dir, True)
285  # These are zip files with generated resources (e. g. strings from GRD).
286  logging.info('Extracting resource zips')
287  for resource_zip in resource_zips:
288    # Use a consistent root and name rather than a temporary file so that
289    # suppressions can be local to the lint target and the resource target.
290    resource_dir = os.path.join(resource_root_dir, resource_zip)
291    os.makedirs(resource_dir)
292    resource_sources.extend(
293        build_utils.ExtractAll(resource_zip, path=resource_dir))
294
295  logging.info('Extracting aars')
296  aar_root_dir = os.path.join(lint_gen_dir, _AAR_DIR)
297  shutil.rmtree(aar_root_dir, True)
298  custom_lint_jars = []
299  custom_annotation_zips = []
300  if aars:
301    for aar in aars:
302      # Use relative source for aar files since they are not generated.
303      aar_dir = os.path.join(aar_root_dir,
304                             os.path.splitext(_SrcRelative(aar))[0])
305      os.makedirs(aar_dir)
306      aar_files = build_utils.ExtractAll(aar, path=aar_dir)
307      for f in aar_files:
308        if f.endswith('lint.jar'):
309          custom_lint_jars.append(f)
310        elif f.endswith('annotations.zip'):
311          custom_annotation_zips.append(f)
312
313  logging.info('Extracting srcjars')
314  srcjar_root_dir = os.path.join(lint_gen_dir, _SRCJAR_DIR)
315  shutil.rmtree(srcjar_root_dir, True)
316  srcjar_sources = []
317  if srcjars:
318    for srcjar in srcjars:
319      # Use path without extensions since otherwise the file name includes
320      # .srcjar and lint treats it as a srcjar.
321      srcjar_dir = os.path.join(srcjar_root_dir, os.path.splitext(srcjar)[0])
322      os.makedirs(srcjar_dir)
323      # Sadly lint's srcjar support is broken since it only considers the first
324      # srcjar. Until we roll a lint version with that fixed, we need to extract
325      # it ourselves.
326      srcjar_sources.extend(build_utils.ExtractAll(srcjar, path=srcjar_dir))
327
328  logging.info('Generating project file')
329  project_file_root = _GenerateProjectFile(
330      lint_android_manifest_path, android_sdk_root, cache_dir, partials_dir,
331      sources, classpath, srcjar_sources, resource_sources, custom_lint_jars,
332      custom_annotation_zips, android_sdk_version, baseline)
333
334  project_xml_path = os.path.join(lint_gen_dir, 'project.xml')
335  _WriteXmlFile(project_file_root, project_xml_path)
336  cmd += ['--project', project_xml_path]
337
338  def stdout_filter(output):
339    filter_patterns = [
340        # This filter is necessary for JDK11.
341        'No issues found',
342        # Custom checks are not always available in every lint run so an
343        # UnknownIssueId warning is sometimes printed for custom checks in the
344        # _DISABLED_ALWAYS list.
345        r'\[UnknownIssueId\]',
346        # If all the warnings are filtered, we should not fail on the final
347        # summary line.
348        r'\d+ errors?, \d+ warnings?',
349    ]
350    return build_utils.FilterLines(output, '|'.join(filter_patterns))
351
352  def stderr_filter(output):
353    output = build_utils.FilterReflectiveAccessJavaWarnings(output)
354    # Presumably a side-effect of our manual manifest merging, but does not
355    # seem to actually break anything:
356    # "Manifest merger failed with multiple errors, see logs"
357    return build_utils.FilterLines(output, 'Manifest merger failed')
358
359  start = time.time()
360  logging.debug('Lint command %s', ' '.join(cmd))
361  failed = False
362
363  if creating_baseline and not warnings_as_errors:
364    # Allow error code 6 when creating a baseline: ERRNO_CREATED_BASELINE
365    fail_func = lambda returncode, _: returncode not in (0, 6)
366  else:
367    fail_func = lambda returncode, _: returncode != 0
368
369  try:
370    build_utils.CheckOutput(cmd,
371                            print_stdout=True,
372                            stdout_filter=stdout_filter,
373                            stderr_filter=stderr_filter,
374                            fail_on_output=warnings_as_errors,
375                            fail_func=fail_func)
376  except build_utils.CalledProcessError as e:
377    failed = True
378    # Do not output the python stacktrace because it is lengthy and is not
379    # relevant to the actual lint error.
380    sys.stderr.write(e.output)
381  finally:
382    # When not treating warnings as errors, display the extra footer.
383    is_debug = os.environ.get('LINT_DEBUG', '0') != '0'
384
385    end = time.time() - start
386    logging.info('Lint command took %ss', end)
387    if not is_debug:
388      shutil.rmtree(aar_root_dir, ignore_errors=True)
389      shutil.rmtree(resource_root_dir, ignore_errors=True)
390      shutil.rmtree(srcjar_root_dir, ignore_errors=True)
391      if os.path.exists(project_xml_path):
392        os.unlink(project_xml_path)
393      shutil.rmtree(partials_dir, ignore_errors=True)
394
395    if failed:
396      print('- For more help with lint in Chrome:', _LINT_MD_URL)
397      if is_debug:
398        print('- DEBUG MODE: Here is the project.xml: {}'.format(
399            _SrcRelative(project_xml_path)))
400      else:
401        print('- Run with LINT_DEBUG=1 to enable lint configuration debugging')
402      sys.exit(1)
403
404  logging.info('Lint completed')
405
406
407def _ParseArgs(argv):
408  parser = argparse.ArgumentParser()
409  action_helpers.add_depfile_arg(parser)
410  parser.add_argument('--target-name', help='Fully qualified GN target name.')
411  parser.add_argument('--skip-build-server',
412                      action='store_true',
413                      help='Avoid using the build server.')
414  parser.add_argument('--use-build-server',
415                      action='store_true',
416                      help='Always use the build server.')
417  parser.add_argument('--lint-jar-path',
418                      required=True,
419                      help='Path to the lint jar.')
420  parser.add_argument('--custom-lint-jar-path',
421                      required=True,
422                      help='Path to our custom lint jar.')
423  parser.add_argument('--backported-methods',
424                      help='Path to backported methods file created by R8.')
425  parser.add_argument('--cache-dir',
426                      help='Path to the directory in which the android cache '
427                      'directory tree should be stored.')
428  parser.add_argument('--config-path', help='Path to lint suppressions file.')
429  parser.add_argument('--lint-gen-dir',
430                      required=True,
431                      help='Path to store generated xml files.')
432  parser.add_argument('--stamp', help='Path to stamp upon success.')
433  parser.add_argument('--android-sdk-version',
434                      help='Version (API level) of the Android SDK used for '
435                      'building.')
436  parser.add_argument('--min-sdk-version',
437                      required=True,
438                      help='Minimal SDK version to lint against.')
439  parser.add_argument('--android-sdk-root',
440                      required=True,
441                      help='Lint needs an explicit path to the android sdk.')
442  parser.add_argument('--create-cache',
443                      action='store_true',
444                      help='Whether this invocation is just warming the cache.')
445  parser.add_argument('--warnings-as-errors',
446                      action='store_true',
447                      help='Treat all warnings as errors.')
448  parser.add_argument('--sources',
449                      help='A list of files containing java and kotlin source '
450                      'files.')
451  parser.add_argument('--aars', help='GN list of included aars.')
452  parser.add_argument('--srcjars', help='GN list of included srcjars.')
453  parser.add_argument('--manifest-path',
454                      help='Path to original AndroidManifest.xml')
455  parser.add_argument('--extra-manifest-paths',
456                      action='append',
457                      help='GYP-list of manifest paths to merge into the '
458                      'original AndroidManifest.xml')
459  parser.add_argument('--resource-sources',
460                      default=[],
461                      action='append',
462                      help='GYP-list of resource sources files, similar to '
463                      'java sources files, but for resource files.')
464  parser.add_argument('--resource-zips',
465                      default=[],
466                      action='append',
467                      help='GYP-list of resource zips, zip files of generated '
468                      'resource files.')
469  parser.add_argument('--classpath',
470                      help='List of jars to add to the classpath.')
471  parser.add_argument('--baseline',
472                      help='Baseline file to ignore existing errors and fail '
473                      'on new errors.')
474
475  args = parser.parse_args(build_utils.ExpandFileArgs(argv))
476  args.sources = action_helpers.parse_gn_list(args.sources)
477  args.aars = action_helpers.parse_gn_list(args.aars)
478  args.srcjars = action_helpers.parse_gn_list(args.srcjars)
479  args.resource_sources = action_helpers.parse_gn_list(args.resource_sources)
480  args.extra_manifest_paths = action_helpers.parse_gn_list(
481      args.extra_manifest_paths)
482  args.resource_zips = action_helpers.parse_gn_list(args.resource_zips)
483  args.classpath = action_helpers.parse_gn_list(args.classpath)
484
485  if args.baseline:
486    assert os.path.basename(args.baseline) == 'lint-baseline.xml', (
487        'The baseline file needs to be named "lint-baseline.xml" in order for '
488        'the autoroller to find and update it whenever lint is rolled to a new '
489        'version.')
490
491  return args
492
493
494def main():
495  build_utils.InitLogging('LINT_DEBUG')
496  args = _ParseArgs(sys.argv[1:])
497
498  sources = []
499  for sources_file in args.sources:
500    sources.extend(build_utils.ReadSourcesList(sources_file))
501  resource_sources = []
502  for resource_sources_file in args.resource_sources:
503    resource_sources.extend(build_utils.ReadSourcesList(resource_sources_file))
504
505  possible_depfile_deps = (args.srcjars + args.resource_zips + sources +
506                           resource_sources + [
507                               args.baseline,
508                               args.manifest_path,
509                           ])
510  depfile_deps = [p for p in possible_depfile_deps if p]
511
512  if args.depfile:
513    action_helpers.write_depfile(args.depfile, args.stamp, depfile_deps)
514
515  # TODO(wnwen): Consider removing lint cache now that there are only two lint
516  #              invocations.
517  # Avoid parallelizing cache creation since lint runs without the cache defeat
518  # the purpose of creating the cache in the first place. Forward the command
519  # after the depfile has been written as siso requires it.
520  if (not args.create_cache and not args.skip_build_server
521      and server_utils.MaybeRunCommand(name=args.target_name,
522                                       argv=sys.argv,
523                                       stamp_file=args.stamp,
524                                       use_build_server=args.use_build_server)):
525    return
526
527  _RunLint(args.custom_lint_jar_path,
528           args.lint_jar_path,
529           args.backported_methods,
530           args.config_path,
531           args.manifest_path,
532           args.extra_manifest_paths,
533           sources,
534           args.classpath,
535           args.cache_dir,
536           args.android_sdk_version,
537           args.aars,
538           args.srcjars,
539           args.min_sdk_version,
540           resource_sources,
541           args.resource_zips,
542           args.android_sdk_root,
543           args.lint_gen_dir,
544           args.baseline,
545           args.create_cache,
546           warnings_as_errors=args.warnings_as_errors)
547  logging.info('Creating stamp file')
548  server_utils.MaybeTouch(args.stamp)
549
550
551if __name__ == '__main__':
552  sys.exit(main())
553