• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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