Created
February 21, 2025 07:55
-
-
Save schneidersoft/bee457435e142c8f4f14ef07876d7d7e to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#! /usr/bin/env python3 | |
""" | |
Thanks to pyWindeployqt by Alexey Elymanov ([email protected]) | |
This is a python implementation of windeployqt, which is missing in mxe | |
I don"t know why they disabled it, but here it is. | |
Example: | |
mxedeployqt --qtplugins="canbus;iconengines;imageformats;platforminputcontexts;platforms;sqldrivers;styles" --skiplibs="qsqlmysql.dll;qsqlodbc.dll;qsqlpsql.dll;qsqltds.dll" --additionallibs="libwinpthread-1.dll;icudt56.dll;icuin56.dll;icuuc56.dll" --mxepath=~/mxe/usr --qmlrootpath=$PWD/src/app $PWD/build/dist | |
This program then scans your mxe installation for available executables and dlls and copies their dependencies to the target directory | |
""" | |
__author__ = "Dinesh Manajipet ([email protected])" | |
import subprocess | |
import os | |
import sys | |
import argparse | |
import logging | |
import json | |
import shutil | |
def getoutput(cmd): | |
result = "" | |
try: | |
result = subprocess.check_output(cmd, shell=True, universal_newlines=True, stderr=subprocess.STDOUT) | |
except subprocess.CalledProcessError as e: | |
result = e.output | |
return result.strip("\n") | |
def list_dependencies(objdump, binary): | |
'''returns the list of dlls the linked to a binary, given it's path and objdump path''' | |
result = [] | |
if objdump and binary: | |
#command = objdump_path + " -p " + lib + " | grep -o ": .*\.dll$"" | |
output = getoutput(objdump + " -x " + binary).split("\n") | |
result = [ l.replace("\tDLL Name: ", "", 1).strip().lower() for l in output if l.find("DLL Name") != -1 ] | |
else: | |
logging.error("Error! Unable to fetch dependencies for" + binary + "with" + objdump) | |
return result | |
def process_binary(objdump, available_dlls, binary_path, target, skip, done = [], level = 1): | |
'''Recursively copy all the dependencies of a given binary/dll to the target directory''' | |
result = [binary_path] | |
dlls = list_dependencies(objdump, binary_path) | |
for dll in dlls: | |
if dll in available_dlls: | |
if dll not in done and dll not in skip: | |
logging.info(level*" " + "Processing: " + dll) | |
if not os.path.exists(os.path.join(target, dll)): | |
try: | |
shutil.copy(available_dlls[dll], target) | |
except shutil.SameFileError: | |
logging.warning(available_dlls[dll] + "has already been copied") | |
done.append(dll) | |
result += process_binary(objdump, available_dlls, available_dlls[dll], target, skip, done, level + 1) | |
else: | |
if dll not in skip: | |
skip.append(dll) | |
logging.warning(level*" " + dll + " Not found (SKIPPING)") | |
return result | |
def copy_dir(source_dirpath, destination_dirpath, skip): | |
logging.info("Copying:" + source_dirpath + "-> " + destination_dirpath) | |
try: | |
shutil.copytree(source_dirpath, destination_dirpath, ignore=shutil.ignore_patterns(*skip)) | |
except: | |
logging.warning("Error copying " + source_dirpath + " -> " + destination_dirpath) | |
def copy_additionaldlls(available_dlls, dlls, target): | |
'''Copy the list of dlls, if present in available_dlls, to target''' | |
result = [] | |
for dll in dlls: | |
if dll in available_dlls: | |
logging.info("Copying:" + dll) | |
try: | |
shutil.copy(available_dlls[dll], target) | |
except shutil.SameFileError: | |
logging.warning(available_dlls[dll] + " has already been copied") | |
result.append(available_dlls[dll]) | |
else: | |
logging.warning(dll + " Not found (SKIPPING)") | |
return result | |
def copy_qtplugins(qmake, qtplugins, target, skip): | |
'''Copies all qtplugins in the plugin path pointed by qmake to the target. | |
if qtplugins is None, it copies all available plugins | |
''' | |
plugins = [] | |
path = getoutput(qmake + " -query QT_INSTALL_PLUGINS") | |
if path: | |
available_plugins = [f for f in os.listdir(path) if os.path.isdir(os.path.join(path, f))] | |
if qtplugins is None: | |
qtplugins = available_plugins | |
for p in qtplugins: | |
if p in available_plugins: | |
plugins.append(p) | |
else: | |
logging.warning("Qt plugin Not found:" + p + "(SKIPPING)") | |
for p in plugins: | |
copy_dir(os.path.join(path, p), os.path.join(target, p), skip) | |
else: | |
logging.error("Unable to retrieve QT_INSTALL_PLUGINS from qmake") | |
return plugins | |
def copy_qmlmodules(qmake, qmlimportscanner, qmlrootpath, qmlmodules, target, skip): | |
'''Copy the qmlmodules to the target. | |
If qmlmodules is not provided, try try to autodetect all the imported modules in qmlrootpath. | |
If module detection isn't possible, copy all available qml modules''' | |
if (qmlrootpath == None or len(qmlrootpath) == 0 or not os.path.exists(qmlrootpath)) and qmlmodules == None: | |
logging.warning("No qmlmodules are specified and Invalid qmlrootpath provided: " + str(qmlrootpath)) | |
return False | |
qt_install_qml = getoutput(qmake + " -query QT_INSTALL_QML") | |
if qt_install_qml: | |
logging.info("Copying qml plugins from: " + qt_install_qml) | |
used_qmlimports = json.loads(getoutput(qmlimportscanner + " -rootPath " + qmlrootpath + " -importPath " + qt_install_qml)) | |
used_qmlmodules = {} | |
for i in used_qmlimports: | |
if i["type"] == "module" and "path" in i and (qmlmodules is None or i["name"] in qmlmodules): | |
used_qmlmodules[os.path.abspath(i["path"])] = os.path.join(target, os.path.relpath(i["path"], qt_install_qml)) | |
#QTBUG-48424, QTBUG-45977: In release mode, qmlimportscanner does not report | |
#the dependency of QtQuick.Controls on QtQuick.PrivateWidgets due to missing files. | |
#Recreate the run-time logic here as best as we can - deploy it if | |
# 1) QtWidgets is used | |
# 2) QtQuick.Controls is used | |
if i["name"] == "QtQuick.Controls": | |
used_qmlmodules[os.path.abspath(os.path.join(i["path"], ".." ,"PrivateWidgets"))] = os.path.join(target, "QtQuick/PrivateWidgets") | |
#Only copy the outermost directory when there are nested directories | |
sorted_modules = sorted(used_qmlmodules.keys(), key=lambda x: (x, len(x))) | |
copied_dirs = set() | |
for module_source in sorted_modules: | |
if os.path.abspath(os.path.join(module_source, os.pardir)) not in copied_dirs: | |
copy_dir(module_source, used_qmlmodules[module_source], skip) | |
copied_dirs.add(module_source) | |
else: | |
logging.error("Unable to retrieve QT_INSTALL_QMLS from qmake") | |
return True | |
def find_files(path): | |
'''Find all files in the given path and it's subdirectories''' | |
logging.info("Scanning path: " + path + " for files") | |
result = [] | |
for (d, _, files) in os.walk(path): | |
for f in files: | |
path = os.path.join(d, f) | |
if os.path.exists(path): | |
result.append(path) | |
return result | |
def create_qtconf(target, plugins, imports, qmlimports): | |
contents = ("[Paths]\n" | |
"Prefix = ./\n" | |
"Plugins = {}\n" | |
"Imports = {}\n" | |
"Qml2Imports = {}\n").format(plugins, imports, qmlimports) | |
with open(os.path.join(target, "qt.conf"), "w") as qtconf: | |
qtconf.write(contents) | |
def main(): | |
logging.basicConfig(format="[%(levelname)s] %(message)s", level=logging.DEBUG) | |
parser = argparse.ArgumentParser(conflict_handler='resolve') | |
parser.add_argument("--mxepath", | |
help="Path to mxe installation. (Default: /opt/mxe)") | |
parser.add_argument("--mxetarget", | |
help="Mxe target to use. (Default: i686-w64-mingw32.shared)") | |
parser.add_argument("--qtplugins", | |
help="; separated list of qt plugins to copy. (Default: all Qt plugins)") | |
parser.add_argument("--qmlmodules", | |
help="; separated list of qml modules to copy. (Default autodetected, if qmlrootpath is supplied and qmlimportscanner is available.") | |
parser.add_argument("--skiplibs", | |
help="; separated list of libraries to skip. (Default: None) ") | |
parser.add_argument("--additionallibs", | |
help="; separated list of libraries to copy. (Default: None) ") | |
parser.add_argument("--qmlrootpath", | |
help="Path in the source directory to scan qml files for, for using modules.") | |
parser.add_argument("--angle", | |
help="Force deployment of ANGLE libraries. (Default: no)", | |
action="store_true") | |
parser.add_argument("--binaries", | |
help="Path to binaries. (Default: same as target)") | |
parser.add_argument("target") | |
args = parser.parse_args() | |
target = os.path.expanduser(args.target) | |
mxe_path = args.mxepath | |
if mxe_path: | |
mxe_path = os.path.expanduser(mxe_path) | |
else: | |
mxe_path = "/opt/mxe" | |
mxe_target = args.mxetarget | |
if not mxe_target: | |
mxe_target = "i686-w64-mingw32.shared" | |
qt_plugins = args.qtplugins | |
if qt_plugins: | |
qt_plugins = [p for p in qt_plugins.split(";") if len(p) > 0] | |
qml_modules = args.qmlmodules | |
if qml_modules: | |
qml_modules = [m for m in qml_modules.split(";") if len(m) > 0] | |
binaries = args.binaries | |
if binaries: | |
binaries = os.path.expanduser(args.binaries) | |
else: | |
binaries = target | |
skip = args.skiplibs | |
if skip: | |
skip = skip.split(";") | |
else: | |
skip = [] | |
additionallibs = args.additionallibs | |
if additionallibs: | |
additionallibs = additionallibs.split(";") | |
else: | |
additionallibs = [] | |
if args.angle: | |
additionallibs.append('libGLESv2.dll') | |
additionallibs.append('libEGL.dll') | |
additionallibs.append('opengl32sw.dll') | |
mxe_files = find_files(mxe_path) | |
mxe_target_files = [f for f in mxe_files if f.find(mxe_target) != -1] | |
mxe_dlls = [f for f in mxe_target_files if f.lower().endswith(".dll")] | |
objdump = next(iter([f for f in mxe_target_files if f.endswith("objdump")]), None) | |
qmake = next(iter([f for f in mxe_target_files if f.endswith("qmake-qt5") or os.path.basename(f) == "qmake"]), None) | |
qmlimportscanner = next(iter([f for f in mxe_target_files if f.endswith("qmlimportscanner")]), None) | |
error = None | |
if not objdump: | |
error = "objdump not found" | |
if not qmake: | |
error = "qmake not found" | |
if len(sys.argv) == 1 or error: | |
logging.error("Error: " + error) | |
parser.print_help() | |
sys.exit(1) | |
logging.info("Using the following binaries:") | |
logging.info(" objdump: " + str(objdump)) | |
logging.info(" qmake: " + str(qmake)) | |
logging.info(" qmlimportscanner: " + str(qmlimportscanner)) | |
copy_additionaldlls(dict([(os.path.basename(f), f) for f in mxe_dlls]), additionallibs, target) | |
copy_qtplugins(qmake, qt_plugins, os.path.join(target, "plugins"), skip) | |
copy_qmlmodules(qmake, qmlimportscanner, args.qmlrootpath, qml_modules, os.path.join(target, "qml"), skip) | |
target_files = find_files(binaries) | |
target_dlls = [f for f in target_files if f.lower().endswith(".dll")] | |
target_executables = [f for f in target_files if f.lower().endswith(".exe")] | |
target_binaries = target_executables + target_dlls | |
available_dlls = mxe_dlls + target_dlls | |
#FIXME: The assumption that there is only one dll per given filename in the mxe path | |
available_dlls_dict = dict([(os.path.basename(f).lower(), f) for f in available_dlls]) | |
processsed_binaries = [] | |
for binary in target_binaries: | |
logging.info("Processing: " + binary) | |
processsed_binaries += process_binary(objdump, available_dlls_dict, binary, target, skip) | |
create_qtconf(target, "plugins", "qml", "qml") | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment