1# Copyright (C) 2022 The Android Open Source Project 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15import argparse 16import json 17import functools 18import os 19import shutil 20import subprocess 21import sys 22import zipfile 23 24ANDROID_BUILD_TOP = os.environ.get("ANDROID_BUILD_TOP") 25ANDROID_PRODUCT_OUT = os.environ.get("ANDROID_PRODUCT_OUT") 26PRODUCT_OUT = ANDROID_PRODUCT_OUT.removeprefix(f"{ANDROID_BUILD_TOP}/") 27 28SOONG_UI = "build/soong/soong_ui.bash" 29PATH_PREFIX = "out/soong/.intermediates" 30PATH_SUFFIX = "android_common/lint" 31FIX_ZIP = "suggested-fixes.zip" 32MODULE_JAVA_DEPS = "out/soong/module_bp_java_deps.json" 33 34 35class SoongModule: 36 """A Soong module to lint. 37 38 The constructor takes the name of the module (for example, 39 "framework-minus-apex"). find() must be called to extract the intermediate 40 module path from Soong's module-info.json 41 """ 42 def __init__(self, name): 43 self._name = name 44 45 def find(self, module_info): 46 """Finds the module in the loaded module_info.json.""" 47 if self._name not in module_info: 48 raise Exception(f"Module {self._name} not found!") 49 50 partial_path = module_info[self._name]["path"][0] 51 print(f"Found module {partial_path}/{self._name}.") 52 self._path = f"{PATH_PREFIX}/{partial_path}/{self._name}/{PATH_SUFFIX}" 53 54 def find_java_deps(self, module_java_deps): 55 """Finds the dependencies of a Java module in the loaded module_bp_java_deps.json. 56 57 Returns: 58 A list of module names. 59 """ 60 if self._name not in module_java_deps: 61 raise Exception(f"Module {self._name} not found!") 62 63 return module_java_deps[self._name]["dependencies"] 64 65 @property 66 def name(self): 67 return self._name 68 69 @property 70 def path(self): 71 return self._path 72 73 @property 74 def lint_report(self): 75 return f"{self._path}/lint-report.txt" 76 77 @property 78 def suggested_fixes(self): 79 return f"{self._path}/{FIX_ZIP}" 80 81 82class SoongLintWrapper: 83 """ 84 This class wraps the necessary calls to Soong and/or shell commands to lint 85 platform modules and apply suggested fixes if desired. 86 87 It breaks up these operations into a few methods that are available to 88 sub-classes (see SoongLintFix for an example). 89 """ 90 def __init__(self, check=None, lint_module=None): 91 self._check = check 92 self._lint_module = lint_module 93 self._kwargs = None 94 95 def _setup(self): 96 env = os.environ.copy() 97 if self._check: 98 env["ANDROID_LINT_CHECK"] = self._check 99 if self._lint_module: 100 env["ANDROID_LINT_CHECK_EXTRA_MODULES"] = self._lint_module 101 102 self._kwargs = { 103 "env": env, 104 "executable": "/bin/bash", 105 "shell": True, 106 } 107 108 os.chdir(ANDROID_BUILD_TOP) 109 110 @functools.cached_property 111 def _module_info(self): 112 """Returns the JSON content of module-info.json.""" 113 print("Refreshing Soong modules...") 114 try: 115 os.mkdir(ANDROID_PRODUCT_OUT) 116 except OSError: 117 pass 118 subprocess.call(f"{SOONG_UI} --make-mode {PRODUCT_OUT}/module-info.json", **self._kwargs) 119 print("done.") 120 121 with open(f"{ANDROID_PRODUCT_OUT}/module-info.json") as f: 122 return json.load(f) 123 124 def _find_module(self, module_name): 125 """Returns a SoongModule from a module name. 126 127 Ensures that the module is known to Soong. 128 """ 129 module = SoongModule(module_name) 130 module.find(self._module_info) 131 return module 132 133 def _find_modules(self, module_names): 134 modules = [] 135 for module_name in module_names: 136 modules.append(self._find_module(module_name)) 137 return modules 138 139 @functools.cached_property 140 def _module_java_deps(self): 141 """Returns the JSON content of module_bp_java_deps.json.""" 142 print("Refreshing Soong Java deps...") 143 subprocess.call(f"{SOONG_UI} --make-mode {MODULE_JAVA_DEPS}", **self._kwargs) 144 print("done.") 145 146 with open(f"{MODULE_JAVA_DEPS}") as f: 147 return json.load(f) 148 149 def _find_module_java_deps(self, module): 150 """Returns a list a dependencies for a module. 151 152 Args: 153 module: A SoongModule. 154 155 Returns: 156 A list of SoongModule. 157 """ 158 deps = [] 159 dep_names = module.find_java_deps(self._module_java_deps) 160 for dep_name in dep_names: 161 dep = SoongModule(dep_name) 162 dep.find(self._module_info) 163 deps.append(dep) 164 return deps 165 166 def _lint(self, modules): 167 print("Cleaning up any old lint results...") 168 for module in modules: 169 try: 170 os.remove(f"{module.lint_report}") 171 os.remove(f"{module.suggested_fixes}") 172 except FileNotFoundError: 173 pass 174 print("done.") 175 176 target = " ".join([ module.lint_report for module in modules ]) 177 print(f"Generating {target}") 178 subprocess.call(f"{SOONG_UI} --make-mode {target}", **self._kwargs) 179 print("done.") 180 181 def _fix(self, modules): 182 for module in modules: 183 print(f"Copying suggested fixes for {module.name} to the tree...") 184 with zipfile.ZipFile(f"{module.suggested_fixes}") as zip: 185 for name in zip.namelist(): 186 if name.startswith("out") or not name.endswith(".java"): 187 continue 188 with zip.open(name) as src, open(f"{ANDROID_BUILD_TOP}/{name}", "wb") as dst: 189 shutil.copyfileobj(src, dst) 190 print("done.") 191 192 def _print(self, modules): 193 for module in modules: 194 print(f"### lint-report.txt {module.name} ###", end="\n\n") 195 with open(module.lint_report, "r") as f: 196 print(f.read()) 197 198 199class SoongLintFix(SoongLintWrapper): 200 """ 201 Basic usage: 202 ``` 203 from soong_lint_fix import SoongLintFix 204 205 opts = SoongLintFixOptions() 206 opts.parse_args() 207 SoongLintFix(opts).run() 208 ``` 209 """ 210 def __init__(self, opts): 211 super().__init__(check=opts.check, lint_module=opts.lint_module) 212 self._opts = opts 213 214 def run(self): 215 self._setup() 216 modules = self._find_modules(self._opts.modules) 217 self._lint(modules) 218 219 if not self._opts.no_fix: 220 self._fix(modules) 221 222 if self._opts.print: 223 self._print(modules) 224 225 226class SoongLintFixOptions: 227 """Options for SoongLintFix""" 228 229 def __init__(self): 230 self.modules = [] 231 self.check = None 232 self.lint_module = None 233 self.no_fix = False 234 self.print = False 235 236 def parse_args(self, args=None): 237 _setup_parser().parse_args(args, self) 238 239 240def _setup_parser(): 241 parser = argparse.ArgumentParser(description=""" 242 This is a python script that applies lint fixes to the platform: 243 1. Set up the environment, etc. 244 2. Run lint on the specified target. 245 3. Copy the modified files, from soong's intermediate directory, back into the tree. 246 247 **Gotcha**: You must have run `source build/envsetup.sh` and `lunch` first. 248 """, formatter_class=argparse.RawTextHelpFormatter) 249 250 parser.add_argument('modules', 251 nargs='+', 252 help='The soong build module to run ' 253 '(e.g. framework-minus-apex or services.core.unboosted)') 254 255 parser.add_argument('--check', 256 help='Which lint to run. Passed to the ANDROID_LINT_CHECK environment variable.') 257 258 parser.add_argument('--lint-module', 259 help='Specific lint module to run. Passed to the ANDROID_LINT_CHECK_EXTRA_MODULES environment variable.') 260 261 parser.add_argument('--no-fix', action='store_true', 262 help='Just build and run the lint, do NOT apply the fixes.') 263 264 parser.add_argument('--print', action='store_true', 265 help='Print the contents of the generated lint-report.txt at the end.') 266 267 return parser 268 269if __name__ == "__main__": 270 opts = SoongLintFixOptions() 271 opts.parse_args() 272 SoongLintFix(opts).run() 273