File indexing completed on 2025-01-05 04:00:17

0001 #!/usr/bin/python
0002 
0003 # Python script that takes an EXE file, automatically figures out all the DLL dependencies,
0004 # and copies them next to the EXE.
0005 #
0006 # SPDX-FileCopyrightText:      2015 by Martin Preisler <martin at preisler dot me>
0007 # SPDX-FileCopyrightText: 2016-2024 by Gilles Caulier <caulier dot gilles at gmail dot com>
0008 #
0009 # Blog post         : https://martin.preisler.me/2015/03/mingw-bundledlls-automatically-bundle-dlls/
0010 # Github repository : https://github.com/mpreisler/mingw-bundledlls
0011 #
0012 # SPDX-License-Identifier: BSD-3-Clause
0013 
0014 import subprocess
0015 import os.path
0016 import argparse
0017 import shutil
0018 import string
0019 
0020 # -----------------------------------------------
0021 
0022 # Blacklist of native Windows dlls (may need extending)
0023 # Note : Lowercase file names.
0024 blacklist = [
0025     "advapi32.dll",
0026     "kernel32.dll",
0027     "msvcrt.dll",
0028     "ole32.dll",
0029     "user32.dll",
0030     "ws2_32.dll",
0031     "comdlg32.dll",
0032     "gdi32.dll",
0033     "imm32.dll",
0034     "oleaut32.dll",
0035     "shell32.dll",
0036     "winmm.dll",
0037     "winspool.drv",
0038     "wldap32.dll",
0039     "ntdll.dll",
0040     "d3d9.dll",
0041     "mpr.dll",
0042     "crypt32.dll",
0043     "dnsapi.dll",
0044     "shlwapi.dll",
0045     "version.dll",
0046     "iphlpapi.dll",
0047     "msimg32.dll",
0048     "netapi32.dll",
0049     "userenv.dll",
0050     "opengl32.dll",
0051     "secur32.dll",
0052     "psapi.dll",
0053     "wsock32.dll",
0054     "setupapi.dll",
0055     "avicap32.dll",
0056     "avifil32.dll",
0057     "comctl32.dll",
0058     "msvfw32.dll",
0059     "shfolder.dll",
0060     "odbc32.dll",
0061     "dxva2.dll",
0062     "evr.dll",
0063     "mf.dll",
0064     "mfplat.dll",
0065     "glu32.dll",
0066     "dwmapi.dll",
0067     "uxtheme.dll",
0068     "bcrypt.dll",
0069     "wtsapi32.dll",
0070     "d2d1.dll",
0071     "d3d11.dll",
0072     "dxgi.dll",
0073     "dwrite.dll",
0074     "ncrypt.dll",
0075     "dbghelp.dll",      # blacklisted dll from DrMinGW as it use MSVC dll to show debug dialog.
0076     "dbgcore.dll",      # blacklisted dll from DrMinGW.
0077     "gdiplus.dll",
0078     "urlmon.dll",
0079 ]
0080 
0081 # -----------------------------------------------
0082 
0083 def find_full_path(filename, path_prefixes):
0084 
0085     path = None
0086     #print(filename)
0087 
0088     for path_prefix in path_prefixes:
0089         path_candidate1 = os.path.join(path_prefix, filename)
0090         #print(path_candidate1)
0091 
0092         if os.path.exists(path_candidate1):
0093             path = path_candidate1
0094             #print("Found")
0095             break
0096 
0097         path_candidate2 = os.path.join(path_prefix, filename.lower())
0098         #print(path_candidate2)
0099 
0100         if os.path.exists(path_candidate2):
0101             path = path_candidate2
0102             #print("Found")
0103             break
0104 
0105     if path is None:
0106         raise RuntimeError(
0107             "Can't find " + filename + ". If it is an inbuilt Windows DLL, "
0108             "please add it to the blacklist variable in this script.")
0109 
0110     return path
0111 
0112 # -----------------------------------------------
0113 
0114 def gather_deps(path, path_prefixes, seen):
0115 
0116     ret    = [path]
0117     output = subprocess.check_output(["objdump", "-p", path]).decode('utf-8').split("\n")
0118 
0119     for line in output:
0120 
0121         if not line.startswith("\tDLL Name: "):
0122             continue
0123 
0124         dep  = line.split("DLL Name: ")[1].strip()
0125         ldep = dep.lower()
0126 
0127         #print("Searching: " + ldep)
0128 
0129         if ldep in blacklist:
0130             continue
0131 
0132         if ldep in seen:
0133             continue
0134 
0135         dep_path = find_full_path(dep, path_prefixes)
0136         seen.extend([ldep])
0137         subdeps  = gather_deps(dep_path, path_prefixes, seen)
0138 
0139         ret.extend(subdeps)
0140 
0141     return ret
0142 
0143 # -----------------------------------------------
0144 
0145 def main():
0146 
0147     parser = argparse.ArgumentParser()
0148 
0149     parser.add_argument(
0150         "--installprefix",
0151         type = str,
0152         action = "store",
0153         help = "Install prefix path in build directory."
0154     )
0155 
0156     parser.add_argument(
0157         "--efile",
0158         type = str,
0159         action = "store",
0160         help = "EXE or DLL file that you need to bundle dependencies for"
0161     )
0162 
0163     parser.add_argument(
0164         "--odir",
0165         type = str,
0166         action = "store",
0167         help = "Directory to store found dlls"
0168     )
0169 
0170     parser.add_argument(
0171         "--copy",
0172         action = "store_true",
0173         help   = "In addition to printing out the dependencies, also copy them next to the exe_file"
0174     )
0175 
0176     parser.add_argument(
0177         "--upx",
0178         action = "store_true",
0179         help   = "Only valid if --copy is provided. Run UPX on all the DLLs and EXE. See https://en.wikipedia.org/wiki/UPX for details"
0180     )
0181 
0182     args = parser.parse_args()
0183 
0184     if args.upx and not args.copy:
0185         raise RuntimeError("Can't run UPX if --copy hasn't been provided.")
0186 
0187     print("Scan dependencies for " + args.efile)
0188 
0189     # The mingw paths matches in MXE build directory
0190     default_path_prefixes = [
0191         args.installprefix + "/qt5/bin/",
0192         args.installprefix + "/bin/",
0193     ]
0194 
0195     all_deps = set(gather_deps(args.efile, default_path_prefixes, []))
0196     all_deps.remove(args.efile)
0197 
0198     #print("\n".join(all_deps))
0199 
0200     if args.copy:
0201 
0202         #print("Copying enabled, will now copy recursively all dependencies near to the exe file.\n")
0203 
0204         for dep in all_deps:
0205             target = os.path.join(args.odir, os.path.basename(dep))
0206 
0207             # Only copy target file to bundle only if it do not exists yet.
0208             if not os.path.exists(target):
0209                 print("Copying '%s' to '%s'" % (dep, target))
0210                 shutil.copy(dep, args.odir)
0211 
0212                 if args.upx:
0213                     subprocess.call(["upx", target])
0214 
0215 # -----------------------------------------------
0216 
0217 if __name__ == "__main__":
0218     main()