summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMike Frysinger <vapier@google.com>2025-08-21 10:40:51 -0400
committerLUCI <gerrit-scoped@luci-project-accounts.iam.gserviceaccount.com>2025-08-21 11:16:35 -0700
commit80d1a5ad3ec862c64a3bbe9919d4547340950183 (patch)
treece00fff8d509cb9292dcd0e42922b8235fde224c
parentc615c964fb0c40f1ff2b70681336d0d5d89ddcd7 (diff)
downloadgit-repo-main.tar.gz
run_tests: add file header checker for licensing blocksmain
Change-Id: Ic0bfa3b03e2ba46d565a5bc2c1b7a7463b7dca2c Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/500103 Commit-Queue: Mike Frysinger <vapier@google.com> Tested-by: Mike Frysinger <vapier@google.com> Reviewed-by: Scott Lee <ddoman@google.com>
-rwxr-xr-xrelease/check-metadata.py152
-rw-r--r--release/util.py9
-rwxr-xr-xrun_tests10
3 files changed, 167 insertions, 4 deletions
diff --git a/release/check-metadata.py b/release/check-metadata.py
new file mode 100755
index 00000000..e17932da
--- /dev/null
+++ b/release/check-metadata.py
@@ -0,0 +1,152 @@
1#!/usr/bin/env python3
2# Copyright (C) 2025 The Android Open Source Project
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
16"""Helper tool to check various metadata (e.g. licensing) in source files."""
17
18import argparse
19from pathlib import Path
20import re
21import sys
22
23import util
24
25
26_FILE_HEADER_RE = re.compile(
27 r"""# Copyright \(C\) 20[0-9]{2} The Android Open Source Project
28#
29# Licensed under the Apache License, Version 2\.0 \(the "License"\);
30# you may not use this file except in compliance with the License\.
31# You may obtain a copy of the License at
32#
33# http://www\.apache\.org/licenses/LICENSE-2\.0
34#
35# Unless required by applicable law or agreed to in writing, software
36# distributed under the License is distributed on an "AS IS" BASIS,
37# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied\.
38# See the License for the specific language governing permissions and
39# limitations under the License\.
40"""
41)
42
43
44def check_license(path: Path, lines: list[str]) -> bool:
45 """Check license header."""
46 # Enforce licensing on configs & scripts.
47 if not (
48 path.suffix in (".bash", ".cfg", ".ini", ".py", ".toml")
49 or lines[0] in ("#!/bin/bash", "#!/bin/sh", "#!/usr/bin/env python3")
50 ):
51 return True
52
53 # Extract the file header.
54 header_lines = []
55 for line in lines:
56 if line.startswith("#"):
57 header_lines.append(line)
58 else:
59 break
60 if not header_lines:
61 print(
62 f"error: {path.relative_to(util.TOPDIR)}: "
63 "missing file header (copyright+licensing)",
64 file=sys.stderr,
65 )
66 return False
67
68 # Skip the shebang.
69 if header_lines[0].startswith("#!"):
70 header_lines.pop(0)
71
72 # If this file is imported into the tree, then leave it be.
73 if header_lines[0] == "# DO NOT EDIT THIS FILE":
74 return True
75
76 header = "".join(f"{x}\n" for x in header_lines)
77 if not _FILE_HEADER_RE.match(header):
78 print(
79 f"error: {path.relative_to(util.TOPDIR)}: "
80 "file header incorrectly formatted",
81 file=sys.stderr,
82 )
83 print(
84 "".join(f"> {x}\n" for x in header_lines), end="", file=sys.stderr
85 )
86 return False
87
88 return True
89
90
91def check_path(opts: argparse.Namespace, path: Path) -> bool:
92 """Check a single path."""
93 data = path.read_text(encoding="utf-8")
94 lines = data.splitlines()
95 # NB: Use list comprehension and not a generator so we run all the checks.
96 return all(
97 [
98 check_license(path, lines),
99 ]
100 )
101
102
103def check_paths(opts: argparse.Namespace, paths: list[Path]) -> bool:
104 """Check all the paths."""
105 # NB: Use list comprehension and not a generator so we check all paths.
106 return all([check_path(opts, x) for x in paths])
107
108
109def find_files(opts: argparse.Namespace) -> list[Path]:
110 """Find all the files in the source tree."""
111 result = util.run(
112 opts,
113 ["git", "ls-tree", "-r", "-z", "--name-only", "HEAD"],
114 cwd=util.TOPDIR,
115 capture_output=True,
116 encoding="utf-8",
117 )
118 return [util.TOPDIR / x for x in result.stdout.split("\0")[:-1]]
119
120
121def get_parser() -> argparse.ArgumentParser:
122 """Get a CLI parser."""
123 parser = argparse.ArgumentParser(description=__doc__)
124 parser.add_argument(
125 "-n",
126 "--dry-run",
127 dest="dryrun",
128 action="store_true",
129 help="show everything that would be done",
130 )
131 parser.add_argument(
132 "paths",
133 nargs="*",
134 help="the paths to scan",
135 )
136 return parser
137
138
139def main(argv: list[str]) -> int:
140 """The main func!"""
141 parser = get_parser()
142 opts = parser.parse_args(argv)
143
144 paths = opts.paths
145 if not opts.paths:
146 paths = find_files(opts)
147
148 return 0 if check_paths(opts, paths) else 1
149
150
151if __name__ == "__main__":
152 sys.exit(main(sys.argv[1:]))
diff --git a/release/util.py b/release/util.py
index c839b872..8596324f 100644
--- a/release/util.py
+++ b/release/util.py
@@ -14,7 +14,7 @@
14 14
15"""Random utility code for release tools.""" 15"""Random utility code for release tools."""
16 16
17import os 17from pathlib import Path
18import re 18import re
19import shlex 19import shlex
20import subprocess 20import subprocess
@@ -24,8 +24,9 @@ import sys
24assert sys.version_info >= (3, 6), "This module requires Python 3.6+" 24assert sys.version_info >= (3, 6), "This module requires Python 3.6+"
25 25
26 26
27TOPDIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 27THIS_FILE = Path(__file__).resolve()
28HOMEDIR = os.path.expanduser("~") 28TOPDIR = THIS_FILE.parent.parent
29HOMEDIR = Path("~").expanduser()
29 30
30 31
31# These are the release keys we sign with. 32# These are the release keys we sign with.
@@ -54,7 +55,7 @@ def run(opts, cmd, check=True, **kwargs):
54def import_release_key(opts): 55def import_release_key(opts):
55 """Import the public key of the official release repo signing key.""" 56 """Import the public key of the official release repo signing key."""
56 # Extract the key from our repo launcher. 57 # Extract the key from our repo launcher.
57 launcher = getattr(opts, "launcher", os.path.join(TOPDIR, "repo")) 58 launcher = getattr(opts, "launcher", TOPDIR / "repo")
58 print(f'Importing keys from "{launcher}" launcher script') 59 print(f'Importing keys from "{launcher}" launcher script')
59 with open(launcher, encoding="utf-8") as fp: 60 with open(launcher, encoding="utf-8") as fp:
60 data = fp.read() 61 data = fp.read()
diff --git a/run_tests b/run_tests
index 4720ac20..04f2deb4 100755
--- a/run_tests
+++ b/run_tests
@@ -102,6 +102,15 @@ def run_isort():
102 ).returncode 102 ).returncode
103 103
104 104
105def run_check_metadata():
106 """Returns the exit code from check-metadata."""
107 return subprocess.run(
108 [sys.executable, "release/check-metadata.py"],
109 check=False,
110 cwd=ROOT_DIR,
111 ).returncode
112
113
105def run_update_manpages() -> int: 114def run_update_manpages() -> int:
106 """Returns the exit code from release/update-manpages.""" 115 """Returns the exit code from release/update-manpages."""
107 # Allow this to fail on CI, but not local devs. 116 # Allow this to fail on CI, but not local devs.
@@ -124,6 +133,7 @@ def main(argv):
124 run_black, 133 run_black,
125 run_flake8, 134 run_flake8,
126 run_isort, 135 run_isort,
136 run_check_metadata,
127 run_update_manpages, 137 run_update_manpages,
128 ) 138 )
129 # Run all the tests all the time to get full feedback. Don't exit on the 139 # Run all the tests all the time to get full feedback. Don't exit on the