blob: f3021c3c8b6a935c777ef29c37b3b09d80daf271 [file] [log] [blame]
Ole Troan5c318c72020-05-05 12:23:47 +02001#!/usr/bin/env python3
2
Ole Troanab9f5732020-12-15 10:19:25 +01003'''
4crcchecker is a tool to used to enforce that .api messages do not change.
5API files with a semantic version < 1.0.0 are ignored.
6'''
7
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
17ROOTDIR = os.path.dirname(os.path.realpath(__file__)) + '/../..'
18APIGENBIN = f'{ROOTDIR}/src/tools/vppapigen/vppapigen.py'
19
Ole Troan5c318c72020-05-05 12:23:47 +020020
21def crc_from_apigen(revision, filename):
Ole Troanab9f5732020-12-15 10:19:25 +010022 '''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):
25 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:
Ole Troanab9f5732020-12-15 10:19:25 +010030 apigen = (f'{APIGENBIN} --git-revision {revision} --includedir src '
Ole Troan5c318c72020-05-05 12:23:47 +020031 f'--input {filename} CRC')
32 else:
Ole Troanab9f5732020-12-15 10:19:25 +010033 apigen = (f'{APIGENBIN} --includedir src --input {filename} CRC')
34 returncode = run(apigen.split(), stdout=PIPE, stderr=PIPE)
35 if returncode.returncode == 2: # No such file
36 print(f'skipping: {revision}:{filename} {returncode}', file=sys.stderr)
Ole Troan5c318c72020-05-05 12:23:47 +020037 return {}
Ole Troanab9f5732020-12-15 10:19:25 +010038 if returncode.returncode != 0:
39 print(f'vppapigen failed for {revision}:{filename} with '
40 'command\n {apigen}\n error: {rv}',
41 returncode.stderr.decode('ascii'), file=sys.stderr)
Ole Troan5c318c72020-05-05 12:23:47 +020042 sys.exit(-2)
43
Ole Troanab9f5732020-12-15 10:19:25 +010044 return json.loads(returncode.stdout)
Ole Troan5c318c72020-05-05 12:23:47 +020045
46
Ole Troanab9f5732020-12-15 10:19:25 +010047def dict_compare(dict1, dict2):
48 '''Compare two dictionaries returning added, removed, modified
49 and equal entries'''
50 d1_keys = set(dict1.keys())
51 d2_keys = set(dict2.keys())
Ole Troan5c318c72020-05-05 12:23:47 +020052 intersect_keys = d1_keys.intersection(d2_keys)
53 added = d1_keys - d2_keys
54 removed = d2_keys - d1_keys
Ole Troanab9f5732020-12-15 10:19:25 +010055 modified = {o: (dict1[o], dict2[o]) for o in intersect_keys
56 if dict1[o]['crc'] != dict2[o]['crc']}
57 same = set(o for o in intersect_keys if dict1[o] == dict2[o])
Ole Troan5c318c72020-05-05 12:23:47 +020058 return added, removed, modified, same
59
60
61def filelist_from_git_ls():
Ole Troanab9f5732020-12-15 10:19:25 +010062 '''Returns a list of all api files in the git repository'''
Ole Troan5c318c72020-05-05 12:23:47 +020063 filelist = []
64 git_ls = 'git ls-files *.api'
Ole Troanab9f5732020-12-15 10:19:25 +010065 returncode = run(git_ls.split(), stdout=PIPE, stderr=PIPE)
66 if returncode.returncode != 0:
67 sys.exit(returncode.returncode)
Ole Troan5c318c72020-05-05 12:23:47 +020068
Ole Troanab9f5732020-12-15 10:19:25 +010069 for line in returncode.stdout.decode('ascii').split('\n'):
70 if line:
71 filelist.append(line)
Ole Troan5c318c72020-05-05 12:23:47 +020072 return filelist
73
74
75def is_uncommitted_changes():
Ole Troanab9f5732020-12-15 10:19:25 +010076 '''Returns true if there are uncommitted changes in the repo'''
Ole Troan5c318c72020-05-05 12:23:47 +020077 git_status = 'git status --porcelain -uno'
Ole Troanab9f5732020-12-15 10:19:25 +010078 returncode = run(git_status.split(), stdout=PIPE, stderr=PIPE)
79 if returncode.returncode != 0:
80 sys.exit(returncode.returncode)
Ole Troan5c318c72020-05-05 12:23:47 +020081
Ole Troanab9f5732020-12-15 10:19:25 +010082 if returncode.stdout:
Ole Troan5c318c72020-05-05 12:23:47 +020083 return True
84 return False
85
86
87def filelist_from_git_grep(filename):
Ole Troanab9f5732020-12-15 10:19:25 +010088 '''Returns a list of api files that this <filename> api files imports.'''
Ole Troan5c318c72020-05-05 12:23:47 +020089 filelist = []
90 try:
Ole Troanab9f5732020-12-15 10:19:25 +010091 returncode = check_output(f'git grep -e "import .*{filename}"'
92 ' -- *.api',
93 shell=True)
94 except CalledProcessError:
Ole Troan5c318c72020-05-05 12:23:47 +020095 return []
Ole Troanab9f5732020-12-15 10:19:25 +010096 for line in returncode.decode('ascii').split('\n'):
97 if line:
98 filename, _ = line.split(':')
99 filelist.append(filename)
Ole Troan5c318c72020-05-05 12:23:47 +0200100 return filelist
101
102
Ole Troanab9f5732020-12-15 10:19:25 +0100103def filelist_from_patchset(pattern):
104 '''Returns list of api files in changeset and the list of api
105 files they import.'''
Ole Troan5c318c72020-05-05 12:23:47 +0200106 filelist = []
Ole Troanab9f5732020-12-15 10:19:25 +0100107 git_cmd = ('((git diff HEAD~1.. --name-only;git ls-files -m) | '
108 'sort -u | grep "\\.api$")')
109 try:
110 res = check_output(git_cmd, shell=True)
111 except CalledProcessError:
112 return []
Ole Troan5c318c72020-05-05 12:23:47 +0200113
114 # Check for dependencies (imports)
115 imported_files = []
Ole Troanab9f5732020-12-15 10:19:25 +0100116 for line in res.decode('ascii').split('\n'):
117 if not line:
118 continue
119 if not re.search(pattern, line):
120 continue
121 filelist.append(line)
122 imported_files.extend(filelist_from_git_grep(os.path.basename(line)))
Ole Troan5c318c72020-05-05 12:23:47 +0200123
124 filelist.extend(imported_files)
125 return set(filelist)
126
Ole Troanab9f5732020-12-15 10:19:25 +0100127
128def is_deprecated(message):
129 '''Given a message, return True if message is deprecated'''
130 if 'options' in message:
131 if 'deprecated' in message['options']:
Andrew Yourtchenko6a3d4cc2020-09-22 15:11:51 +0000132 return True
133 # recognize the deprecated format
Ole Troanab9f5732020-12-15 10:19:25 +0100134 if 'status' in message['options'] and \
135 message['options']['status'] == 'deprecated':
Andrew Yourtchenko6a3d4cc2020-09-22 15:11:51 +0000136 print("WARNING: please use 'option deprecated;'")
137 return True
Ole Troan5c318c72020-05-05 12:23:47 +0200138 return False
139
Ole Troanab9f5732020-12-15 10:19:25 +0100140
141def is_in_progress(message):
142 '''Given a message, return True if message is marked as in_progress'''
143 if 'options' in message:
144 if 'in_progress' in message['options']:
Ole Troan5c318c72020-05-05 12:23:47 +0200145 return True
Andrew Yourtchenko6a3d4cc2020-09-22 15:11:51 +0000146 # recognize the deprecated format
Ole Troanab9f5732020-12-15 10:19:25 +0100147 if 'status' in message['options'] and \
148 message['options']['status'] == 'in_progress':
Andrew Yourtchenko6a3d4cc2020-09-22 15:11:51 +0000149 print("WARNING: please use 'option in_progress;'")
150 return True
151 return False
Ole Troan5c318c72020-05-05 12:23:47 +0200152
Ole Troanab9f5732020-12-15 10:19:25 +0100153
Andrew Yourtchenko62bd50d2020-09-11 17:40:52 +0000154def report(new, old):
Ole Troanab9f5732020-12-15 10:19:25 +0100155 '''Given a dictionary of new crcs and old crcs, print all the
156 added, removed, modified, in-progress, deprecated messages.
157 Return the number of backwards incompatible changes made.'''
158
159 # pylint: disable=too-many-branches
160
161 new.pop('_version', None)
162 old.pop('_version', None)
163 added, removed, modified, _ = dict_compare(new, old)
Ole Troan5c318c72020-05-05 12:23:47 +0200164 backwards_incompatible = 0
Ole Troanab9f5732020-12-15 10:19:25 +0100165
Andrew Yourtchenko8b0cd692020-09-16 09:48:59 +0000166 # print the full list of in-progress messages
167 # they should eventually either disappear of become supported
168 for k in new.keys():
169 newversion = int(new[k]['version'])
Ole Troanab9f5732020-12-15 10:19:25 +0100170 if newversion == 0 or is_in_progress(new[k]):
Andrew Yourtchenko8b0cd692020-09-16 09:48:59 +0000171 print(f'in-progress: {k}')
Ole Troan5c318c72020-05-05 12:23:47 +0200172 for k in added:
173 print(f'added: {k}')
174 for k in removed:
175 oldversion = int(old[k]['version'])
Ole Troanab9f5732020-12-15 10:19:25 +0100176 if oldversion > 0 and not is_deprecated(old[k]) and not \
177 is_in_progress(old[k]):
Ole Troan5c318c72020-05-05 12:23:47 +0200178 backwards_incompatible += 1
179 print(f'removed: ** {k}')
180 else:
181 print(f'removed: {k}')
182 for k in modified.keys():
183 oldversion = int(old[k]['version'])
184 newversion = int(new[k]['version'])
Ole Troanab9f5732020-12-15 10:19:25 +0100185 if oldversion > 0 and not is_in_progress(old[k]):
Ole Troan5c318c72020-05-05 12:23:47 +0200186 backwards_incompatible += 1
187 print(f'modified: ** {k}')
188 else:
189 print(f'modified: {k}')
Andrew Yourtchenko62bd50d2020-09-11 17:40:52 +0000190
191 # check which messages are still there but were marked for deprecation
192 for k in new.keys():
193 newversion = int(new[k]['version'])
Ole Troanab9f5732020-12-15 10:19:25 +0100194 if newversion > 0 and is_deprecated(new[k]):
Andrew Yourtchenko62bd50d2020-09-11 17:40:52 +0000195 if k in old:
Ole Troanab9f5732020-12-15 10:19:25 +0100196 if not is_deprecated(old[k]):
Andrew Yourtchenko62bd50d2020-09-11 17:40:52 +0000197 print(f'deprecated: {k}')
198 else:
199 print(f'added+deprecated: {k}')
200
Ole Troan5c318c72020-05-05 12:23:47 +0200201 return backwards_incompatible
202
203
Ole Troanab9f5732020-12-15 10:19:25 +0100204def check_patchset():
205 '''Compare the changes to API messages in this changeset.
206 Ignores API files with version < 1.0.0.
207 Only considers API files located under the src directory in the repo.
208 '''
209 files = filelist_from_patchset('^src/')
210 revision = 'HEAD~1'
211
212 oldcrcs = {}
213 newcrcs = {}
214 for filename in files:
215 # Ignore files that have version < 1.0.0
216 _ = crc_from_apigen(None, filename)
Dave Barach592dbd02021-03-11 15:12:29 -0500217 # Ignore removed files
218 if isinstance(_, set) == 0:
219 if isinstance(_, set) == 0 and _['_version']['major'] == '0':
220 continue
221 newcrcs.update(_)
Ole Troanab9f5732020-12-15 10:19:25 +0100222
Ole Troanab9f5732020-12-15 10:19:25 +0100223 oldcrcs.update(crc_from_apigen(revision, filename))
224
225 backwards_incompatible = report(newcrcs, oldcrcs)
226 if backwards_incompatible:
227 # alert on changing production API
228 print("crcchecker: Changing production APIs in an incompatible way",
229 file=sys.stderr)
230 sys.exit(-1)
231 else:
232 print('*' * 67)
233 print('* VPP CHECKAPI SUCCESSFULLY COMPLETED')
234 print('*' * 67)
235
236
Ole Troan5c318c72020-05-05 12:23:47 +0200237def main():
Ole Troanab9f5732020-12-15 10:19:25 +0100238 '''Main entry point.'''
Ole Troan5c318c72020-05-05 12:23:47 +0200239 parser = argparse.ArgumentParser(description='VPP CRC checker.')
240 parser.add_argument('--git-revision',
241 help='Git revision to compare against')
242 parser.add_argument('--dump-manifest', action='store_true',
243 help='Dump CRC for all messages')
244 parser.add_argument('--check-patchset', action='store_true',
Ole Troanab9f5732020-12-15 10:19:25 +0100245 help='Check patchset for backwards incompatbile changes')
Ole Troan5c318c72020-05-05 12:23:47 +0200246 parser.add_argument('files', nargs='*')
Ole Troanab9f5732020-12-15 10:19:25 +0100247 parser.add_argument('--diff', help='Files to compare (on filesystem)',
248 nargs=2)
Ole Troan5c318c72020-05-05 12:23:47 +0200249
250 args = parser.parse_args()
251
252 if args.diff and args.files:
253 parser.print_help()
254 sys.exit(-1)
255
256 # Diff two files
257 if args.diff:
258 oldcrcs = crc_from_apigen(None, args.diff[0])
259 newcrcs = crc_from_apigen(None, args.diff[1])
Andrew Yourtchenko62bd50d2020-09-11 17:40:52 +0000260 backwards_incompatible = report(newcrcs, oldcrcs)
Ole Troan5c318c72020-05-05 12:23:47 +0200261 sys.exit(0)
262
263 # Dump CRC for messages in given files / revision
264 if args.dump_manifest:
265 files = args.files if args.files else filelist_from_git_ls()
266 crcs = {}
Ole Troanab9f5732020-12-15 10:19:25 +0100267 for filename in files:
268 crcs.update(crc_from_apigen(args.git_revision, filename))
269 for k, value in crcs.items():
270 print(f'{k}: {value}')
Ole Troan5c318c72020-05-05 12:23:47 +0200271 sys.exit(0)
272
273 # Find changes between current patchset and given revision (previous)
274 if args.check_patchset:
275 if args.git_revision:
276 print('Argument git-revision ignored', file=sys.stderr)
277 # Check there are no uncomitted changes
278 if is_uncommitted_changes():
Ole Troanab9f5732020-12-15 10:19:25 +0100279 print('Please stash or commit changes in workspace',
280 file=sys.stderr)
Ole Troan5c318c72020-05-05 12:23:47 +0200281 sys.exit(-1)
Ole Troanab9f5732020-12-15 10:19:25 +0100282 check_patchset()
283 sys.exit(0)
284
285 # Find changes between current workspace and revision
286 # Find changes between a given file and a revision
287 files = args.files if args.files else filelist_from_git_ls()
Ole Troan5c318c72020-05-05 12:23:47 +0200288
289 revision = args.git_revision if args.git_revision else 'HEAD~1'
290
291 oldcrcs = {}
292 newcrcs = {}
Ole Troanab9f5732020-12-15 10:19:25 +0100293 for file in files:
294 newcrcs.update(crc_from_apigen(None, file))
295 oldcrcs.update(crc_from_apigen(revision, file))
Ole Troan5c318c72020-05-05 12:23:47 +0200296
Andrew Yourtchenko62bd50d2020-09-11 17:40:52 +0000297 backwards_incompatible = report(newcrcs, oldcrcs)
Ole Troan5c318c72020-05-05 12:23:47 +0200298
299 if args.check_patchset:
300 if backwards_incompatible:
301 # alert on changing production API
302 print("crcchecker: Changing production APIs in an incompatible way", file=sys.stderr)
303 sys.exit(-1)
304 else:
305 print('*' * 67)
306 print('* VPP CHECKAPI SUCCESSFULLY COMPLETED')
307 print('*' * 67)
308
Ole Troanab9f5732020-12-15 10:19:25 +0100309
Ole Troan5c318c72020-05-05 12:23:47 +0200310if __name__ == '__main__':
311 main()