1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177
|
#!/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
# ------------------------------------------------------------------------------------------------------------------
#
# SPDX-License-Identifier: GPL-2.0-or-later
#
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)
|