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 # Some renames only apply to some APIs 38 if 'api' in node.attrib and 'vulkan' not in node.attrib['api'].split(','): 39 continue 40 41 yield node.attrib['name'], node.attrib['alias'] 42 43 44def remove_prefix(string: str, prefix: str): 45 """ 46 Remove prefix if string starts with it, and return the full string 47 otherwise. 48 """ 49 if not string.startswith(prefix): 50 return string 51 return string[len(prefix):] 52 53 54# Function from https://stackoverflow.com/a/312464 55def chunks(lst: list, n: int): 56 """ 57 Yield successive n-sized chunks from lst. 58 """ 59 for i in range(0, len(lst), n): 60 yield lst[i:i + n] 61 62 63def main(paths: list[str]): 64 """ 65 Entrypoint; perform the search for all the aliases and replace them. 66 """ 67 def prepare_identifier(identifier: str) -> str: 68 prefixes_seen = [] 69 for prefix in [ 70 # Various macros prepend these, so they will not appear in the code using them. 71 # List generated using this command: 72 # $ prefixes=$(git grep -woiE 'VK_\w+_' -- src/ ':!src/vulkan/registry/' | cut -d: -f2 | sort -u) 73 # $ for prefix in $prefixes; do grep -q $prefix src/vulkan/registry/vk.xml && echo "'$prefix',"; done 74 # (the second part eliminates prefixes used only in mesa code and not upstream) 75 'VK_BLEND_FACTOR_', 76 'VK_BLEND_OP_', 77 'VK_BORDER_COLOR_', 78 'VK_COMMAND_BUFFER_RESET_', 79 'VK_COMMAND_POOL_RESET_', 80 'VK_COMPARE_OP_', 81 'VK_COMPONENT_SWIZZLE_', 82 'VK_DESCRIPTOR_TYPE_', 83 'VK_DRIVER_ID_', 84 'VK_DYNAMIC_STATE_', 85 'VK_ERROR_', 86 'VK_FORMAT_', 87 'VK_IMAGE_ASPECT_MEMORY_PLANE_', 88 'VK_IMAGE_ASPECT_PLANE_', 89 'VK_IMAGE_USAGE_', 90 'VK_NV_', 91 'VK_PERFORMANCE_COUNTER_UNIT_', 92 'VK_PIPELINE_BIND_POINT_', 93 'VK_SAMPLER_ADDRESS_MODE_', 94 'VK_SHADER_STAGE_TESSELLATION_', 95 'VK_SHADER_STAGE_', 96 'VK_STENCIL_OP_', 97 'VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_', 98 'VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_', 99 'VK_STRUCTURE_TYPE_', 100 'VK_USE_PLATFORM_', 101 'VK_VERSION_', 102 103 # Many places use the identifier without the `vk` prefix 104 # (eg. with the driver name as a prefix instead) 105 'VK_', 106 'Vk', 107 'vk', 108 ]: 109 # The order matters! A shorter substring will match before a longer 110 # one, hiding its matches. 111 for prefix_seen in prefixes_seen: 112 assert not prefix.startswith(prefix_seen), f'{prefix_seen} must come before {prefix}' 113 prefixes_seen.append(prefix) 114 115 identifier = remove_prefix(identifier, prefix) 116 117 return identifier 118 119 aliases = {} 120 for old_name, alias_for in get_aliases(VK_XML): 121 old_name = prepare_identifier(old_name) 122 alias_for = prepare_identifier(alias_for) 123 aliases[old_name] = alias_for 124 125 print(f'Found {len(aliases)} aliases in {VK_XML.name}') 126 127 # Some aliases have aliases 128 recursion_needs_checking = True 129 while recursion_needs_checking: 130 recursion_needs_checking = False 131 for old, new in aliases.items(): 132 if new in aliases: 133 aliases[old] = aliases[new] 134 recursion_needs_checking = True 135 136 # Doing the whole search in a single command breaks grep, so only 137 # look for 500 aliases at a time. Searching them one at a time would 138 # be extremely slow. 139 files_with_aliases = set() 140 for aliases_chunk in chunks([*aliases], 500): 141 grep_cmd = [ 142 'git', 143 'grep', 144 '-rlP', 145 '|'.join(aliases_chunk), 146 ] + paths 147 search_output = subprocess.run( 148 grep_cmd, 149 check=False, 150 stdout=subprocess.PIPE, 151 stderr=subprocess.DEVNULL, 152 ).stdout.decode() 153 files_with_aliases.update(search_output.splitlines()) 154 155 156 def file_matches_path(file: str, path: str) -> bool: 157 # if path is a folder; match any file within 158 if path.endswith('/') and file.startswith(path): 159 return True 160 return file == path 161 162 for excluded_path in EXCLUDE_PATHS: 163 files_with_aliases = { 164 file for file in files_with_aliases 165 if not file_matches_path(file, excluded_path) 166 } 167 168 if not files_with_aliases: 169 print('No alias found in any file.') 170 sys.exit(0) 171 172 print(f'{len(files_with_aliases)} files contain aliases:') 173 print('\n'.join(f'- {file}' for file in sorted(files_with_aliases))) 174 175 command = [ 176 'sed', 177 '-i', 178 ";".join([f's/{old}/{new}/g' for old, new in aliases.items()]), 179 ] 180 command += files_with_aliases 181 subprocess.check_call(command, stderr=subprocess.DEVNULL) 182 print('All aliases have been replaced') 183 184 185if __name__ == '__main__': 186 parser = argparse.ArgumentParser() 187 parser.add_argument('paths', 188 nargs=argparse.ZERO_OR_MORE, 189 default=['src/'], 190 help='Limit script to these paths (default: `src/`)') 191 args = parser.parse_args() 192 main(**vars(args)) 193