__main__.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. #-----------------------------------------------------------------------------
  2. # Copyright (c) 2013-2022, 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. import argparse
  15. import os
  16. import platform
  17. from collections import defaultdict
  18. from PyInstaller import __version__
  19. from PyInstaller import log as logging
  20. # Note: do not import anything else until compat.check_requirements function is run!
  21. from PyInstaller import compat
  22. logger = logging.getLogger(__name__)
  23. # Taken from https://stackoverflow.com/a/22157136 to format args more flexibly: any help text which beings with ``R|``
  24. # will have all newlines preserved; the help text will be line wrapped. See
  25. # https://docs.python.org/3/library/argparse.html#formatter-class.
  26. # This is used by the ``--debug`` option.
  27. class _SmartFormatter(argparse.HelpFormatter):
  28. def _split_lines(self, text, width):
  29. if text.startswith('R|'):
  30. # The underlying implementation of ``RawTextHelpFormatter._split_lines`` invokes this; mimic it.
  31. return text[2:].splitlines()
  32. else:
  33. # Invoke the usual formatter.
  34. return super()._split_lines(text, width)
  35. def run_makespec(filenames, **opts):
  36. # Split pathex by using the path separator
  37. temppaths = opts['pathex'][:]
  38. pathex = opts['pathex'] = []
  39. for p in temppaths:
  40. pathex.extend(p.split(os.pathsep))
  41. import PyInstaller.building.makespec
  42. spec_file = PyInstaller.building.makespec.main(filenames, **opts)
  43. logger.info('wrote %s' % spec_file)
  44. return spec_file
  45. def run_build(pyi_config, spec_file, **kwargs):
  46. import PyInstaller.building.build_main
  47. PyInstaller.building.build_main.main(pyi_config, spec_file, **kwargs)
  48. def __add_options(parser):
  49. parser.add_argument(
  50. '-v',
  51. '--version',
  52. action='version',
  53. version=__version__,
  54. help='Show program version info and exit.',
  55. )
  56. class _PyiArgumentParser(argparse.ArgumentParser):
  57. def __init__(self, *args, **kwargs):
  58. self._pyi_action_groups = defaultdict(list)
  59. super().__init__(*args, **kwargs)
  60. def _add_options(self, __add_options: callable, name: str = ""):
  61. """
  62. Mutate self with the given callable, storing any new actions added in a named group
  63. """
  64. n_actions_before = len(getattr(self, "_actions", []))
  65. __add_options(self) # preserves old behavior
  66. new_actions = getattr(self, "_actions", [])[n_actions_before:]
  67. self._pyi_action_groups[name].extend(new_actions)
  68. def _option_name(self, action):
  69. """
  70. Get the option name(s) associated with an action
  71. For options that define both short and long names, this function will
  72. return the long names joined by "/"
  73. """
  74. longnames = [name for name in action.option_strings if name.startswith("--")]
  75. if longnames:
  76. name = "/".join(longnames)
  77. else:
  78. name = action.option_strings[0]
  79. return name
  80. def _forbid_options(self, args: argparse.Namespace, group: str, errmsg: str = ""):
  81. """Forbid options from a named action group"""
  82. options = defaultdict(str)
  83. for action in self._pyi_action_groups[group]:
  84. dest = action.dest
  85. name = self._option_name(action)
  86. if getattr(args, dest) is not self.get_default(dest):
  87. if dest in options:
  88. options[dest] += "/"
  89. options[dest] += name
  90. # if any options from the forbidden group are not the default values,
  91. # the user must have passed them in, so issue an error report
  92. if options:
  93. sep = "\n "
  94. bad = sep.join(options.values())
  95. if errmsg:
  96. errmsg = "\n" + errmsg
  97. raise SystemExit(f"option(s) not allowed:{sep}{bad}{errmsg}")
  98. def generate_parser() -> _PyiArgumentParser:
  99. """
  100. Build an argparse parser for PyInstaller's main CLI.
  101. """
  102. import PyInstaller.building.build_main
  103. import PyInstaller.building.makespec
  104. import PyInstaller.log
  105. parser = _PyiArgumentParser(formatter_class=_SmartFormatter)
  106. parser.prog = "pyinstaller"
  107. parser._add_options(__add_options)
  108. parser._add_options(PyInstaller.building.makespec.__add_options, name="makespec")
  109. parser._add_options(PyInstaller.building.build_main.__add_options, name="build_main")
  110. parser._add_options(PyInstaller.log.__add_options, name="log")
  111. parser.add_argument(
  112. 'filenames',
  113. metavar='scriptname',
  114. nargs='+',
  115. help="Name of scriptfiles to be processed or exactly one .spec file. If a .spec file is specified, most "
  116. "options are unnecessary and are ignored.",
  117. )
  118. return parser
  119. def run(pyi_args=None, pyi_config=None):
  120. """
  121. pyi_args allows running PyInstaller programmatically without a subprocess
  122. pyi_config allows checking configuration once when running multiple tests
  123. """
  124. compat.check_requirements()
  125. import PyInstaller.log
  126. try:
  127. parser = generate_parser()
  128. args = parser.parse_args(pyi_args)
  129. PyInstaller.log.__process_options(parser, args)
  130. # Print PyInstaller version, Python version, and platform as the first line to stdout. This helps us identify
  131. # PyInstaller, Python, and platform version when users report issues.
  132. logger.info('PyInstaller: %s' % __version__)
  133. logger.info('Python: %s%s', platform.python_version(), " (conda)" if compat.is_conda else "")
  134. logger.info('Platform: %s' % platform.platform())
  135. # Skip creating .spec when .spec file is supplied.
  136. if args.filenames[0].endswith('.spec'):
  137. parser._forbid_options(
  138. args, group="makespec", errmsg="makespec options not valid when a .spec file is given"
  139. )
  140. spec_file = args.filenames[0]
  141. else:
  142. spec_file = run_makespec(**vars(args))
  143. run_build(pyi_config, spec_file, **vars(args))
  144. except KeyboardInterrupt:
  145. raise SystemExit("Aborted by user request.")
  146. except RecursionError:
  147. from PyInstaller import _recursion_to_deep_message
  148. _recursion_to_deep_message.raise_with_msg()
  149. if __name__ == '__main__':
  150. run()