"""Emit the LLDB-side imports of a script interpreter plugin's objects.

The result is fed into liblldb's exports file so the dynamic loader can
satisfy the plugin's references at runtime without growing liblldb's
re-export surface beyond what the plugin actually consumes.
"""

import argparse
import subprocess
import sys


# Mangled-name prefixes liblldb owns. Symbols outside these namespaces
# are resolved through other libraries on the loader path.
EXPORT_PREFIXES = (
    "_ZN12lldb_private",
    "_ZNK12lldb_private",
    "_ZTVN12lldb_private",
    "_ZTSN12lldb_private",
    "_ZTIN12lldb_private",
    "_ZN4lldb",
    "_ZNK4lldb",
    "_ZTVN4lldb",
    "_ZTSN4lldb",
    "_ZTIN4lldb",
    "_ZN4llvm",
    "_ZNK4llvm",
    "_ZTVN4llvm",
    "_ZTSN4llvm",
    "_ZTIN4llvm",
)


def normalize(sym, mach_o):
    # Mach-O reports an extra leading underscore that the .exports file
    # format does not carry.
    if mach_o and sym.startswith("_"):
        return sym[1:]
    return sym


def collect(nm, obj, mach_o):
    """Return (undefined, defined) symbol-name sets for `obj`.

    Both halves are needed so cross-object references that the plugin's
    own objects satisfy can be subtracted before emitting.
    """
    undefined = set()
    defined = set()

    out = subprocess.check_output(
        [nm, "--undefined-only", "--just-symbol-name", obj], text=True
    )
    for line in out.splitlines():
        sym = line.strip()
        if sym:
            undefined.add(normalize(sym, mach_o))

    out = subprocess.check_output(
        [nm, "--defined-only", "--extern-only", "--just-symbol-name", obj],
        text=True,
    )
    for line in out.splitlines():
        sym = line.strip()
        if sym:
            defined.add(normalize(sym, mach_o))

    return undefined, defined


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--nm", required=True, help="Path to llvm-nm.")
    parser.add_argument("-o", "--output", required=True, help="Output exports file.")
    parser.add_argument(
        "--mach-o",
        action="store_true",
        help="Strip the leading underscore Mach-O prepends to every symbol.",
    )
    parser.add_argument(
        "objects", nargs="+", help="Object files (or archives) to scan."
    )
    args = parser.parse_args()

    undefined = set()
    defined = set()
    for obj in args.objects:
        u, d = collect(args.nm, obj, args.mach_o)
        undefined.update(u)
        defined.update(d)

    syms = {s for s in (undefined - defined) if s.startswith(EXPORT_PREFIXES)}

    with open(args.output, "w", newline="\n") as f:
        for s in sorted(syms):
            f.write(s + "\n")


if __name__ == "__main__":
    sys.exit(main())
