#!/usr/bin/env python3 # Copyright (C) 2025 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. """Helper tool to check various metadata (e.g. licensing) in source files.""" import argparse from pathlib import Path import re import sys import util _FILE_HEADER_RE = re.compile( r"""# Copyright \(C\) 20[0-9]{2} 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\..* """ ) def check_license(path: Path, lines: list[str]) -> bool: """Check license header.""" # Enforce licensing on configs & scripts. if not ( path.suffix in (".bash", ".cfg", ".ini", ".py", ".toml") or lines[0] in ("#!/bin/bash", "#!/bin/sh", "#!/usr/bin/env python3") ): return True # Extract the file header. header_lines = [] for line in lines: if line.startswith("#"): header_lines.append(line) else: break if not header_lines: print( f"error: {path.relative_to(util.TOPDIR)}: " "missing file header (copyright+licensing)", file=sys.stderr, ) return False # Skip the shebang. if header_lines[0].startswith("#!"): header_lines.pop(0) # If this file is imported into the tree, then leave it be. if header_lines[0] == "# DO NOT EDIT THIS FILE": return True header = "".join(f"{x}\n" for x in header_lines) if not _FILE_HEADER_RE.match(header): print( f"error: {path.relative_to(util.TOPDIR)}: " "file header incorrectly formatted", file=sys.stderr, ) print( "".join(f"> {x}\n" for x in header_lines), end="", file=sys.stderr ) return False return True def check_path(opts: argparse.Namespace, path: Path) -> bool: """Check a single path.""" data = path.read_text(encoding="utf-8") lines = data.splitlines() # NB: Use list comprehension and not a generator so we run all the checks. return all( [ check_license(path, lines), ] ) def check_paths(opts: argparse.Namespace, paths: list[Path]) -> bool: """Check all the paths.""" # NB: Use list comprehension and not a generator so we check all paths. return all([check_path(opts, x) for x in paths]) def find_files(opts: argparse.Namespace) -> list[Path]: """Find all the files in the source tree.""" result = util.run( opts, ["git", "ls-tree", "-r", "-z", "--name-only", "HEAD"], cwd=util.TOPDIR, capture_output=True, encoding="utf-8", ) return [util.TOPDIR / x for x in result.stdout.split("\0")[:-1]] def get_parser() -> argparse.ArgumentParser: """Get a CLI parser.""" parser = argparse.ArgumentParser(description=__doc__) parser.add_argument( "-n", "--dry-run", dest="dryrun", action="store_true", help="show everything that would be done", ) parser.add_argument( "paths", nargs="*", help="the paths to scan", ) return parser def main(argv: list[str]) -> int: """The main func!""" parser = get_parser() opts = parser.parse_args(argv) paths = opts.paths if not opts.paths: paths = find_files(opts) return 0 if check_paths(opts, paths) else 1 if __name__ == "__main__": sys.exit(main(sys.argv[1:]))