aboutsummaryrefslogtreecommitdiffstats
path: root/tools
diff options
context:
space:
mode:
Diffstat (limited to 'tools')
-rw-r--r--tools/checklibs.py386
-rw-r--r--tools/debug_windows.py360
-rw-r--r--tools/missing_bindings.py448
3 files changed, 1194 insertions, 0 deletions
diff --git a/tools/checklibs.py b/tools/checklibs.py
new file mode 100644
index 000000000..18aa11e93
--- /dev/null
+++ b/tools/checklibs.py
@@ -0,0 +1,386 @@
+#############################################################################
+##
+## Copyright (C) 2017 The Qt Company Ltd.
+## Contact: https://www.qt.io/licensing/
+##
+## This file is part of Qt for Python.
+##
+## $QT_BEGIN_LICENSE:LGPL$
+## Commercial License Usage
+## Licensees holding valid commercial Qt licenses may use this file in
+## accordance with the commercial license agreement provided with the
+## Software or, alternatively, in accordance with the terms contained in
+## a written agreement between you and The Qt Company. For licensing terms
+## and conditions see https://www.qt.io/terms-conditions. For further
+## information use the contact form at https://www.qt.io/contact-us.
+##
+## GNU Lesser General Public License Usage
+## Alternatively, this file may be used under the terms of the GNU Lesser
+## General Public License version 3 as published by the Free Software
+## Foundation and appearing in the file LICENSE.LGPL3 included in the
+## packaging of this file. Please review the following information to
+## ensure the GNU Lesser General Public License version 3 requirements
+## will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+##
+## GNU General Public License Usage
+## Alternatively, this file may be used under the terms of the GNU
+## General Public License version 2.0 or (at your option) the GNU General
+## Public license version 3 or any later version approved by the KDE Free
+## Qt Foundation. The licenses are as published by the Free Software
+## Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+## included in the packaging of this file. Please review the following
+## information to ensure the GNU General Public License requirements will
+## be met: https://www.gnu.org/licenses/gpl-2.0.html and
+## https://www.gnu.org/licenses/gpl-3.0.html.
+##
+## $QT_END_LICENSE$
+##
+#############################################################################
+
+#!/usr/bin/env python
+#
+# checklibs.py
+#
+# Check Mach-O dependencies.
+#
+# See http://www.entropy.ch/blog/Developer/2011/03/05/2011-Update-to-checklibs-Script-for-dynamic-library-dependencies.html
+#
+# Written by Marc Liyanage <http://www.entropy.ch>
+#
+#
+
+import subprocess, sys, re, os.path, optparse, collections
+from pprint import pprint
+
+
+class MachOFile:
+
+ def __init__(self, image_path, arch, parent = None):
+ self.image_path = image_path
+ self._dependencies = []
+ self._cache = dict(paths = {}, order = [])
+ self.arch = arch
+ self.parent = parent
+ self.header_info = {}
+ self.load_info()
+ self.add_to_cache()
+
+ def load_info(self):
+ if not self.image_path.exists():
+ return
+ self.load_header()
+ self.load_rpaths()
+
+ def load_header(self):
+ # Get the mach-o header info, we're interested in the file type
+ # (executable, dylib)
+ cmd = 'otool -arch {0} -h "{1}"'
+ output = self.shell(cmd, [self.arch, self.image_path.resolved_path],
+ fatal = True)
+ if not output:
+ print("Unable to load mach header for {} ({}), architecture "
+ "mismatch? Use --arch option to pick architecture".format(
+ self.image_path.resolved_path, self.arch), file=sys.stderr)
+ exit()
+ (keys, values) = output.splitlines()[2:]
+ self.header_info = dict(zip(keys.split(), values.split()))
+
+ def load_rpaths(self):
+ output = self.shell('otool -arch {0} -l "{1}"',
+ [self.arch, self.image_path.resolved_path], fatal = True)
+ # skip file name on first line
+ load_commands = re.split('Load command (\d+)', output)[1:]
+ self._rpaths = []
+ load_commands = collections.deque(load_commands)
+ while load_commands:
+ load_commands.popleft() # command index
+ command = load_commands.popleft().strip().splitlines()
+ if command[0].find('LC_RPATH') == -1:
+ continue
+
+ path = re.findall('path (.+) \(offset \d+\)$', command[2])[0]
+ image_path = self.image_path_for_recorded_path(path)
+ image_path.rpath_source = self
+ self._rpaths.append(image_path)
+
+ def ancestors(self):
+ ancestors = []
+ parent = self.parent
+ while parent:
+ ancestors.append(parent)
+ parent = parent.parent
+
+ return ancestors
+
+ def self_and_ancestors(self):
+ return [self] + self.ancestors()
+
+ def rpaths(self):
+ return self._rpaths
+
+ def all_rpaths(self):
+ rpaths = []
+ for image in self.self_and_ancestors():
+ rpaths.extend(image.rpaths())
+ return rpaths
+
+ def root(self):
+ if not self.parent:
+ return self
+ return self.ancestors()[-1]
+
+ def executable_path(self):
+ root = self.root()
+ if root.is_executable():
+ return root.image_path
+ return None
+
+ def filetype(self):
+ return long(self.header_info.get('filetype', 0))
+
+ def is_dylib(self):
+ return self.filetype() == MachOFile.MH_DYLIB
+
+ def is_executable(self):
+ return self.filetype() == MachOFile.MH_EXECUTE
+
+ def all_dependencies(self):
+ self.walk_dependencies()
+ return self.cache()['order']
+
+ def walk_dependencies(self, known = {}):
+ if known.get(self.image_path.resolved_path):
+ return
+
+ known[self.image_path.resolved_path] = self
+
+ for item in self.dependencies():
+ item.walk_dependencies(known)
+
+ def dependencies(self):
+ if not self.image_path.exists():
+ return []
+
+ if self._dependencies:
+ return self._dependencies
+
+ output = self.shell('otool -arch {0} -L "{1}"',
+ [self.arch, self.image_path.resolved_path], fatal = True)
+ output = [line.strip() for line in output.splitlines()]
+ del(output[0])
+ if self.is_dylib():
+ # In the case of dylibs, the first line is the id line
+ del(output[0])
+
+ self._dependencies = []
+ for line in output:
+ match = re.match('^(.+)\s+(\(.+)\)$', line)
+ if not match:
+ continue
+ recorded_path = match.group(1)
+ image_path = self.image_path_for_recorded_path(recorded_path)
+ image = self.lookup_or_make_item(image_path)
+ self._dependencies.append(image)
+
+ return self._dependencies
+
+ # The root item holds the cache, all lower-level requests bubble up
+ # the parent chain
+ def cache(self):
+ if self.parent:
+ return self.parent.cache()
+ return self._cache
+
+ def add_to_cache(self):
+ cache = self.cache()
+ cache['paths'][self.image_path.resolved_path] = self
+ cache['order'].append(self)
+
+ def cached_item_for_path(self, path):
+ if not path:
+ return None
+ return self.cache()['paths'].get(path)
+
+ def lookup_or_make_item(self, image_path):
+ image = self.cached_item_for_path(image_path.resolved_path)
+ if not image: # cache miss
+ image = MachOFile(image_path, self.arch, parent = self)
+ return image
+
+ def image_path_for_recorded_path(self, recorded_path):
+ path = ImagePath(None, recorded_path)
+
+ # handle @executable_path
+ if recorded_path.startswith(ImagePath.EXECUTABLE_PATH_TOKEN):
+ executable_image_path = self.executable_path()
+ if executable_image_path:
+ path.resolved_path = os.path.normpath(
+ recorded_path.replace(
+ ImagePath.EXECUTABLE_PATH_TOKEN,
+ os.path.dirname(executable_image_path.resolved_path)))
+
+ # handle @loader_path
+ elif recorded_path.startswith(ImagePath.LOADER_PATH_TOKEN):
+ path.resolved_path = os.path.normpath(recorded_path.replace(
+ ImagePath.LOADER_PATH_TOKEN,
+ os.path.dirname(self.image_path.resolved_path)))
+
+ # handle @rpath
+ elif recorded_path.startswith(ImagePath.RPATH_TOKEN):
+ for rpath in self.all_rpaths():
+ resolved_path = os.path.normpath(recorded_path.replace(
+ ImagePath.RPATH_TOKEN, rpath.resolved_path))
+ if os.path.exists(resolved_path):
+ path.resolved_path = resolved_path
+ path.rpath_source = rpath.rpath_source
+ break
+
+ # handle absolute path
+ elif recorded_path.startswith('/'):
+ path.resolved_path = recorded_path
+
+ return path
+
+ def __repr__(self):
+ return str(self.image_path)
+
+ def dump(self):
+ print(self.image_path)
+ for dependency in self.dependencies():
+ print('\t{0}'.format(dependency))
+
+ @staticmethod
+ def shell(cmd_format, args, fatal = False):
+ cmd = cmd_format.format(*args)
+ popen = subprocess.Popen(cmd, shell = True, stdout = subprocess.PIPE)
+ output = popen.communicate()[0]
+ if popen.returncode and fatal:
+ print("Nonzero exit status for shell command '{}'".format(cmd),
+ file=sys.stderr)
+ sys.exit(1)
+
+ return output
+
+ @classmethod
+ def architectures_for_image_at_path(cls, path):
+ output = cls.shell('file "{}"', [path])
+ file_architectures = re.findall(r' executable (\w+)', output)
+ ordering = 'x86_64 i386'.split()
+ file_architectures = sorted(file_architectures, lambda a, b: cmp(
+ ordering.index(a), ordering.index(b)))
+ return file_architectures
+
+ MH_EXECUTE = 0x2
+ MH_DYLIB = 0x6
+ MH_BUNDLE = 0x8
+
+
+# ANSI terminal coloring sequences
+class Color:
+ HEADER = '\033[95m'
+ BLUE = '\033[94m'
+ GREEN = '\033[92m'
+ RED = '\033[91m'
+ ENDC = '\033[0m'
+
+ @staticmethod
+ def red(string):
+ return Color.wrap(string, Color.RED)
+
+ @staticmethod
+ def blue(string):
+ return Color.wrap(string, Color.BLUE)
+
+ @staticmethod
+ def wrap(string, color):
+ return Color.HEADER + color + string + Color.ENDC
+
+
+# This class holds path information for a mach-0 image file.
+# It holds the path as it was recorded in the loading binary as well as
+# the effective, resolved file system path.
+# The former can contain @-replacement tokens.
+# In the case where the recorded path contains an @rpath token that was
+# resolved successfully, we also capture the path of the binary that
+# supplied the rpath value that was used.
+# That path itself can contain replacement tokens such as @loader_path.
+class ImagePath:
+
+ def __init__(self, resolved_path, recorded_path = None):
+ self.recorded_path = recorded_path
+ self.resolved_path = resolved_path
+ self.rpath_source = None
+
+ def __repr__(self):
+ description = None
+
+ if self.resolved_equals_recorded() or self.recorded_path == None:
+ description = self.resolved_path
+ else:
+ description = '{0} ({1})'.format(self.resolved_path,
+ self.recorded_path)
+
+ if (not self.is_system_location()) and (not self.uses_dyld_token()):
+ description = Color.blue(description)
+
+ if self.rpath_source:
+ description += ' (rpath source: {0})'.format(
+ self.rpath_source.image_path.resolved_path)
+
+ if not self.exists():
+ description += Color.red(' (missing)')
+
+ return description
+
+ def exists(self):
+ return self.resolved_path and os.path.exists(self.resolved_path)
+
+ def resolved_equals_recorded(self):
+ return (self.resolved_path and self.recorded_path and
+ self.resolved_path == self.recorded_path)
+
+ def uses_dyld_token(self):
+ return self.recorded_path and self.recorded_path.startswith('@')
+
+ def is_system_location(self):
+ system_prefixes = ['/System/Library', '/usr/lib']
+ for prefix in system_prefixes:
+ if self.resolved_path and self.resolved_path.startswith(prefix):
+ return True
+
+ EXECUTABLE_PATH_TOKEN = '@executable_path'
+ LOADER_PATH_TOKEN = '@loader_path'
+ RPATH_TOKEN = '@rpath'
+
+
+# Command line driver
+parser = optparse.OptionParser(
+ usage = "Usage: %prog [options] path_to_mach_o_file")
+parser.add_option(
+ "--arch", dest = "arch", help = "architecture", metavar = "ARCH")
+parser.add_option(
+ "--all", dest = "include_system_libraries",
+ help = "Include system frameworks and libraries", action="store_true")
+(options, args) = parser.parse_args()
+
+if len(args) < 1:
+ parser.print_help()
+ sys.exit(1)
+
+archs = MachOFile.architectures_for_image_at_path(args[0])
+if archs and not options.arch:
+ print('Analyzing architecture {}, override with --arch if needed'.format(
+ archs[0]), file=sys.stderr)
+ options.arch = archs[0]
+
+toplevel_image = MachOFile(ImagePath(args[0]), options.arch)
+
+for dependency in toplevel_image.all_dependencies():
+ if (dependency.image_path.exists() and
+ (not options.include_system_libraries) and
+ dependency.image_path.is_system_location()):
+ continue
+
+ dependency.dump()
+ print("\n")
+
diff --git a/tools/debug_windows.py b/tools/debug_windows.py
new file mode 100644
index 000000000..ab1c03aba
--- /dev/null
+++ b/tools/debug_windows.py
@@ -0,0 +1,360 @@
+#############################################################################
+##
+## Copyright (C) 2018 The Qt Company Ltd.
+## Contact: https://www.qt.io/licensing/
+##
+## This file is part of Qt for Python.
+##
+## $QT_BEGIN_LICENSE:LGPL$
+## Commercial License Usage
+## Licensees holding valid commercial Qt licenses may use this file in
+## accordance with the commercial license agreement provided with the
+## Software or, alternatively, in accordance with the terms contained in
+## a written agreement between you and The Qt Company. For licensing terms
+## and conditions see https://www.qt.io/terms-conditions. For further
+## information use the contact form at https://www.qt.io/contact-us.
+##
+## GNU Lesser General Public License Usage
+## Alternatively, this file may be used under the terms of the GNU Lesser
+## General Public License version 3 as published by the Free Software
+## Foundation and appearing in the file LICENSE.LGPL3 included in the
+## packaging of this file. Please review the following information to
+## ensure the GNU Lesser General Public License version 3 requirements
+## will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+##
+## GNU General Public License Usage
+## Alternatively, this file may be used under the terms of the GNU
+## General Public License version 2.0 or (at your option) the GNU General
+## Public license version 3 or any later version approved by the KDE Free
+## Qt Foundation. The licenses are as published by the Free Software
+## Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+## included in the packaging of this file. Please review the following
+## information to ensure the GNU General Public License requirements will
+## be met: https://www.gnu.org/licenses/gpl-2.0.html and
+## https://www.gnu.org/licenses/gpl-3.0.html.
+##
+## $QT_END_LICENSE$
+##
+###############
+
+"""
+This is a troubleshooting script that assists finding out which DLLs or
+which symbols in a DLL are missing when executing a PySide2 python
+script.
+It can also be used with any other non Python executable.
+
+Usage: python debug_windows.py
+ When no arguments are given the script will try to import
+ PySide2.QtCore.
+
+Usage: python debug_windows.py python -c "import PySide2.QtWebEngine"
+ python debug_windows.py my_executable.exe arg1 arg2 --arg3=4
+ Any arguments given after the script name will be considered
+ as the target executable and the arguments passed to that
+ executable.
+
+The script requires administrator privileges.
+
+The script uses cdb.exe and gflags.exe under the hood, which are
+installed together with the Windows Kit found at:
+https://developer.microsoft.com/en-us/windows/downloads/windows-10-sdk
+
+"""
+
+from __future__ import print_function
+
+import sys
+import re
+import subprocess
+import ctypes
+import logging
+import argparse
+from os import path
+from textwrap import dedent
+
+is_win = sys.platform == "win32"
+is_py_3 = sys.version_info[0] == 3
+if is_win:
+ if is_py_3:
+ import winreg
+ else:
+ import _winreg as winreg
+ import exceptions
+
+
+def get_parser_args():
+ desc_msg = "Run an executable under cdb with loader snaps set."
+ help_msg = "Pass the executable and the arguments passed to it as a list."
+ parser = argparse.ArgumentParser(description=desc_msg)
+ parser.add_argument('args', nargs='*', help=help_msg)
+ # Prepend -- so that python options like '-c' are ignored by
+ # argparse.
+ massaged_args = ['--'] + sys.argv[1:]
+ return parser.parse_args(massaged_args)
+
+
+parser_args = get_parser_args()
+verbose_log_file_name = path.join(path.dirname(path.abspath(__file__)),
+ 'log_debug_windows.txt')
+
+
+def is_admin():
+ try:
+ return ctypes.windll.shell32.IsUserAnAdmin()
+ except Exception as e:
+ log.error("is_admin: Exception error: {}".format(e))
+ return False
+
+
+def get_verbose_logger():
+ handler = logging.FileHandler(verbose_log_file_name, mode='w')
+ main_logger = logging.getLogger('main')
+ main_logger.setLevel(logging.INFO)
+ main_logger.addHandler(handler)
+ return main_logger
+
+
+def get_non_verbose_logger():
+ handler = logging.StreamHandler()
+ main_logger = logging.getLogger('main.non_verbose')
+ main_logger.setLevel(logging.INFO)
+ main_logger.addHandler(handler)
+ return main_logger
+
+
+big_log = get_verbose_logger()
+log = get_non_verbose_logger()
+
+
+def sub_keys(key):
+ i = 0
+ while True:
+ try:
+ sub_key = winreg.EnumKey(key, i)
+ yield sub_key
+ i += 1
+ except WindowsError as e:
+ log.error(e)
+ break
+
+
+def sub_values(key):
+ i = 0
+ while True:
+ try:
+ v = winreg.EnumValue(key, i)
+ yield v
+ i += 1
+ except WindowsError as e:
+ log.error(e)
+ break
+
+
+def get_installed_windows_kits():
+ roots_key = r"SOFTWARE\Microsoft\Windows Kits\Installed Roots"
+ log.info("Searching for Windows kits in registry path: "
+ "{}".format(roots_key))
+ roots = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, roots_key, 0,
+ winreg.KEY_READ)
+ kits = []
+ pattern = re.compile(r'KitsRoot(\d+)')
+
+ for (name, value, value_type) in sub_values(roots):
+ if value_type == winreg.REG_SZ and name.startswith('KitsRoot'):
+ match = pattern.search(name)
+ if match:
+ version = match.group(1)
+ kits.append({'version': version, 'value': value})
+
+ if not kits:
+ log.error(dedent("""
+ No windows kits found in the registry.
+ Consider downloading and installing the latest kit, either from
+ https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/debugger-download-tools
+ or from
+ https://developer.microsoft.com/en-us/windows/downloads/windows-10-sdk
+ """))
+ exit(1)
+ return kits
+
+
+def get_appropriate_kit(kits):
+ # Fixme, figure out if there is a more special way to choose a kit
+ # and not just latest version.
+ log.info("Found Windows kits are: {}".format(kits))
+ chosen_kit = {'version': "0", 'value': None}
+ for kit in kits:
+ if (kit['version'] > chosen_kit['version'] and
+ # version 8.1 is actually '81', so consider everything
+ # above version 20, as '2.0', etc.
+ kit['version'] < "20"):
+ chosen_kit = kit
+ first_kit = kits[0]
+ return first_kit
+
+
+def get_cdb_and_gflags_path(kits):
+ first_kit = get_appropriate_kit(kits)
+ first_path_path = first_kit['value']
+ log.info('Using kit found at {}'.format(first_path_path))
+ bits = 'x64' if (sys.maxsize > 2 ** 32) else 'x32'
+ debuggers_path = path.join(first_path_path, 'Debuggers', bits)
+ cdb_path = path.join(debuggers_path, 'cdb.exe')
+ if not path.exists(cdb_path): # Try for older "Debugging Tools" packages
+ debuggers_path = "C:\\Program Files\\Debugging Tools for Windows (x64)"
+ cdb_path = path.join(debuggers_path, 'cdb.exe')
+
+ if not path.exists(cdb_path):
+ log.error("Couldn't find cdb.exe at: {}.".format(cdb_path))
+ exit(1)
+ else:
+ log.info("Found cdb.exe at: {}.".format(cdb_path))
+
+ gflags_path = path.join(debuggers_path, 'gflags.exe')
+
+ if not path.exists(gflags_path):
+ log.error('Couldn\'t find gflags.exe at: {}.'.format(gflags_path))
+ exit(1)
+ else:
+ log.info('Found gflags.exe at: {}.'.format(cdb_path))
+
+ return cdb_path, gflags_path
+
+
+def toggle_loader_snaps(executable_name, gflags_path, enable=True):
+ arg = '+sls' if enable else '-sls'
+ gflags_args = [gflags_path, '-i', executable_name, arg]
+ try:
+ log.info('Invoking gflags: {}'.format(gflags_args))
+ output = subprocess.check_output(gflags_args, stderr=subprocess.STDOUT,
+ universal_newlines=True)
+ log.info(output)
+ except exceptions.WindowsError as e:
+ log.error("\nRunning {} exited with exception: "
+ "\n{}".format(gflags_args, e))
+ exit(1)
+ except subprocess.CalledProcessError as e:
+ log.error("\nRunning {} exited with: {} and stdout was: "
+ "{}".format(gflags_args, e.returncode, e.output))
+ exit(1)
+
+
+def find_error_like_snippets(content):
+ snippets = []
+ lines = content.splitlines()
+ context_lines = 4
+
+ def error_predicate(l):
+ # A list of mostly false positives are filtered out.
+ # For deeper inspection, the full log exists.
+ errors = {'errorhandling',
+ 'windowserrorreporting',
+ 'core-winrt-error',
+ 'RtlSetLastWin32Error',
+ 'RaiseInvalid16BitExeError',
+ 'BaseWriteErrorElevationRequiredEvent',
+ 'for DLL "Unknown"',
+ 'LdrpGetProcedureAddress',
+ 'X509_STORE_CTX_get_error',
+ 'ERR_clear_error',
+ 'ERR_peek_last_error',
+ 'ERR_error_string',
+ 'ERR_get_error',
+ ('ERROR: Module load completed but symbols could '
+ 'not be loaded')}
+ return (re.search('error', l, re.IGNORECASE)
+ and all(e not in errors for e in errors))
+
+ for i in range(1, len(lines)):
+ line = lines[i]
+ if error_predicate(line):
+ snippets.append(lines[i - context_lines:i + context_lines + 1])
+
+ return snippets
+
+
+def print_error_snippets(snippets):
+ if len(snippets) > 0:
+ log.info("\nThe following possible errors were found:\n")
+
+ for i in range(1, len(snippets)):
+ log.info("Snippet {}:".format(i))
+ for line in snippets[i]:
+ log.info(line)
+ log.info("")
+
+
+def call_command_under_cdb_with_gflags(executable_path, args):
+ executable_name = path.basename(executable_path)
+ invocation = [executable_path] + args
+
+ kits = get_installed_windows_kits()
+ cdb_path, gflags_path = get_cdb_and_gflags_path(kits)
+
+ toggle_loader_snaps(executable_name, gflags_path, enable=True)
+
+ log.info("Debugging the following command invocation: "
+ "{}".format(invocation))
+
+ cdb_args = [cdb_path] + invocation
+
+ log.info('Invoking cdb: {}'.format(cdb_args))
+
+ p = subprocess.Popen(cdb_args,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ stdin=subprocess.PIPE,
+ shell=False)
+
+ # Symbol fix, start process, print all thread stack traces, exit.
+ cdb_commands = ['.symfix', 'g', '!uniqstack', 'q']
+ cdb_commands_text = '\n'.join(cdb_commands)
+ out, err = p.communicate(input=cdb_commands_text.encode('utf-8'))
+
+ out_decoded = out.decode('utf-8')
+ big_log.info('stdout: {}'.format(out_decoded))
+ if err:
+ big_log.info('stderr: {}'.format(err.decode('utf-8')))
+
+ log.info('Finished execution of process under cdb.')
+
+ toggle_loader_snaps(executable_name, gflags_path, enable=False)
+
+ snippets = find_error_like_snippets(out_decoded)
+ print_error_snippets(snippets)
+
+ log.info("Finished processing.\n !!! Full log can be found at:\n"
+ "{}".format(verbose_log_file_name))
+
+
+def test_run_import_qt_core_under_cdb_with_gflags():
+ # The weird characters are there for faster grepping of the output
+ # because there is a lot of content in the full log.
+ # The 2+2 is just ensure that Python itself works.
+ python_code = """
+print(">>>>>>>>>>>>>>>>>>>>>>> Test computation of 2+2 is: {}".format(2+2))
+import PySide2.QtCore
+print(">>>>>>>>>>>>>>>>>>>>>>> QtCore object instance: {}".format(PySide2.QtCore))
+"""
+ call_command_under_cdb_with_gflags(sys.executable, ["-c", python_code])
+
+
+def handle_args():
+ if not parser_args.args:
+ test_run_import_qt_core_under_cdb_with_gflags()
+ else:
+ call_command_under_cdb_with_gflags(parser_args.args[0],
+ parser_args.args[1:])
+
+
+if __name__ == '__main__':
+ if not is_win:
+ log.error("This script only works on Windows.")
+ exit(1)
+
+ if is_admin():
+ handle_args()
+ else:
+ log.error("Please rerun the script with administrator privileges. "
+ "It is required for gflags.exe to work. ")
+ exit(1)
diff --git a/tools/missing_bindings.py b/tools/missing_bindings.py
new file mode 100644
index 000000000..8f3c0b808
--- /dev/null
+++ b/tools/missing_bindings.py
@@ -0,0 +1,448 @@
+#############################################################################
+##
+## Copyright (C) 2017 The Qt Company Ltd.
+## Contact: https://www.qt.io/licensing/
+##
+## This file is part of Qt for Python.
+##
+## $QT_BEGIN_LICENSE:LGPL$
+## Commercial License Usage
+## Licensees holding valid commercial Qt licenses may use this file in
+## accordance with the commercial license agreement provided with the
+## Software or, alternatively, in accordance with the terms contained in
+## a written agreement between you and The Qt Company. For licensing terms
+## and conditions see https://www.qt.io/terms-conditions. For further
+## information use the contact form at https://www.qt.io/contact-us.
+##
+## GNU Lesser General Public License Usage
+## Alternatively, this file may be used under the terms of the GNU Lesser
+## General Public License version 3 as published by the Free Software
+## Foundation and appearing in the file LICENSE.LGPL3 included in the
+## packaging of this file. Please review the following information to
+## ensure the GNU Lesser General Public License version 3 requirements
+## will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+##
+## GNU General Public License Usage
+## Alternatively, this file may be used under the terms of the GNU
+## General Public License version 2.0 or (at your option) the GNU General
+## Public license version 3 or any later version approved by the KDE Free
+## Qt Foundation. The licenses are as published by the Free Software
+## Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+## included in the packaging of this file. Please review the following
+## information to ensure the GNU General Public License requirements will
+## be met: https://www.gnu.org/licenses/gpl-2.0.html and
+## https://www.gnu.org/licenses/gpl-3.0.html.
+##
+## $QT_END_LICENSE$
+##
+#############################################################################
+
+# This script is used to generate a summary of missing types / classes
+# which are present in C++ Qt5, but are missing in PySide2.
+#
+# Required packages: bs4
+# Installed via: pip install bs4
+#
+# The script uses beautiful soup 4 to parse out the class names from
+# the online Qt documentation. It then tries to import the types from
+# PySide2.
+#
+# Example invocation of script:
+# python missing_bindings.py --qt-version 5.9 -w all
+# --qt-version - specify which version of qt documentation to load.
+# -w - if PyQt5 is an installed package, check if the tested
+# class also exists there.
+
+from __future__ import print_function
+
+try:
+ import urllib.request as urllib2
+except ImportError:
+ import urllib2
+
+import argparse
+from bs4 import BeautifulSoup
+from collections import OrderedDict
+from time import gmtime, strftime
+import sys
+import os.path
+
+modules_to_test = OrderedDict()
+
+# Essentials
+modules_to_test['QtCore'] = 'qtcore-module.html'
+modules_to_test['QtGui'] = 'qtgui-module.html'
+modules_to_test['QtMultimedia'] = 'qtmultimedia-module.html'
+modules_to_test['QtMultimediaWidgets'] = 'qtmultimediawidgets-module.html'
+modules_to_test['QtNetwork'] = 'qtnetwork-module.html'
+modules_to_test['QtQml'] = 'qtqml-module.html'
+modules_to_test['QtQuick'] = 'qtquick-module.html'
+modules_to_test['QtSql'] = 'qtsql-module.html'
+modules_to_test['QtTest'] = 'qttest-module.html'
+modules_to_test['QtWidgets'] = 'qtwidgets-module.html'
+
+# Addons
+modules_to_test['Qt3DCore'] = 'qt3dcore-module.html'
+modules_to_test['Qt3DInput'] = 'qt3dinput-module.html'
+modules_to_test['Qt3DLogic'] = 'qt3dlogic-module.html'
+modules_to_test['Qt3DRender'] = 'qt3drender-module.html'
+modules_to_test['Qt3DAnimation'] = 'qt3danimation-module.html'
+modules_to_test['Qt3DExtras'] = 'qt3dextras-module.html'
+modules_to_test['QtConcurrent'] = 'qtconcurrent-module.html'
+modules_to_test['QtNetworkAuth'] = 'qtnetworkauth-module.html'
+modules_to_test['QtHelp'] = 'qthelp-module.html'
+modules_to_test['QtLocation'] = 'qtlocation-module.html'
+modules_to_test['QtPrintSupport'] = 'qtprintsupport-module.html'
+modules_to_test['QtSCXML'] = 'qtscxml-module.html'
+modules_to_test['QtSpeech'] = 'qtspeech-module.html'
+modules_to_test['QtSvg'] = 'qtsvg-module.html'
+modules_to_test['QtUiTools'] = 'qtuitools-module.html'
+modules_to_test['QtWebChannel'] = 'qtwebchannel-module.html'
+modules_to_test['QtWebEngine'] = 'qtwebengine-module.html'
+modules_to_test['QtWebEngineWidgets'] = 'qtwebenginewidgets-module.html'
+modules_to_test['QtWebSockets'] = 'qtwebsockets-module.html'
+modules_to_test['QtMacExtras'] = 'qtmacextras-module.html'
+modules_to_test['QtX11Extras'] = 'qtx11extras-module.html'
+modules_to_test['QtWinExtras'] = 'qtwinextras-module.html'
+modules_to_test['QtXml'] = 'qtxml-module.html'
+modules_to_test['QtXmlPatterns'] = 'qtxmlpatterns-module.html'
+modules_to_test['QtCharts'] = 'qtcharts-module.html'
+modules_to_test['QtDataVisualization'] = 'qtdatavisualization-module.html'
+
+types_to_ignore = set()
+# QtCore
+types_to_ignore.add('QFlag')
+types_to_ignore.add('QFlags')
+types_to_ignore.add('QGlobalStatic')
+types_to_ignore.add('QDebug')
+types_to_ignore.add('QDebugStateSaver')
+types_to_ignore.add('QMetaObject.Connection')
+types_to_ignore.add('QPointer')
+types_to_ignore.add('QAssociativeIterable')
+types_to_ignore.add('QSequentialIterable')
+types_to_ignore.add('QStaticPlugin')
+types_to_ignore.add('QChar')
+types_to_ignore.add('QLatin1Char')
+types_to_ignore.add('QHash')
+types_to_ignore.add('QMultiHash')
+types_to_ignore.add('QLinkedList')
+types_to_ignore.add('QList')
+types_to_ignore.add('QMap')
+types_to_ignore.add('QMultiMap')
+types_to_ignore.add('QMap.key_iterator')
+types_to_ignore.add('QPair')
+types_to_ignore.add('QQueue')
+types_to_ignore.add('QScopedArrayPointer')
+types_to_ignore.add('QScopedPointer')
+types_to_ignore.add('QScopedValueRollback')
+types_to_ignore.add('QMutableSetIterator')
+types_to_ignore.add('QSet')
+types_to_ignore.add('QSet.const_iterator')
+types_to_ignore.add('QSet.iterator')
+types_to_ignore.add('QExplicitlySharedDataPointer')
+types_to_ignore.add('QSharedData')
+types_to_ignore.add('QSharedDataPointer')
+types_to_ignore.add('QEnableSharedFromThis')
+types_to_ignore.add('QSharedPointer')
+types_to_ignore.add('QWeakPointer')
+types_to_ignore.add('QStack')
+types_to_ignore.add('QLatin1String')
+types_to_ignore.add('QString')
+types_to_ignore.add('QStringRef')
+types_to_ignore.add('QStringList')
+types_to_ignore.add('QStringMatcher')
+types_to_ignore.add('QVarLengthArray')
+types_to_ignore.add('QVector')
+types_to_ignore.add('QFutureIterator')
+types_to_ignore.add('QHashIterator')
+types_to_ignore.add('QMutableHashIterator')
+types_to_ignore.add('QLinkedListIterator')
+types_to_ignore.add('QMutableLinkedListIterator')
+types_to_ignore.add('QListIterator')
+types_to_ignore.add('QMutableListIterator')
+types_to_ignore.add('QMapIterator')
+types_to_ignore.add('QMutableMapIterator')
+types_to_ignore.add('QSetIterator')
+types_to_ignore.add('QMutableVectorIterator')
+types_to_ignore.add('QVectorIterator')
+
+# QtGui
+types_to_ignore.add('QIconEnginePlugin')
+types_to_ignore.add('QImageIOPlugin')
+types_to_ignore.add('QGenericPlugin')
+types_to_ignore.add('QGenericPluginFactory')
+types_to_ignore.add('QGenericMatrix')
+types_to_ignore.add('QOpenGLExtraFunctions')
+types_to_ignore.add('QOpenGLFunctions')
+types_to_ignore.add('QOpenGLFunctions_1_0')
+types_to_ignore.add('QOpenGLFunctions_1_1')
+types_to_ignore.add('QOpenGLFunctions_1_2')
+types_to_ignore.add('QOpenGLFunctions_1_3')
+types_to_ignore.add('QOpenGLFunctions_1_4')
+types_to_ignore.add('QOpenGLFunctions_1_5')
+types_to_ignore.add('QOpenGLFunctions_2_0')
+types_to_ignore.add('QOpenGLFunctions_2_1')
+types_to_ignore.add('QOpenGLFunctions_3_0')
+types_to_ignore.add('QOpenGLFunctions_3_1')
+types_to_ignore.add('QOpenGLFunctions_3_2_Compatibility')
+types_to_ignore.add('QOpenGLFunctions_3_2_Core')
+types_to_ignore.add('QOpenGLFunctions_3_3_Compatibility')
+types_to_ignore.add('QOpenGLFunctions_3_3_Core')
+types_to_ignore.add('QOpenGLFunctions_4_0_Compatibility')
+types_to_ignore.add('QOpenGLFunctions_4_0_Core')
+types_to_ignore.add('QOpenGLFunctions_4_1_Compatibility')
+types_to_ignore.add('QOpenGLFunctions_4_1_Core')
+types_to_ignore.add('QOpenGLFunctions_4_2_Compatibility')
+types_to_ignore.add('QOpenGLFunctions_4_2_Core')
+types_to_ignore.add('QOpenGLFunctions_4_3_Compatibility')
+types_to_ignore.add('QOpenGLFunctions_4_3_Core')
+types_to_ignore.add('QOpenGLFunctions_4_4_Compatibility')
+types_to_ignore.add('QOpenGLFunctions_4_4_Core')
+types_to_ignore.add('QOpenGLFunctions_4_5_Compatibility')
+types_to_ignore.add('QOpenGLFunctions_4_5_Core')
+types_to_ignore.add('QOpenGLFunctions_ES2')
+
+# QtWidgets
+types_to_ignore.add('QItemEditorCreator')
+types_to_ignore.add('QStandardItemEditorCreator')
+types_to_ignore.add('QStylePlugin')
+
+# QtSql
+types_to_ignore.add('QSqlDriverCreator')
+types_to_ignore.add('QSqlDriverPlugin')
+
+qt_documentation_website_prefixes = OrderedDict()
+qt_documentation_website_prefixes['5.6'] = 'http://doc.qt.io/qt-5.6/'
+qt_documentation_website_prefixes['5.8'] = 'http://doc.qt.io/qt-5.8/'
+qt_documentation_website_prefixes['5.9'] = 'http://doc.qt.io/qt-5.9/'
+qt_documentation_website_prefixes['5.10'] = 'http://doc.qt.io/qt-5.10/'
+qt_documentation_website_prefixes['5.11'] = 'http://doc.qt.io/qt-5.11/'
+qt_documentation_website_prefixes['5.11'] = 'http://doc.qt.io/qt-5.11/'
+qt_documentation_website_prefixes['5.12'] = 'http://doc.qt.io/qt-5.12/'
+qt_documentation_website_prefixes['dev'] = 'http://doc-snapshots.qt.io/qt5-dev/'
+
+
+def qt_version_to_doc_prefix(version):
+ if version in qt_documentation_website_prefixes:
+ return qt_documentation_website_prefixes[version]
+ else:
+ raise RuntimeError("The specified qt version is not supported")
+
+
+def create_doc_url(module_doc_page_url, version):
+ return qt_version_to_doc_prefix(version) + module_doc_page_url
+
+parser = argparse.ArgumentParser()
+parser.add_argument("module",
+ default='all',
+ choices=list(modules_to_test.keys()).append('all'),
+ nargs='?',
+ type=str,
+ help="the Qt module for which to get the missing types")
+parser.add_argument("--qt-version",
+ "-v",
+ default='5.12',
+ choices=['5.6', '5.9', '5.11', '5.12', 'dev'],
+ type=str,
+ dest='version',
+ help="the Qt version to use to check for types")
+parser.add_argument("--which-missing",
+ "-w",
+ default='all',
+ choices=['all', 'in-pyqt', 'not-in-pyqt'],
+ type=str,
+ dest='which_missing',
+ help="Which missing types to show (all, or just those "
+ "that are not present in PyQt)")
+
+args = parser.parse_args()
+
+if hasattr(args, "module") and args.module != 'all':
+ saved_value = modules_to_test[args.module]
+ modules_to_test.clear()
+ modules_to_test[args.module] = saved_value
+
+pyside_package_name = "PySide2"
+pyqt_package_name = "PyQt5"
+
+total_missing_types_count = 0
+total_missing_types_count_compared_to_pyqt = 0
+total_missing_modules_count = 0
+
+wiki_file = open('missing_bindings_for_wiki_qt_io.txt', 'w')
+wiki_file.truncate()
+
+
+def log(*pargs, **kw):
+ print(*pargs)
+
+ computed_str = ''
+ for arg in pargs:
+ computed_str += str(arg)
+
+ style = 'text'
+ if 'style' in kw:
+ style = kw['style']
+
+ if style == 'heading1':
+ computed_str = '= ' + computed_str + ' ='
+ elif style == 'heading5':
+ computed_str = '===== ' + computed_str + ' ====='
+ elif style == 'with_newline':
+ computed_str += '\n'
+ elif style == 'bold_colon':
+ computed_str = computed_str.replace(':', ":'''")
+ computed_str += "'''"
+ computed_str += '\n'
+ elif style == 'error':
+ computed_str = "''" + computed_str.strip('\n') + "''\n"
+ elif style == 'text_with_link':
+ computed_str = computed_str
+ elif style == 'code':
+ computed_str = ' ' + computed_str
+ elif style == 'end':
+ return
+
+ print(computed_str, file=wiki_file)
+
+log('PySide2 bindings for Qt {}'.format(args.version), style='heading1')
+
+log("""Using Qt version {} documentation to find public API Qt types and test
+if the types are present in the PySide2 package.""".format(args.version))
+
+log("""Results are usually stored at
+https://wiki.qt.io/PySide2_Missing_Bindings
+so consider taking the contents of the generated
+missing_bindings_for_wiki_qt_io.txt file and updating the linked wiki page.""",
+style='end')
+
+log("""Similar report:
+https://gist.github.com/ethanhs/6c626ca4e291f3682589699296377d3a""",
+style='text_with_link')
+
+python_executable = os.path.basename(sys.executable or '')
+command_line_arguments = ' '.join(sys.argv)
+report_date = strftime("%Y-%m-%d %H:%M:%S %Z", gmtime())
+
+log("""
+This report was generated by running the following command:
+ {} {}
+on the following date:
+ {}
+""".format(python_executable, command_line_arguments, report_date))
+
+for module_name in modules_to_test.keys():
+ log(module_name, style='heading5')
+
+ url = create_doc_url(modules_to_test[module_name], args.version)
+ log('Documentation link: {}\n'.format(url), style='text_with_link')
+
+ # Import the tested module
+ try:
+ pyside_tested_module = getattr(__import__(pyside_package_name,
+ fromlist=[module_name]), module_name)
+ except Exception as e:
+ log('\nCould not load {}.{}. Received error: {}. Skipping.\n'.format(
+ pyside_package_name, module_name, str(e).replace("'", '')),
+ style='error')
+ total_missing_modules_count += 1
+ continue
+
+ try:
+ pyqt_module_name = module_name
+ if module_name == "QtCharts":
+ pyqt_module_name = module_name[:-1]
+
+ pyqt_tested_module = getattr(__import__(pyqt_package_name,
+ fromlist=[pyqt_module_name]), pyqt_module_name)
+ except Exception as e:
+ log("\nCould not load {}.{} for comparison. "
+ "Received error: {}.\n".format(pyqt_package_name, module_name,
+ str(e).replace("'", '')), style='error')
+
+ # Get C++ class list from documentation page.
+ page = urllib2.urlopen(url)
+ soup = BeautifulSoup(page, 'html.parser')
+
+ # Extract the Qt type names from the documentation classes table
+ links = soup.body.select('.annotated a')
+ types_on_html_page = []
+
+ for link in links:
+ link_text = link.text
+ link_text = link_text.replace('::', '.')
+ if link_text not in types_to_ignore:
+ types_on_html_page.append(link_text)
+
+ log('Number of types in {}: {}'.format(module_name,
+ len(types_on_html_page)), style='bold_colon')
+
+ missing_types_count = 0
+ missing_types_compared_to_pyqt = 0
+ missing_types = []
+ for qt_type in types_on_html_page:
+ try:
+ pyside_qualified_type = 'pyside_tested_module.'
+
+ if "QtCharts" == module_name:
+ pyside_qualified_type += 'QtCharts.'
+ elif "DataVisualization" in module_name:
+ pyside_qualified_type += 'QtDataVisualization.'
+
+ pyside_qualified_type += qt_type
+ eval(pyside_qualified_type)
+ except:
+ missing_type = qt_type
+ missing_types_count += 1
+ total_missing_types_count += 1
+
+ is_present_in_pyqt = False
+ try:
+ pyqt_qualified_type = 'pyqt_tested_module.'
+
+ if "Charts" in module_name:
+ pyqt_qualified_type += 'QtCharts.'
+ elif "DataVisualization" in module_name:
+ pyqt_qualified_type += 'QtDataVisualization.'
+
+ pyqt_qualified_type += qt_type
+ eval(pyqt_qualified_type)
+ missing_type += " (is present in PyQt5)"
+ missing_types_compared_to_pyqt += 1
+ total_missing_types_count_compared_to_pyqt += 1
+ is_present_in_pyqt = True
+ except:
+ pass
+
+ if args.which_missing == 'all':
+ missing_types.append(missing_type)
+ elif args.which_missing == 'in-pyqt' and is_present_in_pyqt:
+ missing_types.append(missing_type)
+ elif (args.which_missing == 'not-in-pyqt' and
+ not is_present_in_pyqt):
+ missing_types.append(missing_type)
+
+ if len(missing_types) > 0:
+ log('Missing types in {}:'.format(module_name), style='with_newline')
+ missing_types.sort()
+ for missing_type in missing_types:
+ log(missing_type, style='code')
+ log('')
+
+ log('Number of missing types: {}'.format(missing_types_count),
+ style='bold_colon')
+ if len(missing_types) > 0:
+ log('Number of missing types that are present in PyQt5: {}'
+ .format(missing_types_compared_to_pyqt), style='bold_colon')
+ log('End of missing types for {}\n'.format(module_name), style='end')
+ else:
+ log('', style='end')
+
+log('Summary', style='heading5')
+log('Total number of missing types: {}'.format(total_missing_types_count),
+ style='bold_colon')
+log('Total number of missing types that are present in PyQt5: {}'
+ .format(total_missing_types_count_compared_to_pyqt), style='bold_colon')
+log('Total number of missing modules: {}'
+ .format(total_missing_modules_count), style='bold_colon')
+wiki_file.close()