# Copyright (C) 2022 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import argparse
import json
import functools
import os
import shutil
import subprocess
import sys
import zipfile
ANDROID_BUILD_TOP = os.environ.get(
"ANDROID_BUILD_TOP")
ANDROID_PRODUCT_OUT = os.environ.get(
"ANDROID_PRODUCT_OUT")
PRODUCT_OUT = ANDROID_PRODUCT_OUT.removeprefix(f
"{ANDROID_BUILD_TOP}/")
SOONG_UI =
"build/soong/soong_ui.bash"
PATH_PREFIX =
"out/soong/.intermediates"
PATH_SUFFIX =
"android_common/lint"
FIX_ZIP =
"suggested-fixes.zip"
MODULE_JAVA_DEPS =
"out/soong/module_bp_java_deps.json"
class SoongModule:
"""A Soong module to lint.
The constructor takes the name of the module (
for example,
"framework-minus-apex"). find() must be called to extract the intermediate
module path using aninja.
"""
def __init__(self, name):
self._name = name
def find(self):
"""Finds the module path using aninja."""
print(f
"Finding module {self._name} with aninja...")
try:
cmd = [
"aninja",
"-t",
"query", f
"{self._name}-lint"]
result = subprocess.run(
cmd,
capture_output=
True,
text=
True,
check=
True,
cwd=ANDROID_BUILD_TOP)
output = result.stdout
lines = output.splitlines()
try:
input_phony_index = next(
i
for i, line
in enumerate(lines)
if line.strip() ==
"input: phony")
except StopIteration:
raise Exception(
f
"Could not find 'input: phony' in aninja output for {self._name}-lint\nOutput:\n{output}"
)
if input_phony_index +
1 >= len(lines):
raise Exception(
f
"No path found after 'input: phony' for {self._name}-lint")
path_line = lines[input_phony_index +
1].strip()
module_path = os.path.dirname(path_line)
print(f
"Found module {self._name} path: {module_path}.")
self._path = module_path
except FileNotFoundError:
raise Exception(
"`aninja` not found. Make sure you have run `m aninja` and it is in your PATH."
)
except subprocess.CalledProcessError
as e:
raise Exception(
f
"aninja failed for module {self._name}-lint:\n{e.stderr}")
def find_java_deps(self, module_java_deps):
"""Finds the dependencies of a Java module in the loaded module_bp_java_deps.json.
Returns:
A list of module names.
"""
if self._name
not in module_java_deps:
raise Exception(f
"Module {self._name} not found!")
return module_java_deps[self._name][
"dependencies"]
@property
def name(self):
return self._name
@property
def path(self):
return self._path
@property
def lint_report(self):
return f
"{self._path}/lint-report.txt"
@property
def suggested_fixes(self):
return f
"{self._path}/{FIX_ZIP}"
class SoongLintWrapper:
"""
This
class wraps the necessary calls to Soong
and/
or shell commands to lint
platform modules
and apply suggested fixes
if desired.
It breaks up these operations into a few methods that are available to
sub-classes (see SoongLintFix
for an example).
"""
def __init__(self, check=
None, lint_module=
None):
self._check = check
self._lint_module = lint_module
self._kwargs =
None
def _setup(self):
env = os.environ.copy()
if self._check:
env[
"ANDROID_LINT_CHECK"] = self._check
if self._lint_module:
env[
"ANDROID_LINT_CHECK_EXTRA_MODULES"] = self._lint_module
self._kwargs = {
"env": env,
"executable":
"/bin/bash",
"shell":
True,
}
os.chdir(ANDROID_BUILD_TOP)
def _find_module(self, module_name):
"""Returns a SoongModule from a module name.
Ensures that the module
is known to Soong.
"""
module = SoongModule(module_name)
module.find()
return module
def _find_modules(self, module_names):
modules = []
for module_name
in module_names:
modules.append(self._find_module(module_name))
return modules
@functools.cached_property
def _module_java_deps(self):
"""Returns the JSON content of module_bp_java_deps.json."""
print(
"Refreshing Soong Java deps...")
subprocess.call(f
"{SOONG_UI} --make-mode {MODULE_JAVA_DEPS}", **self._kwargs)
print(
"done.")
with open(f
"{MODULE_JAVA_DEPS}")
as f:
return json.load(f)
def _find_module_java_deps(self, module):
"""Returns a list a dependencies for a module.
Args:
module: A SoongModule.
Returns:
A list of SoongModule.
"""
deps = []
dep_names = module.find_java_deps(self._module_java_deps)
for dep_name
in dep_names:
dep = SoongModule(dep_name)
dep.find()
deps.append(dep)
return deps
def _lint(self, modules):
print(
"Cleaning up any old lint results...")
for module
in modules:
try:
os.remove(f
"{module.lint_report}")
os.remove(f
"{module.suggested_fixes}")
except FileNotFoundError:
pass
print(
"done.")
target =
" ".join([ module.lint_report
for module
in modules ])
print(f
"Generating {target}")
subprocess.call(f
"{SOONG_UI} --make-mode {target}", **self._kwargs)
print(
"done.")
def _fix(self, modules):
for module
in modules:
print(f
"Copying suggested fixes for {module.name} to the tree...")
with zipfile.ZipFile(f
"{module.suggested_fixes}")
as zip:
for name
in zip.namelist():
if name.startswith(
"out")
or not name.endswith(
".java"):
continue
with zip.open(name)
as src, open(f
"{ANDROID_BUILD_TOP}/{name}",
"wb")
as dst:
shutil.copyfileobj(src, dst)
print(
"done.")
def _print(self, modules):
for module
in modules:
print(f
"### lint-report.txt {module.name} ###", end="\n\n")
with open(module.lint_report,
"r")
as f:
print(f.read())
class SoongLintFix(SoongLintWrapper):
"""
Basic usage:
```
from soong_lint_fix
import SoongLintFix
opts = SoongLintFixOptions()
opts.parse_args()
SoongLintFix(opts).run()
```
"""
def __init__(self, opts):
super().__init__(check=opts.check, lint_module=opts.lint_module)
self._opts = opts
def run(self):
self._setup()
modules = self._find_modules(self._opts.modules)
self._lint(modules)
if not self._opts.no_fix:
self._fix(modules)
if self._opts.print:
self._print(modules)
class SoongLintFixOptions:
"""Options for SoongLintFix"""
def __init__(self):
self.modules = []
self.check =
None
self.lint_module =
None
self.no_fix =
False
self.print =
False
def parse_args(self, args=
None):
_setup_parser().parse_args(args, self)
def _setup_parser():
parser = argparse.ArgumentParser(description=
"""
This
is a python script that applies lint fixes to the platform:
1. Set up the environment, etc.
2. Run lint on the specified target.
3. Copy the modified files,
from soong
's intermediate directory, back into the tree.
**Gotcha**: You must have run `source build/envsetup.sh`
and `lunch` first.
""", formatter_class=argparse.RawTextHelpFormatter)
parser.add_argument(
'modules',
nargs=
'+',
help=
'The soong build module to run '
'(e.g. framework-minus-apex or services.core.unboosted)')
parser.add_argument(
'--check',
help=
'Which lint to run. Passed to the ANDROID_LINT_CHECK environment variable.')
parser.add_argument(
'--lint-module',
help=
'Specific lint module to run. Passed to the ANDROID_LINT_CHECK_EXTRA_MODULES environment variable.')
parser.add_argument(
'--no-fix', action=
'store_true',
help=
'Just build and run the lint, do NOT apply the fixes.')
parser.add_argument(
'--print', action=
'store_true',
help=
'Print the contents of the generated lint-report.txt at the end.')
return parser
if __name__ ==
"__main__":
opts = SoongLintFixOptions()
opts.parse_args()
SoongLintFix(opts).run()