• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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