Repository: pyinstaller/pyinstaller

Issue:
libeay32.dll and ssleay32.dll needs to be manually bundled to use PyQt5.QNetwork with SSL
If you are having errors like:

```
qt.network.ssl: QSslSocket: cannot call unresolved function SSLv23_client_method
qt.network.ssl: QSslSocket: cannot call unresolved function SSL_CTX_new
qt.network.ssl: QSslSocket: cannot call unresolved function SSL_library_init
qt.network.ssl: QSslSocket: cannot call unresolved function ERR_get_error
qt.network.ssl: QSslSocket: cannot call unresolved function ERR_get_error
```

with PyInstaller and PyQt5 on Windows, you need to manually add libeay32.dll and ssleay32.dll from your PyQt5 site-packages (probably located somewhere in `PyQt5\Qt\bin\`) to your output dir or your frozen binary in a similar path.

In my final specfile, it looks like this:

```python
# -*- mode: python -*-

block_cipher = None


a = Analysis(['cddagl\\launcher.py'],
             pathex=['C:\\Program Files (x86)\\Windows Kits\\10\\Redist\\ucrt\\DLLs\\x86\\', 'C:\\Users\\remy\\Projects\\CDDA-Game-Launcher'],
             binaries=[('C:\\Users\\remy\\VirtualEnvs\\CDDA-Game-Launcher\\lib\\site-packages\\PyQt5\\Qt\\bin\\libeay32.dll', 'PyQt5\\Qt\\bin'), ('C:\\Users\\remy\\VirtualEnvs\\CDDA-Game-Launcher\\lib\\site-packages\\PyQt5\\Qt\\bin\\ssleay32.dll', 'PyQt5\\Qt\\bin')],
             datas=[('alembic', 'alembic'), ('bin/updated.bat', '.'), ('data', 'data'), ('cddagl/resources', 'cddagl/resources'), ('C:\\Users\\remy\\AppData\\Local\\Programs\\Python\\Python36-32\\unrar.exe', '.'), ('cddagl/locale/en/LC_MESSAGES/cddagl.mo', 'cddagl/locale/en/LC_MESSAGES'), ('cddagl/locale/fr/LC_MESSAGES/cddagl.mo', 'cddagl/locale/fr/LC_MESSAGES'), ('cddagl/locale/it/LC_MESSAGES/cddagl.mo', 'cddagl/locale/it/LC_MESSAGES'), ('cddagl/locale/ru/LC_MESSAGES/cddagl.mo', 'cddagl/locale/ru/LC_MESSAGES')],
             hiddenimports=['lxml.cssselect', 'babel.numbers'],
             hookspath=[],
             runtime_hooks=[],
             excludes=[],
             win_no_prefer_redirects=False,
             win_private_assemblies=False,
             cipher=block_cipher)
pyz = PYZ(a.pure, a.zipped_data,
             cipher=block_cipher)
exe = EXE(pyz,
          a.scripts,
          a.binaries,
          a.zipfiles,
          a.datas,
          name='launcher',
          debug=True,
          strip=False,
          upx=False,
          runtime_tmpdir=None,
          console=True , icon='cddagl\\resources\\launcher.ico')
```

I'm not sure how this can be improved, but this solution should work if you are having a similar issue. This was tested with **PyInstaller==3.4.dev0+1033a8770**


Discussion:
With the latest PyQt5 PyPI wheel, I did inspect the various PyQt DLLs with Dependency Walker and they don't seem to be linked to libeay32.dll and ssleay32.dll. This might be the reason why it is not detected by PyInstaller.
Thanks for reporting this. Would you provide a simple test script that demonstrates this? I can then integrate this into the functional tests. Or, if you can, add a test similar to https://github.com/pyinstaller/pyinstaller/blob/develop/tests/functional/test_libraries.py#L247.

I'm guessing that https://github.com/pyinstaller/pyinstaller/blob/develop/PyInstaller/hooks/hook-PyQt5.QtNetwork.py should look something like:

``` Python
from PyInstaller.utils.hooks import add_qt5_dependencies, pyqt5_library_info
from PyInstaller.compat import is_win

hiddenimports, binaries, datas = add_qt5_dependencies(__file__)

if is_win:
    rel_data_path = ['PyQt5', 'Qt']
    binaries += [
        (os.path.join(pyqt5_library_info.location['LibraryPath'], 'libeay32.dll'),
         os.path.join(*rel_data_path)),
        (os.path.join(pyqt5_library_info.location['LibraryPath'], 'ssleay32.dll'),
         os.path.join(*rel_data_path))
    ]
```

Would you mind testing/debugging this?
I'll test this and I'll check if you I can add a functional test.
I believe `rel_data_path` should be slightly different in your change. I believe it should be like:

```python
rel_data_path = ['PyQt5', 'Qt', 'bin']
```
Good catch -- yes, that's correct. Thanks for the help!

Resolving patch:
diff --git a/PyInstaller/hooks/hook-PyQt5.QtNetwork.py b/PyInstaller/hooks/hook-PyQt5.QtNetwork.py
--- a/PyInstaller/hooks/hook-PyQt5.QtNetwork.py
+++ b/PyInstaller/hooks/hook-PyQt5.QtNetwork.py
@@ -6,6 +6,21 @@
 #
 # The full license is in the file COPYING.txt, distributed with this software.
 #-----------------------------------------------------------------------------
-from PyInstaller.utils.hooks import add_qt5_dependencies
+import os.path
+
+from PyInstaller.utils.hooks import pyqt5_library_info, add_qt5_dependencies
+from PyInstaller.compat import is_win
 
 hiddenimports, binaries, datas = add_qt5_dependencies(__file__)
+
+# Add libraries needed for SSL. See issue #3520.
+if is_win:
+    rel_data_path = ['PyQt5', 'Qt', 'bin']
+    binaries += [
+        (os.path.join(pyqt5_library_info.location['BinariesPath'],
+                      'libeay32.dll'),
+         os.path.join(*rel_data_path)),
+        (os.path.join(pyqt5_library_info.location['BinariesPath'],
+                      'ssleay32.dll'),
+         os.path.join(*rel_data_path))
+    ]


Test patch:
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
@@ -297,6 +297,15 @@ def test_PyQt5_QtQuick(pyi_builder):
         """)
 
 
+@importorskip('PyQt5')
+def test_PyQt5_SSL_support(pyi_builder):
+    pyi_builder.test_source(
+        """
+        from PyQt5.QtNetwork import QSslSocket
+        assert QSslSocket.supportsSsl()
+        """)
+
+
 # Test that the ``PyQt5.Qt`` module works by importing something from it.
 #
 # The Qt Bluetooth API (which any import to ``PyQt5.Qt`` implicitly imports)
@@ -799,7 +808,7 @@ def test_web3(pyi_builder):
 def test_phonenumbers(pyi_builder):
     pyi_builder.test_source("""
         import phonenumbers
-        
+
         number = '+17034820623'
         parsed_number = phonenumbers.parse(number)