__main__.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. #-----------------------------------------------------------------------------
  2. # Copyright (c) 2013-2023, PyInstaller Development Team.
  3. #
  4. # Distributed under the terms of the GNU General Public License (version 2
  5. # or later) with exception for distributing the bootloader.
  6. #
  7. # The full license is in the file COPYING.txt, distributed with this software.
  8. #
  9. # SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
  10. #-----------------------------------------------------------------------------
  11. """
  12. Main command-line interface to PyInstaller.
  13. """
  14. from __future__ import annotations
  15. import argparse
  16. import os
  17. import platform
  18. import sys
  19. import pathlib
  20. from collections import defaultdict
  21. from PyInstaller import __version__
  22. from PyInstaller import log as logging
  23. # Note: do not import anything else until compat.check_requirements function is run!
  24. from PyInstaller import compat
  25. try:
  26. from argcomplete import autocomplete
  27. except ImportError:
  28. def autocomplete(parser):
  29. return None
  30. logger = logging.getLogger(__name__)
  31. # Taken from https://stackoverflow.com/a/22157136 to format args more flexibly: any help text which beings with ``R|``
  32. # will have all newlines preserved; the help text will be line wrapped. See
  33. # https://docs.python.org/3/library/argparse.html#formatter-class.
  34. # This is used by the ``--debug`` option.
  35. class _SmartFormatter(argparse.HelpFormatter):
  36. def _split_lines(self, text, width):
  37. if text.startswith('R|'):
  38. # The underlying implementation of ``RawTextHelpFormatter._split_lines`` invokes this; mimic it.
  39. return text[2:].splitlines()
  40. else:
  41. # Invoke the usual formatter.
  42. return super()._split_lines(text, width)
  43. def run_makespec(filenames, **opts):
  44. # Split pathex by using the path separator
  45. temppaths = opts['pathex'][:]
  46. pathex = opts['pathex'] = []
  47. for p in temppaths:
  48. pathex.extend(p.split(os.pathsep))
  49. import PyInstaller.building.makespec
  50. spec_file = PyInstaller.building.makespec.main(filenames, **opts)
  51. logger.info('wrote %s' % spec_file)
  52. return spec_file
  53. def run_build(pyi_config, spec_file, **kwargs):
  54. import PyInstaller.building.build_main
  55. PyInstaller.building.build_main.main(pyi_config, spec_file, **kwargs)
  56. def __add_options(parser):
  57. parser.add_argument(
  58. '-v',
  59. '--version',
  60. action='version',
  61. version=__version__,
  62. help='Show program version info and exit.',
  63. )
  64. class _PyiArgumentParser(argparse.ArgumentParser):
  65. def __init__(self, *args, **kwargs):
  66. self._pyi_action_groups = defaultdict(list)
  67. super().__init__(*args, **kwargs)
  68. def _add_options(self, __add_options: callable, name: str = ""):
  69. """
  70. Mutate self with the given callable, storing any new actions added in a named group
  71. """
  72. n_actions_before = len(getattr(self, "_actions", []))
  73. __add_options(self) # preserves old behavior
  74. new_actions = getattr(self, "_actions", [])[n_actions_before:]
  75. self._pyi_action_groups[name].extend(new_actions)
  76. def _option_name(self, action):
  77. """
  78. Get the option name(s) associated with an action
  79. For options that define both short and long names, this function will
  80. return the long names joined by "/"
  81. """
  82. longnames = [name for name in action.option_strings if name.startswith("--")]
  83. if longnames:
  84. name = "/".join(longnames)
  85. else:
  86. name = action.option_strings[0]
  87. return name
  88. def _forbid_options(self, args: argparse.Namespace, group: str, errmsg: str = ""):
  89. """Forbid options from a named action group"""
  90. options = defaultdict(str)
  91. for action in self._pyi_action_groups[group]:
  92. dest = action.dest
  93. name = self._option_name(action)
  94. if getattr(args, dest) is not self.get_default(dest):
  95. if dest in options:
  96. options[dest] += "/"
  97. options[dest] += name
  98. # if any options from the forbidden group are not the default values,
  99. # the user must have passed them in, so issue an error report
  100. if options:
  101. sep = "\n "
  102. bad = sep.join(options.values())
  103. if errmsg:
  104. errmsg = "\n" + errmsg
  105. raise SystemExit(f"ERROR: option(s) not allowed:{sep}{bad}{errmsg}")
  106. def generate_parser() -> _PyiArgumentParser:
  107. """
  108. Build an argparse parser for PyInstaller's main CLI.
  109. """
  110. import PyInstaller.building.build_main
  111. import PyInstaller.building.makespec
  112. import PyInstaller.log
  113. parser = _PyiArgumentParser(formatter_class=_SmartFormatter)
  114. parser.prog = "pyinstaller"
  115. parser._add_options(__add_options)
  116. parser._add_options(PyInstaller.building.makespec.__add_options, name="makespec")
  117. parser._add_options(PyInstaller.building.build_main.__add_options, name="build_main")
  118. parser._add_options(PyInstaller.log.__add_options, name="log")
  119. parser.add_argument(
  120. 'filenames',
  121. metavar='scriptname',
  122. nargs='+',
  123. help="Name of scriptfiles to be processed or exactly one .spec file. If a .spec file is specified, most "
  124. "options are unnecessary and are ignored.",
  125. )
  126. return parser
  127. def run(pyi_args: list | None = None, pyi_config: dict | None = None):
  128. """
  129. pyi_args allows running PyInstaller programmatically without a subprocess
  130. pyi_config allows checking configuration once when running multiple tests
  131. """
  132. compat.check_requirements()
  133. check_unsafe_privileges()
  134. import PyInstaller.log
  135. old_sys_argv = sys.argv
  136. try:
  137. parser = generate_parser()
  138. autocomplete(parser)
  139. if pyi_args is None:
  140. pyi_args = sys.argv[1:]
  141. try:
  142. index = pyi_args.index("--")
  143. except ValueError:
  144. index = len(pyi_args)
  145. args = parser.parse_args(pyi_args[:index])
  146. spec_args = pyi_args[index + 1:]
  147. PyInstaller.log.__process_options(parser, args)
  148. # Print PyInstaller version, Python version, and platform as the first line to stdout. This helps us identify
  149. # PyInstaller, Python, and platform version when users report issues.
  150. try:
  151. from _pyinstaller_hooks_contrib import __version__ as contrib_hooks_version
  152. except Exception:
  153. contrib_hooks_version = 'unknown'
  154. logger.info('PyInstaller: %s, contrib hooks: %s', __version__, contrib_hooks_version)
  155. logger.info('Python: %s%s', platform.python_version(), " (conda)" if compat.is_conda else "")
  156. logger.info('Platform: %s', platform.platform())
  157. logger.info('Python environment: %s', sys.prefix)
  158. # Skip creating .spec when .spec file is supplied.
  159. if args.filenames[0].endswith('.spec'):
  160. parser._forbid_options(
  161. args, group="makespec", errmsg="makespec options not valid when a .spec file is given"
  162. )
  163. spec_file = args.filenames[0]
  164. else:
  165. # Ensure that the given script files exist, before trying to generate the .spec file.
  166. # This prevents us from overwriting an existing (and customized) .spec file if user makes a typo in the
  167. # .spec file's suffix when trying to build it, for example, `pyinstaller program.cpes` (see #8276).
  168. # It also prevents creation of a .spec file when `pyinstaller program.py` is accidentally ran from a
  169. # directory that does not contain the script (for example, due to failing to change the directory prior
  170. # to running the command).
  171. for filename in args.filenames:
  172. if not os.path.isfile(filename):
  173. raise SystemExit(f"ERROR: Script file {filename!r} does not exist.")
  174. spec_file = run_makespec(**vars(args))
  175. sys.argv = [spec_file, *spec_args]
  176. run_build(pyi_config, spec_file, **vars(args))
  177. except KeyboardInterrupt:
  178. raise SystemExit("Aborted by user request.")
  179. except RecursionError:
  180. from PyInstaller import _recursion_too_deep_message
  181. _recursion_too_deep_message.raise_with_msg()
  182. finally:
  183. sys.argv = old_sys_argv
  184. def _console_script_run():
  185. # Python prepends the main script's parent directory to sys.path. When PyInstaller is ran via the usual
  186. # `pyinstaller` CLI entry point, this directory is $pythonprefix/bin which should not be in sys.path.
  187. if os.path.basename(sys.path[0]) in ("bin", "Scripts"):
  188. sys.path.pop(0)
  189. run()
  190. def check_unsafe_privileges():
  191. """
  192. Forbid dangerous usage of PyInstaller with escalated privileges
  193. """
  194. if compat.is_win and not compat.is_win_wine:
  195. # Discourage (with the intention to eventually block) people using *run as admin* with PyInstaller.
  196. # There are 4 cases, block case 3 but be careful not to also block case 2.
  197. # 1. User has no admin access: TokenElevationTypeDefault
  198. # 2. User is an admin/UAC disabled (common on CI/VMs): TokenElevationTypeDefault
  199. # 3. User has used *run as administrator* to elevate: TokenElevationTypeFull
  200. # 4. User can escalate but hasn't: TokenElevationTypeLimited
  201. # https://techcommunity.microsoft.com/t5/windows-blog-archive/how-to-determine-if-a-user-is-a-member-of-the-administrators/ba-p/228476
  202. import ctypes
  203. advapi32 = ctypes.CDLL("Advapi32.dll")
  204. kernel32 = ctypes.CDLL("kernel32.dll")
  205. kernel32.GetCurrentProcess.restype = ctypes.c_void_p
  206. process = kernel32.GetCurrentProcess()
  207. token = ctypes.c_void_p()
  208. try:
  209. TOKEN_QUERY = 8
  210. assert advapi32.OpenProcessToken(ctypes.c_void_p(process), TOKEN_QUERY, ctypes.byref(token))
  211. elevation_type = ctypes.c_int()
  212. TokenElevationType = 18
  213. assert advapi32.GetTokenInformation(
  214. token, TokenElevationType, ctypes.byref(elevation_type), ctypes.sizeof(elevation_type),
  215. ctypes.byref(ctypes.c_int())
  216. )
  217. finally:
  218. kernel32.CloseHandle(token)
  219. if elevation_type.value == 2: # TokenElevationTypeFull
  220. logger.log(
  221. logging.DEPRECATION,
  222. "Running PyInstaller as admin is not necessary nor sensible. Run PyInstaller from a non-administrator "
  223. "terminal. PyInstaller 7.0 will block this."
  224. )
  225. elif compat.is_darwin or compat.is_linux:
  226. # Discourage (with the intention to eventually block) people using *sudo* with PyInstaller.
  227. # Again there are 4 cases, block only case 4.
  228. # 1. Non-root: os.getuid() != 0
  229. # 2. Logged in as root (usually a VM): os.getlogin() == "root", os.getuid() == 0
  230. # 3. No named users (e.g. most Docker containers): os.getlogin() fails
  231. # 4. Regular user using escalation: os.getlogin() != "root", os.getuid() == 0
  232. try:
  233. user = os.getlogin()
  234. except OSError:
  235. user = ""
  236. if os.getuid() == 0 and user and user != "root":
  237. logger.log(
  238. logging.DEPRECATION,
  239. "Running PyInstaller as root is not necessary nor sensible. Do not use PyInstaller with sudo. "
  240. "PyInstaller 7.0 will block this."
  241. )
  242. if compat.is_win:
  243. # Do not let people run PyInstaller from admin cmd's default working directory (C:\Windows\system32)
  244. cwd = pathlib.Path.cwd()
  245. try:
  246. win_dir = compat.win32api.GetWindowsDirectory()
  247. except Exception:
  248. win_dir = None
  249. win_dir = None if win_dir is None else pathlib.Path(win_dir).resolve()
  250. inside_win_dir = cwd == win_dir or win_dir in cwd.parents
  251. # The only exception to the above is if user's home directory is also located under %WINDIR%, which happens
  252. # when PyInstaller is ran under SYSTEM user.
  253. if inside_win_dir:
  254. home_dir = pathlib.Path.home().resolve()
  255. if cwd == home_dir or home_dir in cwd.parents:
  256. inside_win_dir = False
  257. if inside_win_dir:
  258. raise SystemExit(
  259. f"ERROR: Do not run pyinstaller from {cwd}. cd to where your code is and run pyinstaller from there. "
  260. "Hint: You can open a terminal where your code is by going to the parent folder in Windows file "
  261. "explorer and typing cmd into the address bar."
  262. )
  263. if __name__ == '__main__':
  264. run()