#!/usr/bin/env python3

"""
defcheck.py -- Consistency check for the .def and .gir files.
Copyright (C) 2006  Simon Budig <simon@gimp.org>

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.


This is a hack to check the consistency of the .def and .gir files compared to
the respective libraries.

Invoke in the build directory and pass the name
of the built .def files on the command-line.

Needs the tool "nm", "objdump", "dumpbin" or "dyld_info" to work

"""

import os, sys, subprocess, shutil, glob
from os import getenv, path
import xml.etree.ElementTree as ET

def_files = sys.argv[1:]

def read_def_symbols(filename):
   symbols = []
   with open(filename, encoding="utf-8") as def_file:
      for line in def_file:
         line = line.split(";", 1)[0].strip()
         if not line or line == "EXPORTS":
            continue
         # DATA marks variable exports in .def files and is not part of the symbol name.
         parts = line.split()
         symbols.append(parts[0])
   return symbols

#gegl_glX* symbols are Linux-specific and gegl_clCreateCommandQueue macOS-specific
exclude_symbols = [
    "gegl_glXGetCurrentContext",
    "gegl_glXGetCurrentDisplay",
    "gegl_clCreateCommandQueue"
]

# Some .def files are concatenated, which can result in an unsorted error.
# We allow those cases to be skipped by defining them here.
ignore_sorting_errors = [
   # in gegl.def
   "gegl_downscale_2x2_get_fun_x86_64_v2",
   "gegl_downscale_2x2_arm_neon",
   # in gegl-sc.def:
   "AdvancingFront_set_head",
]

have_errors = 0


#READ LIBRARY SYMBOLS
libextension   = ".so"
#command        = getenv("NM", default="nm") + " --defined-only --extern-only "
command        = getenv("NM", default="nm") + " -D " #see https://gitlab.gnome.org/GNOME/gimp/-/work_items/16255
libprefix      = "lib"
platform_linux = True
platform_win32 = False
platform_macos = False
if sys.platform in ['win32', 'cygwin']:
   libextension   = ".dll"
   command        = "objdump -p "
   if shutil.which("dumpbin"):
     command      = "dumpbin /EXPORTS "
     libprefix    = ""
   platform_linux = False
   platform_win32 = True
   platform_macos = False
elif sys.platform == 'darwin':
   libextension   = ".dylib"
   command        = getenv("NM", default="nm") + " -gU "
   if shutil.which("dyld_info"):
     command      = "dyld_info -exports "
   platform_linux = False
   platform_win32 = False
   platform_macos = True

for df in def_files:
   if df.endswith(".gir") or df.endswith(".typelib"):
      continue
   directory, name = path.split (df)
   basename, extension = name.split (".")
   libname = path.join(os.getcwd(), directory, libprefix + basename + "-*" + libextension)
   matches = glob.glob(libname)
   lib_is_win32 = platform_win32
   if matches:
     libname = matches[0]
   elif libextension != ".dll":
     # Linux-hosted MinGW cross builds still produce Windows DLLs.
     libname = path.join(os.getcwd(), directory, libprefix + basename + "-*.dll")
     matches = glob.glob(libname)
     if matches:
       libname = matches[0]
       lib_is_win32 = True
       objdump = shutil.which("x86_64-w64-mingw32-objdump") or shutil.which("objdump") or "objdump"
       command = objdump + " -p "
   #FIXME: This leaks to ninja stdout, which should not happen
   #print ("platform: " + sys.platform + " - extracting symbols from " + libname)

   filename = df
   try:
      defsymbols = read_def_symbols (filename)
   except IOError as message:
      print(message)
      sys.exit (-1)

   doublesymbols = []
   for i in range (len (defsymbols)-1, 0, -1):
      if defsymbols[i] in defsymbols[:i]:
         doublesymbols.append ((defsymbols[i], i+2))

   sorterrors = ""
   sortok = True
   for i in range (len (defsymbols)-1):
      if defsymbols[i].lower() > defsymbols[i+1].lower():
         if not defsymbols[i+1] in ignore_sorting_errors:
            sorterrors += f"{defsymbols[i]} > {defsymbols[i+1]}\n"
            sortok = False
   sorterrors = sorterrors.split(sep='\n')

   status, nm = subprocess.getstatusoutput (command + f'"{libname}"')
   if status != 0:
      print("trouble reading {} - has it been compiled?".format(libname))
      print(nm)
      have_errors = -1
      continue

   nmsymbols = ""
   if platform_linux and not lib_is_win32:
      #nmsymbols = nm
      lines = nm.split(sep='\n')
      for line in lines:
         parts = line.split()
         if len(parts) == 3 and parts[1].upper() in "TDBR":
            nmsymbols += " 0 0 " + parts[2].split('@')[0]
         elif len(parts) == 2 and parts[0].upper() in "TDBR":
            #If the address is omitted for certain sections
            nmsymbols += " 0 0 " + parts[1].split('@')[0]

   elif lib_is_win32 and not shutil.which("dumpbin"): # Windows MSYS2 or MinGW cross build
      objnm = nm.split(sep='\n')
      found = False
      nmsymbols = ""
      for s in objnm:
         if ("Ordinal   Hint Name" in s or
             " Ordinal      RVA  Name" in s or
             "[Ordinal/Name Pointer] Table" in s):
            found = True
         elif found:
            s = s.strip()
            if not s:
               break
            nmsymbols += " 0 0 " + s.split()[-1] # Keep the [2::3] logic happy

   elif lib_is_win32: # Windows MSVC
      dbin = nm.split(sep='\n')
      found = False
      nmsymbols = ""
      for s in dbin:
         if "ordinal" in s and "hint" in s and "RVA" in s:
            found = True
         elif found and s.strip() and "Summary" not in s:
            parts = s.split()
            if len(parts) >= 4:
               nmsymbols += " 0 0 " + parts[3] # Keep the [2::3] logic happy

   elif platform_macos and not shutil.which("dyld_info"): # macOS 11 and earlier
      lines = nm.split(sep='\n')
      for line in lines:
         parts = line.split()
         if len(parts) == 3 and parts[1].upper() in "TDBRS":
            symbol = parts[2]
         elif len(parts) == 2 and parts[0].upper() in "TDBRS":
            symbol = parts[1]
         else:
            continue
         if symbol.startswith('_'):
            symbol = symbol[1:]
         nmsymbols += " 0 0 " + symbol # Keep the [2::3] logic happy

   elif platform_macos: # macOS 12 and up
      lines = nm.split(sep='\n')
      for line in lines:
         parts = line.split()
         if parts and parts[0].startswith("0x"):
            symbol = parts[-1]
            if symbol.startswith('_'):
               symbol = symbol[1:]
            nmsymbols += " 0 0 " + symbol # Keep the [2::3] logic happy

   nmsymbols = nmsymbols.split()[2::3]
   nmsymbols = [s for s in nmsymbols if s[0] != '_' and not s.startswith("OBJC_")]

   missing_defs = [s for s in nmsymbols  if s not in defsymbols and s not in exclude_symbols]
   missing_nms  = [s for s in defsymbols if s not in nmsymbols  and s not in exclude_symbols]


   #READ GIR/TYPELIB SYMBOLS
   girsymbols = {}
   gir_mode = any(arg.endswith(".gir") for arg in sys.argv[1:])
   if gir_mode:
      current_idx = sys.argv.index(df)
      if current_idx + 1 < len(sys.argv) and sys.argv[current_idx + 1].endswith(".gir"):
         gir_filename = sys.argv[current_idx + 1]
      else:
         continue
      try:
         tree = ET.parse(gir_filename)
         for elem in tree.iter():
            c_id = None
            for k, v in elem.attrib.items():
               if k == 'c:identifier' or k.endswith('}identifier'):
                  c_id = v
            if c_id and not elem.tag.endswith('function-macro'):
               if any(child.tag.endswith('varargs') or child.get('name') == 'va_list' for child in elem.iter()):
                  continue
               introspectable = elem.get('introspectable') != '0'
               has_skip_reason = False
               for child in elem:
                  if child.tag == 'attribute' or child.tag.endswith('}attribute'):
                     if child.get('name') == 'skip-reason':
                        has_skip_reason = True
               girsymbols[c_id] = (introspectable, has_skip_reason)
      except Exception as e:
         print("trouble reading {} - {}".format(gir_filename, e))
         have_errors = -1
         continue

   missing_gir = []
   #missing_gir = [s for s in nmsymbols if s not in girsymbols and s not in exclude_symbols] if gir_mode else []
   missing_introspect = []
   #missing_introspect = [s for s in nmsymbols if s in girsymbols and not girsymbols[s][0] and not girsymbols[s][1]] if gir_mode else []
   missing_skip = [s for s, (intro, skip) in girsymbols.items() if not intro and not skip and s not in missing_introspect] if gir_mode else []


   if missing_defs or missing_nms or doublesymbols or not sortok or missing_gir or missing_introspect or missing_skip:
      print()
      print("Problem found in", filename)

      if missing_defs:
         print("  the following symbols are in the library,")
         print("  but are not listed in the .def-file:")
         for s in missing_defs:
            print("     +", s)
         print()

      if missing_nms:
         print("  the following symbols are listed in the .def-file,")
         print("  but are not exported by the library.")
         for s in missing_nms:
            print("     -", s)
         print()

      if doublesymbols:
         print("  the following symbols are listed multiple times in the .def-file,")
         for s in doublesymbols:
            print("     : %s (line %d)" % s)
         print()

      if not sortok:
         print("  the .def-file is not properly sorted in the following cases")
         for s in sorterrors:
            if s != "":
               print("     * ", s)

      #if missing_gir:
      #   print("  the following symbols are in the library,")
      #   print("  but are not listed in the .gir-file:")
      #   for s in missing_gir:
      #      print("     +", s)
      #   print("  Please add GI annotations on the pertinent headers.")
      #   print()

      #if missing_introspect:
      #   print("  the following symbols are in both library and gir,")
      #   print("  but are implied as non-introspectable in the .gir-file:")
      #   for s in missing_introspect:
      #      print("     !", s)
      #   print("  Please add explicit `(skip)` on the pertinent headers.")
      #   print()

      if missing_skip:
         print("  the following symbols are marked as non-introspectable,")
         print("  but do not have a skip-reason attribute, which is ambiguous:")
         for s in missing_skip:
            print("     ?", s)
         print("  Please add `skip-reason` on the pertinent headers.")
         print()

      have_errors = -1

sys.exit (have_errors)
