symbol-check.py raw

   1  #!/usr/bin/env python3
   2  """Check that a libsecp256k1 shared library exports only expected symbols.
   3  
   4  Usage examples:
   5    - When building with Autotools:
   6      ./tools/symbol-check.py .libs/libsecp256k1.so
   7      ./tools/symbol-check.py .libs/libsecp256k1-<V>.dll
   8      ./tools/symbol-check.py .libs/libsecp256k1.dylib
   9  
  10    - When building with CMake:
  11      ./tools/symbol-check.py build/lib/libsecp256k1.so
  12      ./tools/symbol-check.py build/bin/libsecp256k1-<V>.dll
  13      ./tools/symbol-check.py build/lib/libsecp256k1.dylib"""
  14  
  15  import re
  16  import sys
  17  import subprocess
  18  
  19  import lief
  20  
  21  
  22  class UnexpectedExport(RuntimeError):
  23      pass
  24  
  25  
  26  def get_exported_exports(library) -> list[str]:
  27      """Adapter function to get exported symbols based on the library format."""
  28      if library.format == lief.Binary.FORMATS.ELF:
  29          return [symbol.name for symbol in library.exported_symbols]
  30      elif library.format == lief.Binary.FORMATS.PE:
  31          return [entry.name for entry in library.get_export().entries]
  32      elif library.format == lief.Binary.FORMATS.MACHO:
  33          return [symbol.name[1:] for symbol in library.exported_symbols]
  34      raise NotImplementedError(f"Unsupported format: {library.format}")
  35  
  36  
  37  def grep_expected_symbols() -> list[str]:
  38      """Guess the list of expected exported symbols from the source code."""
  39      grep_output = subprocess.check_output(
  40          ["git", "grep", r"^\s*SECP256K1_API", "--", "include"],
  41          universal_newlines=True,
  42          encoding="utf-8"
  43      )
  44      lines = grep_output.split("\n")
  45      pattern = re.compile(r'\bsecp256k1_\w+')
  46      exported: list[str] = [pattern.findall(line)[-1] for line in lines if line.strip()]
  47      return exported
  48  
  49  
  50  def check_symbols(library, expected_exports) -> None:
  51      """Check that the library exports only the expected symbols."""
  52      actual_exports = get_exported_exports(library)
  53      unexpected_exports = set(actual_exports) - set(expected_exports)
  54      if unexpected_exports != set():
  55          raise UnexpectedExport(f"Unexpected exported symbols: {unexpected_exports}")
  56  
  57  def main():
  58      if len(sys.argv) != 2:
  59          print(__doc__)
  60          return 1
  61      library = lief.parse(sys.argv[1])
  62      expected_exports = grep_expected_symbols()
  63      try:
  64          check_symbols(library, expected_exports)
  65      except UnexpectedExport as e:
  66          print(f"{sys.argv[0]}: In {sys.argv[1]}: {e}")
  67          return 1
  68      return 0
  69  
  70  
  71  if __name__ == "__main__":
  72      sys.exit(main())
  73