Ole Troan | 5c318c7 | 2020-05-05 12:23:47 +0200 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
| 2 | |
Ole Troan | ab9f573 | 2020-12-15 10:19:25 +0100 | [diff] [blame] | 3 | ''' |
| 4 | crcchecker is a tool to used to enforce that .api messages do not change. |
| 5 | API files with a semantic version < 1.0.0 are ignored. |
| 6 | ''' |
| 7 | |
Ole Troan | 5c318c7 | 2020-05-05 12:23:47 +0200 | [diff] [blame] | 8 | import sys |
| 9 | import os |
| 10 | import json |
| 11 | import argparse |
Ole Troan | ab9f573 | 2020-12-15 10:19:25 +0100 | [diff] [blame] | 12 | import re |
Ole Troan | 5c318c7 | 2020-05-05 12:23:47 +0200 | [diff] [blame] | 13 | from subprocess import run, PIPE, check_output, CalledProcessError |
| 14 | |
Ole Troan | ab9f573 | 2020-12-15 10:19:25 +0100 | [diff] [blame] | 15 | # pylint: disable=subprocess-run-check |
| 16 | |
| 17 | ROOTDIR = os.path.dirname(os.path.realpath(__file__)) + '/../..' |
| 18 | APIGENBIN = f'{ROOTDIR}/src/tools/vppapigen/vppapigen.py' |
| 19 | |
Ole Troan | 5c318c7 | 2020-05-05 12:23:47 +0200 | [diff] [blame] | 20 | |
| 21 | def crc_from_apigen(revision, filename): |
Ole Troan | ab9f573 | 2020-12-15 10:19:25 +0100 | [diff] [blame] | 22 | '''Runs vppapigen with crc plugin returning a JSON object with CRCs for |
| 23 | all APIs in filename''' |
Ole Troan | 5c318c7 | 2020-05-05 12:23:47 +0200 | [diff] [blame] | 24 | if not revision and not os.path.isfile(filename): |
| 25 | print(f'skipping: {filename}', file=sys.stderr) |
Dave Barach | 592dbd0 | 2021-03-11 15:12:29 -0500 | [diff] [blame] | 26 | # Return <class 'set'> instead of <class 'dict'> |
| 27 | return {-1} |
Ole Troan | ab9f573 | 2020-12-15 10:19:25 +0100 | [diff] [blame] | 28 | |
Ole Troan | 5c318c7 | 2020-05-05 12:23:47 +0200 | [diff] [blame] | 29 | if revision: |
Ole Troan | ab9f573 | 2020-12-15 10:19:25 +0100 | [diff] [blame] | 30 | apigen = (f'{APIGENBIN} --git-revision {revision} --includedir src ' |
Ole Troan | 5c318c7 | 2020-05-05 12:23:47 +0200 | [diff] [blame] | 31 | f'--input {filename} CRC') |
| 32 | else: |
Ole Troan | ab9f573 | 2020-12-15 10:19:25 +0100 | [diff] [blame] | 33 | 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 Troan | 5c318c7 | 2020-05-05 12:23:47 +0200 | [diff] [blame] | 37 | return {} |
Ole Troan | ab9f573 | 2020-12-15 10:19:25 +0100 | [diff] [blame] | 38 | 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 Troan | 5c318c7 | 2020-05-05 12:23:47 +0200 | [diff] [blame] | 42 | sys.exit(-2) |
| 43 | |
Ole Troan | ab9f573 | 2020-12-15 10:19:25 +0100 | [diff] [blame] | 44 | return json.loads(returncode.stdout) |
Ole Troan | 5c318c7 | 2020-05-05 12:23:47 +0200 | [diff] [blame] | 45 | |
| 46 | |
Ole Troan | ab9f573 | 2020-12-15 10:19:25 +0100 | [diff] [blame] | 47 | def 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 Troan | 5c318c7 | 2020-05-05 12:23:47 +0200 | [diff] [blame] | 52 | intersect_keys = d1_keys.intersection(d2_keys) |
| 53 | added = d1_keys - d2_keys |
| 54 | removed = d2_keys - d1_keys |
Ole Troan | ab9f573 | 2020-12-15 10:19:25 +0100 | [diff] [blame] | 55 | 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 Troan | 5c318c7 | 2020-05-05 12:23:47 +0200 | [diff] [blame] | 58 | return added, removed, modified, same |
| 59 | |
| 60 | |
| 61 | def filelist_from_git_ls(): |
Ole Troan | ab9f573 | 2020-12-15 10:19:25 +0100 | [diff] [blame] | 62 | '''Returns a list of all api files in the git repository''' |
Ole Troan | 5c318c7 | 2020-05-05 12:23:47 +0200 | [diff] [blame] | 63 | filelist = [] |
| 64 | git_ls = 'git ls-files *.api' |
Ole Troan | ab9f573 | 2020-12-15 10:19:25 +0100 | [diff] [blame] | 65 | returncode = run(git_ls.split(), stdout=PIPE, stderr=PIPE) |
| 66 | if returncode.returncode != 0: |
| 67 | sys.exit(returncode.returncode) |
Ole Troan | 5c318c7 | 2020-05-05 12:23:47 +0200 | [diff] [blame] | 68 | |
Ole Troan | ab9f573 | 2020-12-15 10:19:25 +0100 | [diff] [blame] | 69 | for line in returncode.stdout.decode('ascii').split('\n'): |
| 70 | if line: |
| 71 | filelist.append(line) |
Ole Troan | 5c318c7 | 2020-05-05 12:23:47 +0200 | [diff] [blame] | 72 | return filelist |
| 73 | |
| 74 | |
| 75 | def is_uncommitted_changes(): |
Ole Troan | ab9f573 | 2020-12-15 10:19:25 +0100 | [diff] [blame] | 76 | '''Returns true if there are uncommitted changes in the repo''' |
Ole Troan | 5c318c7 | 2020-05-05 12:23:47 +0200 | [diff] [blame] | 77 | git_status = 'git status --porcelain -uno' |
Ole Troan | ab9f573 | 2020-12-15 10:19:25 +0100 | [diff] [blame] | 78 | returncode = run(git_status.split(), stdout=PIPE, stderr=PIPE) |
| 79 | if returncode.returncode != 0: |
| 80 | sys.exit(returncode.returncode) |
Ole Troan | 5c318c7 | 2020-05-05 12:23:47 +0200 | [diff] [blame] | 81 | |
Ole Troan | ab9f573 | 2020-12-15 10:19:25 +0100 | [diff] [blame] | 82 | if returncode.stdout: |
Ole Troan | 5c318c7 | 2020-05-05 12:23:47 +0200 | [diff] [blame] | 83 | return True |
| 84 | return False |
| 85 | |
| 86 | |
| 87 | def filelist_from_git_grep(filename): |
Ole Troan | ab9f573 | 2020-12-15 10:19:25 +0100 | [diff] [blame] | 88 | '''Returns a list of api files that this <filename> api files imports.''' |
Ole Troan | 5c318c7 | 2020-05-05 12:23:47 +0200 | [diff] [blame] | 89 | filelist = [] |
| 90 | try: |
Ole Troan | ab9f573 | 2020-12-15 10:19:25 +0100 | [diff] [blame] | 91 | returncode = check_output(f'git grep -e "import .*{filename}"' |
| 92 | ' -- *.api', |
| 93 | shell=True) |
| 94 | except CalledProcessError: |
Ole Troan | 5c318c7 | 2020-05-05 12:23:47 +0200 | [diff] [blame] | 95 | return [] |
Ole Troan | ab9f573 | 2020-12-15 10:19:25 +0100 | [diff] [blame] | 96 | for line in returncode.decode('ascii').split('\n'): |
| 97 | if line: |
| 98 | filename, _ = line.split(':') |
| 99 | filelist.append(filename) |
Ole Troan | 5c318c7 | 2020-05-05 12:23:47 +0200 | [diff] [blame] | 100 | return filelist |
| 101 | |
| 102 | |
Ole Troan | ab9f573 | 2020-12-15 10:19:25 +0100 | [diff] [blame] | 103 | def filelist_from_patchset(pattern): |
| 104 | '''Returns list of api files in changeset and the list of api |
| 105 | files they import.''' |
Ole Troan | 5c318c7 | 2020-05-05 12:23:47 +0200 | [diff] [blame] | 106 | filelist = [] |
Ole Troan | ab9f573 | 2020-12-15 10:19:25 +0100 | [diff] [blame] | 107 | 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 Troan | 5c318c7 | 2020-05-05 12:23:47 +0200 | [diff] [blame] | 113 | |
| 114 | # Check for dependencies (imports) |
| 115 | imported_files = [] |
Ole Troan | ab9f573 | 2020-12-15 10:19:25 +0100 | [diff] [blame] | 116 | 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 Troan | 5c318c7 | 2020-05-05 12:23:47 +0200 | [diff] [blame] | 123 | |
| 124 | filelist.extend(imported_files) |
| 125 | return set(filelist) |
| 126 | |
Ole Troan | ab9f573 | 2020-12-15 10:19:25 +0100 | [diff] [blame] | 127 | |
| 128 | def 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 Yourtchenko | 6a3d4cc | 2020-09-22 15:11:51 +0000 | [diff] [blame] | 132 | return True |
| 133 | # recognize the deprecated format |
Ole Troan | ab9f573 | 2020-12-15 10:19:25 +0100 | [diff] [blame] | 134 | if 'status' in message['options'] and \ |
| 135 | message['options']['status'] == 'deprecated': |
Andrew Yourtchenko | 6a3d4cc | 2020-09-22 15:11:51 +0000 | [diff] [blame] | 136 | print("WARNING: please use 'option deprecated;'") |
| 137 | return True |
Ole Troan | 5c318c7 | 2020-05-05 12:23:47 +0200 | [diff] [blame] | 138 | return False |
| 139 | |
Ole Troan | ab9f573 | 2020-12-15 10:19:25 +0100 | [diff] [blame] | 140 | |
| 141 | def 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 Troan | 5c318c7 | 2020-05-05 12:23:47 +0200 | [diff] [blame] | 145 | return True |
Andrew Yourtchenko | 6a3d4cc | 2020-09-22 15:11:51 +0000 | [diff] [blame] | 146 | # recognize the deprecated format |
Ole Troan | ab9f573 | 2020-12-15 10:19:25 +0100 | [diff] [blame] | 147 | if 'status' in message['options'] and \ |
| 148 | message['options']['status'] == 'in_progress': |
Andrew Yourtchenko | 6a3d4cc | 2020-09-22 15:11:51 +0000 | [diff] [blame] | 149 | print("WARNING: please use 'option in_progress;'") |
| 150 | return True |
| 151 | return False |
Ole Troan | 5c318c7 | 2020-05-05 12:23:47 +0200 | [diff] [blame] | 152 | |
Ole Troan | ab9f573 | 2020-12-15 10:19:25 +0100 | [diff] [blame] | 153 | |
Andrew Yourtchenko | 62bd50d | 2020-09-11 17:40:52 +0000 | [diff] [blame] | 154 | def report(new, old): |
Ole Troan | ab9f573 | 2020-12-15 10:19:25 +0100 | [diff] [blame] | 155 | '''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 Troan | 5c318c7 | 2020-05-05 12:23:47 +0200 | [diff] [blame] | 164 | backwards_incompatible = 0 |
Ole Troan | ab9f573 | 2020-12-15 10:19:25 +0100 | [diff] [blame] | 165 | |
Andrew Yourtchenko | 8b0cd69 | 2020-09-16 09:48:59 +0000 | [diff] [blame] | 166 | # 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 Troan | ab9f573 | 2020-12-15 10:19:25 +0100 | [diff] [blame] | 170 | if newversion == 0 or is_in_progress(new[k]): |
Andrew Yourtchenko | 8b0cd69 | 2020-09-16 09:48:59 +0000 | [diff] [blame] | 171 | print(f'in-progress: {k}') |
Ole Troan | 5c318c7 | 2020-05-05 12:23:47 +0200 | [diff] [blame] | 172 | for k in added: |
| 173 | print(f'added: {k}') |
| 174 | for k in removed: |
| 175 | oldversion = int(old[k]['version']) |
Ole Troan | ab9f573 | 2020-12-15 10:19:25 +0100 | [diff] [blame] | 176 | if oldversion > 0 and not is_deprecated(old[k]) and not \ |
| 177 | is_in_progress(old[k]): |
Ole Troan | 5c318c7 | 2020-05-05 12:23:47 +0200 | [diff] [blame] | 178 | 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 Troan | ab9f573 | 2020-12-15 10:19:25 +0100 | [diff] [blame] | 185 | if oldversion > 0 and not is_in_progress(old[k]): |
Ole Troan | 5c318c7 | 2020-05-05 12:23:47 +0200 | [diff] [blame] | 186 | backwards_incompatible += 1 |
| 187 | print(f'modified: ** {k}') |
| 188 | else: |
| 189 | print(f'modified: {k}') |
Andrew Yourtchenko | 62bd50d | 2020-09-11 17:40:52 +0000 | [diff] [blame] | 190 | |
| 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 Troan | ab9f573 | 2020-12-15 10:19:25 +0100 | [diff] [blame] | 194 | if newversion > 0 and is_deprecated(new[k]): |
Andrew Yourtchenko | 62bd50d | 2020-09-11 17:40:52 +0000 | [diff] [blame] | 195 | if k in old: |
Ole Troan | ab9f573 | 2020-12-15 10:19:25 +0100 | [diff] [blame] | 196 | if not is_deprecated(old[k]): |
Andrew Yourtchenko | 62bd50d | 2020-09-11 17:40:52 +0000 | [diff] [blame] | 197 | print(f'deprecated: {k}') |
| 198 | else: |
| 199 | print(f'added+deprecated: {k}') |
| 200 | |
Ole Troan | 5c318c7 | 2020-05-05 12:23:47 +0200 | [diff] [blame] | 201 | return backwards_incompatible |
| 202 | |
| 203 | |
Ole Troan | ab9f573 | 2020-12-15 10:19:25 +0100 | [diff] [blame] | 204 | def 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 Barach | 592dbd0 | 2021-03-11 15:12:29 -0500 | [diff] [blame] | 217 | # Ignore removed files |
| 218 | if isinstance(_, set) == 0: |
| 219 | if isinstance(_, set) == 0 and _['_version']['major'] == '0': |
| 220 | continue |
| 221 | newcrcs.update(_) |
Ole Troan | ab9f573 | 2020-12-15 10:19:25 +0100 | [diff] [blame] | 222 | |
Ole Troan | ab9f573 | 2020-12-15 10:19:25 +0100 | [diff] [blame] | 223 | 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 Troan | 5c318c7 | 2020-05-05 12:23:47 +0200 | [diff] [blame] | 237 | def main(): |
Ole Troan | ab9f573 | 2020-12-15 10:19:25 +0100 | [diff] [blame] | 238 | '''Main entry point.''' |
Ole Troan | 5c318c7 | 2020-05-05 12:23:47 +0200 | [diff] [blame] | 239 | 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 Troan | ab9f573 | 2020-12-15 10:19:25 +0100 | [diff] [blame] | 245 | help='Check patchset for backwards incompatbile changes') |
Ole Troan | 5c318c7 | 2020-05-05 12:23:47 +0200 | [diff] [blame] | 246 | parser.add_argument('files', nargs='*') |
Ole Troan | ab9f573 | 2020-12-15 10:19:25 +0100 | [diff] [blame] | 247 | parser.add_argument('--diff', help='Files to compare (on filesystem)', |
| 248 | nargs=2) |
Ole Troan | 5c318c7 | 2020-05-05 12:23:47 +0200 | [diff] [blame] | 249 | |
| 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 Yourtchenko | 62bd50d | 2020-09-11 17:40:52 +0000 | [diff] [blame] | 260 | backwards_incompatible = report(newcrcs, oldcrcs) |
Ole Troan | 5c318c7 | 2020-05-05 12:23:47 +0200 | [diff] [blame] | 261 | 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 Troan | ab9f573 | 2020-12-15 10:19:25 +0100 | [diff] [blame] | 267 | 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 Troan | 5c318c7 | 2020-05-05 12:23:47 +0200 | [diff] [blame] | 271 | 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 Troan | ab9f573 | 2020-12-15 10:19:25 +0100 | [diff] [blame] | 279 | print('Please stash or commit changes in workspace', |
| 280 | file=sys.stderr) |
Ole Troan | 5c318c7 | 2020-05-05 12:23:47 +0200 | [diff] [blame] | 281 | sys.exit(-1) |
Ole Troan | ab9f573 | 2020-12-15 10:19:25 +0100 | [diff] [blame] | 282 | 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 Troan | 5c318c7 | 2020-05-05 12:23:47 +0200 | [diff] [blame] | 288 | |
| 289 | revision = args.git_revision if args.git_revision else 'HEAD~1' |
| 290 | |
| 291 | oldcrcs = {} |
| 292 | newcrcs = {} |
Ole Troan | ab9f573 | 2020-12-15 10:19:25 +0100 | [diff] [blame] | 293 | for file in files: |
| 294 | newcrcs.update(crc_from_apigen(None, file)) |
| 295 | oldcrcs.update(crc_from_apigen(revision, file)) |
Ole Troan | 5c318c7 | 2020-05-05 12:23:47 +0200 | [diff] [blame] | 296 | |
Andrew Yourtchenko | 62bd50d | 2020-09-11 17:40:52 +0000 | [diff] [blame] | 297 | backwards_incompatible = report(newcrcs, oldcrcs) |
Ole Troan | 5c318c7 | 2020-05-05 12:23:47 +0200 | [diff] [blame] | 298 | |
| 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 Troan | ab9f573 | 2020-12-15 10:19:25 +0100 | [diff] [blame] | 309 | |
Ole Troan | 5c318c7 | 2020-05-05 12:23:47 +0200 | [diff] [blame] | 310 | if __name__ == '__main__': |
| 311 | main() |