1# Copyright (C) 2023 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"""
15This script enumerates pre-built resources and updates Android.bp files:
16
17  - $ANDROID_BUILD_TOP/device/google/cuttlefish_vmm/Android.bp
18  - $ANDROID_BUILD_TOP/device/google/cuttlefish_vmm/$ARCH/Android.bp
19  - $ANDROID_BUILD_TOP/device/google/cuttlefish/Android.bp
20
21It is intended to be run manually:
22
23  python3 $ANDROID_BUILD_TOP/device/google/cuttlefish_vmm/gen_android_bp.py
24
25"""
26import datetime
27import os
28import re
29import sys
30import textwrap
31from dataclasses import dataclass
32from enum import auto
33from enum import Enum
34from pathlib import Path
35from typing import Dict
36from typing import List
37from typing import Iterator
38from typing import TypeAlias
39
40
41def tool_name() -> str:
42    """Returns a short name for this generation tool."""
43    return Path(__file__).name
44
45
46class Architecture(Enum):
47    """Host instruction set architectures."""
48
49    AARCH64 = 'aarch64'
50    X86_64 = 'x86_64'
51
52    def dir(self) -> Path:
53        "Returns the relative directory path to the specified architecture."
54        return Path(f"{self.name.lower()}-linux-gnu")
55
56
57# Android.bp variant value type.
58Value: TypeAlias = str | Path | bool | list["Value"]
59
60
61def value_to_bp(value: Value) -> str:
62    """Returns `bp` expression for the specified value"""
63    if isinstance(value, list):
64        if len(value) == 1:
65            return "[%s]" % value_to_bp(value[0])
66        return "[\n    %s,\n]" % ",\n    ".join(value_to_bp(e) for e in value)
67    elif isinstance(value, bool):
68        return str(value).lower()
69    elif isinstance(value, Path):
70        return value_to_bp(str(value))
71    else:
72        return f'"{value}"'
73
74
75@dataclass
76class Module:
77    """Android bp build rule."""
78
79    module_type: str
80    parameters: Dict[str, Value]
81
82    @property
83    def name(self) -> str:
84        assert isinstance(self.parameters.get("name"), str)
85        return self.parameters["name"]
86
87    def __str__(self) -> str:
88        """Returns a `.bp` string for this modules with its parameters."""
89        body = "\n  ".join(
90            f"{k}: {value_to_bp(v)}," for k, v in self.parameters.items()
91        )
92        return f"{self.module_type} {{\n  {body}\n}}\n"
93
94
95def update_generated_section(file_path: Path, tag: str, content: str):
96    """Reads a text file, matches and replaces the content between
97    a start beacon and an end beacon with the specified one, and
98    modifies the file in place.
99
100    The generated content is delimited by `// Start of generated` and
101    `// End of generated`. The specified content is indented the same
102    way as the start beacon is.
103
104    Args:
105      file_path: path to the text file to be modified.
106      tag: marks the beginning aned end of the string generated content.
107      content: text to replace the content between the start and end beacons with.
108    """
109    # Read the contents of the text file.
110    with open(file_path, "rt", encoding="utf-8") as f:
111        file_contents = f.read()
112
113    # Find the start and end beacon positions in the file contents.
114    start = f"// Start of generated {tag}\n"
115    end = f"// End of generated {tag}\n"
116
117    match = re.match(
118        f"^(?P<head>.*)^(?P<indent>[ \t]*){re.escape(start)}.*{re.escape(end)}(?P<tail>.*)$",
119        file_contents,
120        re.DOTALL | re.MULTILINE,
121    )
122    if not match:
123        raise ValueError(
124            f"Generated content beacons {(start, end)} not matched in file {file_path}"
125        )
126
127    with open(file_path, "wt", encoding="utf-8") as f:
128        f.write(f"{match.group('head')}{match.group('indent')}{start}")
129        f.write(f"{match.group('indent')}// Generated by {tool_name()}\n")
130        f.write(textwrap.indent(content, match.group("indent")))
131        f.write(f"{match.group('indent')}{end}{match.group('tail')}")
132
133
134def license() -> str:
135    """Returns a license header at the current date, with generation warning."""
136    current_year = datetime.datetime.now().year
137    return textwrap.dedent(
138        f"""\
139    // Autogenerated via {tool_name()}
140    //
141    // Copyright (C) {current_year} The Android Open Source Project
142    //
143    // Licensed under the Apache License, Version 2.0 (the "License");
144    // you may not use this file except in compliance with the License.
145    // You may obtain a copy of the License at
146    //
147    //      http://www.apache.org/licenses/LICENSE-2.0
148    //
149    // Unless required by applicable law or agreed to in writing, software
150    // distributed under the License is distributed on an "AS IS" BASIS,
151    // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
152    // See the License for the specific language governing permissions and
153    // limitations under the License.
154
155    """
156    )
157
158
159def comment(text: str) -> str:
160    """Prefixes each lines with '/// ' and return the commented content."""
161    return re.sub("^(?!$)", "// ", text, flags=re.MULTILINE)
162
163
164def gen_android_bp4seccomp(path: Path, arch: Architecture):
165    """Regenerates the specified '.bp' file for the given architecture."""
166
167    with open(path, "wt") as out:
168        subdir = Path("etc/seccomp")
169        seccomp_dir = arch.dir() / subdir
170        where_in_etc_on_user_machine = "cuttlefish" / arch.dir() / "seccomp"
171        out.write(license())
172
173        for path in sorted(seccomp_dir.iterdir()):
174            module = Module(
175                "prebuilt_usr_share_host",
176                dict(
177                    name=f"{path.name}_at_{arch.name.lower()}",
178                    src=subdir / path.name,
179                    filename=path.name,
180                    sub_dir=where_in_etc_on_user_machine,
181                ),
182            )
183            out.write(str(module))
184
185
186def crosvm_binaries(arch: Architecture) -> Iterator[Path]:
187    """Lists crosvm binary paths."""
188    dir = arch.dir() / "bin"
189    yield dir / "crosvm"
190    yield dir / "gfxstream_graphics_detector"
191    yield dir / "libminijail.so"
192    yield dir / "libgfxstream_backend.so"
193    yield from dir.glob("*.so.?")
194
195
196def qemu_binaries(arch: Architecture) -> Iterator[Path]:
197    """Lists qemu binary paths."""
198    dir = Path(f"qemu/{arch.value}-linux-gnu/bin")
199    yield from dir.glob("*.so.?")
200    yield from dir.glob("qemu-system*")
201
202
203def gen_main_android_bp_modules() -> Iterator[Module]:
204    """Returns the Modules to write in the main 'Android.bp' file."""
205    for arch in Architecture:
206        for path in crosvm_binaries(arch):
207            name = re.sub("/bin/|/|-|_bin_", "_", str(path))
208            if path.stem != "crosvm":
209                name = f"{name}_for_crosvm"
210
211            yield Module(
212                "cc_prebuilt_binary",
213                dict(
214                    name=name,
215                    srcs=[path],
216                    stem=path.name,
217                    relative_install_path=arch.dir(),
218                    defaults=["cuttlefish_host"],
219                    check_elf_files=False,
220                ),
221            )
222
223    for arch in list(Architecture):
224        for binary_path in qemu_binaries(arch):
225            yield Module(
226                "cc_prebuilt_binary",
227                dict(
228                    name=f"{arch.value}_linux_gnu_{binary_path.name}_binary_for_qemu",
229                    srcs=[binary_path],
230                    stem=binary_path.name,
231                    relative_install_path=arch.dir() / "qemu",
232                    defaults=["cuttlefish_host"],
233                    check_elf_files=False,
234                ),
235            )
236
237    resource_paths = [
238        Path(f"efi-virtio.rom"),
239        Path(f"keymaps/en-us"),
240        Path(f"opensbi-riscv64-generic-fw_dynamic.bin"),
241    ]
242
243    for arch in list(Architecture):
244        for resource_path in resource_paths:
245            base_name = resource_path.name
246            subdir = f"qemu/{arch.value}-linux-gnu" / resource_path.parents[0]
247            yield Module(
248                "prebuilt_usr_share_host",
249                dict(
250                    name=f"{arch.value}_{base_name}_resource_for_qemu",
251                    src=f"qemu/{arch.value}-linux-gnu/usr/share/qemu/{resource_path}",
252                    filename=base_name,
253                    sub_dir=subdir,
254                ),
255            )
256
257
258def gen_main_android_bp_file(android_bp_path: Path, modules: List[Module]):
259    """Writes the main 'Android.bp' file with the specified modules."""
260
261    disclamer = f"""\
262        // NOTE: Using cc_prebuilt_binary because cc_prebuilt_library will add
263        //       unwanted .so file extensions when installing shared libraries
264
265        """
266    crosvm_comment = """\
267        // Note: This is commented out to avoid a conflict with the binary built
268        // from external/crosvm. This should be uncommented out when backporting to
269        // older branches with just use the prebuilt and which do not build from
270        // source.
271        """
272
273    with open(android_bp_path, "wt") as out:
274        out.write(license())
275        out.write(textwrap.dedent(disclamer))
276
277        sort_key = lambda m: (
278            m.name,
279            m.parameters["name"].rsplit("_")[-1],
280            m.parameters["name"],
281        )
282        for module in sorted(modules, key=sort_key):
283            module_text = str(module)
284            if module.parameters["name"] == "x86_64_linux_gnu_crosvm":
285                out.write(textwrap.dedent(crosvm_comment))
286                module_text = comment(module_text)
287            out.write(module_text)
288
289
290def generate_all_cuttlefish_host_package_android_bp(path: Path, modules: list[Module]):
291    """Updates the list of module in 'device/google/cuttlefish/Android.bp'."""
292
293    def update_list(list_name: str, modules:list[Module]):
294        names = sorted(m.parameters["name"] for m in modules)
295        text = f"{list_name} = {value_to_bp(names)}\n"
296        update_generated_section(path, list_name, text)
297
298    for arch in list(Architecture):
299        qemu_binaries = [m for m in modules if m.name.endswith("_binary_for_qemu") and m.name.startswith(arch.value)]
300        qemu_resources = [m for m in modules if m.name.endswith("_resource_for_qemu") and m.name.startswith(arch.value)]
301
302        update_list(f"qemu_{arch.value}_linux_gnu_binary", qemu_binaries)
303        update_list(f"qemu_{arch.value}_linux_gnu_resource", qemu_resources)
304
305
306def main(argv):
307    if len(argv) != 1:
308        print(f"Usage error: {argv[0]} does not accept any argument.")
309        return 1
310
311    # Set the current directory to the script location.
312    os.chdir(Path(__file__).parent)
313
314    modules = list(gen_main_android_bp_modules())
315    gen_main_android_bp_file(Path("Android.bp"), modules)
316
317    generate_all_cuttlefish_host_package_android_bp(
318        Path("../cuttlefish/build/Android.bp"), modules
319    )
320
321    for arch in Architecture:
322        gen_android_bp4seccomp(arch.dir() / "Android.bp", arch)
323
324
325if __name__ == "__main__":
326    sys.exit(main(sys.argv))
327