#! /usr/bin/env python3 assert __name__ == '__main__' ''' To update ANGLE in Gecko, use Windows with git-bash, and setup depot_tools, python2, and python3. Because depot_tools expects `python` to be `python2` (shame!), python2 must come before python3 in your path. Upstream: https://chromium.googlesource.com/angle/angle Our repo: https://github.com/mozilla/angle It has branches like 'firefox-60' which is the branch we use for pulling into Gecko with this script. This script leaves a record of the merge-base and cherry-picks that we pull into Gecko. (gfx/angle/cherries.log) ANGLE<->Chrome version mappings are here: https://omahaproxy.appspot.com/ An easy choice is to grab Chrome's Beta's ANGLE branch. ## Usage Prepare your env: ~~~ export PATH="$PATH:/path/to/depot_tools" ~~~ If this is a new repo, don't forget: ~~~ # In the angle repo: ./scripts/bootstrap.py gclient sync ~~~ Update: (in the angle repo) ~~~ # In the angle repo: /path/to/gecko/gfx/angle/update-angle.py origin/chromium/XXXX git push moz # Push the firefox-XX branch to github.com/mozilla/angle ~~~~ ''' import json import os import pathlib import re import shutil import subprocess import sys from typing import * # mypy annotations REPO_DIR = pathlib.Path.cwd() GECKO_ANGLE_DIR = pathlib.Path(__file__).parent OUT_DIR = pathlib.Path('out') COMMON_HEADER = [ '# Generated by update-angle.py', '', "include('../../moz.build.common')", ] ROOTS = ['//:translator', '//:libEGL', '//:libGLESv2'] CHECK_ONLY = False args = sys.argv[1:] while True: arg = args.pop(0) if arg == '--check': CHECK_ONLY = True continue args.insert(0, arg) break GN_ENV = dict(os.environ) GN_ENV['DEPOT_TOOLS_WIN_TOOLCHAIN'] = '0' (GIT_REMOTE, ) = args # Not always 'origin'! # ------------------------------------------------------------------------------ def run_checked(*args, **kwargs): print(' ', args) sys.stdout.flush() return subprocess.run(args, check=True, **kwargs) def sorted_items(x): for k in sorted(x.keys()): yield (k, x[k]) def collapse_dotdots(path): split = path.split('/') ret = [] for x in split: if x == '..' and ret: ret.pop() continue ret.append(x) continue return '/'.join(ret) def dag_traverse(root_keys: Sequence[str], pre_recurse_func: Callable[[str], list]): visited_keys: Set[str] = set() def recurse(key): if key in visited_keys: return visited_keys.add(key) t = pre_recurse_func(key) try: (next_keys, post_recurse_func) = t except ValueError: (next_keys,) = t post_recurse_func = None for x in next_keys: recurse(x) if post_recurse_func: post_recurse_func(key) return for x in root_keys: recurse(x) return # ------------------------------------------------------------------------------ print('Importing graph') #shutil.rmtree(str(OUT_DIR), True) OUT_DIR.mkdir(exist_ok=True) GN_ARGS = b''' # Build arguments go here. # See "gn args --list" for available build arguments. is_clang = false angle_enable_gl = false angle_enable_gl_null = false angle_enable_null = false angle_enable_vulkan = false '''[1:] args_gn_path = OUT_DIR / 'args.gn' args_gn_path.write_bytes(GN_ARGS) try: run_checked('gn', 'gen', str(OUT_DIR), shell=True, env=GN_ENV) except subprocess.CalledProcessError: sys.stderr.buffer.write(b'`gn` failed. Is depot_tools in your PATH?\n') exit(1) p = run_checked('gn', 'desc', '--format=json', str(OUT_DIR), '*', stdout=subprocess.PIPE, shell=True, env=GN_ENV) # - print('\nProcessing graph') descs = json.loads(p.stdout.decode()) # - # HACKHACKHACK: Inject linux/mac sources instead of trying to merge graphs of different # platforms. descs['//:angle_common']['sources'] += [ '//src/common/system_utils_linux.cpp', '//src/common/system_utils_mac.cpp', '//src/common/system_utils_posix.cpp', ] # Ready to traverse # ------------------------------------------------------------------------------ LIBRARY_TYPES = ('shared_library', 'static_library') def flattened_target(target_name: str, descs: dict, stop_at_lib: bool =True) -> dict: flattened = dict(descs[target_name]) EXPECTED_TYPES = LIBRARY_TYPES + ('source_set', 'group', 'action') def pre(k): dep = descs[k] dep_type = dep['type'] deps = dep['deps'] if stop_at_lib and dep_type in LIBRARY_TYPES: return ((),) if dep_type == 'copy': assert not deps, (target_name, dep['deps']) else: assert dep_type in EXPECTED_TYPES, (k, dep_type) for (k,v) in dep.items(): if type(v) in (list, tuple): flattened[k] = flattened.get(k, []) + v else: #flattened.setdefault(k, v) pass return (deps,) dag_traverse(descs[target_name]['deps'], pre) return flattened # ------------------------------------------------------------------------------ # Check that includes are valid. (gn's version of this check doesn't seem to work!) INCLUDE_REGEX = re.compile(b'(?:^|\\n) *# *include +([<"])([^>"]+)[>"]') assert INCLUDE_REGEX.match(b'#include "foo"') assert INCLUDE_REGEX.match(b'\n#include "foo"') IGNORED_INCLUDES = { b'compiler/translator/TranslatorVulkan.h', b'libANGLE/renderer/d3d/d3d11/winrt/NativeWindow11WinRT.h', b'libANGLE/renderer/gl/glx/DisplayGLX.h', b'libANGLE/renderer/gl/cgl/DisplayCGL.h', b'libANGLE/renderer/gl/egl/ozone/DisplayOzone.h', b'libANGLE/renderer/gl/egl/android/DisplayAndroid.h', b'libANGLE/renderer/gl/wgl/DisplayWGL.h', b'libANGLE/renderer/null/DisplayNULL.h', b'libANGLE/renderer/vulkan/android/DisplayVkAndroid.h', b'libANGLE/renderer/vulkan/fuchsia/DisplayVkFuchsia.h', b'libANGLE/renderer/vulkan/win32/DisplayVkWin32.h', b'libANGLE/renderer/vulkan/xcb/DisplayVkXcb.h', b'kernel/image.h', } IGNORED_INCLUDE_PREFIXES = { b'android', b'Carbon', b'CoreFoundation', b'CoreServices', b'IOSurface', b'mach', b'mach-o', b'OpenGL', b'pci', b'sys', b'wrl', b'X11', } def has_all_includes(target_name: str, descs: dict) -> bool: flat = flattened_target(target_name, descs, stop_at_lib=False) acceptable_sources = flat.get('sources', []) + flat.get('outputs', []) acceptable_sources = (x.rsplit('/', 1)[-1].encode() for x in acceptable_sources) acceptable_sources = set(acceptable_sources) ret = True desc = descs[target_name] for cur_file in desc.get('sources', []): assert cur_file.startswith('/'), cur_file if not cur_file.startswith('//'): continue cur_file = pathlib.Path(cur_file[2:]) text = cur_file.read_bytes() for m in INCLUDE_REGEX.finditer(text): if m.group(1) == b'<': continue include = m.group(2) if include in IGNORED_INCLUDES: continue try: (prefix, _) = include.split(b'/', 1) if prefix in IGNORED_INCLUDE_PREFIXES: continue except ValueError: pass include_file = include.rsplit(b'/', 1)[-1] if include_file not in acceptable_sources: #print(' acceptable_sources:') #for x in sorted(acceptable_sources): # print(' ', x) print('Warning in {}: {}: Invalid include: {}'.format(target_name, cur_file, include)) ret = False #print('Looks valid:', m.group()) continue return ret # - # Gather real targets: def gather_libraries(roots: Sequence[str], descs: dict) -> Set[str]: libraries = set() def fn(target_name): cur = descs[target_name] print(' ' + cur['type'], target_name) assert has_all_includes(target_name, descs), target_name if cur['type'] in ('shared_library', 'static_library'): libraries.add(target_name) return (cur['deps'], ) dag_traverse(roots, fn) return libraries # - libraries = gather_libraries(ROOTS, descs) print(f'\n{len(libraries)} libraries:') for k in libraries: print(' ', k) if CHECK_ONLY: print('\n--check complete.') exit(0) # ------------------------------------------------------------------------------ # Output to moz.builds import vendor_from_git print('') vendor_from_git.record_cherry_picks(GECKO_ANGLE_DIR, GIT_REMOTE) # -- def sortedi(x): return sorted(x, key=str.lower) def append_arr(dest, name, vals, indent=0): if not vals: return dest.append('{}{} += ['.format(' '*4*indent, name)) for x in sortedi(vals): dest.append("{}'{}',".format(' '*4*(indent+1), x)) dest.append('{}]'.format(' '*4*indent)) dest.append('') return REGISTERED_DEFINES = { 'ANGLE_EGL_LIBRARY_NAME': False, 'ANGLE_ENABLE_D3D11': True, 'ANGLE_ENABLE_D3D9': True, 'ANGLE_ENABLE_DEBUG_ANNOTATIONS': True, 'ANGLE_ENABLE_NULL': False, 'ANGLE_ENABLE_OPENGL': False, 'ANGLE_ENABLE_OPENGL_NULL': False, 'ANGLE_ENABLE_ESSL': True, 'ANGLE_ENABLE_GLSL': True, 'ANGLE_ENABLE_HLSL': True, 'ANGLE_GENERATE_SHADER_DEBUG_INFO': True, 'ANGLE_GLESV2_LIBRARY_NAME': True, 'ANGLE_IS_64_BIT_CPU': False, 'ANGLE_PRELOADED_D3DCOMPILER_MODULE_NAMES': False, 'ANGLE_USE_EGL_LOADER': True, 'CERT_CHAIN_PARA_HAS_EXTRA_FIELDS': False, 'CHROMIUM_BUILD': False, 'COMPONENT_BUILD': False, 'DYNAMIC_ANNOTATIONS_ENABLED': True, 'EGL_EGL_PROTOTYPES': True, 'EGL_EGLEXT_PROTOTYPES': True, 'EGLAPI': True, 'FIELDTRIAL_TESTING_ENABLED': False, 'FULL_SAFE_BROWSING': False, 'GL_API': True, 'GL_APICALL': True, 'GL_GLES_PROTOTYPES': True, 'GL_GLEXT_PROTOTYPES': True, 'GPU_INFO_USE_SETUPAPI': True, 'LIBANGLE_IMPLEMENTATION': True, 'LIBEGL_IMPLEMENTATION': True, 'LIBGLESV2_IMPLEMENTATION': True, 'NOMINMAX': True, 'NO_TCMALLOC': False, # Else: gfx/angle/checkout/src/libANGLE/renderer/d3d/d3d11/win32/NativeWindow11Win32.cpp(89): error C2787: 'IDCompositionDevice': no GUID has been associated with this object 'NTDDI_VERSION': True, 'PSAPI_VERSION': False, 'SAFE_BROWSING_CSD': False, 'SAFE_BROWSING_DB_LOCAL': False, 'UNICODE': True, 'USE_AURA': False, 'V8_DEPRECATION_WARNINGS': False, 'WIN32': False, 'WIN32_LEAN_AND_MEAN': False, 'WINAPI_FAMILY': False, 'WINVER': True, # Otherwise: # gfx/angle/targets/libANGLE # In file included from c:/dev/mozilla/gecko4/gfx/angle/checkout/src/libANGLE/renderer/d3d/d3d11/converged/CompositorNativeWindow11.cpp:10: # In file included from c:/dev/mozilla/gecko4/gfx/angle/checkout/src\libANGLE/renderer/d3d/d3d11/converged/CompositorNativeWindow11.h:17: # C:\Program Files (x86)\Windows Kits\10\include\10.0.17763.0\winrt\Windows.ui.composition.interop.h(103,20): error: unknown type name 'POINTER_INFO' # _In_ const POINTER_INFO& pointerInfo # ^ 'WTF_USE_DYNAMIC_ANNOTATIONS': False, '_ATL_NO_OPENGL': True, '_CRT_RAND_S': True, '_CRT_SECURE_NO_DEPRECATE': True, '_DEBUG': False, '_HAS_EXCEPTIONS': True, '_HAS_ITERATOR_DEBUGGING': False, '_SCL_SECURE_NO_DEPRECATE': True, '_SECURE_ATL': True, '_UNICODE': True, '_USING_V110_SDK71_': False, '_WIN32_WINNT': False, '_WINDOWS': False, '__STD_C': False, } # - print('\nRun actions') required_files: Set[str] = set() run_checked('ninja', '-C', str(OUT_DIR), ':commit_id') required_files |= set(descs['//:commit_id']['outputs']) # - # Export our targets print('\nExport targets') # Clear our dest directories targets_dir = pathlib.Path(GECKO_ANGLE_DIR, 'targets') checkout_dir = pathlib.Path(GECKO_ANGLE_DIR, 'checkout') shutil.rmtree(targets_dir, True) shutil.rmtree(checkout_dir, True) targets_dir.mkdir(exist_ok=True) checkout_dir.mkdir(exist_ok=True) # - def export_target(target_name) -> Set[str]: #print(' ', target_name) desc = descs[target_name] flat = flattened_target(target_name, descs) assert target_name.startswith('//:'), target_name name = target_name[3:] required_files: Set[str] = set(flat['sources']) # Create our manifest lines target_dir = targets_dir / name target_dir.mkdir(exist_ok=True) lines = list(COMMON_HEADER) lines.append('') for x in sorted(set(desc['defines'])): try: (k, v) = x.split('=', 1) v = f"'{v}'" except ValueError: (k, v) = (x, 'True') try: line = f"DEFINES['{k}'] = {v}" if REGISTERED_DEFINES[k] == False: line = '#' + line lines.append(line) except KeyError: print(f'[{name}] Unrecognized define: {k}') lines.append('') cxxflags = set(desc['cflags'] + desc['cflags_cc']) def fixup_paths(listt): for x in set(listt): assert x.startswith('//'), x yield '../../checkout/' + x[2:] sources_by_config: Dict[str,List[str]] = {} extras: Dict[str,str] = dict() for x in fixup_paths(flat['sources']): #print(' '*5, x) (b, e) = x.rsplit('.', 1) if e in ['h', 'y', 'l', 'inc', 'inl']: continue elif e in ['cpp', 'cc', 'c']: if b.endswith('_win'): config = "CONFIG['OS_ARCH'] == 'WINNT'" elif b.endswith('_linux'): # Include these on BSDs too. config = "CONFIG['OS_ARCH'] not in ('Darwin', 'WINNT')" elif b.endswith('_mac'): config = "CONFIG['OS_ARCH'] == 'Darwin'" elif b.endswith('_posix'): config = "CONFIG['OS_ARCH'] != 'WINNT'" else: config = '' # None can't compare against str. sources_by_config.setdefault(config, []).append(x) continue elif e == 'rc': assert 'RCFILE' not in extras, (target_name, extras['RCFILE'], x) extras['RCFILE'] = f"'{x}'" continue elif e == 'def': assert 'DEFFILE' not in extras, (target_name, extras['DEFFILE'], x) extras['DEFFILE'] = f"'{x}'" continue else: assert False, ("Unhandled ext:", x) ldflags = set(desc['ldflags']) DEF_PREFIX = '/DEF:' for x in set(ldflags): if x.startswith(DEF_PREFIX): def_path = x[len(DEF_PREFIX):] required_files.add(def_path) assert 'DEFFILE' not in extras ldflags.remove(x) def_path = str(OUT_DIR) + '/' + def_path def_path = '//' + collapse_dotdots(def_path) def_rel_path = list(fixup_paths([def_path]))[0] extras['DEFFILE'] = "'{}'".format(def_rel_path) os_libs = list(map( lambda x: x[:-len('.lib')], set(desc.get('libs', [])) )) def append_arr_commented(dest, name, src): lines = [] append_arr(lines, name, src) def comment(x): if x: x = '#' + x return x lines = map(comment, lines) dest += lines append_arr(lines, 'LOCAL_INCLUDES', fixup_paths(desc['include_dirs'])) append_arr_commented(lines, 'CXXFLAGS', cxxflags) for (config,v) in sorted_items(sources_by_config): indent = 0 if config: lines.append("if {}:".format(config)) indent = 1 append_arr(lines, 'SOURCES', v, indent=indent) dep_libs: Set[str] = set() for dep_name in set(flat['deps']): dep = descs[dep_name] if dep['type'] in LIBRARY_TYPES: assert dep_name.startswith('//:'), dep_name dep_libs.add(dep_name[3:]) append_arr(lines, 'USE_LIBS', dep_libs) append_arr(lines, 'DIRS', ['../' + x for x in dep_libs]) append_arr(lines, 'OS_LIBS', os_libs) append_arr_commented(lines, 'LDFLAGS', ldflags) for (k,v) in sorted(extras.items()): lines.append('{} = {}'.format(k, v)) lib_type = desc['type'] if lib_type == 'shared_library': lines.append(f"GeckoSharedLibrary('{name}', linkage=None)") elif lib_type == 'static_library': lines.append(f"Library('{name}')") else: assert False, lib_type # Write it out mozbuild = target_dir / 'moz.build' print(' ', ' ', f'Writing {mozbuild}') data = b'\n'.join((x.encode() for x in lines)) mozbuild.write_bytes(data) return required_files # - for target_name in libraries: reqs = export_target(target_name) required_files |= reqs # Copy all the files print('\nMigrate required files') i = 0 for x in required_files: i += 1 sys.stdout.write(f'\r Copying {i}/{len(required_files)}') sys.stdout.flush() assert x.startswith('//'), x x = x[2:] src = REPO_DIR / x dest = checkout_dir / x dest.parent.mkdir(parents=True, exist_ok=True) data = src.read_bytes() data = data.replace(b'\r\n', b'\n') dest.write_bytes(data) print('\n\nDone')