nuitka_helper.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. # Copyright (C) 2022 The Qt Company Ltd.
  2. # SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
  3. from __future__ import annotations
  4. # enables to use typehints for classes that has not been defined yet or imported
  5. # used for resolving circular imports
  6. from __future__ import annotations
  7. import logging
  8. import os
  9. import shlex
  10. import sys
  11. from pathlib import Path
  12. from project_lib import DesignStudioProject
  13. from . import MAJOR_VERSION, run_command, DEFAULT_IGNORE_DIRS, PLUGINS_TO_REMOVE
  14. from .config import DesktopConfig
  15. class Nuitka:
  16. """
  17. Wrapper class around the nuitka executable, enabling its usage through python code
  18. """
  19. def __init__(self, nuitka):
  20. self.nuitka = nuitka
  21. # plugins to ignore. The sensible plugins are include by default by Nuitka for PySide6
  22. # application deployment
  23. self.qt_plugins_to_ignore = ["imageformats", # being Nuitka `sensible`` plugins
  24. "iconengines",
  25. "mediaservice",
  26. "printsupport",
  27. "platforms",
  28. "platformthemes",
  29. "styles",
  30. "wayland-shell-integration",
  31. "wayland-decoration-client",
  32. "wayland-graphics-integration-client",
  33. "egldeviceintegrations",
  34. "xcbglintegrations",
  35. "tls", # end Nuitka `sensible` plugins
  36. "generic" # plugins that error with Nuitka
  37. ]
  38. self.files_to_ignore = [".cpp.o", ".qsb"]
  39. @staticmethod
  40. def icon_option():
  41. if sys.platform == "linux":
  42. return "--linux-icon"
  43. elif sys.platform == "win32":
  44. return "--windows-icon-from-ico"
  45. else:
  46. return "--macos-app-icon"
  47. def _create_windows_command(self, source_file: Path, command: list):
  48. """
  49. Special case for Windows where the command length is limited to 8191 characters.
  50. """
  51. # if the platform is windows and the command is more than 8191 characters, the command
  52. # will fail with the error message "The command line is too long". To avoid this, we will
  53. # we will move the source_file to the intermediate source file called deploy_main.py, and
  54. # include the Nuitka options direcly in the main file as mentioned in
  55. # https://nuitka.net/user-documentation/user-manual.html#nuitka-project-options
  56. # convert command into a format recognized by Nuitka when written to the main file
  57. # the first item is ignore because it is 'python -m nuitka'
  58. nuitka_comment_options = []
  59. for command_entry in command[4:]:
  60. nuitka_comment_options.append(f"# nuitka-project: {command_entry}")
  61. nuitka_comment_options_str = "\n".join(nuitka_comment_options)
  62. nuitka_comment_options_str += "\n"
  63. # read the content of the source file
  64. new_source_content = (nuitka_comment_options_str
  65. + Path(source_file).read_text(encoding="utf-8"))
  66. # create and write back the new source content to deploy_main.py
  67. new_source_file = source_file.parent / "deploy_main.py"
  68. new_source_file.write_text(new_source_content, encoding="utf-8")
  69. return new_source_file
  70. def create_executable(self, source_file: Path, extra_args: str, qml_files: list[Path],
  71. qt_plugins: list[str], excluded_qml_plugins: list[str], icon: str,
  72. dry_run: bool, permissions: list[str],
  73. mode: DesktopConfig.NuitkaMode) -> str:
  74. qt_plugins = [plugin for plugin in qt_plugins if plugin not in self.qt_plugins_to_ignore]
  75. extra_args = shlex.split(extra_args)
  76. # macOS uses the --standalone option by default to create an app bundle
  77. if sys.platform == "darwin":
  78. # create an app bundle
  79. extra_args.extend(["--standalone", "--macos-create-app-bundle"])
  80. permission_pattern = "--macos-app-protected-resource={permission}"
  81. for permission in permissions:
  82. extra_args.append(permission_pattern.format(permission=permission))
  83. else:
  84. extra_args.append(f"--{mode.value}")
  85. qml_args = []
  86. if qml_files:
  87. # include all the subdirectories in the project directory as data directories
  88. # This includes all the qml modules
  89. all_relevant_subdirs = []
  90. for subdir in source_file.parent.iterdir():
  91. if subdir.is_dir() and subdir.name not in DEFAULT_IGNORE_DIRS:
  92. extra_args.append(f"--include-data-dir={subdir}="
  93. f"./{subdir.name}")
  94. all_relevant_subdirs.append(subdir)
  95. # find all the qml files that are not included via the data directories
  96. extra_qml_files = [file for file in qml_files
  97. if file.parent not in all_relevant_subdirs]
  98. # This will generate options for each file using:
  99. # --include-data-files=ABSOLUTE_PATH_TO_FILE=RELATIVE_PATH_TO ROOT
  100. # for each file.
  101. qml_args.extend(
  102. [f"--include-data-files={qml_file.resolve()}="
  103. f"./{qml_file.resolve().relative_to(source_file.resolve().parent)}"
  104. for qml_file in extra_qml_files]
  105. )
  106. if qml_files or DesignStudioProject.is_ds_project(source_file):
  107. # add qml plugin. The `qml`` plugin name is not present in the module json files shipped
  108. # with Qt and hence not in `qt_plugins``. However, Nuitka uses the 'qml' plugin name to
  109. # include the necessary qml plugins. There we have to add it explicitly for a qml
  110. # application
  111. qt_plugins.append("qml")
  112. if excluded_qml_plugins:
  113. prefix = "lib" if sys.platform != "win32" else ""
  114. for plugin in excluded_qml_plugins:
  115. dll_name = plugin.replace("Qt", f"Qt{MAJOR_VERSION}")
  116. qml_args.append(f"--noinclude-dlls={prefix}{dll_name}*")
  117. # Exclude .qen json files from QtQuickEffectMaker
  118. # These files are not relevant for PySide6 applications
  119. qml_args.append("--noinclude-dlls=*/qml/QtQuickEffectMaker/*")
  120. # Exclude files that cannot be processed by Nuitka
  121. for file in self.files_to_ignore:
  122. extra_args.append(f"--noinclude-dlls=*{file}")
  123. output_dir = source_file.parent / "deployment"
  124. if not dry_run:
  125. output_dir.mkdir(parents=True, exist_ok=True)
  126. logging.info("[DEPLOY] Running Nuitka")
  127. command = self.nuitka + [
  128. os.fspath(source_file),
  129. "--follow-imports",
  130. "--enable-plugin=pyside6",
  131. f"--output-dir={output_dir}",
  132. ]
  133. command.extend(extra_args + qml_args)
  134. command.append(f"{self.__class__.icon_option()}={icon}")
  135. if qt_plugins:
  136. # sort qt_plugins so that the result is definitive when testing
  137. qt_plugins.sort()
  138. # remove the following plugins from the qt_plugins list as Nuitka only checks
  139. # for plugins within PySide6/Qt/plugins folder, and the following plugins
  140. # are not present in the PySide6/Qt/plugins folder
  141. qt_plugins = [plugin for plugin in qt_plugins if plugin not in PLUGINS_TO_REMOVE]
  142. qt_plugins_str = ",".join(qt_plugins)
  143. command.append(f"--include-qt-plugins={qt_plugins_str}")
  144. long_command = False
  145. if sys.platform == "win32" and len(" ".join(str(cmd) for cmd in command)) > 7000:
  146. logging.info("[DEPLOY] Nuitka command too long for Windows. "
  147. "Copying the contents of main Python file to an intermediate "
  148. "deploy_main.py file")
  149. long_command = True
  150. new_source_file = self._create_windows_command(source_file=source_file, command=command)
  151. command = self.nuitka + [os.fspath(new_source_file)]
  152. command_str, _ = run_command(command=command, dry_run=dry_run)
  153. # if deploy_main.py exists, delete it after the command is run
  154. if long_command:
  155. os.remove(source_file.parent / "deploy_main.py")
  156. return command_str