diff options
author | Mike Frysinger <vapier@google.com> | 2025-08-21 10:40:51 -0400 |
---|---|---|
committer | LUCI <gerrit-scoped@luci-project-accounts.iam.gserviceaccount.com> | 2025-08-21 11:16:35 -0700 |
commit | 80d1a5ad3ec862c64a3bbe9919d4547340950183 (patch) | |
tree | ce00fff8d509cb9292dcd0e42922b8235fde224c | |
parent | c615c964fb0c40f1ff2b70681336d0d5d89ddcd7 (diff) | |
download | git-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-x | release/check-metadata.py | 152 | ||||
-rw-r--r-- | release/util.py | 9 | ||||
-rwxr-xr-x | run_tests | 10 |
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 | |||
18 | import argparse | ||
19 | from pathlib import Path | ||
20 | import re | ||
21 | import sys | ||
22 | |||
23 | import 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 | |||
44 | def 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 | |||
91 | def 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 | |||
103 | def 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 | |||
109 | def 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 | |||
121 | def 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 | |||
139 | def 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 | |||
151 | if __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 | ||
17 | import os | 17 | from pathlib import Path |
18 | import re | 18 | import re |
19 | import shlex | 19 | import shlex |
20 | import subprocess | 20 | import subprocess |
@@ -24,8 +24,9 @@ import sys | |||
24 | assert sys.version_info >= (3, 6), "This module requires Python 3.6+" | 24 | assert sys.version_info >= (3, 6), "This module requires Python 3.6+" |
25 | 25 | ||
26 | 26 | ||
27 | TOPDIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | 27 | THIS_FILE = Path(__file__).resolve() |
28 | HOMEDIR = os.path.expanduser("~") | 28 | TOPDIR = THIS_FILE.parent.parent |
29 | HOMEDIR = 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): | |||
54 | def import_release_key(opts): | 55 | def 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() |
@@ -102,6 +102,15 @@ def run_isort(): | |||
102 | ).returncode | 102 | ).returncode |
103 | 103 | ||
104 | 104 | ||
105 | def 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 | |||
105 | def run_update_manpages() -> int: | 114 | def 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 |