2 # ------------------------------------------------------------------------------------------------------------------
3 # list or check dependencies for binary distributions based on MSYS2 (requires the package mingw-w64-ntldd)
5 # run './msys2checkdeps.py --help' for usage information
6 # ------------------------------------------------------------------------------------------------------------------
8 # SPDX-License-Identifier: GPL-2.0-or-later
11 from __future__
import print_function
20 SYSTEMROOT
= os
.environ
['SYSTEMROOT']
26 self
.dependents
= set()
30 print("Warning: " + msg
, file=sys
.stderr
)
34 print("Error: " + msg
, file=sys
.stderr
)
38 def call_ntldd(filename
):
40 output
= subprocess
.check_output(['ntldd', '-R', filename
], stderr
=subprocess
.STDOUT
)
41 except subprocess
.CalledProcessError
as e
:
42 error("'ntldd' failed with '" + str(e
) + "'")
43 except WindowsError as e
:
44 error("Calling 'ntldd' failed with '" + str(e
) + "' (have you installed 'mingw-w64-ntldd-git'?)")
45 except Exception as e
:
46 error("Calling 'ntldd' failed with '" + str(e
) + "'")
47 return output
.decode('utf-8')
50 def get_dependencies(filename
, deps
):
51 raw_list
= call_ntldd(filename
)
53 skip_indent
= float('Inf')
55 parents
[0] = os
.path
.basename(filename
)
56 for line
in raw_list
.splitlines():
58 indent
= len(line
) - len(line
.lstrip())
59 if indent
> skip_indent
:
62 skip_indent
= float('Inf')
64 # if the dependency is not found in the working directory ntldd tries to find it on the search path
65 # which is indicated by the string '=>' followed by the determined location or 'not found'
67 (lib
, location
) = line
.lstrip().split(' => ')
68 if location
== 'not found':
71 location
= location
.rsplit('(', 1)[0].strip()
73 lib
= line
.rsplit('(', 1)[0].strip()
74 location
= os
.getcwd()
76 parents
[indent
+1] = lib
78 # we don't care about Microsoft libraries and their dependencies
79 if location
and SYSTEMROOT
in location
:
84 deps
[lib
] = Dependency()
85 deps
[lib
].location
= location
86 deps
[lib
].dependents
.add(parents
[indent
])
90 def collect_dependencies(path
):
91 # collect dependencies
92 # - each key in 'deps' will be the filename of a dependency
93 # - the corresponding value is an instance of class Dependency (containing full path and dependents)
95 if os
.path
.isfile(path
):
96 deps
= get_dependencies(path
, deps
)
97 elif os
.path
.isdir(path
):
98 extensions
= ['.exe', '.pyd', '.dll']
99 exclusions
= ['distutils/command/wininst'] # python
100 for base
, dirs
, files
in os
.walk(path
):
102 filepath
= os
.path
.join(base
, f
)
103 (_
, ext
) = os
.path
.splitext(f
)
104 if (ext
.lower() not in extensions
) or any(exclusion
in filepath
for exclusion
in exclusions
):
106 deps
= get_dependencies(filepath
, deps
)
110 if __name__
== '__main__':
111 modes
= ['list', 'list-compact', 'check', 'check-missing', 'check-unused']
113 # parse arguments from command line
114 parser
= argparse
.ArgumentParser(description
="List or check dependencies for binary distributions based on MSYS2.\n"
115 "(requires the package 'mingw-w64-ntldd')",
116 formatter_class
=argparse
.RawTextHelpFormatter
)
117 parser
.add_argument('mode', metavar
="MODE", choices
=modes
,
118 help="One of the following:\n"
119 " list - list dependencies in human-readable form\n"
120 " with full path and list of dependents\n"
121 " list-compact - list dependencies in compact form (as a plain list of filenames)\n"
122 " check - check for missing or unused dependencies (see below for details)\n"
123 " check-missing - check if all required dependencies are present in PATH\n"
124 " exits with error code 2 if missing dependencies are found\n"
125 " and prints the list to stderr\n"
126 " check-unused - check if any of the libraries in the root of PATH are unused\n"
127 " and prints the list to stderr")
128 parser
.add_argument('path', metavar
='PATH',
129 help="full or relative path to a single file or a directory to work on\n"
130 "(directories will be checked recursively)")
131 parser
.add_argument('-w', '--working-directory', metavar
="DIR",
132 help="Use custom working directory (instead of 'dirname PATH')")
133 args
= parser
.parse_args()
135 # check if path exists
136 args
.path
= os
.path
.abspath(args
.path
)
137 if not os
.path
.exists(args
.path
):
138 error("Can't find file/folder '" + args
.path
+ "'")
140 # get root and set it as working directory (unless one is explicitly specified)
141 if args
.working_directory
:
142 root
= os
.path
.abspath(args
.working_directory
)
143 elif os
.path
.isdir(args
.path
):
145 elif os
.path
.isfile(args
.path
):
146 root
= os
.path
.dirname(args
.path
)
149 # get dependencies for path recursively
150 deps
= collect_dependencies(args
.path
)
152 # print output / prepare exit code
154 for dep
in sorted(deps
):
155 location
= deps
[dep
].location
156 dependents
= deps
[dep
].dependents
158 if args
.mode
== 'list':
159 if (location
is None):
160 location
= '---MISSING---'
161 print(dep
+ " - " + location
+ " (" + ", ".join(dependents
) + ")")
162 elif args
.mode
== 'list-compact':
164 elif args
.mode
in ['check', 'check-missing']:
165 if ((location
is None) or (root
not in os
.path
.abspath(location
))):
166 warning("Missing dependency " + dep
+ " (" + ", ".join(dependents
) + ")")
169 # check for unused libraries
170 if args
.mode
in ['check', 'check-unused']:
171 installed_libs
= [file for file in os
.listdir(root
) if file.endswith(".dll")]
172 deps_lower
= [dep
.lower() for dep
in deps
]
173 top_level_libs
= [lib
for lib
in installed_libs
if lib
.lower() not in deps_lower
]
174 for top_level_lib
in top_level_libs
:
175 warning("Unused dependency " + top_level_lib
)