Home | History | Annotate | Download | only in depstest
      1 #! /usr/bin/python
      2 # -*- coding: utf-8 -*-
      3 #
      4 # Copyright (C) 2016 and later: Unicode, Inc. and others.
      5 # License & terms of use: http://www.unicode.org/copyright.html
      6 # Copyright (C) 2011-2015, International Business Machines
      7 # Corporation and others. All Rights Reserved.
      8 #
      9 # file name: depstest.py
     10 #
     11 # created on: 2011may24
     12 
     13 """ICU dependency tester.
     14 
     15 This probably works only on Linux.
     16 
     17 The exit code is 0 if everything is fine, 1 for errors, 2 for only warnings.
     18 
     19 Sample invocation:
     20   ~/svn.icu/trunk/src/source/test/depstest$ ./depstest.py ~/svn.icu/trunk/dbg
     21 """
     22 
     23 __author__ = "Markus W. Scherer"
     24 
     25 import glob
     26 import os.path
     27 import subprocess
     28 import sys
     29 
     30 import dependencies
     31 
     32 _ignored_symbols = set()
     33 _obj_files = {}
     34 _symbols_to_files = {}
     35 _return_value = 0
     36 
     37 # Classes with vtables (and thus virtual methods).
     38 _virtual_classes = set()
     39 # Classes with weakly defined destructors.
     40 # nm shows a symbol class of "W" rather than "T".
     41 _weak_destructors = set()
     42 
     43 def _ReadObjFile(root_path, library_name, obj_name):
     44   global _ignored_symbols, _obj_files, _symbols_to_files
     45   global _virtual_classes, _weak_destructors
     46   lib_obj_name = library_name + "/" + obj_name
     47   if lib_obj_name in _obj_files:
     48     print "Warning: duplicate .o file " + lib_obj_name
     49     _return_value = 2
     50     return
     51 
     52   path = os.path.join(root_path, library_name, obj_name)
     53   nm_result = subprocess.Popen(["nm", "--demangle", "--format=sysv",
     54                                 "--extern-only", "--no-sort", path],
     55                                stdout=subprocess.PIPE).communicate()[0]
     56   obj_imports = set()
     57   obj_exports = set()
     58   for line in nm_result.splitlines():
     59     fields = line.split("|")
     60     if len(fields) == 1: continue
     61     name = fields[0].strip()
     62     # Ignore symbols like '__cxa_pure_virtual',
     63     # 'vtable for __cxxabiv1::__si_class_type_info' or
     64     # 'DW.ref.__gxx_personality_v0'.
     65     # '__dso_handle' belongs to __cxa_atexit().
     66     if (name.startswith("__cxa") or "__cxxabi" in name or "__gxx" in name or
     67         name == "__dso_handle"):
     68       _ignored_symbols.add(name)
     69       continue
     70     type = fields[2].strip()
     71     if type == "U":
     72       obj_imports.add(name)
     73     else:
     74       obj_exports.add(name)
     75       _symbols_to_files[name] = lib_obj_name
     76       # Is this a vtable? E.g., "vtable for icu_49::ByteSink".
     77       if name.startswith("vtable for icu"):
     78         _virtual_classes.add(name[name.index("::") + 2:])
     79       # Is this a destructor? E.g., "icu_49::ByteSink::~ByteSink()".
     80       index = name.find("::~")
     81       if index >= 0 and type == "W":
     82         _weak_destructors.add(name[index + 3:name.index("(", index)])
     83   _obj_files[lib_obj_name] = {"imports": obj_imports, "exports": obj_exports}
     84 
     85 def _ReadLibrary(root_path, library_name):
     86   obj_paths = glob.glob(os.path.join(root_path, library_name, "*.o"))
     87   for path in obj_paths:
     88     _ReadObjFile(root_path, library_name, os.path.basename(path))
     89 
     90 def _Resolve(name, parents):
     91   global _ignored_symbols, _obj_files, _symbols_to_files, _return_value
     92   item = dependencies.items[name]
     93   item_type = item["type"]
     94   if name in parents:
     95     sys.exit("Error: %s %s has a circular dependency on itself: %s" %
     96              (item_type, name, parents))
     97   # Check if already cached.
     98   exports = item.get("exports")
     99   if exports != None: return item
    100   # Calculcate recursively.
    101   parents.append(name)
    102   imports = set()
    103   exports = set()
    104   system_symbols = item.get("system_symbols")
    105   if system_symbols == None: system_symbols = item["system_symbols"] = set()
    106   files = item.get("files")
    107   if files:
    108     for file_name in files:
    109       obj_file = _obj_files[file_name]
    110       imports |= obj_file["imports"]
    111       exports |= obj_file["exports"]
    112   imports -= exports | _ignored_symbols
    113   deps = item.get("deps")
    114   if deps:
    115     for dep in deps:
    116       dep_item = _Resolve(dep, parents)
    117       # Detect whether this item needs to depend on dep,
    118       # except when this item has no files, that is, when it is just
    119       # a deliberate umbrella group or library.
    120       dep_exports = dep_item["exports"]
    121       dep_system_symbols = dep_item["system_symbols"]
    122       if files and imports.isdisjoint(dep_exports) and imports.isdisjoint(dep_system_symbols):
    123         print "Info:  %s %s  does not need to depend on  %s\n" % (item_type, name, dep)
    124       # We always include the dependency's exports, even if we do not need them
    125       # to satisfy local imports.
    126       exports |= dep_exports
    127       system_symbols |= dep_system_symbols
    128   item["exports"] = exports
    129   item["system_symbols"] = system_symbols
    130   imports -= exports | system_symbols
    131   for symbol in imports:
    132     for file_name in files:
    133       if symbol in _obj_files[file_name]["imports"]:
    134         neededFile = _symbols_to_files.get(symbol)
    135         if neededFile in dependencies.file_to_item:
    136           neededItem = "but %s does not depend on %s (for %s)" % (name, dependencies.file_to_item[neededFile], neededFile)
    137         else:
    138           neededItem = "- is this a new system symbol?"
    139         sys.stderr.write("Error: in %s %s: %s imports %s %s\n" %
    140                          (item_type, name, file_name, symbol, neededItem))
    141     _return_value = 1
    142   del parents[-1]
    143   return item
    144 
    145 def Process(root_path):
    146   """Loads dependencies.txt, reads the libraries' .o files, and processes them.
    147 
    148   Modifies dependencies.items: Recursively builds each item's system_symbols and exports.
    149   """
    150   global _ignored_symbols, _obj_files, _return_value
    151   global _virtual_classes, _weak_destructors
    152   dependencies.Load()
    153   for name_and_item in dependencies.items.iteritems():
    154     name = name_and_item[0]
    155     item = name_and_item[1]
    156     system_symbols = item.get("system_symbols")
    157     if system_symbols:
    158       for symbol in system_symbols:
    159         _symbols_to_files[symbol] = name
    160   for library_name in dependencies.libraries:
    161     _ReadLibrary(root_path, library_name)
    162   o_files_set = set(_obj_files.keys())
    163   files_missing_from_deps = o_files_set - dependencies.files
    164   files_missing_from_build = dependencies.files - o_files_set
    165   if files_missing_from_deps:
    166     sys.stderr.write("Error: files missing from dependencies.txt:\n%s\n" %
    167                      sorted(files_missing_from_deps))
    168     _return_value = 1
    169   if files_missing_from_build:
    170     sys.stderr.write("Error: files in dependencies.txt but not built:\n%s\n" %
    171                      sorted(files_missing_from_build))
    172     _return_value = 1
    173   if not _return_value:
    174     for library_name in dependencies.libraries:
    175       _Resolve(library_name, [])
    176   if not _return_value:
    177     virtual_classes_with_weak_destructors = _virtual_classes & _weak_destructors
    178     if virtual_classes_with_weak_destructors:
    179       sys.stderr.write("Error: Some classes have virtual methods, and "
    180                        "an implicit or inline destructor "
    181                        "(see ICU ticket #8454 for details):\n%s\n" %
    182                        sorted(virtual_classes_with_weak_destructors))
    183       _return_value = 1
    184 
    185 def main():
    186   global _return_value
    187   if len(sys.argv) <= 1:
    188     sys.exit(("Command line error: " +
    189              "need one argument with the root path to the built ICU libraries/*.o files."))
    190   Process(sys.argv[1])
    191   if _ignored_symbols:
    192     print "Info: ignored symbols:\n%s" % sorted(_ignored_symbols)
    193   if not _return_value:
    194     print "OK: Specified and actual dependencies match."
    195   else:
    196     print "Error: There were errors, please fix them and re-run. Processing may have terminated abnormally."
    197   return _return_value
    198 
    199 if __name__ == "__main__":
    200   sys.exit(main())
    201