Repository: marcelotduarte/cx_Freeze (language: python)

Issue:
PySide6.7.2 + cx_Freeze=7.1.1 + Ubuntu 20.04 very big size of build directory - not used qt plugins and *.so files duplicates many times
**Describe the bug**

I attach simple Qt App like this:

```
import sys

from PySide6.QtCore import Qt
from PySide6.QtWidgets import QApplication, QLabel, QWidget


def main() -> int:
    app = QApplication(sys.argv)
    window = QWidget()
    window.setWindowTitle("Simple")
    window.setGeometry(300, 300, 300, 300)
    label = QLabel(window)
    label.setText("Hello World!")
    label.setGeometry(0, 0, 300, 300)
    label.setAlignment(Qt.AlignCenter)
    window.show()
    return app.exec()


if __name__ == "__main__":
    sys.exit(main())
```

After build with cx_Freeze the build folder size is 1Gb ! It is to big.
1) Inside build folder i found many folder that not necessary in this app - many qt plugins. After i delete it build folder size became smaller - about 230 Mb. This is quite acceptable. And after deleting - app in build folder still work. I wrote small script for this deletion - it in attachment.

2) Many *.so files in build folder has duplicate - like libicudata.so.73 - it size about 30 Mb - 
first location --- build/exe.linux-x86_64-3.12/lib/PySide6/Qt/lib/ 
second location --- build/exe.linux-x86_64-3.12/lib/PySide6/Qt/plugins/platforms/ 
third location --- build/exe.linux-x86_64-3.12/lib/PySide6/Qt/plugins/xcbglintegrations/

It is 3*30 Mb instead of 30 Mb And there are many such files - like libQt6Core.so.6 (7Mb), libQt6Gui.so.6 (10 MB) and other.

I replace duplicate of some of this files to sym link - and all still work.

If i use simple app with QWebEngine it's even worse. The size of build folder about 1.6 Gb before cleaning.

I try this simple project on Win10 and bug not present there. The size is approximately the same as after cleaning in Ubuntu.

