#! /usr/bin/env python3 # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. 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 = true is_debug = 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_CAPTURE_ENABLED": True, "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, # clang specific "CR_CLANG_REVISION": True, "NDEBUG": False, "NVALGRIND": False, "_HAS_NODISCARD": 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) if v.startswith('"'): v = f"'{v}'" else: 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 lines.append("") # EOF newline # 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")