Repository: pyinstaller/pyinstaller

Issue:
PySide2 QtWebEngine missing files
When packaged with Pyinstaller importing QtWebEngine fails.
The hook for this does not fetch the needed files.
If you manually copy these files from PySide2 to the directory where your generated .exe is then it will work:
- QtWebEngineProcess.exe

This folder is in the translations folder of PySide2:
- qtwebengine_locales

These files are in the resources folder of PySide2:
- icudtl.dat
- qtwebengine_devtools_resources.pak
- qtwebengine_resources.pak
- qtwebengine_resources_100p.pak
- qtwebengine_resources_200p.pak

I've also created a modified version of the hook that copies the needed files:
https://github.com/steveway/papagayo-ng/blob/master/hooks/hook-PySide2.QtWebEngineWidgets.py
That hook works if you create a one dir version of your project but fails on the one file version.
If used with the one file version it says 
> Could not find QtWebEngineProcess.exe

I'm using Pyinstaller 3.5 with Python 3.7.2 on Windows 10

It should be easy to reproduce, likely a script that only tries to `import PySide2.QtWebEngineWidgets` will already fail.

The existing hook likely needs to be changed, possibly in the direction I have already with my costum hook.


Discussion:
Thanks for taking a look. I'd like to keep this hook similar to the (working) PyQt5 hook at https://github.com/pyinstaller/pyinstaller/blob/develop/PyInstaller/hooks/hook-PyQt5.QtWebEngineWidgets.py. Would you be available to adapt this code to PySide2?
I ran into this as well, but found I had to override a runtime hook explicitly to make it work on macos (https://github.com/tangm/pyinstaller-pyside2webview-sample/blob/master/main.py#L9). The layout for the `PySide2`. I also used a custom hook, but started from the newer PyQt5 hook (https://github.com/tangm/pyinstaller-pyside2webview-sample/blob/master/hooks/hook-PySide2.QtWebEngineWidgets.py). This is probably also the cause for #4361 .

I'm not sure if PyQt5 has the same issue, but I found that the Pyside2 module distribution has significantly different layouts between windows and mac/linux, so some ugly dir hackery was required :( 

Resolving patch:
diff --git a/PyInstaller/hooks/hook-PySide2.QtWebEngineWidgets.py b/PyInstaller/hooks/hook-PySide2.QtWebEngineWidgets.py
--- a/PyInstaller/hooks/hook-PySide2.QtWebEngineWidgets.py
+++ b/PyInstaller/hooks/hook-PySide2.QtWebEngineWidgets.py
@@ -8,44 +8,93 @@
 #-----------------------------------------------------------------------------
 
 import os
-from PyInstaller.utils.hooks import collect_data_files, get_qmake_path
+from PyInstaller.utils.hooks.qt import add_qt5_dependencies, \
+    pyside2_library_info
+from PyInstaller.utils.hooks import get_module_file_attribute, \
+    collect_system_data_files
+from PyInstaller.depend.bindepend import getImports
 import PyInstaller.compat as compat
 
-hiddenimports = ['PySide2.QtCore',
-                 'PySide2.QtGui',
-                 'PySide2.QtNetwork',
-                 'PySide2.QtWebChannel',
-                 'PySide2.QtWebEngineCore',
-                 ]
-
-# Find the additional files necessary for QtWebEngine.
-datas = (collect_data_files('PySide2', True, os.path.join('Qt', 'resources')) +
-         collect_data_files('PySide2', True, os.path.join('Qt', 'translations')) +
-         [x for x in collect_data_files('PySide2', False, os.path.join('Qt', 'bin'))
-          if x[0].endswith('QtWebEngineProcess.exe')])
-
-# Note that for QtWebEngineProcess to be able to find icudtl.dat the bundle_identifier
-# must be set to 'org.qt-project.Qt.QtWebEngineCore'. This can be done by passing
-# bundle_identifier='org.qt-project.Qt.QtWebEngineCore' to the BUNDLE command in
-# the .spec file. FIXME: This is not ideal and a better solution is required.
-qmake = get_qmake_path('5')
-if qmake:
-    libdir = compat.exec_command(qmake, "-query", "QT_INSTALL_LIBS").strip()
 
-    if compat.is_darwin:
-        binaries = [
-            (os.path.join(libdir, 'QtWebEngineCore.framework', 'Versions', '5',
-                          'Helpers', 'QtWebEngineProcess.app', 'Contents', 'MacOS', 'QtWebEngineProcess'),
-             os.path.join('QtWebEngineProcess.app', 'Contents', 'MacOS'))
-        ]
+def get_relative_path_if_possible(actual, possible_prefix):
+    possible_relative_path = os.path.relpath(actual, possible_prefix)
+    if possible_relative_path.startswith(os.pardir):
+        return actual
+    else:
+        return possible_relative_path
+
+
+def prefix_with_path(prefix_path, *paths):
+    if compat.is_py2:
+        return os.path.join(*(prefix_path + list(paths)))
+    else:
+        return os.path.join(*prefix_path, *paths)  # noqa: E999
+
 
-        resources_dir = os.path.join(libdir, 'QtWebEngineCore.framework', 'Versions', '5', 'Resources')
+# Ensure PySide2 is importable before adding info depending on it.
+if pyside2_library_info.version:
+    hiddenimports, binaries, datas = add_qt5_dependencies(__file__)
+
+    # Include the web engine process, translations, and resources.
+    # According to https://bugreports.qt.io/browse/PYSIDE-642
+    # there's no subdir for windows
+    if compat.is_win:
+        rel_data_path = ['PySide2']
+    else:
+        rel_data_path = ['PySide2', 'Qt']
+
+    pyside2_locations = pyside2_library_info.location
+    if compat.is_darwin:
+        # This is based on the layout of the Mac wheel from PyPi.
+        data_path = pyside2_locations['DataPath']
+        libraries = ['QtCore', 'QtWebEngineCore', 'QtQuick', 'QtQml',
+                     'QtNetwork', 'QtGui', 'QtWebChannel',
+                     'QtPositioning']
+        for i in libraries:
+            datas += collect_system_data_files(
+                os.path.join(data_path, 'lib', i + '.framework'),
+                prefix_with_path(rel_data_path, 'lib'), True)
+        datas += [(os.path.join(data_path, 'lib', 'QtWebEngineCore.framework',
+                                'Resources'), os.curdir)]
+    else:
+        locales = 'qtwebengine_locales'
+        resources = 'resources'
         datas += [
-            (os.path.join(resources_dir, 'icudtl.dat'), '.'),
-            (os.path.join(resources_dir, 'qtwebengine_resources.pak'), '.'),
-            # The distributed Info.plist has LSUIElement set to true, which prevents the
-            # icon from appearing in the dock.
-            (os.path.join(libdir, 'QtWebEngineCore.framework', 'Versions', '5',
-                          'Helpers', 'QtWebEngineProcess.app', 'Contents', 'Info.plist'),
-             os.path.join('QtWebEngineProcess.app', 'Contents'))
+            # Gather translations needed by Chromium.
+            (os.path.join(pyside2_locations['TranslationsPath'], locales),
+             prefix_with_path(rel_data_path, 'translations', locales)),
+            # Per the `docs
+            # <https://doc.qt.io/qt-5.10/qtwebengine-deploying.html#deploying-resources>`_,
+            # ``DataPath`` is the base directory for ``resources``.
+            #
+            (os.path.join(pyside2_locations['DataPath'], resources),
+             prefix_with_path(rel_data_path, resources)),
+            # Include the webengine process. The ``LibraryExecutablesPath``
+            # is only valid on Windows and Linux.
+            #
+            (os.path.join(pyside2_locations['LibraryExecutablesPath'],
+                          'QtWebEngineProcess*'),
+             prefix_with_path(rel_data_path, get_relative_path_if_possible(
+                 pyside2_locations['LibraryExecutablesPath'],
+                 pyside2_locations['PrefixPath'] + '/')))
         ]
+
+    # Add Linux-specific libraries.
+    if compat.is_linux:
+        # The automatic library detection fails for `NSS
+        # <https://packages.ubuntu.com/search?keywords=libnss3>`_, which is
+        # used by QtWebEngine. In some distributions, the ``libnss``
+        # supporting libraries are stored in a subdirectory ``nss``. Since
+        # ``libnss`` is not statically linked to these, but dynamically loads
+        # them, we need to search for and add them.
+        #
+        # First, get all libraries linked to ``PyQt5.QtWebEngineWidgets``.
+        for imp in getImports(
+                get_module_file_attribute('PySide2.QtWebEngineWidgets')):
+            # Look for ``libnss3.so``.
+            if os.path.basename(imp).startswith('libnss3.so'):
+                # Find the location of NSS: given a ``/path/to/libnss.so``,
+                # add ``/path/to/nss/*.so`` to get the missing NSS libraries.
+                nss_subdir = os.path.join(os.path.dirname(imp), 'nss')
+                if os.path.exists(nss_subdir):
+                    binaries.append((os.path.join(nss_subdir, '*.so'), 'nss'))
diff --git a/PyInstaller/hooks/hook-PySide2.py b/PyInstaller/hooks/hook-PySide2.py
--- a/PyInstaller/hooks/hook-PySide2.py
+++ b/PyInstaller/hooks/hook-PySide2.py
@@ -6,17 +6,23 @@
 #
 # The full license is in the file COPYING.txt, distributed with this software.
 #-----------------------------------------------------------------------------
-import os.path
 
+import os
 from PyInstaller.utils.hooks import collect_system_data_files
 from PyInstaller.utils.hooks.qt import pyside2_library_info, get_qt_binaries
+from PyInstaller.compat import is_win
 
 hiddenimports = ['shiboken2']
 
 # Collect the ``qt.conf`` file.
+if is_win:
+    target_qt_conf_dir = os.curdir
+else:
+    target_qt_conf_dir = 'PySide2'
+
 datas = [x for x in
          collect_system_data_files(pyside2_library_info.location['PrefixPath'],
-                                   'PySide2')
+                                   target_qt_conf_dir)
          if os.path.basename(x[0]) == 'qt.conf']
 
 # Collect required Qt binaries.
