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