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 os
18import shutil
19import subprocess
20import sys
21import zipfile
22
23ANDROID_BUILD_TOP = os.environ.get("ANDROID_BUILD_TOP")
24ANDROID_PRODUCT_OUT = os.environ.get("ANDROID_PRODUCT_OUT")
25PRODUCT_OUT = ANDROID_PRODUCT_OUT.removeprefix(f"{ANDROID_BUILD_TOP}/")
26
27SOONG_UI = "build/soong/soong_ui.bash"
28PATH_PREFIX = "out/soong/.intermediates"
29PATH_SUFFIX = "android_common/lint"
30FIX_ZIP = "suggested-fixes.zip"
31
32class SoongLintFix:
33    """
34    This class creates a command line tool that will
35    apply lint fixes to the platform via the necessary
36    combination of soong and shell commands.
37
38    It breaks up these operations into a few "private" methods
39    that are intentionally exposed so experimental code can tweak behavior.
40
41    The entry point, `run`, will apply lint fixes using the
42    intermediate `suggested-fixes` directory that soong creates during its
43    invocation of lint.
44
45    Basic usage:
46    ```
47    from soong_lint_fix import SoongLintFix
48
49    SoongLintFix().run()
50    ```
51    """
52    def __init__(self):
53        self._parser = _setup_parser()
54        self._args = None
55        self._kwargs = None
56        self._path = None
57        self._target = None
58
59
60    def run(self, additional_setup=None, custom_fix=None):
61        """
62        Run the script
63        """
64        self._setup()
65        self._find_module()
66        self._lint()
67
68        if not self._args.no_fix:
69            self._fix()
70
71        if self._args.print:
72            self._print()
73
74    def _setup(self):
75        self._args = self._parser.parse_args()
76        env = os.environ.copy()
77        if self._args.check:
78            env["ANDROID_LINT_CHECK"] = self._args.check
79        if self._args.lint_module:
80            env["ANDROID_LINT_CHECK_EXTRA_MODULES"] = self._args.lint_module
81
82        self._kwargs = {
83            "env": env,
84            "executable": "/bin/bash",
85            "shell": True,
86        }
87
88        os.chdir(ANDROID_BUILD_TOP)
89
90
91    def _find_module(self):
92        print("Refreshing soong modules...")
93        try:
94            os.mkdir(ANDROID_PRODUCT_OUT)
95        except OSError:
96            pass
97        subprocess.call(f"{SOONG_UI} --make-mode {PRODUCT_OUT}/module-info.json", **self._kwargs)
98        print("done.")
99
100        with open(f"{ANDROID_PRODUCT_OUT}/module-info.json") as f:
101            module_info = json.load(f)
102
103        if self._args.module not in module_info:
104            sys.exit(f"Module {self._args.module} not found!")
105
106        module_path = module_info[self._args.module]["path"][0]
107        print(f"Found module {module_path}/{self._args.module}.")
108
109        self._path = f"{PATH_PREFIX}/{module_path}/{self._args.module}/{PATH_SUFFIX}"
110        self._target = f"{self._path}/lint-report.txt"
111
112
113    def _lint(self):
114        print("Cleaning up any old lint results...")
115        try:
116            os.remove(f"{self._target}")
117            os.remove(f"{self._path}/{FIX_ZIP}")
118        except FileNotFoundError:
119            pass
120        print("done.")
121
122        print(f"Generating {self._target}")
123        subprocess.call(f"{SOONG_UI} --make-mode {self._target}", **self._kwargs)
124        print("done.")
125
126
127    def _fix(self):
128        print("Copying suggested fixes to the tree...")
129        with zipfile.ZipFile(f"{self._path}/{FIX_ZIP}") as zip:
130            for name in zip.namelist():
131                if name.startswith("out") or not name.endswith(".java"):
132                    continue
133                with zip.open(name) as src, open(f"{ANDROID_BUILD_TOP}/{name}", "wb") as dst:
134                    shutil.copyfileobj(src, dst)
135            print("done.")
136
137
138    def _print(self):
139        print("### lint-report.txt ###", end="\n\n")
140        with open(self._target, "r") as f:
141            print(f.read())
142
143
144def _setup_parser():
145    parser = argparse.ArgumentParser(description="""
146        This is a python script that applies lint fixes to the platform:
147        1. Set up the environment, etc.
148        2. Run lint on the specified target.
149        3. Copy the modified files, from soong's intermediate directory, back into the tree.
150
151        **Gotcha**: You must have run `source build/envsetup.sh` and `lunch` first.
152        """, formatter_class=argparse.RawTextHelpFormatter)
153
154    parser.add_argument('module',
155                        help='The soong build module to run '
156                             '(e.g. framework-minus-apex or services.core.unboosted)')
157
158    parser.add_argument('--check',
159                        help='Which lint to run. Passed to the ANDROID_LINT_CHECK environment variable.')
160
161    parser.add_argument('--lint-module',
162                            help='Specific lint module to run. Passed to the ANDROID_LINT_CHECK_EXTRA_MODULES environment variable.')
163
164    parser.add_argument('--no-fix', action='store_true',
165                        help='Just build and run the lint, do NOT apply the fixes.')
166
167    parser.add_argument('--print', action='store_true',
168                        help='Print the contents of the generated lint-report.txt at the end.')
169
170    return parser
171
172if __name__ == "__main__":
173    SoongLintFix().run()