#! /usr/bin/python -B # -*- coding: utf-8 -*- # # Copyright (C) 2016 and later: Unicode, Inc. and others. # License & terms of use: http://www.unicode.org/copyright.html # Copyright (C) 2011-2015, International Business Machines # Corporation and others. All Rights Reserved. # # file name: depstest.py # # created on: 2011may24 """ICU dependency tester. This probably works only on Linux. The exit code is 0 if everything is fine, 1 for errors, 2 for only warnings. Sample invocation: ~/svn.icu/trunk/src/source/test/depstest$ ./depstest.py ~/svn.icu/trunk/dbg """ __author__ = "Markus W. Scherer" import glob import os.path import subprocess import sys import dependencies _ignored_symbols = set() _obj_files = {} _symbols_to_files = {} _return_value = 0 # Classes with vtables (and thus virtual methods). _virtual_classes = set() # Classes with weakly defined destructors. # nm shows a symbol class of "W" rather than "T". _weak_destructors = set() def _ReadObjFile(root_path, library_name, obj_name): global _ignored_symbols, _obj_files, _symbols_to_files global _virtual_classes, _weak_destructors lib_obj_name = library_name + "/" + obj_name if lib_obj_name in _obj_files: print "Warning: duplicate .o file " + lib_obj_name _return_value = 2 return path = os.path.join(root_path, library_name, obj_name) nm_result = subprocess.Popen(["nm", "--demangle", "--format=sysv", "--extern-only", "--no-sort", path], stdout=subprocess.PIPE).communicate()[0] obj_imports = set() obj_exports = set() for line in nm_result.splitlines(): fields = line.split("|") if len(fields) == 1: continue name = fields[0].strip() # Ignore symbols like '__cxa_pure_virtual', # 'vtable for __cxxabiv1::__si_class_type_info' or # 'DW.ref.__gxx_personality_v0'. # '__dso_handle' belongs to __cxa_atexit(). if (name.startswith("__cxa") or "__cxxabi" in name or "__gxx" in name or name == "__dso_handle"): _ignored_symbols.add(name) continue type = fields[2].strip() if type == "U": obj_imports.add(name) else: obj_exports.add(name) _symbols_to_files[name] = lib_obj_name # Is this a vtable? E.g., "vtable for icu_49::ByteSink". if name.startswith("vtable for icu"): _virtual_classes.add(name[name.index("::") + 2:]) # Is this a destructor? E.g., "icu_49::ByteSink::~ByteSink()". index = name.find("::~") if index >= 0 and type == "W": _weak_destructors.add(name[index + 3:name.index("(", index)]) _obj_files[lib_obj_name] = {"imports": obj_imports, "exports": obj_exports} def _ReadLibrary(root_path, library_name): obj_paths = glob.glob(os.path.join(root_path, library_name, "*.o")) for path in obj_paths: _ReadObjFile(root_path, library_name, os.path.basename(path)) def _Resolve(name, parents): global _ignored_symbols, _obj_files, _symbols_to_files, _return_value item = dependencies.items[name] item_type = item["type"] if name in parents: sys.exit("Error: %s %s has a circular dependency on itself: %s" % (item_type, name, parents)) # Check if already cached. exports = item.get("exports") if exports != None: return item # Calculcate recursively. parents.append(name) imports = set() exports = set() system_symbols = item.get("system_symbols") if system_symbols == None: system_symbols = item["system_symbols"] = set() files = item.get("files") if files: for file_name in files: obj_file = _obj_files[file_name] imports |= obj_file["imports"] exports |= obj_file["exports"] imports -= exports | _ignored_symbols deps = item.get("deps") if deps: for dep in deps: dep_item = _Resolve(dep, parents) # Detect whether this item needs to depend on dep, # except when this item has no files, that is, when it is just # a deliberate umbrella group or library. dep_exports = dep_item["exports"] dep_system_symbols = dep_item["system_symbols"] if files and imports.isdisjoint(dep_exports) and imports.isdisjoint(dep_system_symbols): print "Info: %s %s does not need to depend on %s\n" % (item_type, name, dep) # We always include the dependency's exports, even if we do not need them # to satisfy local imports. exports |= dep_exports system_symbols |= dep_system_symbols item["exports"] = exports item["system_symbols"] = system_symbols imports -= exports | system_symbols for symbol in imports: for file_name in files: if symbol in _obj_files[file_name]["imports"]: neededFile = _symbols_to_files.get(symbol) if neededFile in dependencies.file_to_item: neededItem = "but %s does not depend on %s (for %s)" % (name, dependencies.file_to_item[neededFile], neededFile) else: neededItem = "- is this a new system symbol?" sys.stderr.write("Error: in %s %s: %s imports %s %s\n" % (item_type, name, file_name, symbol, neededItem)) _return_value = 1 del parents[-1] return item def Process(root_path): """Loads dependencies.txt, reads the libraries' .o files, and processes them. Modifies dependencies.items: Recursively builds each item's system_symbols and exports. """ global _ignored_symbols, _obj_files, _return_value global _virtual_classes, _weak_destructors dependencies.Load() for name_and_item in dependencies.items.iteritems(): name = name_and_item[0] item = name_and_item[1] system_symbols = item.get("system_symbols") if system_symbols: for symbol in system_symbols: _symbols_to_files[symbol] = name for library_name in dependencies.libraries: _ReadLibrary(root_path, library_name) o_files_set = set(_obj_files.keys()) files_missing_from_deps = o_files_set - dependencies.files files_missing_from_build = dependencies.files - o_files_set if files_missing_from_deps: sys.stderr.write("Error: files missing from dependencies.txt:\n%s\n" % sorted(files_missing_from_deps)) _return_value = 1 if files_missing_from_build: sys.stderr.write("Error: files in dependencies.txt but not built:\n%s\n" % sorted(files_missing_from_build)) _return_value = 1 if not _return_value: for library_name in dependencies.libraries: _Resolve(library_name, []) if not _return_value: virtual_classes_with_weak_destructors = _virtual_classes & _weak_destructors if virtual_classes_with_weak_destructors: sys.stderr.write("Error: Some classes have virtual methods, and " "an implicit or inline destructor " "(see ICU ticket #8454 for details):\n%s\n" % sorted(virtual_classes_with_weak_destructors)) _return_value = 1 def main(): global _return_value if len(sys.argv) <= 1: sys.exit(("Command line error: " + "need one argument with the root path to the built ICU libraries/*.o files.")) Process(sys.argv[1]) if _ignored_symbols: print "Info: ignored symbols:\n%s" % sorted(_ignored_symbols) if not _return_value: print "OK: Specified and actual dependencies match." else: print "Error: There were errors, please fix them and re-run. Processing may have terminated abnormally." return _return_value if __name__ == "__main__": sys.exit(main())