deploy.py 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  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. """ pyside6-deploy deployment tool
  5. Deployment tool that uses Nuitka to deploy PySide6 applications to various desktop (Windows,
  6. Linux, macOS) platforms.
  7. How does it work?
  8. Command: pyside6-deploy path/to/main_file
  9. pyside6-deploy (incase main file is called main.py)
  10. pyside6-deploy -c /path/to/config_file
  11. Platforms supported: Linux, Windows, macOS
  12. Module binary inclusion:
  13. 1. for non-QML cases, only required modules are included
  14. 2. for QML cases, all modules are included because of all QML plugins getting included
  15. with nuitka
  16. Config file:
  17. On the first run of the tool, it creates a config file called pysidedeploy.spec which
  18. controls the various characteristic of the deployment. Users can simply change the value
  19. in this config file to achieve different properties ie. change the application name,
  20. deployment platform etc.
  21. Note: This file is used by both pyside6-deploy and pyside6-android-deploy
  22. """
  23. import sys
  24. import argparse
  25. import logging
  26. import traceback
  27. from pathlib import Path
  28. from textwrap import dedent
  29. from deploy_lib import (MAJOR_VERSION, DesktopConfig, cleanup, config_option_exists,
  30. finalize, create_config_file, PythonExecutable, Nuitka,
  31. HELP_EXTRA_MODULES, HELP_EXTRA_IGNORE_DIRS)
  32. TOOL_DESCRIPTION = dedent(f"""
  33. This tool deploys PySide{MAJOR_VERSION} to desktop (Windows, Linux,
  34. macOS) platforms. The following types of executables are produced as per
  35. the platform:
  36. Windows = .exe
  37. macOS = .app
  38. Linux = .bin
  39. """)
  40. HELP_MODE = dedent("""
  41. The mode in which the application is deployed. The options are: onefile,
  42. standalone. The default value is onefile.
  43. This options translates to the mode Nuitka uses to create the executable.
  44. macOS by default uses the --standalone option.
  45. """)
  46. def main(main_file: Path = None, name: str = None, config_file: Path = None, init: bool = False,
  47. loglevel=logging.WARNING, dry_run: bool = False, keep_deployment_files: bool = False,
  48. force: bool = False, extra_ignore_dirs: str = None, extra_modules_grouped: str = None,
  49. mode: str = None) -> str | None:
  50. """
  51. Entry point for pyside6-deploy command.
  52. :return: If successful, the Nuitka command that was executed. None otherwise.
  53. """
  54. logging.basicConfig(level=loglevel)
  55. # In case pyside6-deploy is run from a completely different location than the project directory
  56. if main_file and main_file.exists():
  57. config_file = main_file.parent / "pysidedeploy.spec"
  58. if config_file and not config_file.exists() and not main_file.exists():
  59. raise RuntimeError(dedent("""
  60. Directory does not contain main.py file.
  61. Please specify the main Python entry point file or the pysidedeploy.spec config file.
  62. Run "pyside6-deploy --help" to see info about CLI options.
  63. pyside6-deploy exiting..."""))
  64. logging.info("[DEPLOY] Start")
  65. if extra_ignore_dirs:
  66. extra_ignore_dirs = extra_ignore_dirs.split(",")
  67. extra_modules = []
  68. if extra_modules_grouped:
  69. tmp_extra_modules = extra_modules_grouped.split(",")
  70. for extra_module in tmp_extra_modules:
  71. if extra_module.startswith("Qt"):
  72. extra_modules.append(extra_module[2:])
  73. else:
  74. extra_modules.append(extra_module)
  75. python = PythonExecutable(dry_run=dry_run, init=init, force=force)
  76. config_file_exists = config_file and config_file.exists()
  77. if config_file_exists:
  78. logging.info(f"[DEPLOY] Using existing config file {config_file}")
  79. else:
  80. config_file = create_config_file(main_file=main_file, dry_run=dry_run)
  81. config = DesktopConfig(config_file=config_file, source_file=main_file, python_exe=python.exe,
  82. dry_run=dry_run, existing_config_file=config_file_exists,
  83. extra_ignore_dirs=extra_ignore_dirs, mode=mode, name=name)
  84. cleanup(config=config)
  85. python.install_dependencies(config=config, packages="packages")
  86. # required by Nuitka for pyenv Python
  87. add_arg = " --static-libpython=no"
  88. if python.is_pyenv_python() and add_arg not in config.extra_args:
  89. config.extra_args += add_arg
  90. config.modules += list(set(extra_modules).difference(set(config.modules)))
  91. # Do not save the config changes if --dry-run is specified
  92. if not dry_run:
  93. config.update_config()
  94. if config.qml_files:
  95. logging.info("[DEPLOY] Included QML files: "
  96. f"{[str(qml_file) for qml_file in config.qml_files]}")
  97. if init:
  98. # Config file created above. Exiting.
  99. logging.info(f"[DEPLOY]: Config file {config.config_file} created")
  100. return
  101. # If modules contain QtSql and the platform is macOS, then pyside6-deploy will not work
  102. # currently. The fix ideally will have to come from Nuitka.
  103. # See PYSIDE-2835
  104. # TODO: Remove this check once the issue is fixed in Nuitka
  105. # Nuitka Issue: https://github.com/Nuitka/Nuitka/issues/3079
  106. if "Sql" in config.modules and sys.platform == "darwin":
  107. print("[DEPLOY] QtSql Application is not supported on macOS with pyside6-deploy")
  108. return
  109. command_str = None
  110. try:
  111. # Run the Nuitka command to create the executable
  112. if not dry_run:
  113. logging.info("[DEPLOY] Deploying application")
  114. nuitka = Nuitka(nuitka=[python.exe, "-m", "nuitka"])
  115. command_str = nuitka.create_executable(source_file=config.source_file,
  116. extra_args=config.extra_args,
  117. qml_files=config.qml_files,
  118. qt_plugins=config.qt_plugins,
  119. excluded_qml_plugins=config.excluded_qml_plugins,
  120. icon=config.icon,
  121. dry_run=dry_run,
  122. permissions=config.permissions,
  123. mode=config.mode)
  124. if not dry_run:
  125. logging.info("[DEPLOY] Successfully deployed application")
  126. except Exception:
  127. print(f"[DEPLOY] Exception occurred: {traceback.format_exc()}")
  128. finally:
  129. if config.generated_files_path:
  130. if not dry_run:
  131. finalize(config=config)
  132. if not keep_deployment_files:
  133. cleanup(config=config)
  134. logging.info("[DEPLOY] End")
  135. return command_str
  136. if __name__ == "__main__":
  137. parser = argparse.ArgumentParser(description=TOOL_DESCRIPTION)
  138. parser.add_argument("-c", "--config-file", type=lambda p: Path(p).absolute(),
  139. default=(Path.cwd() / "pysidedeploy.spec"),
  140. help="Path to the .spec config file")
  141. parser.add_argument(
  142. type=lambda p: Path(p).absolute(),
  143. help="Path to main python file", nargs="?", dest="main_file",
  144. default=None if config_option_exists() else Path.cwd() / "main.py")
  145. parser.add_argument(
  146. "--init", action="store_true",
  147. help="Create pysidedeploy.spec file, if it doesn't already exists")
  148. parser.add_argument(
  149. "-v", "--verbose", help="Run in verbose mode", action="store_const",
  150. dest="loglevel", const=logging.INFO)
  151. parser.add_argument("--dry-run", action="store_true", help="Show the commands to be run")
  152. parser.add_argument(
  153. "--keep-deployment-files", action="store_true",
  154. help="Keep the generated deployment files generated")
  155. parser.add_argument("-f", "--force", action="store_true", help="Force all input prompts")
  156. parser.add_argument("--name", type=str, help="Application name")
  157. parser.add_argument("--extra-ignore-dirs", type=str, help=HELP_EXTRA_IGNORE_DIRS)
  158. parser.add_argument("--extra-modules", type=str, help=HELP_EXTRA_MODULES)
  159. parser.add_argument("--mode", choices=["onefile", "standalone"], default="onefile",
  160. help=HELP_MODE)
  161. args = parser.parse_args()
  162. main(args.main_file, args.name, args.config_file, args.init, args.loglevel, args.dry_run,
  163. args.keep_deployment_files, args.force, args.extra_ignore_dirs, args.extra_modules,
  164. args.mode)