blob: 4a8f9b16669d4583d8dabc45cd4c07fbe1ae3cb5 [file] [log] [blame]
Ole Troan5c318c72020-05-05 12:23:47 +02001#!/usr/bin/env python3
2
Klement Sekerad9b0c6f2022-04-26 19:02:15 +02003"""
Ole Troanab9f5732020-12-15 10:19:25 +01004crcchecker is a tool to used to enforce that .api messages do not change.
5API files with a semantic version < 1.0.0 are ignored.
Klement Sekerad9b0c6f2022-04-26 19:02:15 +02006"""
Ole Troanab9f5732020-12-15 10:19:25 +01007
Ole Troan5c318c72020-05-05 12:23:47 +02008import sys
9import os
10import json
11import argparse
Ole Troanab9f5732020-12-15 10:19:25 +010012import re
Ole Troan5c318c72020-05-05 12:23:47 +020013from subprocess import run, PIPE, check_output, CalledProcessError
14
Ole Troanab9f5732020-12-15 10:19:25 +010015# pylint: disable=subprocess-run-check
16
Klement Sekerad9b0c6f2022-04-26 19:02:15 +020017ROOTDIR = os.path.dirname(os.path.realpath(__file__)) + "/../.."
18APIGENBIN = f"{ROOTDIR}/src/tools/vppapigen/vppapigen.py"
Ole Troanab9f5732020-12-15 10:19:25 +010019
Ole Troan5c318c72020-05-05 12:23:47 +020020
21def crc_from_apigen(revision, filename):
Klement Sekerad9b0c6f2022-04-26 19:02:15 +020022 """Runs vppapigen with crc plugin returning a JSON object with CRCs for
23 all APIs in filename"""
Ole Troan5c318c72020-05-05 12:23:47 +020024 if not revision and not os.path.isfile(filename):
Klement Sekerad9b0c6f2022-04-26 19:02:15 +020025 print(f"skipping: {filename}", file=sys.stderr)
Dave Barach592dbd02021-03-11 15:12:29 -050026 # Return <class 'set'> instead of <class 'dict'>
27 return {-1}
Ole Troanab9f5732020-12-15 10:19:25 +010028
Ole Troan5c318c72020-05-05 12:23:47 +020029 if revision:
Klement Sekerad9b0c6f2022-04-26 19:02:15 +020030 apigen = (
31 f"{APIGENBIN} --git-revision {revision} --includedir src "
32 f"--input {filename} CRC"
33 )
Ole Troan5c318c72020-05-05 12:23:47 +020034 else:
Klement Sekerad9b0c6f2022-04-26 19:02:15 +020035 apigen = f"{APIGENBIN} --includedir src --input {filename} CRC"
Ole Troanab9f5732020-12-15 10:19:25 +010036 returncode = run(apigen.split(), stdout=PIPE, stderr=PIPE)
37 if returncode.returncode == 2: # No such file
Klement Sekerad9b0c6f2022-04-26 19:02:15 +020038 print(f"skipping: {revision}:{filename} {returncode}", file=sys.stderr)
Ole Troan5c318c72020-05-05 12:23:47 +020039 return {}
Ole Troanab9f5732020-12-15 10:19:25 +010040 if returncode.returncode != 0:
Klement Sekerad9b0c6f2022-04-26 19:02:15 +020041 print(
42 f"vppapigen failed for {revision}:{filename} with "
Ole Troan8987d3a2024-09-09 09:17:37 +020043 f"command:\n {apigen}\n error: {returncode.returncode}",
Klement Sekerad9b0c6f2022-04-26 19:02:15 +020044 file=sys.stderr,
45 )
Ole Troan8987d3a2024-09-09 09:17:37 +020046 if returncode.stderr:
47 print(f"stderr: {returncode.stderr.decode('ascii')}", file=sys.stderr)
48 if returncode.stdout:
49 print(f"stdout: {returncode.stdout.decode('ascii')}", file=sys.stderr)
Ole Troan5c318c72020-05-05 12:23:47 +020050 sys.exit(-2)
51
Ole Troanab9f5732020-12-15 10:19:25 +010052 return json.loads(returncode.stdout)
Ole Troan5c318c72020-05-05 12:23:47 +020053
54
Ole Troanab9f5732020-12-15 10:19:25 +010055def dict_compare(dict1, dict2):
Klement Sekerad9b0c6f2022-04-26 19:02:15 +020056 """Compare two dictionaries returning added, removed, modified
57 and equal entries"""
Ole Troanab9f5732020-12-15 10:19:25 +010058 d1_keys = set(dict1.keys())
59 d2_keys = set(dict2.keys())
Ole Troan5c318c72020-05-05 12:23:47 +020060 intersect_keys = d1_keys.intersection(d2_keys)
61 added = d1_keys - d2_keys
62 removed = d2_keys - d1_keys
Klement Sekerad9b0c6f2022-04-26 19:02:15 +020063 modified = {
64 o: (dict1[o], dict2[o])
65 for o in intersect_keys
66 if dict1[o]["crc"] != dict2[o]["crc"]
67 }
Ole Troanab9f5732020-12-15 10:19:25 +010068 same = set(o for o in intersect_keys if dict1[o] == dict2[o])
Ole Troan5c318c72020-05-05 12:23:47 +020069 return added, removed, modified, same
70
71
72def filelist_from_git_ls():
Klement Sekerad9b0c6f2022-04-26 19:02:15 +020073 """Returns a list of all api files in the git repository"""
Ole Troan5c318c72020-05-05 12:23:47 +020074 filelist = []
Klement Sekerad9b0c6f2022-04-26 19:02:15 +020075 git_ls = "git ls-files *.api"
Ole Troanab9f5732020-12-15 10:19:25 +010076 returncode = run(git_ls.split(), stdout=PIPE, stderr=PIPE)
77 if returncode.returncode != 0:
78 sys.exit(returncode.returncode)
Ole Troan5c318c72020-05-05 12:23:47 +020079
Klement Sekerad9b0c6f2022-04-26 19:02:15 +020080 for line in returncode.stdout.decode("ascii").split("\n"):
Ole Troanab9f5732020-12-15 10:19:25 +010081 if line:
82 filelist.append(line)
Ole Troan5c318c72020-05-05 12:23:47 +020083 return filelist
84
85
86def is_uncommitted_changes():
Klement Sekerad9b0c6f2022-04-26 19:02:15 +020087 """Returns true if there are uncommitted changes in the repo"""
Dave Wallace3a0d7d22024-03-14 21:41:00 -040088 # Don't run this check in the Jenkins CI
89 if os.getenv("FDIOTOOLS_IMAGE") is None:
90 git_status = "git status --porcelain -uno"
91 returncode = run(git_status.split(), stdout=PIPE, stderr=PIPE)
92 if returncode.returncode != 0:
93 sys.exit(returncode.returncode)
Ole Troan5c318c72020-05-05 12:23:47 +020094
Dave Wallace3a0d7d22024-03-14 21:41:00 -040095 if returncode.stdout:
96 return True
Ole Troan5c318c72020-05-05 12:23:47 +020097 return False
98
99
100def filelist_from_git_grep(filename):
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200101 """Returns a list of api files that this <filename> api files imports."""
Ole Troan5c318c72020-05-05 12:23:47 +0200102 filelist = []
103 try:
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200104 returncode = check_output(
105 f'git grep -e "import .*{filename}"' " -- *.api", shell=True
106 )
Ole Troanab9f5732020-12-15 10:19:25 +0100107 except CalledProcessError:
Ole Troan5c318c72020-05-05 12:23:47 +0200108 return []
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200109 for line in returncode.decode("ascii").split("\n"):
Ole Troanab9f5732020-12-15 10:19:25 +0100110 if line:
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200111 filename, _ = line.split(":")
Ole Troanab9f5732020-12-15 10:19:25 +0100112 filelist.append(filename)
Ole Troan5c318c72020-05-05 12:23:47 +0200113 return filelist
114
115
Ole Troanab9f5732020-12-15 10:19:25 +0100116def filelist_from_patchset(pattern):
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200117 """Returns list of api files in changeset and the list of api
118 files they import."""
Ole Troan5c318c72020-05-05 12:23:47 +0200119 filelist = []
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200120 git_cmd = (
121 "((git diff HEAD~1.. --name-only;git ls-files -m) | "
122 'sort -u | grep "\\.api$")'
123 )
Ole Troanab9f5732020-12-15 10:19:25 +0100124 try:
125 res = check_output(git_cmd, shell=True)
126 except CalledProcessError:
127 return []
Ole Troan5c318c72020-05-05 12:23:47 +0200128
129 # Check for dependencies (imports)
130 imported_files = []
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200131 for line in res.decode("ascii").split("\n"):
Ole Troanab9f5732020-12-15 10:19:25 +0100132 if not line:
133 continue
134 if not re.search(pattern, line):
135 continue
136 filelist.append(line)
137 imported_files.extend(filelist_from_git_grep(os.path.basename(line)))
Ole Troan5c318c72020-05-05 12:23:47 +0200138
139 filelist.extend(imported_files)
140 return set(filelist)
141
Ole Troanab9f5732020-12-15 10:19:25 +0100142
143def is_deprecated(message):
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200144 """Given a message, return True if message is deprecated"""
145 if "options" in message:
146 if "deprecated" in message["options"]:
Andrew Yourtchenko6a3d4cc2020-09-22 15:11:51 +0000147 return True
148 # recognize the deprecated format
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200149 if (
150 "status" in message["options"]
151 and message["options"]["status"] == "deprecated"
152 ):
Andrew Yourtchenko6a3d4cc2020-09-22 15:11:51 +0000153 print("WARNING: please use 'option deprecated;'")
154 return True
Ole Troan5c318c72020-05-05 12:23:47 +0200155 return False
156
Ole Troanab9f5732020-12-15 10:19:25 +0100157
158def is_in_progress(message):
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200159 """Given a message, return True if message is marked as in_progress"""
160 if "options" in message:
161 if "in_progress" in message["options"]:
Ole Troan5c318c72020-05-05 12:23:47 +0200162 return True
Andrew Yourtchenko6a3d4cc2020-09-22 15:11:51 +0000163 # recognize the deprecated format
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200164 if (
165 "status" in message["options"]
166 and message["options"]["status"] == "in_progress"
167 ):
Andrew Yourtchenko6a3d4cc2020-09-22 15:11:51 +0000168 print("WARNING: please use 'option in_progress;'")
169 return True
170 return False
Ole Troan5c318c72020-05-05 12:23:47 +0200171
Ole Troanab9f5732020-12-15 10:19:25 +0100172
Andrew Yourtchenko62bd50d2020-09-11 17:40:52 +0000173def report(new, old):
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200174 """Given a dictionary of new crcs and old crcs, print all the
Ole Troanab9f5732020-12-15 10:19:25 +0100175 added, removed, modified, in-progress, deprecated messages.
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200176 Return the number of backwards incompatible changes made."""
Ole Troanab9f5732020-12-15 10:19:25 +0100177
178 # pylint: disable=too-many-branches
179
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200180 new.pop("_version", None)
181 old.pop("_version", None)
Ole Troanab9f5732020-12-15 10:19:25 +0100182 added, removed, modified, _ = dict_compare(new, old)
Ole Troan5c318c72020-05-05 12:23:47 +0200183 backwards_incompatible = 0
Ole Troanab9f5732020-12-15 10:19:25 +0100184
Andrew Yourtchenko8b0cd692020-09-16 09:48:59 +0000185 # print the full list of in-progress messages
186 # they should eventually either disappear of become supported
187 for k in new.keys():
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200188 newversion = int(new[k]["version"])
Ole Troanab9f5732020-12-15 10:19:25 +0100189 if newversion == 0 or is_in_progress(new[k]):
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200190 print(f"in-progress: {k}")
Ole Troan5c318c72020-05-05 12:23:47 +0200191 for k in added:
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200192 print(f"added: {k}")
Ole Troan5c318c72020-05-05 12:23:47 +0200193 for k in removed:
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200194 oldversion = int(old[k]["version"])
195 if oldversion > 0 and not is_deprecated(old[k]) and not is_in_progress(old[k]):
Ole Troan5c318c72020-05-05 12:23:47 +0200196 backwards_incompatible += 1
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200197 print(f"removed: ** {k}")
Ole Troan5c318c72020-05-05 12:23:47 +0200198 else:
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200199 print(f"removed: {k}")
Ole Troan5c318c72020-05-05 12:23:47 +0200200 for k in modified.keys():
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200201 oldversion = int(old[k]["version"])
202 newversion = int(new[k]["version"])
Ole Troanab9f5732020-12-15 10:19:25 +0100203 if oldversion > 0 and not is_in_progress(old[k]):
Ole Troan5c318c72020-05-05 12:23:47 +0200204 backwards_incompatible += 1
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200205 print(f"modified: ** {k}")
Ole Troan5c318c72020-05-05 12:23:47 +0200206 else:
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200207 print(f"modified: {k}")
Andrew Yourtchenko62bd50d2020-09-11 17:40:52 +0000208
209 # check which messages are still there but were marked for deprecation
210 for k in new.keys():
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200211 newversion = int(new[k]["version"])
Ole Troanab9f5732020-12-15 10:19:25 +0100212 if newversion > 0 and is_deprecated(new[k]):
Andrew Yourtchenko62bd50d2020-09-11 17:40:52 +0000213 if k in old:
Ole Troanab9f5732020-12-15 10:19:25 +0100214 if not is_deprecated(old[k]):
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200215 print(f"deprecated: {k}")
Andrew Yourtchenko62bd50d2020-09-11 17:40:52 +0000216 else:
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200217 print(f"added+deprecated: {k}")
Andrew Yourtchenko62bd50d2020-09-11 17:40:52 +0000218
Ole Troan5c318c72020-05-05 12:23:47 +0200219 return backwards_incompatible
220
221
Ole Troanab9f5732020-12-15 10:19:25 +0100222def check_patchset():
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200223 """Compare the changes to API messages in this changeset.
Ole Troanab9f5732020-12-15 10:19:25 +0100224 Ignores API files with version < 1.0.0.
225 Only considers API files located under the src directory in the repo.
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200226 """
227 files = filelist_from_patchset("^src/")
228 revision = "HEAD~1"
Ole Troanab9f5732020-12-15 10:19:25 +0100229
230 oldcrcs = {}
231 newcrcs = {}
232 for filename in files:
233 # Ignore files that have version < 1.0.0
234 _ = crc_from_apigen(None, filename)
Dave Barach592dbd02021-03-11 15:12:29 -0500235 # Ignore removed files
236 if isinstance(_, set) == 0:
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200237 if isinstance(_, set) == 0 and _["_version"]["major"] == "0":
Dave Barach592dbd02021-03-11 15:12:29 -0500238 continue
239 newcrcs.update(_)
Ole Troanab9f5732020-12-15 10:19:25 +0100240
Ole Troanab9f5732020-12-15 10:19:25 +0100241 oldcrcs.update(crc_from_apigen(revision, filename))
242
243 backwards_incompatible = report(newcrcs, oldcrcs)
244 if backwards_incompatible:
245 # alert on changing production API
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200246 print(
247 "crcchecker: Changing production APIs in an incompatible way",
248 file=sys.stderr,
249 )
Ole Troanab9f5732020-12-15 10:19:25 +0100250 sys.exit(-1)
251 else:
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200252 print("*" * 67)
253 print("* VPP CHECKAPI SUCCESSFULLY COMPLETED")
254 print("*" * 67)
Ole Troanab9f5732020-12-15 10:19:25 +0100255
256
Ole Troan5c318c72020-05-05 12:23:47 +0200257def main():
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200258 """Main entry point."""
259 parser = argparse.ArgumentParser(description="VPP CRC checker.")
260 parser.add_argument("--git-revision", help="Git revision to compare against")
261 parser.add_argument(
262 "--dump-manifest", action="store_true", help="Dump CRC for all messages"
263 )
264 parser.add_argument(
265 "--check-patchset",
266 action="store_true",
267 help="Check patchset for backwards incompatbile changes",
268 )
269 parser.add_argument("files", nargs="*")
270 parser.add_argument("--diff", help="Files to compare (on filesystem)", nargs=2)
Ole Troan5c318c72020-05-05 12:23:47 +0200271
272 args = parser.parse_args()
273
274 if args.diff and args.files:
275 parser.print_help()
276 sys.exit(-1)
277
278 # Diff two files
279 if args.diff:
280 oldcrcs = crc_from_apigen(None, args.diff[0])
281 newcrcs = crc_from_apigen(None, args.diff[1])
Andrew Yourtchenko62bd50d2020-09-11 17:40:52 +0000282 backwards_incompatible = report(newcrcs, oldcrcs)
Ole Troan5c318c72020-05-05 12:23:47 +0200283 sys.exit(0)
284
285 # Dump CRC for messages in given files / revision
286 if args.dump_manifest:
287 files = args.files if args.files else filelist_from_git_ls()
288 crcs = {}
Ole Troanab9f5732020-12-15 10:19:25 +0100289 for filename in files:
290 crcs.update(crc_from_apigen(args.git_revision, filename))
291 for k, value in crcs.items():
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200292 print(f"{k}: {value}")
Ole Troan5c318c72020-05-05 12:23:47 +0200293 sys.exit(0)
294
295 # Find changes between current patchset and given revision (previous)
296 if args.check_patchset:
297 if args.git_revision:
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200298 print("Argument git-revision ignored", file=sys.stderr)
Ole Troan5c318c72020-05-05 12:23:47 +0200299 # Check there are no uncomitted changes
300 if is_uncommitted_changes():
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200301 print("Please stash or commit changes in workspace", file=sys.stderr)
Ole Troan5c318c72020-05-05 12:23:47 +0200302 sys.exit(-1)
Ole Troanab9f5732020-12-15 10:19:25 +0100303 check_patchset()
304 sys.exit(0)
305
306 # Find changes between current workspace and revision
307 # Find changes between a given file and a revision
308 files = args.files if args.files else filelist_from_git_ls()
Ole Troan5c318c72020-05-05 12:23:47 +0200309
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200310 revision = args.git_revision if args.git_revision else "HEAD~1"
Ole Troan5c318c72020-05-05 12:23:47 +0200311
312 oldcrcs = {}
313 newcrcs = {}
Ole Troanab9f5732020-12-15 10:19:25 +0100314 for file in files:
315 newcrcs.update(crc_from_apigen(None, file))
316 oldcrcs.update(crc_from_apigen(revision, file))
Ole Troan5c318c72020-05-05 12:23:47 +0200317
Andrew Yourtchenko62bd50d2020-09-11 17:40:52 +0000318 backwards_incompatible = report(newcrcs, oldcrcs)
Ole Troan5c318c72020-05-05 12:23:47 +0200319
320 if args.check_patchset:
321 if backwards_incompatible:
322 # alert on changing production API
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200323 print(
324 "crcchecker: Changing production APIs in an incompatible way",
325 file=sys.stderr,
326 )
Ole Troan5c318c72020-05-05 12:23:47 +0200327 sys.exit(-1)
328 else:
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200329 print("*" * 67)
330 print("* VPP CHECKAPI SUCCESSFULLY COMPLETED")
331 print("*" * 67)
Ole Troan5c318c72020-05-05 12:23:47 +0200332
Ole Troanab9f5732020-12-15 10:19:25 +0100333
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200334if __name__ == "__main__":
Ole Troan5c318c72020-05-05 12:23:47 +0200335 main()