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_FORMAT_', 86 'VK_IMAGE_ASPECT_MEMORY_PLANE_', 87 'VK_IMAGE_ASPECT_PLANE_', 88 'VK_IMAGE_USAGE_', 89 'VK_NV_', 90 'VK_PERFORMANCE_COUNTER_UNIT_', 91 'VK_PIPELINE_BIND_POINT_', 92 'VK_SAMPLER_ADDRESS_MODE_', 93 'VK_SHADER_STAGE_TESSELLATION_', 94 'VK_SHADER_STAGE_', 95 'VK_STENCIL_OP_', 96 'VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_', 97 'VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_', 98 'VK_STRUCTURE_TYPE_', 99 'VK_USE_PLATFORM_', 100 'VK_VERSION_', 101 102 # Many places use the identifier without the `vk` prefix 103 # (eg. with the driver name as a prefix instead) 104 'VK_', 105 'Vk', 106 'vk', 107 ]: 108 # The order matters! A shorter substring will match before a longer 109 # one, hiding its matches. 110 for prefix_seen in prefixes_seen: 111 assert not prefix.startswith(prefix_seen), f'{prefix_seen} must come before {prefix}' 112 prefixes_seen.append(prefix) 113 114 identifier = remove_prefix(identifier, prefix) 115 116 return identifier 117 118 aliases = {} 119 for old_name, alias_for in get_aliases(VK_XML): 120 old_name = prepare_identifier(old_name) 121 alias_for = prepare_identifier(alias_for) 122 aliases[old_name] = alias_for 123 124 print(f'Found {len(aliases)} aliases in {VK_XML.name}') 125 126 # Some aliases have aliases 127 recursion_needs_checking = True 128 while recursion_needs_checking: 129 recursion_needs_checking = False 130 for old, new in aliases.items(): 131 if new in aliases: 132 aliases[old] = aliases[new] 133 recursion_needs_checking = True 134 135 # Doing the whole search in a single command breaks grep, so only 136 # look for 500 aliases at a time. Searching them one at a time would 137 # be extremely slow. 138 files_with_aliases = set() 139 for aliases_chunk in chunks([*aliases], 500): 140 grep_cmd = [ 141 'git', 142 'grep', 143 '-rlP', 144 '|'.join(aliases_chunk), 145 ] + paths 146 search_output = subprocess.run( 147 grep_cmd, 148 check=False, 149 stdout=subprocess.PIPE, 150 stderr=subprocess.DEVNULL, 151 ).stdout.decode() 152 files_with_aliases.update(search_output.splitlines()) 153 154 155 def file_matches_path(file: str, path: str) -> bool: 156 # if path is a folder; match any file within 157 if path.endswith('/') and file.startswith(path): 158 return True 159 return file == path 160 161 for excluded_path in EXCLUDE_PATHS: 162 files_with_aliases = { 163 file for file in files_with_aliases 164 if not file_matches_path(file, excluded_path) 165 } 166 167 if not files_with_aliases: 168 print('No alias found in any file.') 169 sys.exit(0) 170 171 print(f'{len(files_with_aliases)} files contain aliases:') 172 print('\n'.join(f'- {file}' for file in sorted(files_with_aliases))) 173 174 command = [ 175 'sed', 176 '-i', 177 ";".join([f's/{old}/{new}/g' for old, new in aliases.items()]), 178 ] 179 command += files_with_aliases 180 subprocess.check_call(command, stderr=subprocess.DEVNULL) 181 print('All aliases have been replaced') 182 183 184if __name__ == '__main__': 185 parser = argparse.ArgumentParser() 186 parser.add_argument('paths', 187 nargs=argparse.ZERO_OR_MORE, 188 default=['src/'], 189 help='Limit script to these paths (default: `src/`)') 190 args = parser.parse_args() 191 main(**vars(args)) 192