gir_verify.py 4.5 KB

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