diff --git a/PyInstaller/loader/rthooks/pyi_rth_pyside2webengine.py b/PyInstaller/loader/rthooks/pyi_rth_pyside2webengine.py
--- a/PyInstaller/loader/rthooks/pyi_rth_pyside2webengine.py
+++ b/PyInstaller/loader/rthooks/pyi_rth_pyside2webengine.py
@@ -10,10 +10,9 @@
 import os
 import sys
 
-# See ``pyi_rth_qt5.py`: use a "standard" PyQt5 layout.
 if sys.platform == 'darwin':
     os.environ['QTWEBENGINEPROCESS_PATH'] = os.path.normpath(os.path.join(
-        sys._MEIPASS, '..', 'MacOS', 'PySide2', 'Qt', 'lib',
+        sys._MEIPASS, 'PySide2', 'Qt', 'lib',
         'QtWebEngineCore.framework', 'Helpers', 'QtWebEngineProcess.app',
         'Contents', 'MacOS', 'QtWebEngineProcess'
     ))


Test patch:
diff --git a/tests/functional/data/PySide2_QWebEngine/test_web_page.html b/tests/functional/data/PySide2_QWebEngine/test_web_page.html
new file mode 100644
--- /dev/null
+++ b/tests/functional/data/PySide2_QWebEngine/test_web_page.html
@@ -0,0 +1,11 @@
+<!doctype html>
+<html lang="en">
+    <head>
+        <meta charset="utf-8">
+        <title>Test web page</title>
+    </head>
+    <body>
+        <p>This is a test web page with internationalised characters.</p>
+        <p>HЯ⾀ÄÉÖÜ</p>
+    </body>
+</html>
diff --git a/tests/functional/test_libraries.py b/tests/functional/test_libraries.py
--- a/tests/functional/test_libraries.py
+++ b/tests/functional/test_libraries.py
@@ -432,9 +432,15 @@ def test_Qt5_QTranslate(pyi_builder, monkeypatch, QtPyLib):
         """.format(QtPyLib))
 
 
-@xfail(True, reason="Hook is old and needs updating.")
 @importorskip('PySide2')
 def test_PySide2_QWebEngine(pyi_builder, data_dir):
+    if is_darwin:
+        # QWebEngine on OS X only works with a onedir build -- onefile builds
+        # don't work. Skip the test execution for onefile builds.
+        if pyi_builder._mode != 'onedir':
+            pytest.skip('The QWebEngine .app bundle '
+                        'only supports onedir mode.')
+
     pyi_builder.test_source(get_QWebEngine_html('PySide2', data_dir),
                             **USE_WINDOWED_KWARG)