blob: 7dcdb681e18d924f690668164e269e4ac2159f1c [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 "
43 "command\n {apigen}\n error: {rv}",
44 returncode.stderr.decode("ascii"),
45 file=sys.stderr,
46 )
Ole Troan5c318c72020-05-05 12:23:47 +020047 sys.exit(-2)
48
Ole Troanab9f5732020-12-15 10:19:25 +010049 return json.loads(returncode.stdout)
Ole Troan5c318c72020-05-05 12:23:47 +020050
51
Ole Troanab9f5732020-12-15 10:19:25 +010052def dict_compare(dict1, dict2):
Klement Sekerad9b0c6f2022-04-26 19:02:15 +020053 """Compare two dictionaries returning added, removed, modified
54 and equal entries"""
Ole Troanab9f5732020-12-15 10:19:25 +010055 d1_keys = set(dict1.keys())
56 d2_keys = set(dict2.keys())
Ole Troan5c318c72020-05-05 12:23:47 +020057 intersect_keys = d1_keys.intersection(d2_keys)
58 added = d1_keys - d2_keys
59 removed = d2_keys - d1_keys
Klement Sekerad9b0c6f2022-04-26 19:02:15 +020060 modified = {
61 o: (dict1[o], dict2[o])
62 for o in intersect_keys
63 if dict1[o]["crc"] != dict2[o]["crc"]
64 }
Ole Troanab9f5732020-12-15 10:19:25 +010065 same = set(o for o in intersect_keys if dict1[o] == dict2[o])
Ole Troan5c318c72020-05-05 12:23:47 +020066 return added, removed, modified, same
67
68
69def filelist_from_git_ls():
Klement Sekerad9b0c6f2022-04-26 19:02:15 +020070 """Returns a list of all api files in the git repository"""
Ole Troan5c318c72020-05-05 12:23:47 +020071 filelist = []
Klement Sekerad9b0c6f2022-04-26 19:02:15 +020072 git_ls = "git ls-files *.api"
Ole Troanab9f5732020-12-15 10:19:25 +010073 returncode = run(git_ls.split(), stdout=PIPE, stderr=PIPE)
74 if returncode.returncode != 0:
75 sys.exit(returncode.returncode)
Ole Troan5c318c72020-05-05 12:23:47 +020076
Klement Sekerad9b0c6f2022-04-26 19:02:15 +020077 for line in returncode.stdout.decode("ascii").split("\n"):
Ole Troanab9f5732020-12-15 10:19:25 +010078 if line:
79 filelist.append(line)
Ole Troan5c318c72020-05-05 12:23:47 +020080 return filelist
81
82
83def is_uncommitted_changes():
Klement Sekerad9b0c6f2022-04-26 19:02:15 +020084 """Returns true if there are uncommitted changes in the repo"""
Dave Wallace3a0d7d22024-03-14 21:41:00 -040085 # Don't run this check in the Jenkins CI
86 if os.getenv("FDIOTOOLS_IMAGE") is None:
87 git_status = "git status --porcelain -uno"
88 returncode = run(git_status.split(), stdout=PIPE, stderr=PIPE)
89 if returncode.returncode != 0:
90 sys.exit(returncode.returncode)
Ole Troan5c318c72020-05-05 12:23:47 +020091
Dave Wallace3a0d7d22024-03-14 21:41:00 -040092 if returncode.stdout:
93 return True
Ole Troan5c318c72020-05-05 12:23:47 +020094 return False
95
96
97def filelist_from_git_grep(filename):
Klement Sekerad9b0c6f2022-04-26 19:02:15 +020098 """Returns a list of api files that this <filename> api files imports."""
Ole Troan5c318c72020-05-05 12:23:47 +020099 filelist = []
100 try:
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200101 returncode = check_output(
102 f'git grep -e "import .*{filename}"' " -- *.api", shell=True
103 )
Ole Troanab9f5732020-12-15 10:19:25 +0100104 except CalledProcessError:
Ole Troan5c318c72020-05-05 12:23:47 +0200105 return []
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200106 for line in returncode.decode("ascii").split("\n"):
Ole Troanab9f5732020-12-15 10:19:25 +0100107 if line:
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200108 filename, _ = line.split(":")
Ole Troanab9f5732020-12-15 10:19:25 +0100109 filelist.append(filename)
Ole Troan5c318c72020-05-05 12:23:47 +0200110 return filelist
111
112
Ole Troanab9f5732020-12-15 10:19:25 +0100113def filelist_from_patchset(pattern):
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200114 """Returns list of api files in changeset and the list of api
115 files they import."""
Ole Troan5c318c72020-05-05 12:23:47 +0200116 filelist = []
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200117 git_cmd = (
118 "((git diff HEAD~1.. --name-only;git ls-files -m) | "
119 'sort -u | grep "\\.api$")'
120 )
Ole Troanab9f5732020-12-15 10:19:25 +0100121 try:
122 res = check_output(git_cmd, shell=True)
123 except CalledProcessError:
124 return []
Ole Troan5c318c72020-05-05 12:23:47 +0200125
126 # Check for dependencies (imports)
127 imported_files = []
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200128 for line in res.decode("ascii").split("\n"):
Ole Troanab9f5732020-12-15 10:19:25 +0100129 if not line:
130 continue
131 if not re.search(pattern, line):
132 continue
133 filelist.append(line)
134 imported_files.extend(filelist_from_git_grep(os.path.basename(line)))
Ole Troan5c318c72020-05-05 12:23:47 +0200135
136 filelist.extend(imported_files)
137 return set(filelist)
138
Ole Troanab9f5732020-12-15 10:19:25 +0100139
140def is_deprecated(message):
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200141 """Given a message, return True if message is deprecated"""
142 if "options" in message:
143 if "deprecated" in message["options"]:
Andrew Yourtchenko6a3d4cc2020-09-22 15:11:51 +0000144 return True
145 # recognize the deprecated format
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200146 if (
147 "status" in message["options"]
148 and message["options"]["status"] == "deprecated"
149 ):
Andrew Yourtchenko6a3d4cc2020-09-22 15:11:51 +0000150 print("WARNING: please use 'option deprecated;'")
151 return True
Ole Troan5c318c72020-05-05 12:23:47 +0200152 return False
153
Ole Troanab9f5732020-12-15 10:19:25 +0100154
155def is_in_progress(message):
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200156 """Given a message, return True if message is marked as in_progress"""
157 if "options" in message:
158 if "in_progress" in message["options"]:
Ole Troan5c318c72020-05-05 12:23:47 +0200159 return True
Andrew Yourtchenko6a3d4cc2020-09-22 15:11:51 +0000160 # recognize the deprecated format
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200161 if (
162 "status" in message["options"]
163 and message["options"]["status"] == "in_progress"
164 ):
Andrew Yourtchenko6a3d4cc2020-09-22 15:11:51 +0000165 print("WARNING: please use 'option in_progress;'")
166 return True
167 return False
Ole Troan5c318c72020-05-05 12:23:47 +0200168
Ole Troanab9f5732020-12-15 10:19:25 +0100169
Andrew Yourtchenko62bd50d2020-09-11 17:40:52 +0000170def report(new, old):
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200171 """Given a dictionary of new crcs and old crcs, print all the
Ole Troanab9f5732020-12-15 10:19:25 +0100172 added, removed, modified, in-progress, deprecated messages.
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200173 Return the number of backwards incompatible changes made."""
Ole Troanab9f5732020-12-15 10:19:25 +0100174
175 # pylint: disable=too-many-branches
176
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200177 new.pop("_version", None)
178 old.pop("_version", None)
Ole Troanab9f5732020-12-15 10:19:25 +0100179 added, removed, modified, _ = dict_compare(new, old)
Ole Troan5c318c72020-05-05 12:23:47 +0200180 backwards_incompatible = 0
Ole Troanab9f5732020-12-15 10:19:25 +0100181
Andrew Yourtchenko8b0cd692020-09-16 09:48:59 +0000182 # print the full list of in-progress messages
183 # they should eventually either disappear of become supported
184 for k in new.keys():
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200185 newversion = int(new[k]["version"])
Ole Troanab9f5732020-12-15 10:19:25 +0100186 if newversion == 0 or is_in_progress(new[k]):
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200187 print(f"in-progress: {k}")
Ole Troan5c318c72020-05-05 12:23:47 +0200188 for k in added:
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200189 print(f"added: {k}")
Ole Troan5c318c72020-05-05 12:23:47 +0200190 for k in removed:
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200191 oldversion = int(old[k]["version"])
192 if oldversion > 0 and not is_deprecated(old[k]) and not is_in_progress(old[k]):
Ole Troan5c318c72020-05-05 12:23:47 +0200193 backwards_incompatible += 1
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200194 print(f"removed: ** {k}")
Ole Troan5c318c72020-05-05 12:23:47 +0200195 else:
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200196 print(f"removed: {k}")
Ole Troan5c318c72020-05-05 12:23:47 +0200197 for k in modified.keys():
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200198 oldversion = int(old[k]["version"])
199 newversion = int(new[k]["version"])
Ole Troanab9f5732020-12-15 10:19:25 +0100200 if oldversion > 0 and not is_in_progress(old[k]):
Ole Troan5c318c72020-05-05 12:23:47 +0200201 backwards_incompatible += 1
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200202 print(f"modified: ** {k}")
Ole Troan5c318c72020-05-05 12:23:47 +0200203 else:
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200204 print(f"modified: {k}")
Andrew Yourtchenko62bd50d2020-09-11 17:40:52 +0000205
206 # check which messages are still there but were marked for deprecation
207 for k in new.keys():
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200208 newversion = int(new[k]["version"])
Ole Troanab9f5732020-12-15 10:19:25 +0100209 if newversion > 0 and is_deprecated(new[k]):
Andrew Yourtchenko62bd50d2020-09-11 17:40:52 +0000210 if k in old:
Ole Troanab9f5732020-12-15 10:19:25 +0100211 if not is_deprecated(old[k]):
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200212 print(f"deprecated: {k}")
Andrew Yourtchenko62bd50d2020-09-11 17:40:52 +0000213 else:
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200214 print(f"added+deprecated: {k}")
Andrew Yourtchenko62bd50d2020-09-11 17:40:52 +0000215
Ole Troan5c318c72020-05-05 12:23:47 +0200216 return backwards_incompatible
217
218
Ole Troanab9f5732020-12-15 10:19:25 +0100219def check_patchset():
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200220 """Compare the changes to API messages in this changeset.
Ole Troanab9f5732020-12-15 10:19:25 +0100221 Ignores API files with version < 1.0.0.
222 Only considers API files located under the src directory in the repo.
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200223 """
224 files = filelist_from_patchset("^src/")
225 revision = "HEAD~1"
Ole Troanab9f5732020-12-15 10:19:25 +0100226
227 oldcrcs = {}
228 newcrcs = {}
229 for filename in files:
230 # Ignore files that have version < 1.0.0
231 _ = crc_from_apigen(None, filename)
Dave Barach592dbd02021-03-11 15:12:29 -0500232 # Ignore removed files
233 if isinstance(_, set) == 0:
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200234 if isinstance(_, set) == 0 and _["_version"]["major"] == "0":
Dave Barach592dbd02021-03-11 15:12:29 -0500235 continue
236 newcrcs.update(_)
Ole Troanab9f5732020-12-15 10:19:25 +0100237
Ole Troanab9f5732020-12-15 10:19:25 +0100238 oldcrcs.update(crc_from_apigen(revision, filename))
239
240 backwards_incompatible = report(newcrcs, oldcrcs)
241 if backwards_incompatible:
242 # alert on changing production API
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200243 print(
244 "crcchecker: Changing production APIs in an incompatible way",
245 file=sys.stderr,
246 )
Ole Troanab9f5732020-12-15 10:19:25 +0100247 sys.exit(-1)
248 else:
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200249 print("*" * 67)
250 print("* VPP CHECKAPI SUCCESSFULLY COMPLETED")
251 print("*" * 67)
Ole Troanab9f5732020-12-15 10:19:25 +0100252
253
Ole Troan5c318c72020-05-05 12:23:47 +0200254def main():
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200255 """Main entry point."""
256 parser = argparse.ArgumentParser(description="VPP CRC checker.")
257 parser.add_argument("--git-revision", help="Git revision to compare against")
258 parser.add_argument(
259 "--dump-manifest", action="store_true", help="Dump CRC for all messages"
260 )
261 parser.add_argument(
262 "--check-patchset",
263 action="store_true",
264 help="Check patchset for backwards incompatbile changes",
265 )
266 parser.add_argument("files", nargs="*")
267 parser.add_argument("--diff", help="Files to compare (on filesystem)", nargs=2)
Ole Troan5c318c72020-05-05 12:23:47 +0200268
269 args = parser.parse_args()
270
271 if args.diff and args.files:
272 parser.print_help()
273 sys.exit(-1)
274
275 # Diff two files
276 if args.diff:
277 oldcrcs = crc_from_apigen(None, args.diff[0])
278 newcrcs = crc_from_apigen(None, args.diff[1])
Andrew Yourtchenko62bd50d2020-09-11 17:40:52 +0000279 backwards_incompatible = report(newcrcs, oldcrcs)
Ole Troan5c318c72020-05-05 12:23:47 +0200280 sys.exit(0)
281
282 # Dump CRC for messages in given files / revision
283 if args.dump_manifest:
284 files = args.files if args.files else filelist_from_git_ls()
285 crcs = {}
Ole Troanab9f5732020-12-15 10:19:25 +0100286 for filename in files:
287 crcs.update(crc_from_apigen(args.git_revision, filename))
288 for k, value in crcs.items():
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200289 print(f"{k}: {value}")
Ole Troan5c318c72020-05-05 12:23:47 +0200290 sys.exit(0)
291
292 # Find changes between current patchset and given revision (previous)
293 if args.check_patchset:
294 if args.git_revision:
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200295 print("Argument git-revision ignored", file=sys.stderr)
Ole Troan5c318c72020-05-05 12:23:47 +0200296 # Check there are no uncomitted changes
297 if is_uncommitted_changes():
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200298 print("Please stash or commit changes in workspace", file=sys.stderr)
Ole Troan5c318c72020-05-05 12:23:47 +0200299 sys.exit(-1)
Ole Troanab9f5732020-12-15 10:19:25 +0100300 check_patchset()
301 sys.exit(0)
302
303 # Find changes between current workspace and revision
304 # Find changes between a given file and a revision
305 files = args.files if args.files else filelist_from_git_ls()
Ole Troan5c318c72020-05-05 12:23:47 +0200306
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200307 revision = args.git_revision if args.git_revision else "HEAD~1"
Ole Troan5c318c72020-05-05 12:23:47 +0200308
309 oldcrcs = {}
310 newcrcs = {}
Ole Troanab9f5732020-12-15 10:19:25 +0100311 for file in files:
312 newcrcs.update(crc_from_apigen(None, file))
313 oldcrcs.update(crc_from_apigen(revision, file))
Ole Troan5c318c72020-05-05 12:23:47 +0200314
Andrew Yourtchenko62bd50d2020-09-11 17:40:52 +0000315 backwards_incompatible = report(newcrcs, oldcrcs)
Ole Troan5c318c72020-05-05 12:23:47 +0200316
317 if args.check_patchset:
318 if backwards_incompatible:
319 # alert on changing production API
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200320 print(
321 "crcchecker: Changing production APIs in an incompatible way",
322 file=sys.stderr,
323 )
Ole Troan5c318c72020-05-05 12:23:47 +0200324 sys.exit(-1)
325 else:
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200326 print("*" * 67)
327 print("* VPP CHECKAPI SUCCESSFULLY COMPLETED")
328 print("*" * 67)
Ole Troan5c318c72020-05-05 12:23:47 +0200329
Ole Troanab9f5732020-12-15 10:19:25 +0100330
Klement Sekerad9b0c6f2022-04-26 19:02:15 +0200331if __name__ == "__main__":
Ole Troan5c318c72020-05-05 12:23:47 +0200332 main()