| #!/usr/bin/env python3 |
| # |
| # 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. |
| |
| """Audit a prebuilt directory to scan for various build issues""" |
| |
| import argparse |
| import inspect |
| from pathlib import Path |
| import re |
| import subprocess |
| import sys |
| |
| import context |
| |
| from android_rust.paths import TOOLCHAIN_RESOURCE_PATH |
| from android_rust.toolchains import CLANG_TOOLCHAIN_HOST |
| from android_rust.utils import TERM_GREEN, TERM_RED, ResolvedPath, print_colored, is_elf_file |
| |
| |
| ALLOW_LIST_PATH = TOOLCHAIN_RESOURCE_PATH / "shared_library_allow_list.txt" |
| |
| NEEDED_LIBRARY_PATTERN = re.compile(r"\(NEEDED\)\s+Shared library: \[([\w\-]+\.so(?:\.\d+)?)\]") |
| |
| |
| def parse_args() -> argparse.Namespace: |
| parser = argparse.ArgumentParser(description=inspect.getdoc(sys.modules[__name__])) |
| |
| parser.add_argument("scandir", type=ResolvedPath, help="Directory to audit") |
| parser.add_argument( |
| "--map", "-m", action="store_true", help="Produce a map of libs to requirements") |
| |
| return parser.parse_args() |
| |
| |
| def get_allow_list() -> list[str]: |
| with open(ALLOW_LIST_PATH, "r") as f: |
| return sorted(f.read().splitlines()) |
| |
| |
| def get_required_libs(scandir: Path) -> list[str]: |
| requirements_map = get_required_libs_map(scandir) |
| |
| required_libs = set() |
| for libs in requirements_map.values(): |
| for lib in libs: |
| required_libs.add(lib) |
| |
| return sorted(required_libs) |
| |
| |
| def get_required_libs_map(scandir: Path) -> dict[str, list[str]]: |
| lib_paths = list(scandir.glob("**/*.so")) + list(scandir.glob("**/*.so.*")) |
| |
| local_libs: list[str] = [p.name for p in lib_paths] |
| required_libs: dict[str, list[str]] = {} |
| |
| for lib_path in lib_paths: |
| if is_elf_file(lib_path): |
| local_libs.append(lib_path.name) |
| |
| result = subprocess.run([CLANG_TOOLCHAIN_HOST.readelf(), "--dynamic", lib_path], |
| stdout=subprocess.PIPE, |
| stderr=subprocess.DEVNULL, |
| text=True) |
| |
| if result.returncode != 0: |
| sys.exit(f"Failed to run readelf on {lib_path}") |
| |
| required_libs[str(lib_path)] = [] |
| for line in result.stdout.splitlines(): |
| search_result = NEEDED_LIBRARY_PATTERN.search(line) |
| if search_result != None: |
| assert search_result is not None |
| required_lib = search_result[1] |
| if required_lib not in local_libs: |
| required_libs[str(lib_path)].append(required_lib) |
| |
| return required_libs |
| |
| |
| def main() -> None: |
| args = parse_args() |
| |
| allowed_libs = get_allow_list() |
| |
| print("Required shared libraries:") |
| if args.map: |
| for (local_lib, requirements) in get_required_libs_map(args.scandir).items(): |
| print(f"{local_lib}:") |
| for req in requirements: |
| print_colored(f"\t{req}", TERM_GREEN if req in allowed_libs else TERM_RED) |
| print("") |
| else: |
| for lib in get_required_libs(args.scandir): |
| print_colored(f"\t{lib}", TERM_GREEN if lib in allowed_libs else TERM_RED) |
| |
| |
| if __name__ == "__main__": |
| main() |