"""Runs Android's lint tool."""
import argparse
import logging
import os
import shutil
import sys
import time
from xml.dom import minidom
from xml.etree import ElementTree
from util import build_utils
from util import server_utils
import action_helpers
_LINT_MD_URL = 'https://chromium.googlesource.com/chromium/src/+/main/build/android/docs/lint.md'
_DISABLED_ALWAYS = [
"AppCompatResource",
"AppLinkUrlError",
"Assert",
"InflateParams",
"CredentialManagerMisuse",
"CredManMissingDal",
"InlinedApi",
"LintBaseline",
"LintBaselineFixed",
"MissingInflatedId",
"MissingApplicationIcon",
"MissingVersion",
"NetworkSecurityConfig",
"ObsoleteLintCustomCheck",
"OldTargetApi",
"PrivateResource",
"StringFormatCount",
"SwitchIntDef",
"Typos",
"VisibleForTests",
"UniqueConstants",
"UnusedAttribute",
"UnusedTranslation",
]
_RES_ZIP_DIR = 'RESZIPS'
_SRCJAR_DIR = 'SRCJARS'
def _SrcRelative(path):
"""Returns relative path to top-level src dir."""
return os.path.relpath(path, build_utils.DIR_SOURCE_ROOT)
def _GenerateProjectFile(android_manifest,
android_sdk_root,
cache_dir,
partials_dir,
sources=None,
classpath=None,
srcjar_sources=None,
resource_sources=None,
aars=None,
android_sdk_version=None,
baseline_path=None):
project = ElementTree.Element('project')
root = ElementTree.SubElement(project, 'root')
root.set('dir', os.getcwd())
sdk = ElementTree.SubElement(project, 'sdk')
sdk.set('dir', os.path.abspath(android_sdk_root))
if baseline_path is not None:
baseline = ElementTree.SubElement(project, 'baseline')
baseline.set('file', baseline_path)
cache = ElementTree.SubElement(project, 'cache')
cache.set('dir', cache_dir)
main_module = ElementTree.SubElement(project, 'module')
main_module.set('name', 'main')
main_module.set('android', 'true')
main_module.set('library', 'false')
main_module.set('partial-results-dir', partials_dir)
if android_sdk_version:
main_module.set('compile_sdk_version', android_sdk_version)
if android_manifest:
manifest = ElementTree.SubElement(main_module, 'merged-manifest')
manifest.set('file', android_manifest)
if srcjar_sources:
srcjar_sources = sorted(set(srcjar_sources))
for srcjar_file in srcjar_sources:
src = ElementTree.SubElement(main_module, 'src')
src.set('file', srcjar_file)
if sources:
sources = sorted(set(sources))
for source in sources:
src = ElementTree.SubElement(main_module, 'src')
src.set('file', source)
if classpath:
for file_path in classpath:
classpath_element = ElementTree.SubElement(main_module, 'classpath')
classpath_element.set('file', file_path)
if resource_sources:
resource_sources = sorted(set(resource_sources))
for resource_file in resource_sources:
resource = ElementTree.SubElement(main_module, 'resource')
resource.set('file', resource_file)
if aars:
aars = sorted(set(aars))
for aar in aars:
lint = ElementTree.SubElement(main_module, 'aar')
lint.set('file', aar)
return project
def _RetrieveBackportedMethods(backported_methods_path):
with open(backported_methods_path, encoding='utf-8') as f:
methods = f.read().splitlines()
methods = (m.replace('/', '\\.') for m in methods)
methods = (m[:m.index('(')] for m in methods)
return sorted(set(methods))
def _GenerateConfigXmlTree(orig_config_path, backported_methods):
if orig_config_path:
root_node = ElementTree.parse(orig_config_path).getroot()
else:
root_node = ElementTree.fromstring('<lint/>')
issue_node = ElementTree.SubElement(root_node, 'issue')
issue_node.attrib['id'] = 'NewApi'
ignore_node = ElementTree.SubElement(issue_node, 'ignore')
ignore_node.attrib['regexp'] = '|'.join(backported_methods)
return root_node
def _WriteXmlFile(root, path):
logging.info('Writing xml file %s', path)
build_utils.MakeDirectory(os.path.dirname(path))
with action_helpers.atomic_output(path, encoding='utf-8') as f:
f.write(
minidom.parseString(ElementTree.tostring(
root, encoding='utf-8')).toprettyxml(indent=' '))
def _RunLint(lint_jar_path,
backported_methods_path,
config_path,
sources,
classpath,
cache_dir,
android_sdk_version,
aars,
srcjars,
resource_sources,
resource_zips,
android_sdk_root,
lint_gen_dir,
baseline,
create_cache,
manifest_path,
warnings_as_errors=False):
logging.info('Lint starting')
if not cache_dir:
cache_dir = os.path.join(lint_gen_dir, 'cache')
os.makedirs(cache_dir, exist_ok=True)
if baseline and not os.path.exists(baseline):
creating_baseline = True
lint_xmx = '6G'
else:
creating_baseline = False
lint_xmx = '4G'
partials_dir = os.path.join(lint_gen_dir, 'partials')
shutil.rmtree(partials_dir, ignore_errors=True)
os.makedirs(partials_dir)
root_path = os.getcwd()
pathvar_src = os.path.join(
root_path, os.path.relpath(build_utils.DIR_SOURCE_ROOT, start=root_path))
cmd = build_utils.JavaCmd(xmx=lint_xmx) + [
'-cp',
lint_jar_path,
'com.android.tools.lint.Main',
'--sdk-home',
android_sdk_root,
'--jdk-home',
build_utils.JAVA_HOME,
'--path-variables',
f'SRC={pathvar_src}',
'--offline',
'--quiet',
'--stacktrace',
]
if not create_cache:
cmd += [
'--disable',
','.join(_DISABLED_ALWAYS),
]
logging.info('Generating config.xml')
backported_methods = _RetrieveBackportedMethods(backported_methods_path)
config_xml_node = _GenerateConfigXmlTree(config_path, backported_methods)
generated_config_path = os.path.join(lint_gen_dir, 'config.xml')
_WriteXmlFile(config_xml_node, generated_config_path)
cmd.extend(['--config', generated_config_path])
resource_root_dir = os.path.join(lint_gen_dir, _RES_ZIP_DIR)
shutil.rmtree(resource_root_dir, True)
logging.info('Extracting resource zips')
for resource_zip in resource_zips:
resource_dir = os.path.join(resource_root_dir, resource_zip)
os.makedirs(resource_dir)
resource_sources.extend(
build_utils.ExtractAll(resource_zip, path=resource_dir))
logging.info('Extracting srcjars')
srcjar_root_dir = os.path.join(lint_gen_dir, _SRCJAR_DIR)
shutil.rmtree(srcjar_root_dir, True)
srcjar_sources = []
if srcjars:
for srcjar in srcjars:
srcjar_dir = os.path.join(srcjar_root_dir, os.path.splitext(srcjar)[0])
os.makedirs(srcjar_dir)
srcjar_sources.extend(build_utils.ExtractAll(srcjar, path=srcjar_dir))
logging.info('Generating project file')
project_file_root = _GenerateProjectFile(manifest_path, android_sdk_root,
cache_dir, partials_dir, sources,
classpath, srcjar_sources,
resource_sources, aars,
android_sdk_version, baseline)
project_xml_path = os.path.join(lint_gen_dir, 'project.xml')
_WriteXmlFile(project_file_root, project_xml_path)
cmd += ['--project', project_xml_path]
def stdout_filter(output):
filter_patterns = [
'No issues found',
r'\[UnknownIssueId\]',
r'\d+ errors?, \d+ warnings?',
]
return build_utils.FilterLines(output, '|'.join(filter_patterns))
start = time.time()
failed = False
if creating_baseline and not warnings_as_errors:
fail_func = lambda returncode, _: returncode not in (0, 6)
else:
fail_func = lambda returncode, _: returncode != 0
try:
build_utils.CheckOutput(cmd,
print_stdout=True,
stdout_filter=stdout_filter,
fail_on_output=warnings_as_errors,
fail_func=fail_func)
except build_utils.CalledProcessError as e:
failed = True
sys.stderr.write(e.output)
finally:
is_debug = os.environ.get('LINT_DEBUG', '0') != '0'
end = time.time() - start
logging.info('Lint command took %ss', end)
if not is_debug:
shutil.rmtree(resource_root_dir, ignore_errors=True)
shutil.rmtree(srcjar_root_dir, ignore_errors=True)
if os.path.exists(project_xml_path):
os.unlink(project_xml_path)
shutil.rmtree(partials_dir, ignore_errors=True)
if failed:
print('- For more help with lint in Chrome:', _LINT_MD_URL)
if is_debug:
print('- DEBUG MODE: Here is the project.xml: {}'.format(
_SrcRelative(project_xml_path)))
else:
print('- Run with LINT_DEBUG=1 to enable lint configuration debugging')
sys.exit(1)
logging.info('Lint completed')
def _ParseArgs(argv):
parser = argparse.ArgumentParser()
action_helpers.add_depfile_arg(parser)
parser.add_argument('--target-name', help='Fully qualified GN target name.')
parser.add_argument('--use-build-server',
action='store_true',
help='Always use the build server.')
parser.add_argument('--lint-jar-path',
required=True,
help='Path to the lint jar.')
parser.add_argument('--backported-methods',
help='Path to backported methods file created by R8.')
parser.add_argument('--cache-dir',
help='Path to the directory in which the android cache '
'directory tree should be stored.')
parser.add_argument('--config-path', help='Path to lint suppressions file.')
parser.add_argument('--lint-gen-dir',
required=True,
help='Path to store generated xml files.')
parser.add_argument('--stamp', help='Path to stamp upon success.')
parser.add_argument('--android-sdk-version',
help='Version (API level) of the Android SDK used for '
'building.')
parser.add_argument('--android-sdk-root',
required=True,
help='Lint needs an explicit path to the android sdk.')
parser.add_argument('--create-cache',
action='store_true',
help='Whether this invocation is just warming the cache.')
parser.add_argument('--warnings-as-errors',
action='store_true',
help='Treat all warnings as errors.')
parser.add_argument('--sources',
help='A list of files containing java and kotlin source '
'files.')
parser.add_argument('--aars', help='GN list of included aars.')
parser.add_argument('--srcjars', help='GN list of included srcjars.')
parser.add_argument('--manifest',
help='Path to the merged AndroidManifest.xml.')
parser.add_argument('--resource-sources',
default=[],
action='append',
help='GYP-list of resource sources files, similar to '
'java sources files, but for resource files.')
parser.add_argument('--resource-zips',
default=[],
action='append',
help='GYP-list of resource zips, zip files of generated '
'resource files.')
parser.add_argument('--classpath',
help='List of jars to add to the classpath.')
parser.add_argument('--baseline',
help='Baseline file to ignore existing errors and fail '
'on new errors.')
args = parser.parse_args(build_utils.ExpandFileArgs(argv))
args.sources = action_helpers.parse_gn_list(args.sources)
args.aars = action_helpers.parse_gn_list(args.aars)
args.srcjars = action_helpers.parse_gn_list(args.srcjars)
args.resource_sources = action_helpers.parse_gn_list(args.resource_sources)
args.resource_zips = action_helpers.parse_gn_list(args.resource_zips)
args.classpath = action_helpers.parse_gn_list(args.classpath)
if args.baseline:
assert os.path.basename(args.baseline) == 'lint-baseline.xml', (
'The baseline file needs to be named "lint-baseline.xml" in order for '
'the autoroller to find and update it whenever lint is rolled to a new '
'version.')
return args
def main():
build_utils.InitLogging('LINT_DEBUG')
args = _ParseArgs(sys.argv[1:])
sources = []
for sources_file in args.sources:
sources.extend(build_utils.ReadSourcesList(sources_file))
resource_sources = []
for resource_sources_file in args.resource_sources:
resource_sources.extend(build_utils.ReadSourcesList(resource_sources_file))
possible_depfile_deps = (args.srcjars + args.resource_zips + sources +
resource_sources + [args.baseline, args.manifest])
depfile_deps = [p for p in possible_depfile_deps if p]
if args.depfile:
action_helpers.write_depfile(args.depfile, args.stamp, depfile_deps)
if (not args.create_cache
and server_utils.MaybeRunCommand(name=args.target_name,
argv=sys.argv,
stamp_file=args.stamp,
use_build_server=args.use_build_server)):
return
_RunLint(args.lint_jar_path,
args.backported_methods,
args.config_path,
sources,
args.classpath,
args.cache_dir,
args.android_sdk_version,
args.aars,
args.srcjars,
resource_sources,
args.resource_zips,
args.android_sdk_root,
args.lint_gen_dir,
args.baseline,
args.create_cache,
args.manifest,
warnings_as_errors=args.warnings_as_errors)
logging.info('Creating stamp file')
server_utils.MaybeTouch(args.stamp)
if __name__ == '__main__':
sys.exit(main())