from collections import OrderedDict from enum import Enum import itertools import locale import os from pathlib import Path import re import shlex import shutil import subprocess import tempfile from typing import Mapping, Sequence, Union from . import env from .machine_spec import MachineSpec REPO_ROOT = Path(__file__).resolve().parent.parent DEVKITS = { "frida-gum": ("frida-gum-1.0", Path("gum") / "gum.h"), "frida-gumjs": ("frida-gumjs-1.0", Path("gumjs") / "gumscriptbackend.h"), "frida-core": ("frida-core-1.0", Path("frida-core.h")), } ASSETS_PATH = Path(__file__).parent / "devkit-assets" INCLUDE_PATTERN = re.compile(r"#include\s+[<\"](.*?)[>\"]") class DepSymbolScope(str, Enum): PREFIXED = "prefixed" ORIGINAL = "original" class CompilerApplication: def __init__(self, kit: str, machine: MachineSpec, meson_config: Mapping[str, Union[str, Sequence[str]]], output_dir: Path, dep_symbol_scope: DepSymbolScope = DepSymbolScope.PREFIXED): self.kit = kit package, umbrella_header = DEVKITS[kit] self.package = package self.umbrella_header = umbrella_header self.machine = machine self.meson_config = meson_config self.compiler_argument_syntax = None self.output_dir = output_dir self.dep_symbol_scope = dep_symbol_scope self.library_filename = None def run(self): output_dir = self.output_dir kit = self.kit self.compiler_argument_syntax = detect_compiler_argument_syntax(self.meson_config) self.library_filename = compute_library_filename(self.kit, self.compiler_argument_syntax) output_dir.mkdir(parents=True, exist_ok=True) (extra_ldflags, thirdparty_symbol_mappings) = self._generate_library() umbrella_header_path = compute_umbrella_header_path(self.machine, self.package, self.umbrella_header, self.meson_config) header_file = output_dir / f"{kit}.h" if not umbrella_header_path.exists(): raise Exception(f"Header not found: {umbrella_header_path}") header_source = self._generate_header(umbrella_header_path, thirdparty_symbol_mappings) header_file.write_text(header_source, encoding="utf-8") example_file = output_dir / f"{kit}-example.c" example_source = self._generate_example(example_file, extra_ldflags) example_file.write_text(example_source, encoding="utf-8") extra_files = [] extra_files += self._generate_gir() if self.compiler_argument_syntax == "msvc": for msvs_asset in itertools.chain(ASSETS_PATH.glob(f"{kit}-*.sln"), ASSETS_PATH.glob(f"{kit}-*.vcxproj*")): shutil.copy(msvs_asset, output_dir) extra_files.append(msvs_asset.name) return [header_file.name, self.library_filename, example_file.name] + extra_files def _generate_gir(self): if self.kit != "frida-core": return [] gir_path = Path(query_pkgconfig_variable("frida_girdir", self.package, self.meson_config)) / "Frida-1.0.gir" gir_name = "frida-core.gir" shutil.copy(gir_path, self.output_dir / gir_name) return [gir_name] def _generate_header(self, umbrella_header_path, thirdparty_symbol_mappings): kit = self.kit package = self.package machine = self.machine meson_config = self.meson_config c_args = meson_config.get("c_args", []) include_cflags = query_pkgconfig_cflags(package, meson_config) if self.compiler_argument_syntax == "msvc": preprocessor = subprocess.run(meson_config["c"] + c_args + ["/nologo", "/E", umbrella_header_path] + include_cflags, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding="utf-8") if preprocessor.returncode != 0: raise Exception(f"Failed to spawn preprocessor: {preprocessor.stderr}") lines = preprocessor.stdout.split("\n") mapping_prefix = "#line " header_refs = [line[line.index("\"") + 1:line.rindex("\"")].replace("\\\\", "/") for line in lines if line.startswith(mapping_prefix)] header_files = deduplicate(header_refs) frida_root_slashed = REPO_ROOT.as_posix() header_files = [Path(h) for h in header_files if bool(re.match("^" + frida_root_slashed, h, re.I))] else: header_dependencies = subprocess.run( meson_config["c"] + c_args + include_cflags + ["-E", "-M", umbrella_header_path], capture_output=True, encoding="utf-8", check=True).stdout _, raw_header_files = header_dependencies.split(": ", maxsplit=1) header_files = [Path(item) for item in shlex.split(raw_header_files) if item != "\n"] header_files = [h for h in header_files if h.is_relative_to(REPO_ROOT)] header_files = exclude_libc_headers(header_files, meson_config, include_cflags) devkit_header_lines = [] umbrella_header = header_files[0] processed_header_files = {umbrella_header} ingest_header(umbrella_header, header_files, processed_header_files, devkit_header_lines) if kit in {"frida-gum", "frida-gumjs"} and machine.is_freestanding: gum_dir = umbrella_header_path.parent if kit == "frida-gumjs": gum_dir = gum_dir.parent.parent / "gum" barebone_header = gum_dir / "backend-barebone" / "include" / "gum" / "gumbarebone.h" ingest_header(barebone_header, header_files, processed_header_files, devkit_header_lines) if kit == "frida-gumjs" and not machine.is_freestanding: inspector_server_header = umbrella_header_path.parent / "guminspectorserver.h" ingest_header(inspector_server_header, header_files, processed_header_files, devkit_header_lines) if kit == "frida-core" and machine.os == "android": selinux_header = umbrella_header_path.parent / "frida-selinux.h" ingest_header(selinux_header, header_files, processed_header_files, devkit_header_lines) if kit == "frida-core" and machine.os not in {"windows", "none"}: for header in query_gio_unix_headers(meson_config): ingest_header(header, header_files, processed_header_files, devkit_header_lines) devkit_header = u"".join(devkit_header_lines) if package.startswith("frida-gum"): config = """#ifndef GUM_STATIC # define GUM_STATIC #endif """ else: config = "" if machine.os == "windows": deps = ["dnsapi", "iphlpapi", "psapi", "shlwapi", "winmm", "ws2_32"] if package == "frida-core-1.0": deps.extend(["advapi32", "crypt32", "gdi32", "kernel32", "ole32", "secur32", "shell32", "user32"]) deps.sort() frida_pragmas = f"#pragma comment(lib, \"{compute_library_filename(kit, self.compiler_argument_syntax)}\")" dep_pragmas = "\n".join([f"#pragma comment(lib, \"{dep}.lib\")" for dep in deps]) config += f"#ifdef _MSC_VER\n\n{frida_pragmas}\n\n{dep_pragmas}\n\n#endif\n\n" if len(thirdparty_symbol_mappings) > 0: public_mappings = [] for original, renamed in extract_public_thirdparty_symbol_mappings(thirdparty_symbol_mappings): public_mappings.append((original, renamed)) if f"define {original}" not in devkit_header and f"define {original}" not in devkit_header: continue def fixup_macro(match): prefix = match.group(1) suffix = re.sub(f"\\b{original}\\b", renamed, match.group(2)) return f"#undef {original}\n{prefix}{original}{suffix}" devkit_header = re.sub(r"^([ \t]*#[ \t]*define[ \t]*){0}\b((.*\\\n)*.*)$".format(original), fixup_macro, devkit_header, flags=re.MULTILINE) config += "#ifndef __FRIDA_SYMBOL_MAPPINGS__\n" config += "#define __FRIDA_SYMBOL_MAPPINGS__\n\n" config += "\n".join([f"#define {original} {renamed}" for original, renamed in public_mappings]) + "\n\n" config += "#endif\n\n" return (config + devkit_header).replace("\r\n", "\n") def _generate_library(self): library_flags = call_pkgconfig(["--static", "--libs", self.package], self.meson_config).split(" ") library_dirs = infer_library_dirs(library_flags) library_names = infer_library_names(library_flags) library_paths, extra_flags = resolve_library_paths(library_names, library_dirs, self.machine, self._build_root()) extra_flags += infer_linker_flags(library_flags) if self.compiler_argument_syntax == "msvc": thirdparty_symbol_mappings = self._do_generate_library_msvc(library_paths) else: thirdparty_symbol_mappings = self._do_generate_library_unix(library_paths) return (extra_flags, thirdparty_symbol_mappings) def _build_root(self): for candidate in [self.output_dir, *self.output_dir.parents]: if is_meson_build_root(candidate): return candidate return None def _do_generate_library_msvc(self, library_paths): subprocess.run(self.meson_config["lib"] + ["/nologo", "/out:" + str(self.output_dir / self.library_filename)] + library_paths, capture_output=True, encoding="utf-8", check=True) thirdparty_symbol_mappings = [] return thirdparty_symbol_mappings def _do_generate_library_unix(self, library_paths): output_path = self.output_dir / self.library_filename output_path.unlink(missing_ok=True) v8_libs = [path for path in library_paths if path.name.startswith("libv8")] if len(v8_libs) > 0: v8_libdir = v8_libs[0].parent libcxx_libs = list((v8_libdir / "c++").glob("*.a")) library_paths.extend(libcxx_libs) meson_config = self.meson_config ar = meson_config.get("ar", ["ar"]) ar_help = subprocess.run(ar + ["--help"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, encoding="utf-8").stdout mri_supported = "-M [ 0: flag = pending_cflags.pop(0) if flag == "-include": pending_cflags.pop(0) else: tweaked_cflags.append(flag) tweaked_cflags = deduplicate(tweaked_cflags) existing_cflags = set(tweaked_cflags) pending_ldflags = ldflags[:] seen_libs = set() seen_flags = set() while len(pending_ldflags) > 0: flag = pending_ldflags.pop(0) if flag in ("-arch", "-isysroot") and flag in existing_cflags: pending_ldflags.pop(0) else: if flag == "-isysroot": sysroot = pending_ldflags.pop(0) if "MacOSX" in sysroot: tweaked_ldflags.append("-isysroot \"$(xcrun --sdk macosx --show-sdk-path)\"") elif "iPhoneOS" in sysroot: tweaked_ldflags.append("-isysroot \"$(xcrun --sdk iphoneos --show-sdk-path)\"") continue elif flag == "-L": pending_ldflags.pop(0) continue elif flag.startswith("-L"): continue elif flag.startswith("-l"): if flag in seen_libs: continue seen_libs.add(flag) elif flag == "-pthread": if flag in seen_flags: continue seen_flags.add(flag) tweaked_ldflags.append(flag) pending_ldflags = tweaked_ldflags tweaked_ldflags = [] while len(pending_ldflags) > 0: flag = pending_ldflags.pop(0) raw_flags = [] while flag.startswith("-Wl,"): raw_flags.append(flag[4:]) if len(pending_ldflags) > 0: flag = pending_ldflags.pop(0) else: flag = None break if len(raw_flags) > 0: merged_flags = "-Wl," + ",".join(raw_flags) if "--icf=" in merged_flags: tweaked_ldflags.append("-fuse-ld=gold") tweaked_ldflags.append(merged_flags) if flag is not None and flag not in existing_cflags: tweaked_ldflags.append(flag) return (tweaked_cflags, tweaked_ldflags) def deduplicate(items): return list(OrderedDict.fromkeys(items))