#!/usr/bin/env python # ------------------------------------------------------------------------------------------------------------------ # list or check dependencies for binary distributions based on MSYS2 (requires the package mingw-w64-ntldd) # # run './msys2checkdeps.py --help' for usage information # ------------------------------------------------------------------------------------------------------------------ from __future__ import print_function import argparse import os import subprocess import sys SYSTEMROOT = os.environ['SYSTEMROOT'] class Dependency: def __init__(self): self.location = None self.dependents = set() def warning(msg): print("Warning: " + msg, file=sys.stderr) def error(msg): print("Error: " + msg, file=sys.stderr) exit(1) def call_ntldd(filename): try: output = subprocess.check_output(['ntldd', '-R', filename], stderr=subprocess.STDOUT) except subprocess.CalledProcessError as e: error("'ntldd' failed with '" + str(e) + "'") except WindowsError as e: error("Calling 'ntldd' failed with '" + str(e) + "' (have you installed 'mingw-w64-ntldd-git'?)") except Exception as e: error("Calling 'ntldd' failed with '" + str(e) + "'") return output.decode('utf-8') def get_dependencies(filename, deps): raw_list = call_ntldd(filename) skip_indent = float('Inf') parents = {} parents[0] = os.path.basename(filename) for line in raw_list.splitlines(): line = line[1:] indent = len(line) - len(line.lstrip()) if indent > skip_indent: continue else: skip_indent = float('Inf') # if the dependency is not found in the working directory ntldd tries to find it on the search path # which is indicated by the string '=>' followed by the determined location or 'not found' if ('=>' in line): (lib, location) = line.lstrip().split(' => ') if location == 'not found': location = None else: location = location.rsplit('(', 1)[0].strip() else: lib = line.rsplit('(', 1)[0].strip() location = os.getcwd() parents[indent+1] = lib # we don't care about Microsoft libraries and their dependencies if location and SYSTEMROOT in location: skip_indent = indent continue if lib not in deps: deps[lib] = Dependency() deps[lib].location = location deps[lib].dependents.add(parents[indent]) return deps def collect_dependencies(path): # collect dependencies # - each key in 'deps' will be the filename of a dependency # - the corresponding value is an instance of class Dependency (containing full path and dependents) deps = {} if os.path.isfile(path): deps = get_dependencies(path, deps) elif os.path.isdir(path): extensions = ['.exe', '.pyd', '.dll'] exclusions = ['distutils/command/wininst'] # python for base, dirs, files in os.walk(path): for f in files: filepath = os.path.join(base, f) (_, ext) = os.path.splitext(f) if (ext.lower() not in extensions) or any(exclusion in filepath for exclusion in exclusions): continue deps = get_dependencies(filepath, deps) return deps if __name__ == '__main__': modes = ['list', 'list-compact', 'check', 'check-missing', 'check-unused'] # parse arguments from command line parser = argparse.ArgumentParser(description="List or check dependencies for binary distributions based on MSYS2.\n" "(requires the package 'mingw-w64-ntldd')", formatter_class=argparse.RawTextHelpFormatter) parser.add_argument('mode', metavar="MODE", choices=modes, help="One of the following:\n" " list - list dependencies in human-readable form\n" " with full path and list of dependents\n" " list-compact - list dependencies in compact form (as a plain list of filenames)\n" " check - check for missing or unused dependencies (see below for details)\n" " check-missing - check if all required dependencies are present in PATH\n" " exits with error code 2 if missing dependencies are found\n" " and prints the list to stderr\n" " check-unused - check if any of the libraries in the root of PATH are unused\n" " and prints the list to stderr") parser.add_argument('path', metavar='PATH', help="full or relative path to a single file or a directory to work on\n" "(directories will be checked recursively)") parser.add_argument('-w', '--working-directory', metavar="DIR", help="Use custom working directory (instead of 'dirname PATH')") args = parser.parse_args() # check if path exists args.path = os.path.abspath(args.path) if not os.path.exists(args.path): error("Can't find file/folder '" + args.path + "'") # get root and set it as working directory (unless one is explicitly specified) if args.working_directory: root = os.path.abspath(args.working_directory) elif os.path.isdir(args.path): root = args.path elif os.path.isfile(args.path): root = os.path.dirname(args.path) os.chdir(root) # get dependencies for path recursively deps = collect_dependencies(args.path) # print output / prepare exit code exit_code = 0 for dep in sorted(deps): location = deps[dep].location dependents = deps[dep].dependents if args.mode == 'list': if (location is None): location = '---MISSING---' print(dep + " - " + location + " (" + ", ".join(dependents) + ")") elif args.mode == 'list-compact': print(dep) elif args.mode in ['check', 'check-missing']: if ((location is None) or (root not in os.path.abspath(location))): warning("Missing dependency " + dep + " (" + ", ".join(dependents) + ")") exit_code = 2 # check for unused libraries if args.mode in ['check', 'check-unused']: installed_libs = [file for file in os.listdir(root) if file.endswith(".dll")] deps_lower = [dep.lower() for dep in deps] top_level_libs = [lib for lib in installed_libs if lib.lower() not in deps_lower] for top_level_lib in top_level_libs: warning("Unused dependency " + top_level_lib) exit(exit_code)