gir_verify.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  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. "Gdk-4.0": "gtk4",
  33. "Gsk-4.0": "gtk4",
  34. "Spelling-1": "libspelling-1",
  35. "Panel-1": "libpanel-1",
  36. "Pango-1.0": "pango",
  37. "PangoCairo-1.0": "pangocairo",
  38. "GdkPixbuf-2.0": "gdk-pixbuf-2.0",
  39. "Graphene-1.0": "graphene-1.0",
  40. "cairo-1.0": "cairo",
  41. }
  42. def run(cmd):
  43. return subprocess.run(cmd, capture_output=True, text=True).stdout
  44. def lib_dirs(pkg):
  45. dirs = []
  46. for tok in run(["pkg-config", "--libs-only-L", pkg]).split():
  47. if tok.startswith("-L"):
  48. dirs.append(tok[2:])
  49. ld = run(["pkg-config", "--variable=libdir", pkg]).strip()
  50. if ld:
  51. dirs.append(ld)
  52. dirs += ["/usr/lib/x86_64-linux-gnu", "/usr/lib", "/lib/x86_64-linux-gnu",
  53. "/lib"]
  54. return dirs
  55. def find_lib(name, dirs):
  56. for d in dirs:
  57. for pat in ("lib%s.so", "lib%s.so.*"):
  58. hits = sorted(glob.glob(os.path.join(d, pat % name)))
  59. if hits:
  60. return hits[0]
  61. return None
  62. def libs_for(pkg):
  63. names = []
  64. for tok in run(["pkg-config", "--libs-only-l", pkg]).split():
  65. if tok.startswith("-l"):
  66. names.append(tok[2:])
  67. dirs = lib_dirs(pkg)
  68. found = []
  69. for n in names:
  70. p = find_lib(n, dirs)
  71. if p:
  72. found.append(p)
  73. return found
  74. def symbols(paths):
  75. syms = set()
  76. for p in paths:
  77. for line in run(["nm", "-D", "--defined-only", p]).splitlines():
  78. parts = line.split()
  79. if len(parts) >= 3 and parts[1] in ("T", "W", "i"):
  80. syms.add(parts[2].split("@")[0])
  81. return syms
  82. def main():
  83. ap = argparse.ArgumentParser()
  84. ap.add_argument("namespace")
  85. ap.add_argument("--gir-dir", default="/usr/share/gir-1.0")
  86. ap.add_argument("--pkg", help="override the pkg-config package")
  87. ap.add_argument("--allowlist",
  88. default=os.path.join(os.path.dirname(
  89. os.path.abspath(__file__)),
  90. "gir_verify_allowlist.txt"),
  91. help="known macro/deprecated identifiers to ignore")
  92. args = ap.parse_args()
  93. gir = os.path.join(args.gir_dir, args.namespace + ".gir")
  94. if not os.path.exists(gir):
  95. sys.stderr.write("gir_verify: not found: %s\n" % gir)
  96. return 2
  97. pkg = args.pkg or PKG.get(args.namespace)
  98. if not pkg:
  99. sys.stderr.write("gir_verify: no package mapping for %s; use --pkg\n"
  100. % args.namespace)
  101. return 2
  102. paths = libs_for(pkg)
  103. libs = paths
  104. if not libs:
  105. sys.stderr.write("gir_verify: no libraries found for %s\n" % pkg)
  106. return 2
  107. root = ET.parse(gir).getroot()
  108. ns = gir2def.Namespace(root)
  109. ids = gir2def.collect_identifiers(ns)
  110. have = symbols(libs)
  111. allowed = set()
  112. if os.path.exists(args.allowlist):
  113. with open(args.allowlist) as f:
  114. for line in f:
  115. line = line.split("#", 1)[0].strip()
  116. if line:
  117. allowed.add(line)
  118. missing = [i for i in ids if i not in have]
  119. known = [m for m in missing if m in allowed]
  120. unexpected = [m for m in missing if m not in allowed]
  121. print("gir_verify: %s: %d identifiers, %d known-macro, %d unexpected "
  122. "(libs: %s)" % (args.namespace, len(ids), len(known),
  123. len(unexpected),
  124. ", ".join(os.path.basename(l) for l in libs)))
  125. for m in known:
  126. print(" known %s" % m)
  127. for m in unexpected:
  128. print(" MISSING %s" % m)
  129. return 1 if unexpected else 0
  130. if __name__ == "__main__":
  131. sys.exit(main())