Repository: pyinstaller/pyinstaller

Issue:
Pyinstaller is pulling the whole Qt package in linux with PySide6
<!--
Welcome to the PyInstaller issue tracker! Before creating an issue, please heed the following:

1. This tracker should only be used to report bugs and request features / enhancements to PyInstaller
    - For questions and general support, use the mailing list, see
	  <http://www.pyinstaller.org/support.html#mailing-list>
2. Use the search function before creating a new issue. Duplicates will be closed and directed to
   the original discussion.
3. When making a bug report, make sure you provide all required information. The easier it is for
   maintainers to reproduce, the faster it'll be fixed.
-->

<!-- +++ ONLY TEXT +++ DO NOT POST IMAGES +++ -->

## Description of the issue

### Context information (for bug reports)

* Output of `pyinstaller --version`: 4.7
* Version of Python: 3.9.7
* Platform: Ubuntu 21.10 x86_64
* How you installed Python: apt
* Did you also try this on another platform? Does it work there? Both MacOS and Windows are good here.


* try the latest development version, using the following command:

```shell
pip install https://github.com/pyinstaller/pyinstaller/archive/develop.zip
```

* follow *all* the instructions in our "If Things Go Wrong" Guide
  (https://github.com/pyinstaller/pyinstaller/wiki/If-Things-Go-Wrong) and

### Make sure [everything is packaged correctly](https://github.com/pyinstaller/pyinstaller/wiki/How-to-Report-Bugs#make-sure-everything-is-packaged-correctly)

  * [x] start with clean installation
  * [x] use the latest development version
  * [x] Run your frozen program **from a command window (shell)** — instead of double-clicking on it
  * [x] Package your program in **--onedir mode**
  * [x] Package **without UPX**, say: use the option `--noupx` or set `upx=False` in your .spec-file
  * [x] Repackage you application in **verbose/debug mode**. For this, pass the option `--debug` to `pyi-makespec` or `pyinstaller` or use `EXE(..., debug=1, ...)` in your .spec file.


### A minimal example program which shows the error

Init a clean workspace

```
python3 -m venv ./venv
source ./venv/bin/activate
pip install pyside6 pyinstaller
```

PySide6 version info:

```
$ python
>>> import PySide6
>>> PySide6.__version__
'6.2.2.1'
>>> from PySide6 import QtCore
>>> QtCore.__version__
'6.2.2'
```

Just a "helloworld" example from PySide6 [document](https://doc.qt.io/qtforpython-6/tutorials/basictutorial/widgets.html)

```
import sys
from PySide6.QtWidgets import QApplication, QLabel

app = QApplication(sys.argv)
label = QLabel("Hello World!")
label.show()
app.exec()
```

Package it

```
$ pyinstaller main.py -w
```

In linux environment described above, pyinstaller produces a >600MB directory, which includes a lot of unused but huge Qt libraries such as `libQt6WebEngineCore.so.6`(175.9MB). In contrast, pyinstaller in MacOS produces a 86.9MB directory with only necessary libraries inside.


Discussion:
Hmm, that's actually PyInstaller's dependency resolution working as intended...

The problem is that in linux PySide6 wheels, all extension modules (e.g., `site-packages/PySide6/QtCore.abi3.so`) are for some reason linked against `libQt6Qml.so.6`, which causes PyInstaller to pull in `PySide6.QtQml`. And because we have no way of tracking QML imports and dependencies, we collect all QML data, which includes plugins that are linked to all the extra libraries (for example `site-packages/PySide6/Qt/qml/QtWebEngine/libqtwebenginequickplugin.so` is linked against `libQt6WebEngineCore.so.6`).

Adding `--exclude PySide6.QtQml` to your PyInstaller command should prevent `PySide6.QtQml` from being pulled in and reduce the amount of collected Qt6 libraries.

We should probably modify our hook utilities to avoid adding the `Py*.QtQml` sub-package to hiddenimports even if there is a shared library dependency, as it makes no sense for a simple widgets-based application to pull in everything...

Resolving patch:
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
@@ -510,6 +510,22 @@ def add_qt_dependencies(hook_file):
 
         logger.debug('add_qt%d_dependencies: raw lib %s -> parsed lib %s', qt_info.qt_major, imp, lib_name)
 
+        # PySide2 and PySide6 on linux seem to link all extension modules against libQt5Core, libQt5Network, and
+        # libQt5Qml (or their libQt6* equivalents). While the first two are reasonable, the libQt5Qml dependency pulls
+        # in whole QtQml module, along with its data and plugins, which in turn pull in several other Qt libraries,
+        # greatly inflating the bundle size (see #6447).
+        #
+        # Similarly, some extension modules (QtWebChannel, QtWebEngine*) seem to be also linked against libQt5Qml,
+        # even when the module can be used without having the whole QtQml module collected.
+        #
+        # Therefore, we explicitly prevent inclusion of QtQml based on the dynamic library dependency, except for
+        # QtQml* and QtQuick* modules, whose use directly implies the use of QtQml.
+        if lib_name in ("qt5qml", "qt6qml"):
+            short_module_name = module_name.split('.', 1)[-1]  # PySide2.QtModule -> QtModule
+            if not short_module_name.startswith(('QtQml', 'QtQuick')):
+                logger.debug('add_qt%d_dependencies: Ignoring import of %s.', qt_info.qt_major, imp)
+                continue
+
         # Follow only Qt dependencies.
         _qt_dynamic_dependencies_dict = (
             _qt5_dynamic_dependencies_dict if qt_info.qt_major == 5 else _qt6_dynamic_dependencies_dict


Test patch: