1#!/usr/bin/python3 2# 3# Copyright 2017 The ANGLE Project Authors. All rights reserved. 4# Use of this source code is governed by a BSD-style license that can be 5# found in the LICENSE file. 6# 7# run_code_generation.py: 8# Runs ANGLE format table and other script code generation scripts. 9 10import argparse 11from concurrent import futures 12import hashlib 13import json 14import os 15import subprocess 16import sys 17import platform 18 19script_dir = sys.path[0] 20root_dir = os.path.abspath(os.path.join(script_dir, '..')) 21 22hash_dir = os.path.join(script_dir, 'code_generation_hashes') 23 24 25def get_child_script_dirname(script): 26 # All script names are relative to ANGLE's root 27 return os.path.dirname(os.path.abspath(os.path.join(root_dir, script))) 28 29 30def get_executable_name(script): 31 with open(script, 'r') as f: 32 # Check shebang 33 binary = os.path.basename(f.readline().strip().replace(' ', '/')) 34 assert binary in ['python3', 'vpython3'] 35 if platform.system() == 'Windows': 36 return binary + '.bat' 37 else: 38 return binary 39 40 41def paths_from_auto_script(script, param): 42 script_dir = get_child_script_dirname(script) 43 # python3 (not vpython3) to get inputs/outputs faster 44 exe = 'python3' 45 try: 46 res = subprocess.check_output([exe, os.path.basename(script), param], 47 cwd=script_dir).decode().strip() 48 except Exception: 49 print('Error with auto_script %s: %s, executable %s' % (param, script, exe)) 50 raise 51 if res == '': 52 return [] 53 return [ 54 os.path.relpath(os.path.join(script_dir, path), root_dir).replace("\\", "/") 55 for path in res.split(',') 56 ] 57 58 59# auto_script is a standard way for scripts to return their inputs and outputs. 60def auto_script(script): 61 info = { 62 'inputs': paths_from_auto_script(script, 'inputs'), 63 'outputs': paths_from_auto_script(script, 'outputs') 64 } 65 return info 66 67 68generators = { 69 'ANGLE format': 70 'src/libANGLE/renderer/gen_angle_format_table.py', 71 'ANGLE load functions table': 72 'src/libANGLE/renderer/gen_load_functions_table.py', 73 'ANGLE shader preprocessor': 74 'src/compiler/preprocessor/generate_parser.py', 75 'ANGLE shader translator': 76 'src/compiler/translator/generate_parser.py', 77 'D3D11 blit shader selection': 78 'src/libANGLE/renderer/d3d/d3d11/gen_blit11helper.py', 79 'D3D11 format': 80 'src/libANGLE/renderer/d3d/d3d11/gen_texture_format_table.py', 81 'DXGI format': 82 'src/libANGLE/renderer/gen_dxgi_format_table.py', 83 'DXGI format support': 84 'src/libANGLE/renderer/gen_dxgi_support_tables.py', 85 'Emulated HLSL functions': 86 'src/compiler/translator/hlsl/gen_emulated_builtin_function_tables.py', 87 'Extension files': 88 'src/libANGLE/gen_extensions.py', 89 'GL copy conversion table': 90 'src/libANGLE/gen_copy_conversion_table.py', 91 'GL CTS (dEQP) build files': 92 'scripts/gen_vk_gl_cts_build.py', 93 'GL/EGL/WGL loader': 94 'scripts/generate_loader.py', 95 'GL/EGL entry points': 96 'scripts/generate_entry_points.py', 97 'GLenum value to string map': 98 'scripts/gen_gl_enum_utils.py', 99 'GL format map': 100 'src/libANGLE/gen_format_map.py', 101 'interpreter utils': 102 'scripts/gen_interpreter_utils.py', 103 'Metal format table': 104 'src/libANGLE/renderer/metal/gen_mtl_format_table.py', 105 'Metal default shaders': 106 'src/libANGLE/renderer/metal/shaders/gen_mtl_internal_shaders.py', 107 'OpenGL dispatch table': 108 'src/libANGLE/renderer/gl/generate_gl_dispatch_table.py', 109 'overlay fonts': 110 'src/libANGLE/gen_overlay_fonts.py', 111 'overlay widgets': 112 'src/libANGLE/gen_overlay_widgets.py', 113 'packed enum': 114 'src/common/gen_packed_gl_enums.py', 115 'proc table': 116 'scripts/gen_proc_table.py', 117 'restricted traces': 118 'src/tests/restricted_traces/gen_restricted_traces.py', 119 'SPIR-V helpers': 120 'src/common/spirv/gen_spirv_builder_and_parser.py', 121 'Static builtins': 122 'src/compiler/translator/gen_builtin_symbols.py', 123 'uniform type': 124 'src/common/gen_uniform_type_table.py', 125 'Vulkan format': 126 'src/libANGLE/renderer/vulkan/gen_vk_format_table.py', 127 'Vulkan internal shader programs': 128 'src/libANGLE/renderer/vulkan/gen_vk_internal_shaders.py', 129 'Vulkan mandatory format support table': 130 'src/libANGLE/renderer/vulkan/gen_vk_mandatory_format_support_table.py', 131} 132 133 134# Fast and supports --verify-only without hashes. 135hashless_generators = { 136 'ANGLE features': 'include/platform/gen_features.py', 137 'Test spec JSON': 'infra/specs/generate_test_spec_json.py', 138} 139 140 141def md5(fname): 142 hash_md5 = hashlib.md5() 143 with open(fname, 'rb') as f: 144 if sys.platform.startswith('win') or sys.platform == 'cygwin': 145 # Beware: Windows crlf + git behavior + unicode in some files 146 hash_md5.update(f.read().replace(b'\r\n', b'\n')) 147 else: 148 for chunk in iter(lambda: f.read(4096), b''): 149 hash_md5.update(chunk) 150 return hash_md5.hexdigest() 151 152 153def get_hash_file_name(name): 154 return name.replace(' ', '_').replace('/', '_') + '.json' 155 156 157def any_hash_dirty(name, filenames, new_hashes, old_hashes): 158 found_dirty_hash = False 159 160 for fname in filenames: 161 if not os.path.isfile(os.path.join(root_dir, fname)): 162 print('File not found: "%s". Code gen dirty for %s' % (fname, name)) 163 found_dirty_hash = True 164 else: 165 new_hashes[fname] = md5(fname) 166 if (not fname in old_hashes) or (old_hashes[fname] != new_hashes[fname]): 167 print('Hash for "%s" dirty for %s generator.' % (fname, name)) 168 found_dirty_hash = True 169 return found_dirty_hash 170 171 172def any_old_hash_missing(all_new_hashes, all_old_hashes): 173 result = False 174 for file, old_hashes in all_old_hashes.items(): 175 if file not in all_new_hashes: 176 print('"%s" does not exist. Code gen dirty.' % file) 177 result = True 178 else: 179 for name, _ in old_hashes.items(): 180 if name not in all_new_hashes[file]: 181 print('Hash for %s is missing from "%s". Code gen is dirty.' % (name, file)) 182 result = True 183 return result 184 185 186def update_output_hashes(script, outputs, new_hashes): 187 for output in outputs: 188 if not os.path.isfile(output): 189 print('Output is missing from %s: %s' % (script, output)) 190 sys.exit(1) 191 new_hashes[output] = md5(output) 192 193 194def load_hashes(): 195 hashes = {} 196 for file in os.listdir(hash_dir): 197 hash_fname = os.path.join(hash_dir, file) 198 with open(hash_fname) as hash_file: 199 try: 200 hashes[file] = json.load(hash_file) 201 except ValueError: 202 raise Exception("Could not decode JSON from %s" % file) 203 return hashes 204 205 206def main(): 207 all_old_hashes = load_hashes() 208 all_new_hashes = {} 209 any_dirty = False 210 211 parser = argparse.ArgumentParser(description='Generate ANGLE internal code.') 212 parser.add_argument( 213 '-v', 214 '--verify-no-dirty', 215 dest='verify_only', 216 action='store_true', 217 help='verify hashes are not dirty') 218 parser.add_argument( 219 '-g', '--generator', action='append', nargs='*', type=str, dest='specified_generators'), 220 221 args = parser.parse_args() 222 223 ranGenerators = generators 224 runningSingleGenerator = False 225 if (args.specified_generators): 226 ranGenerators = {k: v for k, v in generators.items() if k in args.specified_generators[0]} 227 runningSingleGenerator = True 228 229 if len(ranGenerators) == 0: 230 print("No valid generators specified.") 231 return 1 232 233 # Just get 'inputs' and 'outputs' from scripts but this runs the scripts so it's a bit slow 234 infos = {} 235 with futures.ThreadPoolExecutor(max_workers=8) as executor: 236 for _, script in sorted(ranGenerators.items()): 237 infos[script] = executor.submit(auto_script, script) 238 239 for name, script in sorted(ranGenerators.items()): 240 info = infos[script].result() 241 fname = get_hash_file_name(name) 242 filenames = info['inputs'] + info['outputs'] + [script] 243 new_hashes = {} 244 if fname not in all_old_hashes: 245 all_old_hashes[fname] = {} 246 if any_hash_dirty(name, filenames, new_hashes, all_old_hashes[fname]): 247 any_dirty = True 248 249 if not args.verify_only: 250 print('Running ' + name + ' code generator') 251 252 exe = get_executable_name(script) 253 subprocess.check_call([exe, os.path.basename(script)], 254 cwd=get_child_script_dirname(script)) 255 256 # Update the hash dictionary. 257 all_new_hashes[fname] = new_hashes 258 259 if not runningSingleGenerator and any_old_hash_missing(all_new_hashes, all_old_hashes): 260 any_dirty = True 261 262 # Handle hashless_generators separately as these don't have hash maps. 263 hashless_generators_dirty = False 264 for name, script in sorted(hashless_generators.items()): 265 cmd = [get_executable_name(script), os.path.basename(script)] 266 rc = subprocess.call(cmd + ['--verify-only'], cwd=get_child_script_dirname(script)) 267 if rc != 0: 268 print(name + ' generator dirty') 269 # Don't set any_dirty as we don't need git cl format in this case. 270 hashless_generators_dirty = True 271 272 if not args.verify_only: 273 print('Running ' + name + ' code generator') 274 subprocess.check_call(cmd, cwd=get_child_script_dirname(script)) 275 276 if args.verify_only: 277 return int(any_dirty or hashless_generators_dirty) 278 279 if any_dirty: 280 args = ['git.bat'] if os.name == 'nt' else ['git'] 281 args += ['cl', 'format'] 282 print('Calling git cl format') 283 subprocess.check_call(args) 284 285 # Update the output hashes again since they can be formatted. 286 for name, script in sorted(ranGenerators.items()): 287 info = auto_script(script) 288 fname = get_hash_file_name(name) 289 update_output_hashes(name, info['outputs'], all_new_hashes[fname]) 290 291 for fname, new_hashes in all_new_hashes.items(): 292 hash_fname = os.path.join(hash_dir, fname) 293 with open(hash_fname, "w") as f: 294 json.dump(new_hashes, f, indent=2, sort_keys=True, separators=(',', ':\n ')) 295 f.write('\n') # json.dump doesn't end with newline 296 297 return 0 298 299 300if __name__ == '__main__': 301 sys.exit(main()) 302