blob: 5060d9f2e0eb72db62fc86543d2e154c7caa158d [file] [log] [blame]
Ole Troan5c318c72020-05-05 12:23:47 +02001#!/usr/bin/env python3
2
Ole Troan510aaa82020-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 Troan510aaa82020-12-15 10:19:25 +010012import re
Ole Troan5c318c72020-05-05 12:23:47 +020013from subprocess import run, PIPE, check_output, CalledProcessError
14
Ole Troan510aaa82020-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 Troan510aaa82020-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)
26 return {}
Ole Troan510aaa82020-12-15 10:19:25 +010027
Ole Troan5c318c72020-05-05 12:23:47 +020028 if revision:
Ole Troan510aaa82020-12-15 10:19:25 +010029 apigen = (f'{APIGENBIN} --git-revision {revision} --includedir src '
Ole Troan5c318c72020-05-05 12:23:47 +020030 f'--input {filename} CRC')
31 else:
Ole Troan510aaa82020-12-15 10:19:25 +010032 apigen = (f'{APIGENBIN} --includedir src --input {filename} CRC')
33 returncode = run(apigen.split(), stdout=PIPE, stderr=PIPE)
34 if returncode.returncode == 2: # No such file
35 print(f'skipping: {revision}:{filename} {returncode}', file=sys.stderr)
Ole Troan5c318c72020-05-05 12:23:47 +020036 return {}
Ole Troan510aaa82020-12-15 10:19:25 +010037 if returncode.returncode != 0:
38 print(f'vppapigen failed for {revision}:{filename} with '
39 'command\n {apigen}\n error: {rv}',
40 returncode.stderr.decode('ascii'), file=sys.stderr)
Ole Troan5c318c72020-05-05 12:23:47 +020041 sys.exit(-2)
42
Ole Troan510aaa82020-12-15 10:19:25 +010043 return json.loads(returncode.stdout)
Ole Troan5c318c72020-05-05 12:23:47 +020044
45
Ole Troan510aaa82020-12-15 10:19:25 +010046def dict_compare(dict1, dict2):
47 '''Compare two dictionaries returning added, removed, modified
48 and equal entries'''
49 d1_keys = set(dict1.keys())
50 d2_keys = set(dict2.keys())
Ole Troan5c318c72020-05-05 12:23:47 +020051 intersect_keys = d1_keys.intersection(d2_keys)
52 added = d1_keys - d2_keys
53 removed = d2_keys - d1_keys
Ole Troan510aaa82020-12-15 10:19:25 +010054 modified = {o: (dict1[o], dict2[o]) for o in intersect_keys
55 if dict1[o]['crc'] != dict2[o]['crc']}
56 same = set(o for o in intersect_keys if dict1[o] == dict2[o])
Ole Troan5c318c72020-05-05 12:23:47 +020057 return added, removed, modified, same
58
59
60def filelist_from_git_ls():
Ole Troan510aaa82020-12-15 10:19:25 +010061 '''Returns a list of all api files in the git repository'''
Ole Troan5c318c72020-05-05 12:23:47 +020062 filelist = []
63 git_ls = 'git ls-files *.api'
Ole Troan510aaa82020-12-15 10:19:25 +010064 returncode = run(git_ls.split(), stdout=PIPE, stderr=PIPE)
65 if returncode.returncode != 0:
66 sys.exit(returncode.returncode)
Ole Troan5c318c72020-05-05 12:23:47 +020067
Ole Troan510aaa82020-12-15 10:19:25 +010068 for line in returncode.stdout.decode('ascii').split('\n'):
69 if line:
70 filelist.append(line)
Ole Troan5c318c72020-05-05 12:23:47 +020071 return filelist
72
73
74def is_uncommitted_changes():
Ole Troan510aaa82020-12-15 10:19:25 +010075 '''Returns true if there are uncommitted changes in the repo'''
Ole Troan5c318c72020-05-05 12:23:47 +020076 git_status = 'git status --porcelain -uno'
Ole Troan510aaa82020-12-15 10:19:25 +010077 returncode = run(git_status.split(), stdout=PIPE, stderr=PIPE)
78 if returncode.returncode != 0:
79 sys.exit(returncode.returncode)
Ole Troan5c318c72020-05-05 12:23:47 +020080
Ole Troan510aaa82020-12-15 10:19:25 +010081 if returncode.stdout:
Ole Troan5c318c72020-05-05 12:23:47 +020082 return True
83 return False
84
85
86def filelist_from_git_grep(filename):
Ole Troan510aaa82020-12-15 10:19:25 +010087 '''Returns a list of api files that this <filename> api files imports.'''
Ole Troan5c318c72020-05-05 12:23:47 +020088 filelist = []
89 try:
Ole Troan510aaa82020-12-15 10:19:25 +010090 returncode = check_output(f'git grep -e "import .*{filename}"'
91 ' -- *.api',
92 shell=True)
93 except CalledProcessError:
Ole Troan5c318c72020-05-05 12:23:47 +020094 return []
Ole Troan510aaa82020-12-15 10:19:25 +010095 for line in returncode.decode('ascii').split('\n'):
96 if line:
97 filename, _ = line.split(':')
98 filelist.append(filename)
Ole Troan5c318c72020-05-05 12:23:47 +020099 return filelist
100
101
Ole Troan510aaa82020-12-15 10:19:25 +0100102def filelist_from_patchset(pattern):
103 '''Returns list of api files in changeset and the list of api
104 files they import.'''
Ole Troan5c318c72020-05-05 12:23:47 +0200105 filelist = []
Ole Troan510aaa82020-12-15 10:19:25 +0100106 git_cmd = ('((git diff HEAD~1.. --name-only;git ls-files -m) | '
107 'sort -u | grep "\\.api$")')
108 returncode = check_output(git_cmd, shell=True)
Ole Troan5c318c72020-05-05 12:23:47 +0200109
110 # Check for dependencies (imports)
111 imported_files = []
Ole Troan510aaa82020-12-15 10:19:25 +0100112 for line in returncode.decode('ascii').split('\n'):
113 if not line:
114 continue
115 if not re.search(pattern, line):
116 continue
117 filelist.append(line)
118 imported_files.extend(filelist_from_git_grep(os.path.basename(line)))
Ole Troan5c318c72020-05-05 12:23:47 +0200119
120 filelist.extend(imported_files)
121 return set(filelist)
122
Ole Troan510aaa82020-12-15 10:19:25 +0100123
124def is_deprecated(message):
125 '''Given a message, return True if message is deprecated'''
126 if 'options' in message:
127 if 'deprecated' in message['options']:
Andrew Yourtchenko6a3d4cc2020-09-22 15:11:51 +0000128 return True
129 # recognize the deprecated format
Ole Troan510aaa82020-12-15 10:19:25 +0100130 if 'status' in message['options'] and \
131 message['options']['status'] == 'deprecated':
Andrew Yourtchenko6a3d4cc2020-09-22 15:11:51 +0000132 print("WARNING: please use 'option deprecated;'")
133 return True
Ole Troan5c318c72020-05-05 12:23:47 +0200134 return False
135
Ole Troan510aaa82020-12-15 10:19:25 +0100136
137def is_in_progress(message):
138 '''Given a message, return True if message is marked as in_progress'''
139 if 'options' in message:
140 if 'in_progress' in message['options']:
Ole Troan5c318c72020-05-05 12:23:47 +0200141 return True
Andrew Yourtchenko6a3d4cc2020-09-22 15:11:51 +0000142 # recognize the deprecated format
Ole Troan510aaa82020-12-15 10:19:25 +0100143 if 'status' in message['options'] and \
144 message['options']['status'] == 'in_progress':
Andrew Yourtchenko6a3d4cc2020-09-22 15:11:51 +0000145 print("WARNING: please use 'option in_progress;'")
146 return True
147 return False
Ole Troan5c318c72020-05-05 12:23:47 +0200148
Ole Troan510aaa82020-12-15 10:19:25 +0100149
Andrew Yourtchenko62bd50d2020-09-11 17:40:52 +0000150def report(new, old):
Ole Troan510aaa82020-12-15 10:19:25 +0100151 '''Given a dictionary of new crcs and old crcs, print all the
152 added, removed, modified, in-progress, deprecated messages.
153 Return the number of backwards incompatible changes made.'''
154
155 # pylint: disable=too-many-branches
156
157 new.pop('_version', None)
158 old.pop('_version', None)
159 added, removed, modified, _ = dict_compare(new, old)
Ole Troan5c318c72020-05-05 12:23:47 +0200160 backwards_incompatible = 0
Ole Troan510aaa82020-12-15 10:19:25 +0100161
Andrew Yourtchenko8b0cd692020-09-16 09:48:59 +0000162 # print the full list of in-progress messages
163 # they should eventually either disappear of become supported
164 for k in new.keys():
165 newversion = int(new[k]['version'])
Ole Troan510aaa82020-12-15 10:19:25 +0100166 if newversion == 0 or is_in_progress(new[k]):
Andrew Yourtchenko8b0cd692020-09-16 09:48:59 +0000167 print(f'in-progress: {k}')
Ole Troan5c318c72020-05-05 12:23:47 +0200168 for k in added:
169 print(f'added: {k}')
170 for k in removed:
171 oldversion = int(old[k]['version'])
Ole Troan510aaa82020-12-15 10:19:25 +0100172 if oldversion > 0 and not is_deprecated(old[k]) and not \
173 is_in_progress(old[k]):
Ole Troan5c318c72020-05-05 12:23:47 +0200174 backwards_incompatible += 1
175 print(f'removed: ** {k}')
176 else:
177 print(f'removed: {k}')
178 for k in modified.keys():
179 oldversion = int(old[k]['version'])
180 newversion = int(new[k]['version'])
Ole Troan510aaa82020-12-15 10:19:25 +0100181 if oldversion > 0 and not is_in_progress(old[k]):
Ole Troan5c318c72020-05-05 12:23:47 +0200182 backwards_incompatible += 1
183 print(f'modified: ** {k}')
184 else:
185 print(f'modified: {k}')
Andrew Yourtchenko62bd50d2020-09-11 17:40:52 +0000186
187 # check which messages are still there but were marked for deprecation
188 for k in new.keys():
189 newversion = int(new[k]['version'])
Ole Troan510aaa82020-12-15 10:19:25 +0100190 if newversion > 0 and is_deprecated(new[k]):
Andrew Yourtchenko62bd50d2020-09-11 17:40:52 +0000191 if k in old:
Ole Troan510aaa82020-12-15 10:19:25 +0100192 if not is_deprecated(old[k]):
Andrew Yourtchenko62bd50d2020-09-11 17:40:52 +0000193 print(f'deprecated: {k}')
194 else:
195 print(f'added+deprecated: {k}')
196
Ole Troan5c318c72020-05-05 12:23:47 +0200197 return backwards_incompatible
198
199
Ole Troan510aaa82020-12-15 10:19:25 +0100200def check_patchset():
201 '''Compare the changes to API messages in this changeset.
202 Ignores API files with version < 1.0.0.
203 Only considers API files located under the src directory in the repo.
204 '''
205 files = filelist_from_patchset('^src/')
206 revision = 'HEAD~1'
207
208 oldcrcs = {}
209 newcrcs = {}
210 for filename in files:
211 # Ignore files that have version < 1.0.0
212 _ = crc_from_apigen(None, filename)
213 if _['_version']['major'] == '0':
214 continue
215
216 newcrcs.update(_)
217 oldcrcs.update(crc_from_apigen(revision, filename))
218
219 backwards_incompatible = report(newcrcs, oldcrcs)
220 if backwards_incompatible:
221 # alert on changing production API
222 print("crcchecker: Changing production APIs in an incompatible way",
223 file=sys.stderr)
224 sys.exit(-1)
225 else:
226 print('*' * 67)
227 print('* VPP CHECKAPI SUCCESSFULLY COMPLETED')
228 print('*' * 67)
229
230
Ole Troan5c318c72020-05-05 12:23:47 +0200231def main():
Ole Troan510aaa82020-12-15 10:19:25 +0100232 '''Main entry point.'''
Ole Troan5c318c72020-05-05 12:23:47 +0200233 parser = argparse.ArgumentParser(description='VPP CRC checker.')
234 parser.add_argument('--git-revision',
235 help='Git revision to compare against')
236 parser.add_argument('--dump-manifest', action='store_true',
237 help='Dump CRC for all messages')
238 parser.add_argument('--check-patchset', action='store_true',
Ole Troan510aaa82020-12-15 10:19:25 +0100239 help='Check patchset for backwards incompatbile changes')
Ole Troan5c318c72020-05-05 12:23:47 +0200240 parser.add_argument('files', nargs='*')
Ole Troan510aaa82020-12-15 10:19:25 +0100241 parser.add_argument('--diff', help='Files to compare (on filesystem)',
242 nargs=2)
Ole Troan5c318c72020-05-05 12:23:47 +0200243
244 args = parser.parse_args()
245
246 if args.diff and args.files:
247 parser.print_help()
248 sys.exit(-1)
249
250 # Diff two files
251 if args.diff:
252 oldcrcs = crc_from_apigen(None, args.diff[0])
253 newcrcs = crc_from_apigen(None, args.diff[1])
Andrew Yourtchenko62bd50d2020-09-11 17:40:52 +0000254 backwards_incompatible = report(newcrcs, oldcrcs)
Ole Troan5c318c72020-05-05 12:23:47 +0200255 sys.exit(0)
256
257 # Dump CRC for messages in given files / revision
258 if args.dump_manifest:
259 files = args.files if args.files else filelist_from_git_ls()
260 crcs = {}
Ole Troan510aaa82020-12-15 10:19:25 +0100261 for filename in files:
262 crcs.update(crc_from_apigen(args.git_revision, filename))
263 for k, value in crcs.items():
264 print(f'{k}: {value}')
Ole Troan5c318c72020-05-05 12:23:47 +0200265 sys.exit(0)
266
267 # Find changes between current patchset and given revision (previous)
268 if args.check_patchset:
269 if args.git_revision:
270 print('Argument git-revision ignored', file=sys.stderr)
271 # Check there are no uncomitted changes
272 if is_uncommitted_changes():
Ole Troan510aaa82020-12-15 10:19:25 +0100273 print('Please stash or commit changes in workspace',
274 file=sys.stderr)
Ole Troan5c318c72020-05-05 12:23:47 +0200275 sys.exit(-1)
Ole Troan510aaa82020-12-15 10:19:25 +0100276 check_patchset()
277 sys.exit(0)
278
279 # Find changes between current workspace and revision
280 # Find changes between a given file and a revision
281 files = args.files if args.files else filelist_from_git_ls()
Ole Troan5c318c72020-05-05 12:23:47 +0200282
283 revision = args.git_revision if args.git_revision else 'HEAD~1'
284
285 oldcrcs = {}
286 newcrcs = {}
Ole Troan510aaa82020-12-15 10:19:25 +0100287 for file in files:
288 newcrcs.update(crc_from_apigen(None, file))
289 oldcrcs.update(crc_from_apigen(revision, file))
Ole Troan5c318c72020-05-05 12:23:47 +0200290
Andrew Yourtchenko62bd50d2020-09-11 17:40:52 +0000291 backwards_incompatible = report(newcrcs, oldcrcs)
Ole Troan5c318c72020-05-05 12:23:47 +0200292
293 if args.check_patchset:
294 if backwards_incompatible:
295 # alert on changing production API
296 print("crcchecker: Changing production APIs in an incompatible way", file=sys.stderr)
297 sys.exit(-1)
298 else:
299 print('*' * 67)
300 print('* VPP CHECKAPI SUCCESSFULLY COMPLETED')
301 print('*' * 67)
302
Ole Troan510aaa82020-12-15 10:19:25 +0100303
Ole Troan5c318c72020-05-05 12:23:47 +0200304if __name__ == '__main__':
305 main()