| #!/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.""" | |
| try: | |
| data = path.read_text(encoding="utf-8") | |
| except FileNotFoundError: | |
| return True | |
| 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]) # noqa: C419 | |
| 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:])) |