# 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 from Soong's module-info.json """ def __init__(self, name): self._name = name def find(self, module_info): """Finds the module in the loaded module_info.json.""" if self._name not in module_info: raise Exception(f"Module {self._name} not found!") partial_path = module_info[self._name]["path"][0] print(f"Found module {partial_path}/{self._name}.") self._path = f"{PATH_PREFIX}/{partial_path}/{self._name}/{PATH_SUFFIX}" 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) @functools.cached_property def _module_info(self): """Returns the JSON content of module-info.json.""" print("Refreshing Soong modules...") try: os.mkdir(ANDROID_PRODUCT_OUT) except OSError: pass subprocess.call(f"{SOONG_UI} --make-mode {PRODUCT_OUT}/module-info.json", **self._kwargs) print("done.") with open(f"{ANDROID_PRODUCT_OUT}/module-info.json") as f: return json.load(f) 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(self._module_info) 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(self._module_info) 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()