#!/usr/bin/env python3 """gir_verify.py - check that every C identifier gir2def.py would emit actually exists in the installed shared library. It parses the .gir, collects the `c:identifier` of every constructor, method and function that would be generated, then builds the set of exported dynamic symbols from the libraries pkg-config links for the namespace and reports any identifier that is not there. Symbol versions (the `@@LIBADWAITA_1_0` suffixes) are stripped. Usage: tools/gir_verify.py Gtk-4.0 tools/gir_verify.py Adw-1 --pkg libadwaita-1 Exit status is non-zero if anything is missing. """ import argparse import glob import os import subprocess import sys sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import gir2def # noqa: E402 import xml.etree.ElementTree as ET # noqa: E402 # GIR namespace -> pkg-config package that provides its libraries. PKG = { "Gtk-4.0": "gtk4", "Gdk-4.0": "gtk4", "Gsk-4.0": "gtk4", "GLib-2.0": "glib-2.0", "GObject-2.0": "gobject-2.0", "Gio-2.0": "gio-2.0", "Adw-1": "libadwaita-1", "GtkSource-5": "gtksourceview-5", "Gdk-4.0": "gtk4", "Gsk-4.0": "gtk4", "Spelling-1": "libspelling-1", "Panel-1": "libpanel-1", "Pango-1.0": "pango", "PangoCairo-1.0": "pangocairo", "GdkPixbuf-2.0": "gdk-pixbuf-2.0", "Graphene-1.0": "graphene-1.0", "cairo-1.0": "cairo", } def run(cmd): return subprocess.run(cmd, capture_output=True, text=True).stdout def lib_dirs(pkg): dirs = [] for tok in run(["pkg-config", "--libs-only-L", pkg]).split(): if tok.startswith("-L"): dirs.append(tok[2:]) ld = run(["pkg-config", "--variable=libdir", pkg]).strip() if ld: dirs.append(ld) dirs += ["/usr/lib/x86_64-linux-gnu", "/usr/lib", "/lib/x86_64-linux-gnu", "/lib"] return dirs def find_lib(name, dirs): for d in dirs: for pat in ("lib%s.so", "lib%s.so.*"): hits = sorted(glob.glob(os.path.join(d, pat % name))) if hits: return hits[0] return None def libs_for(pkg): names = [] for tok in run(["pkg-config", "--libs-only-l", pkg]).split(): if tok.startswith("-l"): names.append(tok[2:]) dirs = lib_dirs(pkg) found = [] for n in names: p = find_lib(n, dirs) if p: found.append(p) return found def symbols(paths): syms = set() for p in paths: for line in run(["nm", "-D", "--defined-only", p]).splitlines(): parts = line.split() if len(parts) >= 3 and parts[1] in ("T", "W", "i"): syms.add(parts[2].split("@")[0]) return syms def main(): ap = argparse.ArgumentParser() ap.add_argument("namespace") ap.add_argument("--gir-dir", default="/usr/share/gir-1.0") ap.add_argument("--pkg", help="override the pkg-config package") ap.add_argument("--allowlist", default=os.path.join(os.path.dirname( os.path.abspath(__file__)), "gir_verify_allowlist.txt"), help="known macro/deprecated identifiers to ignore") args = ap.parse_args() gir = os.path.join(args.gir_dir, args.namespace + ".gir") if not os.path.exists(gir): sys.stderr.write("gir_verify: not found: %s\n" % gir) return 2 pkg = args.pkg or PKG.get(args.namespace) if not pkg: sys.stderr.write("gir_verify: no package mapping for %s; use --pkg\n" % args.namespace) return 2 paths = libs_for(pkg) libs = paths if not libs: sys.stderr.write("gir_verify: no libraries found for %s\n" % pkg) return 2 root = ET.parse(gir).getroot() ns = gir2def.Namespace(root) ids = gir2def.collect_identifiers(ns) have = symbols(libs) allowed = set() if os.path.exists(args.allowlist): with open(args.allowlist) as f: for line in f: line = line.split("#", 1)[0].strip() if line: allowed.add(line) missing = [i for i in ids if i not in have] known = [m for m in missing if m in allowed] unexpected = [m for m in missing if m not in allowed] print("gir_verify: %s: %d identifiers, %d known-macro, %d unexpected " "(libs: %s)" % (args.namespace, len(ids), len(known), len(unexpected), ", ".join(os.path.basename(l) for l in libs))) for m in known: print(" known %s" % m) for m in unexpected: print(" MISSING %s" % m) return 1 if unexpected else 0 if __name__ == "__main__": sys.exit(main())