blob: 15d44a2e572c4ebb4549c7a386327d94b993090d [file]
#!/usr/bin/env python3
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you 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.
"""
Check that Python, YAML, and shell files carry the ASF license header.
Called by pre-commit with the list of staged files. Reads .rat-excludes at
runtime so known third-party files are automatically respected without any
duplication of the exclusion list.
Usage (pre-commit invokes this automatically):
python scripts/check_asf_headers.py file1.py file2.yml ...
"""
import sys
from fnmatch import fnmatch
from pathlib import Path
from typing import Optional
# Extensions whose source files must carry an ASF header.
CHECKED_EXTENSIONS = {".py", ".yml", ".yaml", ".sh"}
# Only search this many lines from the top of each file.
# Headers are always at the start; searching the whole file would be slow
# and would risk false positives from files that quote the license in prose.
HEADER_SEARCH_LINES = 30
# The one string that appears in every valid ASF license header regardless
# of comment style (# for Python/YAML/shell, // for Java, /* for C, etc.).
ASF_HEADER_MARKER = "Licensed to the Apache Software Foundation (ASF)"
def _find_repo_root(start: Path) -> Path:
"""Walk upward from start until we find .rat-excludes or pyproject.toml."""
for candidate in [start.resolve(), *start.resolve().parents]:
if (candidate / ".rat-excludes").exists() or (candidate / "pyproject.toml").exists():
return candidate
return start.resolve()
def _load_rat_exclude_patterns(repo_root: Path) -> list:
"""Return non-comment, non-blank lines from .rat-excludes as glob patterns."""
path = repo_root / ".rat-excludes"
if not path.exists():
return []
return [
line.strip()
for line in path.read_text(encoding="utf-8").splitlines()
if line.strip() and not line.strip().startswith("#")
]
def _is_excluded(file_path: Path, repo_root: Path, patterns: list) -> bool:
"""Return True if file_path matches any pattern from .rat-excludes.
Patterns use RAT's **/<name> syntax. We handle this by checking the
file's basename against patterns that start with **/, and also checking
the full relative path against each pattern directly.
"""
try:
rel = str(file_path.resolve().relative_to(repo_root.resolve()))
except ValueError:
rel = str(file_path)
name = file_path.name
for pattern in patterns:
if pattern.startswith("**/"):
# Strip the **/ prefix and match against the bare filename.
if fnmatch(name, pattern[3:]):
return True
if fnmatch(rel, pattern):
return True
return False
def _has_asf_header(file_path: Path) -> bool:
"""Return True if the ASF header marker appears within the first HEADER_SEARCH_LINES."""
try:
with file_path.open(encoding="utf-8", errors="replace") as fh:
for i, line in enumerate(fh):
if i >= HEADER_SEARCH_LINES:
break
if ASF_HEADER_MARKER in line:
return True
except OSError:
pass
return False
def main(argv: Optional[list] = None) -> int:
files = [Path(p) for p in (argv if argv is not None else sys.argv[1:])]
if not files:
return 0
repo_root = _find_repo_root(files[0].parent)
patterns = _load_rat_exclude_patterns(repo_root)
violations = []
for f in files:
if f.suffix not in CHECKED_EXTENSIONS:
continue
if _is_excluded(f, repo_root, patterns):
continue
if not _has_asf_header(f):
violations.append(f)
if violations:
print("Missing ASF license header in the following file(s):")
for v in violations:
print(f" {v}")
print()
print("Add the standard Apache 2.0 header block to each file.")
print("See any existing .py file in scripts/ for the correct format.")
return 1
return 0
if __name__ == "__main__":
sys.exit(main())