gir_verify.py 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. #!/usr/bin/env python3
  2. """gir_verify.py - check that every C identifier gir2def.py would emit
  3. actually exists in the installed shared library.
  4. It parses the .gir, collects the `c:identifier` of every constructor,
  5. method and function that would be generated, then builds the set of
  6. exported dynamic symbols from the libraries pkg-config links for the
  7. namespace and reports any identifier that is not there.
  8. Symbol versions (the `@@LIBADWAITA_1_0` suffixes) are stripped.
  9. Usage:
  10. tools/gir_verify.py Gtk-4.0
  11. tools/gir_verify.py Adw-1 --pkg libadwaita-1
  12. Exit status is non-zero if anything is missing.
  13. """
  14. import argparse
  15. import glob
  16. import os
  17. import subprocess
  18. import sys
  19. sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
  20. import gir2def # noqa: E402
  21. import xml.etree.ElementTree as ET # noqa: E402
  22. # GIR namespace -> pkg-config package that provides its libraries.
  23. PKG = {
  24. "Gtk-4.0": "gtk4",
  25. "Gdk-4.0": "gtk4",
  26. "Gsk-4.0": "gtk4",
  27. "GLib-2.0": "glib-2.0",
  28. "GObject-2.0": "gobject-2.0",
  29. "Gio-2.0": "gio-2.0",
  30. "Adw-1": "libadwaita-1",
  31. "GtkSource-5": "gtksourceview-5",
  32. "Pango-1.0": "pango",
  33. "PangoCairo-1.0": "pangocairo",
  34. "GdkPixbuf-2.0": "gdk-pixbuf-2.0",
  35. "Graphene-1.0": "graphene-1.0",
  36. "cairo-1.0": "cairo",
  37. }
  38. def run(cmd):
  39. return subprocess.run(cmd, capture_output=True, text=True).stdout
  40. def lib_dirs(pkg):
  41. dirs = []
  42. for tok in run(["pkg-config", "--libs-only-L", pkg]).split():
  43. if tok.startswith("-L"):
  44. dirs.append(tok[2:])
  45. ld = run(["pkg-config", "--variable=libdir", pkg]).strip()
  46. if ld:
  47. dirs.append(ld)
  48. dirs += ["/usr/lib/x86_64-linux-gnu", "/usr/lib", "/lib/x86_64-linux-gnu",
  49. "/lib"]
  50. return dirs
  51. def find_lib(name, dirs):
  52. for d in dirs:
  53. for pat in ("lib%s.so", "lib%s.so.*"):
  54. hits = sorted(glob.glob(os.path.join(d, pat % name)))
  55. if hits:
  56. return hits[0]
  57. return None
  58. def libs_for(pkg):
  59. names = []
  60. for tok in run(["pkg-config", "--libs-only-l", pkg]).split():
  61. if tok.startswith("-l"):
  62. names.append(tok[2:])
  63. dirs = lib_dirs(pkg)
  64. found = []
  65. for n in names:
  66. p = find_lib(n, dirs)
  67. if p:
  68. found.append(p)
  69. return found
  70. def symbols(paths):
  71. syms = set()
  72. for p in paths:
  73. for line in run(["nm", "-D", "--defined-only", p]).splitlines():
  74. parts = line.split()
  75. if len(parts) >= 3 and parts[1] in ("T", "W", "i"):
  76. syms.add(parts[2].split("@")[0])
  77. return syms
  78. def main():
  79. ap = argparse.ArgumentParser()
  80. ap.add_argument("namespace")
  81. ap.add_argument("--gir-dir", default="/usr/share/gir-1.0")
  82. ap.add_argument("--pkg", help="override the pkg-config package")
  83. ap.add_argument("--allowlist",
  84. default=os.path.join(os.path.dirname(
  85. os.path.abspath(__file__)),
  86. "gir_verify_allowlist.txt"),
  87. help="known macro/deprecated identifiers to ignore")
  88. args = ap.parse_args()
  89. gir = os.path.join(args.gir_dir, args.namespace + ".gir")
  90. if not os.path.exists(gir):
  91. sys.stderr.write("gir_verify: not found: %s\n" % gir)
  92. return 2
  93. pkg = args.pkg or PKG.get(args.namespace)
  94. if not pkg:
  95. sys.stderr.write("gir_verify: no package mapping for %s; use --pkg\n"
  96. % args.namespace)
  97. return 2
  98. paths = libs_for(pkg)
  99. libs = paths
  100. if not libs:
  101. sys.stderr.write("gir_verify: no libraries found for %s\n" % pkg)
  102. return 2
  103. root = ET.parse(gir).getroot()
  104. ns = gir2def.Namespace(root)
  105. ids = gir2def.collect_identifiers(ns)
  106. have = symbols(libs)
  107. allowed = set()
  108. if os.path.exists(args.allowlist):
  109. with open(args.allowlist) as f:
  110. for line in f:
  111. line = line.split("#", 1)[0].strip()
  112. if line:
  113. allowed.add(line)
  114. missing = [i for i in ids if i not in have]
  115. known = [m for m in missing if m in allowed]
  116. unexpected = [m for m in missing if m not in allowed]
  117. print("gir_verify: %s: %d identifiers, %d known-macro, %d unexpected "
  118. "(libs: %s)" % (args.namespace, len(ids), len(known),
  119. len(unexpected),
  120. ", ".join(os.path.basename(l) for l in libs)))
  121. for m in known:
  122. print(" known %s" % m)
  123. for m in unexpected:
  124. print(" MISSING %s" % m)
  125. return 1 if unexpected else 0
  126. if __name__ == "__main__":
  127. sys.exit(main())