#!/usr/bin/env python3 # # Copyright (c) 2024 Raspberry Pi (Trading) Ltd. # # SPDX-License-Identifier: BSD-3-Clause # # Check Bazel build file source coverage. Reports files that: # - Are in the repo but not included in a BUILD.bazel file. # - Are referenced in a BUILD.bazel file but are not present. # # Usage: # python tools/check_source_files_in_bazel_build.py # # Run from anywhere in the pico-sdk repo. import logging from pathlib import Path import shlex import subprocess from typing import ( Container, Iterable, List, Optional, Set, ) import sys from bazel_common import ( SDK_ROOT, bazel_command, override_picotool_arg, parse_common_args, setup_logging, ) _LOG = logging.getLogger(__file__) CPP_HEADER_EXTENSIONS = ( ".h", ".hpp", ".hxx", ".h++", ".hh", ".H", ) CPP_SOURCE_EXTENSIONS = ( ".c", ".cpp", ".cxx", ".c++", ".cc", ".C", ".S", ".inc", ".inl", ) IGNORED_FILE_PATTERNS = ( # Doxygen only files. "**/index.h", "**/doc.h", ) def get_paths_from_command(source_dir: Path, *args, **kwargs) -> Set[Path]: """Runs a command and reads Bazel //-style paths from it.""" process = subprocess.run( args, check=False, capture_output=True, cwd=source_dir, **kwargs ) if process.returncode: _LOG.error("Command invocation failed with return code %d!", process.returncode) _LOG.error( "Command: %s", " ".join(shlex.quote(str(arg)) for arg in args), ) _LOG.error( "Output:\n%s", process.stderr.decode(), ) sys.exit(1) files = set() for line in process.stdout.splitlines(): path = line.strip().lstrip(b"/").replace(b":", b"/").decode() files.add(Path(path)) return files def check_bazel_build_for_files( bazel_extensions_to_check: Container[str], files: Iterable[Path], bazel_dirs: Iterable[Path], picotool_dir: Optional[Path], ) -> List[Path]: """Checks that source files are in the Bazel builds. Args: bazel_extensions_to_check: which file suffixes to look for in Bazel files: the files that should be checked bazel_dirs: directories in which to run bazel query Returns: a list of missing files; will be empty if there were no missing files """ # Collect all paths in the Bazel builds files. bazel_build_source_files: Set[Path] = set() pictool_override = override_picotool_arg(picotool_dir) if picotool_dir else "" for directory in bazel_dirs: bazel_build_source_files.update( get_paths_from_command( directory, bazel_command(), "query", pictool_override, 'kind("source file", //...:*)', ) ) missing_from_bazel: List[Path] = [] referenced_in_bazel_missing: List[Path] = [] if not bazel_dirs: _LOG.error("No bazel directories to check.") raise RuntimeError for path in (p for p in files if p.suffix in bazel_extensions_to_check): if path not in bazel_build_source_files: missing_from_bazel.append(path) for path in ( p for p in bazel_build_source_files if p.suffix in bazel_extensions_to_check ): if path not in files: referenced_in_bazel_missing.append(path) if missing_from_bazel: _LOG.warning( "Files not included in the Bazel build:\n\n%s\n", "\n".join(" " + str(x) for x in sorted(missing_from_bazel)), ) if referenced_in_bazel_missing: _LOG.warning( "Files referenced in the Bazel build that are missing:\n\n%s\n", "\n".join(" " + str(x) for x in sorted(referenced_in_bazel_missing)), ) return missing_from_bazel + referenced_in_bazel_missing def git_ls_files_by_extension(file_suffixes: Iterable[str]) -> Iterable[Path]: """List git source files. Returns: A list of files matching the provided extensions. """ git_command = ["git", "ls-files"] for pattern in file_suffixes: git_command.append("*" + pattern) bazel_file_list = subprocess.run( git_command, cwd=SDK_ROOT, text=True, check=True, capture_output=True, ).stdout bazel_files = [Path(f) for f in bazel_file_list.splitlines()] return bazel_files def check_sources_in_bazel_build(picotool_dir) -> int: # List files using git ls-files all_source_files = git_ls_files_by_extension( CPP_HEADER_EXTENSIONS + CPP_SOURCE_EXTENSIONS ) # Filter out any unwanted files. ignored_files = [] for source in all_source_files: for pattern in IGNORED_FILE_PATTERNS: if source.match(pattern): ignored_files.append(source) _LOG.debug( "Ignoring files:\n\n%s\n", "\n".join(" " + str(f) for f in ignored_files) ) source_files = list(set(all_source_files) - set(ignored_files)) # Check for missing files. _LOG.info("Checking all source files are accounted for in Bazel.") missing_files = check_bazel_build_for_files( bazel_extensions_to_check=CPP_HEADER_EXTENSIONS + CPP_SOURCE_EXTENSIONS, files=source_files, bazel_dirs=[Path(SDK_ROOT)], picotool_dir=picotool_dir, ) if missing_files: _LOG.error("Missing files found.") return 1 _LOG.info("\x1b[32mSuccess!\x1b[0m All files accounted for in Bazel.") return 0 if __name__ == "__main__": setup_logging() args = parse_common_args() sys.exit(check_sources_in_bazel_build(args.picotool_dir))