Ole Troan | 5c318c7 | 2020-05-05 12:23:47 +0200 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
| 2 | |
| 3 | import sys |
| 4 | import os |
| 5 | import json |
| 6 | import argparse |
| 7 | from subprocess import run, PIPE, check_output, CalledProcessError |
| 8 | |
| 9 | rootdir = os.path.dirname(os.path.realpath(__file__)) + '/../..' |
| 10 | |
| 11 | def crc_from_apigen(revision, filename): |
| 12 | if not revision and not os.path.isfile(filename): |
| 13 | print(f'skipping: {filename}', file=sys.stderr) |
| 14 | return {} |
| 15 | apigen_bin = f'{rootdir}/src/tools/vppapigen/vppapigen.py' |
| 16 | if revision: |
| 17 | apigen = (f'{apigen_bin} --git-revision {revision} --includedir src ' |
| 18 | f'--input {filename} CRC') |
| 19 | else: |
| 20 | apigen = (f'{apigen_bin} --includedir src --input {filename} CRC') |
| 21 | rv = run(apigen.split(), stdout=PIPE, stderr=PIPE) |
| 22 | if rv.returncode == 2: # No such file |
| 23 | print(f'skipping: {revision}:{filename} {rv}', file=sys.stderr) |
| 24 | return {} |
| 25 | if rv.returncode != 0: |
| 26 | print(f'vppapigen failed for {revision}:{filename} with command\n {apigen}\n error: {rv}', |
| 27 | rv.stderr.decode('ascii'), file=sys.stderr) |
| 28 | sys.exit(-2) |
| 29 | |
| 30 | return json.loads(rv.stdout) |
| 31 | |
| 32 | |
| 33 | def dict_compare(d1, d2): |
| 34 | d1_keys = set(d1.keys()) |
| 35 | d2_keys = set(d2.keys()) |
| 36 | intersect_keys = d1_keys.intersection(d2_keys) |
| 37 | added = d1_keys - d2_keys |
| 38 | removed = d2_keys - d1_keys |
| 39 | modified = {o: (d1[o], d2[o]) for o in intersect_keys if d1[o]['crc'] != d2[o]['crc']} |
| 40 | same = set(o for o in intersect_keys if d1[o] == d2[o]) |
| 41 | return added, removed, modified, same |
| 42 | |
| 43 | |
| 44 | def filelist_from_git_ls(): |
| 45 | filelist = [] |
| 46 | git_ls = 'git ls-files *.api' |
| 47 | rv = run(git_ls.split(), stdout=PIPE, stderr=PIPE) |
| 48 | if rv.returncode != 0: |
| 49 | sys.exit(rv.returncode) |
| 50 | |
| 51 | for l in rv.stdout.decode('ascii').split('\n'): |
| 52 | if len(l): |
| 53 | filelist.append(l) |
| 54 | return filelist |
| 55 | |
| 56 | |
| 57 | def is_uncommitted_changes(): |
| 58 | git_status = 'git status --porcelain -uno' |
| 59 | rv = run(git_status.split(), stdout=PIPE, stderr=PIPE) |
| 60 | if rv.returncode != 0: |
| 61 | sys.exit(rv.returncode) |
| 62 | |
| 63 | if len(rv.stdout): |
| 64 | return True |
| 65 | return False |
| 66 | |
| 67 | |
| 68 | def filelist_from_git_grep(filename): |
| 69 | filelist = [] |
| 70 | try: |
| 71 | rv = check_output(f'git grep -e "import .*{filename}" -- *.api', shell=True) |
| 72 | except CalledProcessError as err: |
| 73 | return [] |
| 74 | print('RV', err.returncode) |
| 75 | for l in rv.decode('ascii').split('\n'): |
| 76 | if l: |
| 77 | f, p = l.split(':') |
| 78 | filelist.append(f) |
| 79 | return filelist |
| 80 | |
| 81 | |
| 82 | def filelist_from_patchset(): |
| 83 | filelist = [] |
| 84 | git_cmd = '((git diff HEAD~1.. --name-only;git ls-files -m) | sort -u)' |
| 85 | rv = check_output(git_cmd, shell=True) |
| 86 | for l in rv.decode('ascii').split('\n'): |
| 87 | if len(l) and os.path.splitext(l)[1] == '.api': |
| 88 | filelist.append(l) |
| 89 | |
| 90 | # Check for dependencies (imports) |
| 91 | imported_files = [] |
| 92 | for f in filelist: |
| 93 | imported_files.extend(filelist_from_git_grep(os.path.basename(f))) |
| 94 | |
| 95 | filelist.extend(imported_files) |
| 96 | return set(filelist) |
| 97 | |
| 98 | def is_deprecated(d, k): |
Andrew Yourtchenko | 6a3d4cc | 2020-09-22 15:11:51 +0000 | [diff] [blame] | 99 | if 'options' in d[k]: |
| 100 | if 'deprecated' in d[k]['options']: |
| 101 | return True |
| 102 | # recognize the deprecated format |
| 103 | if 'status' in d[k]['options'] and d[k]['options']['status'] == 'deprecated': |
| 104 | print("WARNING: please use 'option deprecated;'") |
| 105 | return True |
Ole Troan | 5c318c7 | 2020-05-05 12:23:47 +0200 | [diff] [blame] | 106 | return False |
| 107 | |
| 108 | def is_in_progress(d, k): |
Andrew Yourtchenko | 6a3d4cc | 2020-09-22 15:11:51 +0000 | [diff] [blame] | 109 | if 'options' in d[k]: |
| 110 | if 'in_progress' in d[k]['options']: |
Ole Troan | 5c318c7 | 2020-05-05 12:23:47 +0200 | [diff] [blame] | 111 | return True |
Andrew Yourtchenko | 6a3d4cc | 2020-09-22 15:11:51 +0000 | [diff] [blame] | 112 | # recognize the deprecated format |
| 113 | if 'status' in d[k]['options'] and d[k]['options']['status'] == 'in_progress': |
| 114 | print("WARNING: please use 'option in_progress;'") |
| 115 | return True |
| 116 | return False |
Ole Troan | 5c318c7 | 2020-05-05 12:23:47 +0200 | [diff] [blame] | 117 | |
Andrew Yourtchenko | 62bd50d | 2020-09-11 17:40:52 +0000 | [diff] [blame] | 118 | def report(new, old): |
| 119 | added, removed, modified, same = dict_compare(new, old) |
Ole Troan | 5c318c7 | 2020-05-05 12:23:47 +0200 | [diff] [blame] | 120 | backwards_incompatible = 0 |
Andrew Yourtchenko | 8b0cd69 | 2020-09-16 09:48:59 +0000 | [diff] [blame] | 121 | # print the full list of in-progress messages |
| 122 | # they should eventually either disappear of become supported |
| 123 | for k in new.keys(): |
| 124 | newversion = int(new[k]['version']) |
| 125 | if newversion == 0 or is_in_progress(new, k): |
| 126 | print(f'in-progress: {k}') |
Ole Troan | 5c318c7 | 2020-05-05 12:23:47 +0200 | [diff] [blame] | 127 | for k in added: |
| 128 | print(f'added: {k}') |
| 129 | for k in removed: |
| 130 | oldversion = int(old[k]['version']) |
| 131 | if oldversion > 0 and not is_deprecated(old, k) and not is_in_progress(old, k): |
| 132 | backwards_incompatible += 1 |
| 133 | print(f'removed: ** {k}') |
| 134 | else: |
| 135 | print(f'removed: {k}') |
| 136 | for k in modified.keys(): |
| 137 | oldversion = int(old[k]['version']) |
| 138 | newversion = int(new[k]['version']) |
| 139 | if oldversion > 0 and not is_in_progress(old, k): |
| 140 | backwards_incompatible += 1 |
| 141 | print(f'modified: ** {k}') |
| 142 | else: |
| 143 | print(f'modified: {k}') |
Andrew Yourtchenko | 62bd50d | 2020-09-11 17:40:52 +0000 | [diff] [blame] | 144 | |
| 145 | # check which messages are still there but were marked for deprecation |
| 146 | for k in new.keys(): |
| 147 | newversion = int(new[k]['version']) |
| 148 | if newversion > 0 and is_deprecated(new, k): |
| 149 | if k in old: |
| 150 | if not is_deprecated(old, k): |
| 151 | print(f'deprecated: {k}') |
| 152 | else: |
| 153 | print(f'added+deprecated: {k}') |
| 154 | |
Ole Troan | 5c318c7 | 2020-05-05 12:23:47 +0200 | [diff] [blame] | 155 | return backwards_incompatible |
| 156 | |
| 157 | |
| 158 | def main(): |
| 159 | parser = argparse.ArgumentParser(description='VPP CRC checker.') |
| 160 | parser.add_argument('--git-revision', |
| 161 | help='Git revision to compare against') |
| 162 | parser.add_argument('--dump-manifest', action='store_true', |
| 163 | help='Dump CRC for all messages') |
| 164 | parser.add_argument('--check-patchset', action='store_true', |
| 165 | help='Dump CRC for all messages') |
| 166 | parser.add_argument('files', nargs='*') |
| 167 | parser.add_argument('--diff', help='Files to compare (on filesystem)', nargs=2) |
| 168 | |
| 169 | args = parser.parse_args() |
| 170 | |
| 171 | if args.diff and args.files: |
| 172 | parser.print_help() |
| 173 | sys.exit(-1) |
| 174 | |
| 175 | # Diff two files |
| 176 | if args.diff: |
| 177 | oldcrcs = crc_from_apigen(None, args.diff[0]) |
| 178 | newcrcs = crc_from_apigen(None, args.diff[1]) |
Andrew Yourtchenko | 62bd50d | 2020-09-11 17:40:52 +0000 | [diff] [blame] | 179 | backwards_incompatible = report(newcrcs, oldcrcs) |
Ole Troan | 5c318c7 | 2020-05-05 12:23:47 +0200 | [diff] [blame] | 180 | sys.exit(0) |
| 181 | |
| 182 | # Dump CRC for messages in given files / revision |
| 183 | if args.dump_manifest: |
| 184 | files = args.files if args.files else filelist_from_git_ls() |
| 185 | crcs = {} |
| 186 | for f in files: |
| 187 | crcs.update(crc_from_apigen(args.git_revision, f)) |
| 188 | for k, v in crcs.items(): |
| 189 | print(f'{k}: {v}') |
| 190 | sys.exit(0) |
| 191 | |
| 192 | # Find changes between current patchset and given revision (previous) |
| 193 | if args.check_patchset: |
| 194 | if args.git_revision: |
| 195 | print('Argument git-revision ignored', file=sys.stderr) |
| 196 | # Check there are no uncomitted changes |
| 197 | if is_uncommitted_changes(): |
| 198 | print('Please stash or commit changes in workspace', file=sys.stderr) |
| 199 | sys.exit(-1) |
| 200 | files = filelist_from_patchset() |
| 201 | else: |
| 202 | # Find changes between current workspace and revision |
| 203 | # Find changes between a given file and a revision |
| 204 | files = args.files if args.files else filelist_from_git_ls() |
| 205 | |
| 206 | revision = args.git_revision if args.git_revision else 'HEAD~1' |
| 207 | |
| 208 | oldcrcs = {} |
| 209 | newcrcs = {} |
| 210 | for f in files: |
| 211 | newcrcs.update(crc_from_apigen(None, f)) |
| 212 | oldcrcs.update(crc_from_apigen(revision, f)) |
| 213 | |
Andrew Yourtchenko | 62bd50d | 2020-09-11 17:40:52 +0000 | [diff] [blame] | 214 | backwards_incompatible = report(newcrcs, oldcrcs) |
Ole Troan | 5c318c7 | 2020-05-05 12:23:47 +0200 | [diff] [blame] | 215 | |
| 216 | if args.check_patchset: |
| 217 | if backwards_incompatible: |
| 218 | # alert on changing production API |
| 219 | print("crcchecker: Changing production APIs in an incompatible way", file=sys.stderr) |
| 220 | sys.exit(-1) |
| 221 | else: |
| 222 | print('*' * 67) |
| 223 | print('* VPP CHECKAPI SUCCESSFULLY COMPLETED') |
| 224 | print('*' * 67) |
| 225 | |
| 226 | if __name__ == '__main__': |
| 227 | main() |