**To Reproduce**
[cx_Freeze.zip](https://github.com/user-attachments/files/16463667/cx_Freeze.zip).

**Expected behavior**
Remove not used Qt plugins, create sym links in build process?

**Desktop:**
 - Ubuntu Linux 20.04:
 - amd64:
 - cx_Freeze  7.1.1:
 - Python version [3.9 - 3.12]:




Discussion / hints:
We're using `cx_Freeze` for building Embeetle IDE (see https://embeetle.com). Recently, we noticed also a blow up in size of `PyQt6` on our Linux builds.
Hi @marcelotduarte ,
Do you have any news on this issue (the size blow up on Linux when building `PyQt6`) ?

Hi @kristofmulier 
This issue is in the top 5 of my list. I want to resolve the issues with Qt before I can release a version with Python 3.13.
Great to hear that @marcelotduarte .
In the meantime, I use the following workaround to replace the duplicates with symlinks on Linux:

```python
def find_duplicated_files(root_dir: str = '.',
                          ignore_filenames: Optional[List[str]] = None,
                          ) -> Dict[str, List[Tuple[str, int, str]]]:
    """
    Scan the directory structure starting from root_dir and identify duplicate files based on
    content.

    Args:
        root_dir (str): The root directory to start scanning from.
        ignore_filenames (Optional[List[str]]): A list of filenames to ignore during scanning.

    Returns:
        Dict[str, List[Tuple[str, int, str]]]: A dictionary where each key is a file hash and the
        value is a list of tuples containing (filename, size, full_path) for each duplicate file.
    """
    if ignore_filenames is None:
        ignore_filenames = ["py.typed", "__init__.pyc", "generator.pyc"]
    file_map: Dict[str, List[Tuple[str, int, str]]] = {}

    for dirpath, _, filenames in os.walk(root_dir):
        for filename in filenames:
            if filename in ignore_filenames:
                continue  # Skip files that should be ignored
            full_path: str = os.path.join(dirpath, filename)
            try:
                size: int = os.path.getsize(full_path)
            except OSError as e:
                print(f"ERROR: Cannot get size of '{full_path}': {e}")
                continue

            hash_value: Optional[str] = compute_file_hash(full_path)
            if hash_value is None:
                print(f"ERROR: Cannot hash file '{full_path}'")
                continue

            if hash_value in file_map:
                file_map[hash_value].append((filename, size, full_path))
            else:
                file_map[hash_value] = [(filename, size, full_path)]

    # Identify duplicated files (same content)
    duplicated_files: Dict[str, List[Tuple[str, int, str]]] = {
        hash_value: file_tuples for hash_value, file_tuples in file_map.items() if len(file_tuples) > 1
    }
    return duplicated_files

def replace_with_symlinks(duplicated_files: Dict[str, List[Tuple[str, int, str]]],
                          dry_run: bool = True,
                          ) -> None:
    """
    Replace duplicate files with symbolic links pointing to the original file.

    Args:
        duplicated_files (Dict[str, List[Tuple[str, int, str]]]): A dictionary of duplicates
                                                                  identified by their hash.
        dry_run (bool): If True, the function will only simulate the replacement without making
                        changes.

    Returns:
        None
    """
    total_size_saved: int = 0
    for hash_value, file_tuples in duplicated_files.items():
        # All files have the same content
        size: int = file_tuples[0][1]  # All files have same size
        size_mb: float = size / (1024 * 1024)
        # print(f"Processing duplicated files (Hash: {hash_value}, Size: {size_mb:.2f} MB)")
        # print("Files:")
        # for filename, size, full_path in file_tuples:
        #     print(f"    {full_path}")
        # Keep the first file, replace others with symlinks
        original_file: str = file_tuples[0][2]  # full_path of the first file
        duplicates: List[Tuple[str, int, str]] = file_tuples[1:]  # Rest of the files

        for filename, size, duplicate_path in duplicates:
            # Calculate relative path from duplicate to original
            relative_path: str = os.path.relpath(original_file, os.path.dirname(duplicate_path))

            # If not in dry-run mode, replace duplicate with symlink
            if not dry_run:
                try:
                    # Remove the duplicate file
                    os.remove(duplicate_path)
                    # Create a symlink pointing to the original file
                    os.symlink(relative_path, duplicate_path)
                    print(f"Replaced '{duplicate_path}' with symlink to '{relative_path}'")
                except OSError as e:
                    print(f"ERROR: Cannot process '{duplicate_path}': {e}")
            else:
                print(f"Would replace '{duplicate_path}' with symlink to '{relative_path}'")

            total_size_saved += size

        print()

    total_size_saved_mb: float = total_size_saved / (1024 * 1024)
    if dry_run:
        print(f"Total size that can be eliminated: {total_size_saved_mb:.2f} MB")
    else:
        print(f"Total size eliminated: {total_size_saved_mb:.2f} MB")

# RUN BOTH FUNCTIONS
duplicated_files = find_duplicated_files(
    root_dir=f'{output_folder}/lib/PyQt6',
)
replace_with_symlinks(duplicated_files, dry_run=False)
```

Fix patch:
diff --git a/cx_Freeze/finder.py b/cx_Freeze/finder.py
index e8ad4bf87..e17936aea 100644
--- a/cx_Freeze/finder.py
+++ b/cx_Freeze/finder.py
@@ -94,6 +94,7 @@ def __init__(
         self._exclude_unused_modules()
         self._tmp_dir = TemporaryDirectory(prefix="cxfreeze-")
         self.cache_path = Path(self._tmp_dir.name)
+        self.optional_files: dict[Path, Path] = {}
 
     def _add_module(
         self,
diff --git a/cx_Freeze/freezer.py b/cx_Freeze/freezer.py
index 02ab96d41..b649c7283 100644
--- a/cx_Freeze/freezer.py
+++ b/cx_Freeze/freezer.py
@@ -930,26 +930,32 @@ def _post_copy_hook(
         target: Path,
         copy_dependent_files: bool,
     ) -> None:
-        if (
+        if not (
             copy_dependent_files
             and source not in self.finder.excluded_dependent_files
         ):
-            library_dir = self.target_dir / "lib"
-            source_dir = source.parent
-            target_dir = target.parent
-            platform_bin_path = self._platform_bin_path
-            for dependent_source in self.get_dependent_files(source):
-                if not self._should_copy_file(dependent_source):
-                    continue
-                # put the dependency in the target_dir (except C runtime)
-                dependent_srcdir = dependent_source.parent
-                dependent_name = dependent_source.name
+            return
+        library_dir = self.target_dir / "lib"
+        source_dir = source.parent
+        target_dir = target.parent
+        optional_files = self.finder.optional_files
+        platform_bin_path = self._platform_bin_path
+        for dependent in self.get_dependent_files(source):
+            if not self._should_copy_file(dependent):
+                continue
+            dependent_source = dependent.resolve()
+            dependent_name = dependent_source.name
+            optional = optional_files.get(dependent_source)
+            if optional:
+                dependent_target = self.target_dir / optional
+            else:
+                # put the dependency (relatively) in the target_dir subtree
+                # this is possible with most packages installed by pip
                 try:
-                    # dependency located with source or in a subdirectory
                     relative = dependent_source.relative_to(source_dir)
-                    dependent_target = target_dir / relative
                 except ValueError:
                     # check if dependency is on default binaries path
+                    dependent_srcdir = dependent_source.parent
                     if dependent_srcdir in platform_bin_path:
                         dependent_target = library_dir / dependent_name
                     else:
@@ -961,13 +967,16 @@ def _post_copy_hook(
                             dependent_target = Path(*parts) / dependent_name
                         except ValueError:
                             dependent_target = target_dir / dependent_name
+                else:
+                    # dependency located with source or in a subdirectory
+                    dependent_target = target_dir / relative
                 # make sure the dependency is in the correct directory because
                 # it cannot be outside the library_dir directory subtree
                 try:
                     dependent_target.relative_to(library_dir)
                 except ValueError:
                     dependent_target = library_dir / dependent_name
-                else:
+                if 0:  # else:
                     _, dependent_target = self._pre_copy_hook(
                         dependent_source, dependent_target
                     )
@@ -976,9 +985,9 @@ def _post_copy_hook(
                             if file.match(dependent_name):
                                 dependent_target = file
                                 break
-                self._copy_file(
-                    dependent_source, dependent_target, copy_dependent_files
-                )
+            self._copy_file(
+                dependent_source, dependent_target, copy_dependent_files
+            )
 
     def _default_bin_excludes(self) -> list[str]:
         return ["comctl32.dll", "oci.dll"]
@@ -1028,7 +1037,7 @@ def _platform_bin_path(self) -> list[Path]:
             if dest_relative:
                 paths.add(prefix / dest_relative)
         # return only valid paths
-        return [path for path in paths if path.is_dir()]
+        return [path.resolve() for path in paths if path.is_dir()]
 
     def _platform_add_extra_dependencies(
         self, dependent_files: set[Path]
@@ -1230,22 +1239,34 @@ def _post_copy_hook(
         target: Path,
         copy_dependent_files: bool,
     ) -> None:
-        if (
+        if not (
             copy_dependent_files
             and source not in self.finder.excluded_dependent_files
         ):
-            source_dir = source.parent
-            target_dir = target.parent
-            fix_rpath = set()
-            for dependent in self.get_dependent_files(source):
-                if not self._should_copy_file(dependent):
-                    continue
-                dependent_file = dependent.resolve()
+            return
+        source_dir = source.parent
+        target_dir = target.parent
+        optional_files = self.finder.optional_files
+        fix_rpath = set()
+        for dependent in self.get_dependent_files(source):
+            if not self._should_copy_file(dependent):
+                continue
+            dependent_source = dependent.resolve()
+            dependent_name = dependent_source.name
+            optional = optional_files.get(dependent_source)
+            if optional:
+                dependent_target = self.target_dir / optional
+                try:
+                    relative = dependent_target.relative_to(target_dir)
+                except ValueError:
+                    relative = Path(
+                        os.path.relpath(dependent_target, target_dir)
+                    )
+            else:
                 # put the dependency (relatively) in the target_dir subtree
                 # this is possible with most packages installed by pip
-                dependent_name = dependent_file.name
                 try:
-                    relative = dependent_file.relative_to(source_dir)
+                    relative = dependent_source.relative_to(source_dir)
                 except ValueError:
                     # put the dependency in target_dir along with the binary
                     # file being copied, unless the dependency has already been
@@ -1264,19 +1285,20 @@ def _post_copy_hook(
                                 dependent_target = file
                             break
                 else:
+                    # dependency located with source or in a subdirectory
                     dependent_target = target_dir / relative
-                dep_libs = os.fspath(relative.parent)
-                fix_rpath.add(dep_libs)
-                self._copy_file(
-                    dependent_file, dependent_target, copy_dependent_files
-                )
-                if dependent.name != dependent_name:
-                    self.replace_needed(target, dependent.name, dependent_name)
-            if fix_rpath:
-                has_rpath = self.get_rpath(target)
-                rpath = ":".join(f"$ORIGIN/{r}" for r in fix_rpath)
-                if has_rpath != rpath:
-                    self.set_rpath(target, rpath)
+            dep_libs = os.fspath(relative.parent)
+            fix_rpath.add(dep_libs)
+            self._copy_file(
+                dependent_source, dependent_target, copy_dependent_files
+            )
+            if dependent.name != dependent_name:
+                self.replace_needed(target, dependent.name, dependent_name)
+        if fix_rpath:
+            has_rpath = self.get_rpath(target)
+            rpath = ":".join(f"$ORIGIN/{r}" for r in fix_rpath)
+            if has_rpath != rpath:
+                self.set_rpath(target, rpath)
 
     def _copy_top_dependency(self, source: Path) -> None:
         """Called for copying the top dependencies in _freeze_executable."""
diff --git a/cx_Freeze/hooks/_qthooks.py b/cx_Freeze/hooks/_qthooks.py
index a02c09c5d..62210e35a 100644
--- a/cx_Freeze/hooks/_qthooks.py
+++ b/cx_Freeze/hooks/_qthooks.py
@@ -13,10 +13,9 @@
 from typing import TYPE_CHECKING
 
 from cx_Freeze._compat import IS_CONDA, IS_MACOS, IS_MINGW, IS_WINDOWS
-from cx_Freeze.finder import ModuleFinder
-from cx_Freeze.parser import PEParser
 
 if TYPE_CHECKING:
+    from cx_Freeze.finder import ModuleFinder
     from cx_Freeze.module import Module
 
 
@@ -108,7 +107,7 @@ def _qt_libraryinfo_paths(name: str) -> dict[str, tuple[Path, Path]]:
             target = target_base
         else:
             target = target_base / source.relative_to(prefix_path)
-        data[key] = source, target
+        data[key] = source.resolve(), target
 
     # debug
     if os.environ.get("QT_DEBUG"):
@@ -125,14 +124,10 @@ def get_qt_paths(name: str, variable: str) -> tuple[Path, Path]:
     return (source_path, target_path)
 
 
-def get_qt_files(
-    finder: ModuleFinder, name: str, variable: str, arg: str
+def _get_qt_files(
+    name: str, variable: str, arg: str
 ) -> list[tuple[Path, Path]]:
     """Helper function to get Qt plugins, resources, translations, etc."""
-    if IS_WINDOWS:
-        source_bin_path, target_bin_path = get_qt_paths(name, "BinariesPath")
-        parser = PEParser(finder.path, [source_bin_path])
-
     source_path, target_path = get_qt_paths(name, variable)
     if source_path.joinpath(arg).is_dir():
         source_path = source_path / arg
@@ -140,30 +135,24 @@ def get_qt_files(
         pattern = "*"
     else:
         pattern = arg
-    include_files = []
-    for source in source_path.glob(pattern):
-        if IS_WINDOWS:
-            include_files += [
-                (file, f"lib/{name}/{file.name}")
-                for file in parser.get_dependent_files(source)
-            ]
-        include_files.append((source, target_path / source.name))
-    return include_files
+    return [
+        (source, target_path / source.name)
+        for source in source_path.glob(pattern)
+    ]
 
 
 def get_qt_plugins_paths(name: str, plugins: str) -> list[tuple[Path, Path]]:
     """Helper function to get a list of source and target paths of Qt plugins,
     indicated to be used in include_files.
     """
-    finder = ModuleFinder()
-    return get_qt_files(finder, name, "PluginsPath", plugins)
+    return _get_qt_files(name, "PluginsPath", plugins)
 
 
 def copy_qt_files(
     finder: ModuleFinder, name: str, variable: str, arg: str
 ) -> None:
     """Helper function to copy Qt plugins, resources, translations, etc."""
-    for source_path, target_path in get_qt_files(finder, name, variable, arg):
+    for source_path, target_path in _get_qt_files(name, variable, arg):
         finder.include_files(source_path, target_path)
 
 
@@ -197,6 +186,14 @@ def load_qt_qtbluetooth(finder: ModuleFinder, module: Module) -> None:
     copy_qt_files(finder, name, "TranslationsPath", "qtconnectivity_*.qm")
 
 
+def load_qt_qtcore(finder: ModuleFinder, module: Module) -> None:
+    """Include plugins for the module."""
+    name = _qt_implementation(module)
+    variable = "BinariesPath" if IS_WINDOWS else "LibrariesPath"
+    for source, target in _get_qt_files(name, variable, "*"):
+        finder.optional_files[source] = target
+
+
 def load_qt_qtdesigner(finder: ModuleFinder, module: Module) -> None:
     """Include plugins for the module."""
     name = _qt_implementation(module)
diff --git a/cx_Freeze/hooks/pyqt5/__init__.py b/cx_Freeze/hooks/pyqt5/__init__.py
index b3eb3daaa..24c1278bd 100644
--- a/cx_Freeze/hooks/pyqt5/__init__.py
+++ b/cx_Freeze/hooks/pyqt5/__init__.py
@@ -10,6 +10,7 @@
 from cx_Freeze._compat import IS_MACOS
 from cx_Freeze.common import get_resource_file_path
 from cx_Freeze.hooks._qthooks import copy_qt_files
+from cx_Freeze.hooks._qthooks import load_qt_qtcore as load_pyqt5_qtcore
 from cx_Freeze.hooks._qthooks import (
     load_qt_qtdesigner as load_pyqt5_qtdesigner,
 )
@@ -125,6 +126,7 @@ def load_pyqt5_qtwebenginecore(finder: ModuleFinder, module: Module) -> None:
 
 __all__ = [
     "load_pyqt5",
+    "load_pyqt5_qtcore",
     "load_pyqt5_qtdesigner",
     "load_pyqt5_qtgui",
     "load_pyqt5_qtmultimedia",
diff --git a/cx_Freeze/hooks/pyqt6/__init__.py b/cx_Freeze/hooks/pyqt6/__init__.py
index d87a8d8ee..8e24060db 100644
--- a/cx_Freeze/hooks/pyqt6/__init__.py
+++ b/cx_Freeze/hooks/pyqt6/__init__.py
@@ -18,6 +18,7 @@
 from cx_Freeze.hooks._qthooks import (
     load_qt_qtbluetooth as load_pyqt6_qtbluetooth,
 )
+from cx_Freeze.hooks._qthooks import load_qt_qtcore as load_pyqt6_qtcore
 from cx_Freeze.hooks._qthooks import (
     load_qt_qtdesigner as load_pyqt6_qtdesigner,
 )
@@ -148,6 +149,7 @@ def load_pyqt6_qtwidgets(finder: ModuleFinder, module: Module) -> None:
     "load_pyqt6_qt3dinput",
     "load_pyqt6_qt3drender",
     "load_pyqt6_qtbluetooth",
+    "load_pyqt6_qtcore",
     "load_pyqt6_qtdesigner",
     "load_pyqt6_qtgui",
     "load_pyqt6_qtlocation",
diff --git a/cx_Freeze/hooks/pyside2/__init__.py b/cx_Freeze/hooks/pyside2/__init__.py
index 9914abd2f..a1e45df90 100644
--- a/cx_Freeze/hooks/pyside2/__init__.py
+++ b/cx_Freeze/hooks/pyside2/__init__.py
@@ -13,6 +13,7 @@
     get_resource_file_path,
 )
 from cx_Freeze.hooks._qthooks import copy_qt_files
+from cx_Freeze.hooks._qthooks import load_qt_qtcore as load_pyside2_qtcore
 from cx_Freeze.hooks._qthooks import (
     load_qt_qtdesigner as load_pyside2_qtdesigner,
 )
@@ -148,6 +149,7 @@ def load_pyside2_qtwebenginecore(finder: ModuleFinder, module: Module) -> None:
 
 __all__ = [
     "load_pyside2",
+    "load_pyside2_qtcore",
     "load_pyside2_qtdesigner",
     "load_pyside2_qtgui",
     "load_pyside2_qtmultimedia",
diff --git a/cx_Freeze/hooks/pyside6/__init__.py b/cx_Freeze/hooks/pyside6/__init__.py
index cbbc286b0..eb621f12c 100644
--- a/cx_Freeze/hooks/pyside6/__init__.py
+++ b/cx_Freeze/hooks/pyside6/__init__.py
@@ -21,6 +21,7 @@
 from cx_Freeze.hooks._qthooks import (
     load_qt_qtbluetooth as load_pyside6_qtbluetooth,
 )
+from cx_Freeze.hooks._qthooks import load_qt_qtcore as load_pyside6_qtcore
 from cx_Freeze.hooks._qthooks import (
     load_qt_qtdesigner as load_pyside6_qtdesigner,
 )
@@ -162,6 +163,7 @@ def {name}(package_dir):
     "load_pyside6_qt3dinput",
     "load_pyside6_qt3drender",
     "load_pyside6_qtbluetooth",
+    "load_pyside6_qtcore",
     "load_pyside6_qtdesigner",
     "load_pyside6_qtgui",
     "load_pyside6_qtlocation",
diff --git a/samples/pyqt6/logo.svg b/samples/pyqt6/logo.svg
new file mode 100644
index 000000000..0e4487751
--- /dev/null
+++ b/samples/pyqt6/logo.svg
@@ -0,0 +1,1 @@
+<svg width="238" height="237" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" overflow="hidden"><defs><clipPath id="clip0"><path d="M706-314 944-314 944-77 706-77Z" fill-rule="evenodd" clip-rule="evenodd"/></clipPath></defs><g clip-path="url(#clip0)" transform="translate(-706 314)"><path d="M792.441-295.763C786.496-295.763 781.697-290.979 781.697-285.062 781.697-279.166 786.496-274.382 792.441-274.382 798.365-274.382 803.184-279.166 803.184-285.062 803.184-290.979 798.365-295.763 792.441-295.763ZM823.472-312.998C833.277-313.043 843.484-312.329 853.336-310.724 868.899-308.185 882-296.728 882-281.516L882-228.072C882-212.398 869.282-199.557 853.336-199.557L796.03-199.557C776.58-199.557 760.189-183.169 760.189-164.641L760.189-139 740.485-139C723.817-139 714.114-150.877 710.037-167.494 704.538-189.82 704.772-203.124 710.037-224.505 714.602-243.159 729.189-253 745.857-253L767.365-253 824.693-253 824.693-260.134 767.365-260.134 767.365-281.516C767.365-297.715 771.76-306.527 796.03-310.724 804.268-312.151 813.668-312.953 823.472-312.998Z" fill="#366A96" fill-rule="evenodd"/><path d="M857.377-117.071C851.466-117.071 846.655-112.267 846.655-106.348 846.655-100.406 851.466-95.6026 857.377-95.6026 863.31-95.6026 868.099-100.406 868.099-106.348 868.099-112.267 863.31-117.071 857.377-117.071ZM889.563-253 911.007-253C927.662-253 935.502-240.696 939.614-224.39 945.334-201.743 945.589-184.804 939.614-167.148 933.828-150 927.642-138.539 911.007-138.539L882.402-138.539 825.211-138.539 825.211-131.375 882.402-131.375 882.402-109.908C882.402-93.6435 868.205-85.4055 853.796-81.2973 832.12-75.1034 814.722-76.0513 796.606-81.2973 781.476-85.6801 768-94.6332 768-109.908L768-163.568C768-179.01 780.947-192.199 796.606-192.199L853.796-192.199C872.846-192.199 889.563-208.568 889.563-227.971Z" fill="#FFC836" fill-rule="evenodd"/></g></svg>
diff --git a/samples/pyqt6/setup.py b/samples/pyqt6/setup.py
index dd712d6a3..79d68b467 100644
--- a/samples/pyqt6/setup.py
+++ b/samples/pyqt6/setup.py
@@ -18,7 +18,8 @@
 except ImportError:
     get_qt_plugins_paths = None
 
-include_files = []
+include_files = [("logo.svg", "share/logo.svg")]
+
 if get_qt_plugins_paths:
     # Inclusion of extra plugins (since cx_Freeze 6.8b2)
     # cx_Freeze automatically imports the following plugins depending on the
diff --git a/samples/pyside6/logo.svg b/samples/pyside6/logo.svg
new file mode 100644
index 000000000..0e4487751
--- /dev/null
+++ b/samples/pyside6/logo.svg
@@ -0,0 +1,1 @@
+<svg width="238" height="237" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" overflow="hidden"><defs><clipPath id="clip0"><path d="M706-314 944-314 944-77 706-77Z" fill-rule="evenodd" clip-rule="evenodd"/></clipPath></defs><g clip-path="url(#clip0)" transform="translate(-706 314)"><path d="M792.441-295.763C786.496-295.763 781.697-290.979 781.697-285.062 781.697-279.166 786.496-274.382 792.441-274.382 798.365-274.382 803.184-279.166 803.184-285.062 803.184-290.979 798.365-295.763 792.441-295.763ZM823.472-312.998C833.277-313.043 843.484-312.329 853.336-310.724 868.899-308.185 882-296.728 882-281.516L882-228.072C882-212.398 869.282-199.557 853.336-199.557L796.03-199.557C776.58-199.557 760.189-183.169 760.189-164.641L760.189-139 740.485-139C723.817-139 714.114-150.877 710.037-167.494 704.538-189.82 704.772-203.124 710.037-224.505 714.602-243.159 729.189-253 745.857-253L767.365-253 824.693-253 824.693-260.134 767.365-260.134 767.365-281.516C767.365-297.715 771.76-306.527 796.03-310.724 804.268-312.151 813.668-312.953 823.472-312.998Z" fill="#366A96" fill-rule="evenodd"/><path d="M857.377-117.071C851.466-117.071 846.655-112.267 846.655-106.348 846.655-100.406 851.466-95.6026 857.377-95.6026 863.31-95.6026 868.099-100.406 868.099-106.348 868.099-112.267 863.31-117.071 857.377-117.071ZM889.563-253 911.007-253C927.662-253 935.502-240.696 939.614-224.39 945.334-201.743 945.589-184.804 939.614-167.148 933.828-150 927.642-138.539 911.007-138.539L882.402-138.539 825.211-138.539 825.211-131.375 882.402-131.375 882.402-109.908C882.402-93.6435 868.205-85.4055 853.796-81.2973 832.12-75.1034 814.722-76.0513 796.606-81.2973 781.476-85.6801 768-94.6332 768-109.908L768-163.568C768-179.01 780.947-192.199 796.606-192.199L853.796-192.199C872.846-192.199 889.563-208.568 889.563-227.971Z" fill="#FFC836" fill-rule="evenodd"/></g></svg>
diff --git a/samples/pyside6/setup.py b/samples/pyside6/setup.py
index 4b967ef44..1138da593 100644
--- a/samples/pyside6/setup.py
+++ b/samples/pyside6/setup.py
@@ -16,7 +16,8 @@
 except ImportError:
     get_qt_plugins_paths = None
 
-include_files = []
+include_files = [("logo.svg", "share/logo.svg")]
+
 if get_qt_plugins_paths:
     # Inclusion of extra plugins (since cx_Freeze 6.8b2)
     # cx_Freeze automatically imports the following plugins depending on the


Test patch:
diff --git a/samples/pyqt6/test_pyqt6.py b/samples/pyqt6/test_pyqt6.py
index c932327a1..dbd27f1bc 100644
--- a/samples/pyqt6/test_pyqt6.py
+++ b/samples/pyqt6/test_pyqt6.py
@@ -1,8 +1,10 @@
 """A simple script to demonstrate PyQt6."""
 
+import os
 import sys
 
 from PyQt6.QtCore import Qt
+from PyQt6.QtGui import QIcon
 from PyQt6.QtWidgets import QApplication, QLabel, QWidget
 
 
@@ -10,6 +12,15 @@ def main() -> int:
     app = QApplication(sys.argv)
     window = QWidget()
     window.setWindowTitle("Simple")
+    # icon located in current directory is copied to "share" folder when frozen
+    # (see setup.py)
+    icon_dir = os.path.join(
+        os.path.join(os.path.dirname(sys.executable), "share")
+        if getattr(sys, "frozen", False)
+        else os.path.dirname(__file__)
+    )
+    icon = QIcon(os.path.join(icon_dir, "logo.svg"))
+    window.setWindowIcon(icon)
     window.setGeometry(300, 300, 300, 300)
     label = QLabel(window)
     label.setText("Hello World!")
diff --git a/samples/pyside6/test_pyside6.py b/samples/pyside6/test_pyside6.py
index 0ba9079b9..eccb3a3b5 100644
--- a/samples/pyside6/test_pyside6.py
+++ b/samples/pyside6/test_pyside6.py
@@ -1,8 +1,10 @@
 """A simple script to demonstrate PySide6."""
 
+import os
 import sys
 
 from PySide6.QtCore import Qt
+from PySide6.QtGui import QIcon
 from PySide6.QtWidgets import QApplication, QLabel, QWidget
 
 
@@ -10,6 +12,15 @@ def main() -> int:
     app = QApplication(sys.argv)
     window = QWidget()
     window.setWindowTitle("Simple")
+    # icon located in current directory is copied to "share" folder when frozen
+    # (see setup.py)
+    icon_dir = os.path.join(
+        os.path.join(os.path.dirname(sys.executable), "share")
+        if getattr(sys, "frozen", False)
+        else os.path.dirname(__file__)
+    )
+    icon = QIcon(os.path.join(icon_dir, "logo.svg"))
+    window.setWindowIcon(icon)
     window.setGeometry(300, 300, 300, 300)
     label = QLabel(window)
     label.setText("Hello World!")