Repository: pyinstaller/pyinstaller

Issue:
PyQt5/QtQuick: qmldir and qmltypes files are not copied
I encountered an error while building a PyQt5 Qml Application. 

The PyQt5.QtQuick hook pulls in the folders QtQml, QtQuick, QtQuick.2 as expected but doesn´t pull in the "qmldir" and "*.qmltypes" files in any of the sub/folders.

After Copying them per hand the application works as expected (Without copying these files, the application quits with the following msg: QtQuick/QtQuick.Layouts/QtQuick.Controls is not installed; list index out of range).

Plattform:
Windows 7 x64 Ultimate
Python 3.4 32bit
VM within VMWare Workstation 12 Pro
PyInstaller Version 3.1.1



Discussion:
Same on OSX with PyInstaller v3.1.1

This is the hook in question: https://github.com/pyinstaller/pyinstaller/blob/develop/PyInstaller/hooks/hook-PyQt5.QtQuick.py


Resolving patch:
diff --git a/PyInstaller/hooks/hook-PyQt5.QtQuick.py b/PyInstaller/hooks/hook-PyQt5.QtQuick.py
--- a/PyInstaller/hooks/hook-PyQt5.QtQuick.py
+++ b/PyInstaller/hooks/hook-PyQt5.QtQuick.py
@@ -15,10 +15,14 @@
                  'PyQt5.QtNetwork'
                  ]
 
-from PyInstaller.utils.hooks import qt5_qml_data, qt5_qml_plugins_binaries
+from PyInstaller.utils.hooks import (
+    qt5_qml_data,
+    qt5_qml_plugins_binaries,
+    qt5_qml_plugins_datas
+)
 
 # TODO: we should parse the Qml files to see what we need to import.
-dirs = [#'Qt',
+dirs = ['Qt',
         #'QtAudioEngine',
         #'QtGraphicalEffects',
         #'QtMultiMedia',
@@ -29,8 +33,14 @@
         #'QtTest'
         ]
 
+# Add base qml directories
 datas = [qt5_qml_data(dir) for dir in dirs]
 
+# Add qmldir and *.qmltypes files
+for dir in dirs:
+    datas.extend(qt5_qml_plugins_datas(dir))
+
+# Add binaries
 binaries = []
 for dir in dirs:
     binaries.extend(qt5_qml_plugins_binaries(dir))
diff --git a/PyInstaller/utils/hooks/qt.py b/PyInstaller/utils/hooks/qt.py
--- a/PyInstaller/utils/hooks/qt.py
+++ b/PyInstaller/utils/hooks/qt.py
@@ -202,7 +202,10 @@ def qt5_qml_plugins_binaries(directory):
     """
     binaries = []
     qmldir = qt5_qml_dir()
-    files = misc.dlls_in_subdirs(os.path.join(qmldir, directory))
+
+    qt5_qml_plugin_dir = os.path.join(qmldir, directory)
+    files = misc.dlls_in_subdirs(qt5_qml_plugin_dir)
+
     for f in files:
         relpath = os.path.relpath(f, qmldir)
         instdir, file = os.path.split(relpath)
@@ -212,5 +215,29 @@ def qt5_qml_plugins_binaries(directory):
         binaries.append((f, instdir))
     return binaries
 
+
+def qt5_qml_plugins_datas(directory):
+    """
+    Return list of data files for mod.binaries. (qmldir, *.qmltypes)
+    """
+    datas = []
+    qmldir = qt5_qml_dir()
+
+    qt5_qml_plugin_dir = os.path.join(qmldir, directory)
+
+    files = []
+    for root, _dirs, _files in os.walk(qt5_qml_plugin_dir):
+        files.extend(misc.files_in_dir(root, ["qmldir", "*.qmltypes"]))
+
+    for f in files:
+        relpath = os.path.relpath(f, qmldir)
+        instdir, file = os.path.split(relpath)
+        instdir = os.path.join("qml", instdir)
+        logger.debug("qt5_qml_plugins_datas installing %s in %s"
+                     % (f, instdir))
+        datas.append((f, instdir))
+    return datas
+
+
 __all__ = ('qt_plugins_dir', 'qt_plugins_binaries', 'qt_menu_nib_dir', 'get_qmake_path', 'qt5_qml_dir', 'qt5_qml_data',
-           'qt5_qml_plugins_binaries')
+           'qt5_qml_plugins_binaries', 'qt5_qml_plugins_datas')
diff --git a/PyInstaller/utils/misc.py b/PyInstaller/utils/misc.py
--- a/PyInstaller/utils/misc.py
+++ b/PyInstaller/utils/misc.py
@@ -34,10 +34,14 @@ def dlls_in_subdirs(directory):
 
 def dlls_in_dir(directory):
     """Returns a list of *.dll, *.so, *.dylib in given directory."""
+    return files_in_dir(directory, ["*.so", "*.dll", "*.dylib"])
+
+
+def files_in_dir(directory, file_patterns=[]):
+    """Returns a list of files which match a pattern in given directory."""
     files = []
-    files.extend(glob.glob(os.path.join(directory, '*.so')))
-    files.extend(glob.glob(os.path.join(directory, '*.dll')))
-    files.extend(glob.glob(os.path.join(directory, '*.dylib')))
+    for file_pattern in file_patterns:
+        files.extend(glob.glob(os.path.join(directory, file_pattern)))
     return files
 
 
@@ -99,7 +103,7 @@ def compile_py_files(toc, workpath):
     Given a TOC or equivalent list of tuples, generates all the required
     pyc/pyo files, writing in a local directory if required, and returns the
     list of tuples with the updated pathnames.
-    
+
     In the old system using ImpTracker, the generated TOC of "pure" modules
     already contains paths to nm.pyc or nm.pyo and it is only necessary
     to check that these files are not older than the source.


Test patch: