1#!/usr/bin/env python3 2""" 3Check for and replace aliases with their new names from vk.xml 4""" 5 6import argparse 7import pathlib 8import subprocess 9import sys 10import xml.etree.ElementTree as et 11 12THIS_FILE = pathlib.Path(__file__) 13CWD = pathlib.Path.cwd() 14 15VK_XML = THIS_FILE.parent / 'vk.xml' 16EXCLUDE_PATHS = [ 17 VK_XML.relative_to(CWD).as_posix(), 18 19 # These files come from other repos, there's no point checking and 20 # fixing them here as that would be overwritten in the next sync. 21 'src/amd/vulkan/radix_sort/', 22 'src/virtio/venus-protocol/', 23] 24 25 26def get_aliases(xml_file: pathlib.Path): 27 """ 28 Get all the aliases defined in vk.xml 29 """ 30 xml = et.parse(xml_file) 31 32 for node in ([] 33 + xml.findall('.//enum[@alias]') 34 + xml.findall('.//type[@alias]') 35 + xml.findall('.//command[@alias]') 36 ): 37 yield node.attrib['name'], node.attrib['alias'] 38 39 40def remove_prefix(string: str, prefix: str): 41 """ 42 Remove prefix if string starts with it, and return the full string 43 otherwise. 44 """ 45 if not string.startswith(prefix): 46 return string 47 return string[len(prefix):] 48 49 50# Function from https://stackoverflow.com/a/312464 51def chunks(lst: list, n: int): 52 """ 53 Yield successive n-sized chunks from lst. 54 """ 55 for i in range(0, len(lst), n): 56 yield lst[i:i + n] 57 58 59def main(check_only: bool): 60 """ 61 Entrypoint; perform the search for all the aliases, and if `check_only` 62 is not True, replace them. 63 """ 64 def prepare_identifier(identifier: str) -> str: 65 # vk_find_struct() prepends `VK_STRUCTURE_TYPE_`, so that prefix 66 # might not appear in the code 67 identifier = remove_prefix(identifier, 'VK_STRUCTURE_TYPE_') 68 return identifier 69 70 aliases = {} 71 for old_name, alias_for in get_aliases(VK_XML): 72 old_name = prepare_identifier(old_name) 73 alias_for = prepare_identifier(alias_for) 74 aliases[old_name] = alias_for 75 76 print(f'Found {len(aliases)} aliases in {VK_XML.name}') 77 78 # Some aliases have aliases 79 recursion_needs_checking = True 80 while recursion_needs_checking: 81 recursion_needs_checking = False 82 for old, new in aliases.items(): 83 if new in aliases: 84 aliases[old] = aliases[new] 85 recursion_needs_checking = True 86 87 # Doing the whole search in a single command breaks grep, so only 88 # look for 500 aliases at a time. Searching them one at a time would 89 # be extremely slow. 90 files_with_aliases = set() 91 for aliases_chunk in chunks([*aliases], 500): 92 search_output = subprocess.check_output([ 93 'git', 94 'grep', 95 '-rlP', 96 '|'.join(aliases_chunk), 97 'src/' 98 ], stderr=subprocess.DEVNULL).decode() 99 files_with_aliases.update(search_output.splitlines()) 100 101 def file_matches_path(file: str, path: str) -> bool: 102 # if path is a folder; match any file within 103 if path.endswith('/') and file.startswith(path): 104 return True 105 return file == path 106 107 for excluded_path in EXCLUDE_PATHS: 108 files_with_aliases = { 109 file for file in files_with_aliases 110 if not file_matches_path(file, excluded_path) 111 } 112 113 if not files_with_aliases: 114 print('No alias found in any file.') 115 sys.exit(0) 116 117 print(f'{len(files_with_aliases)} files contain aliases:') 118 print('\n'.join(f'- {file}' for file in files_with_aliases)) 119 120 if check_only: 121 print('You can automatically fix this by running ' 122 f'`{THIS_FILE.relative_to(CWD)}`.') 123 sys.exit(1) 124 125 command = [ 126 'sed', 127 '-i', 128 ";".join([f's/{old}/{new}/g' for old, new in aliases.items()]), 129 ] 130 command += files_with_aliases 131 subprocess.check_call(command, stderr=subprocess.DEVNULL) 132 print('All aliases have been replaced') 133 134 135if __name__ == '__main__': 136 parser = argparse.ArgumentParser() 137 parser.add_argument('--check-only', 138 action='store_true', 139 help='Replace aliases found') 140 args = parser.parse_args() 141 main(**vars(args)) 142