#-----------------------------------------------------------------------------
# Copyright (c) 2005-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------
"""
Automatically build spec files containing a description of the project.
"""

import argparse
import os
import re
import sys

from PyInstaller import DEFAULT_SPECPATH, HOMEPATH
from PyInstaller import log as logging
from PyInstaller.building.templates import bundleexetmplt, bundletmplt, onedirtmplt, onefiletmplt, splashtmpl
from PyInstaller.compat import is_darwin, is_win

logger = logging.getLogger(__name__)

# This list gives valid choices for the ``--debug`` command-line option, except for the ``all`` choice.
DEBUG_ARGUMENT_CHOICES = ['imports', 'bootloader', 'noarchive']
# This is the ``all`` choice.
DEBUG_ALL_CHOICE = ['all']


def escape_win_filepath(path):
    # escape all \ with another \ after using normpath to clean up the path
    return os.path.normpath(path).replace('\\', '\\\\')


def make_path_spec_relative(filename, spec_dir):
    """
    Make the filename relative to the directory containing .spec file if filename is relative and not absolute.
    Otherwise keep filename untouched.
    """
    if os.path.isabs(filename):
        return filename
    else:
        filename = os.path.abspath(filename)
        # Make it relative.
        filename = os.path.relpath(filename, start=spec_dir)
        return filename


# Support for trying to avoid hard-coded paths in the .spec files. Eg, all files rooted in the Installer directory tree
# will be written using "HOMEPATH", thus allowing this spec file to be used with any Installer installation. Same thing
# could be done for other paths too.
path_conversions = ((HOMEPATH, "HOMEPATH"),)


class SourceDestAction(argparse.Action):
    """
    A command line option which takes multiple source:dest pairs.
    """
    def __init__(self, *args, default=None, metavar=None, **kwargs):
        super().__init__(*args, default=[], metavar='SOURCE:DEST', **kwargs)

    def __call__(self, parser, namespace, value, option_string=None):
        try:
            # Find the only separator that isn't a Windows drive.
            separator, = (m for m in re.finditer(rf"(^\w:[/\\])|[:{os.pathsep}]", value) if not m[1])
        except ValueError:
            # Split into SRC and DEST failed, wrong syntax
            raise argparse.ArgumentError(self, f'Wrong syntax, should be {self.option_strings[0]}=SOURCE:DEST')
        src = value[:separator.start()]
        dest = value[separator.end():]
        if not src or not dest:
            # Syntax was correct, but one or both of SRC and DEST was not given
            raise argparse.ArgumentError(self, "You have to specify both SOURCE and DEST")

        # argparse is not particularly smart with copy by reference typed defaults. If the current list is the default,
        # replace it before modifying it to avoid changing the default.
        if getattr(namespace, self.dest) is self.default:
            setattr(namespace, self.dest, [])
        getattr(namespace, self.dest).append((src, dest))


def make_variable_path(filename, conversions=path_conversions):
    if not os.path.isabs(filename):
        # os.path.commonpath can not compare relative and absolute paths, and if filename is not absolute, none of the
        # paths in conversions will match anyway.
        return None, filename
    for (from_path, to_name) in conversions:
        assert os.path.abspath(from_path) == from_path, ("path '%s' should already be absolute" % from_path)
        try:
            common_path = os.path.commonpath([filename, from_path])
        except ValueError:
            # Per https://docs.python.org/3/library/os.path.html#os.path.commonpath, this raises ValueError in several
            # cases which prevent computing a common path.
            common_path = None
        if common_path == from_path:
            rest = filename[len(from_path):]
            if rest.startswith(('\\', '/')):
                rest = rest[1:]
            return to_name, rest
    return None, filename


def removed_key_option(x):
    from PyInstaller.exceptions import RemovedCipherFeatureError
    raise RemovedCipherFeatureError("Please remove your --key=xxx argument.")


class _RemovedFlagAction(argparse.Action):
    def __init__(self, *args, **kwargs):
        kwargs["help"] = argparse.SUPPRESS
        kwargs["nargs"] = 0
        super().__init__(*args, **kwargs)


class _RemovedNoEmbedManifestAction(_RemovedFlagAction):
    def __call__(self, *args, **kwargs):
        from PyInstaller.exceptions import RemovedExternalManifestError
        raise RemovedExternalManifestError("Please remove your --no-embed-manifest argument.")


class _RemovedWinPrivateAssembliesAction(_RemovedFlagAction):
    def __call__(self, *args, **kwargs):
        from PyInstaller.exceptions import RemovedWinSideBySideSupportError
        raise RemovedWinSideBySideSupportError("Please remove your --win-private-assemblies argument.")


class _RemovedWinNoPreferRedirectsAction(_RemovedFlagAction):
    def __call__(self, *args, **kwargs):
        from PyInstaller.exceptions import RemovedWinSideBySideSupportError
        raise RemovedWinSideBySideSupportError("Please remove your --win-no-prefer-redirects argument.")


# An object used in place of a "path string", which knows how to repr() itself using variable names instead of
# hard-coded paths.
class Path:
    def __init__(self, *parts):
        self.path = os.path.join(*parts)
        self.variable_prefix = self.filename_suffix = None

    def __repr__(self):
        if self.filename_suffix is None:
            self.variable_prefix, self.filename_suffix = make_variable_path(self.path)
        if self.variable_prefix is None:
            return repr(self.path)
        return "os.path.join(" + self.variable_prefix + "," + repr(self.filename_suffix) + ")"


# An object used to construct extra preamble for the spec file, in order to accommodate extra collect_*() calls from the
# command-line
class Preamble:
    def __init__(
        self, datas, binaries, hiddenimports, collect_data, collect_binaries, collect_submodules, collect_all,
        copy_metadata, recursive_copy_metadata
    ):
        # Initialize with literal values - will be switched to preamble variable name later, if necessary
        self.binaries = binaries or []
        self.hiddenimports = hiddenimports or []
        self.datas = datas or []
        # Preamble content
        self.content = []

        # Import statements
        if collect_data:
            self._add_hookutil_import('collect_data_files')
        if collect_binaries:
            self._add_hookutil_import('collect_dynamic_libs')
        if collect_submodules:
            self._add_hookutil_import('collect_submodules')
        if collect_all:
            self._add_hookutil_import('collect_all')
        if copy_metadata or recursive_copy_metadata:
            self._add_hookutil_import('copy_metadata')
        if self.content:
            self.content += ['']  # empty line to separate the section
        # Variables
        if collect_data or copy_metadata or collect_all or recursive_copy_metadata:
            self._add_var('datas', self.datas)
            self.datas = 'datas'  # switch to variable
        if collect_binaries or collect_all:
            self._add_var('binaries', self.binaries)
            self.binaries = 'binaries'  # switch to variable
        if collect_submodules or collect_all:
            self._add_var('hiddenimports', self.hiddenimports)
            self.hiddenimports = 'hiddenimports'  # switch to variable
        # Content - collect_data_files
        for entry in collect_data:
            self._add_collect_data(entry)
        # Content - copy_metadata
        for entry in copy_metadata:
            self._add_copy_metadata(entry)
        # Content - copy_metadata(..., recursive=True)
        for entry in recursive_copy_metadata:
            self._add_recursive_copy_metadata(entry)
        # Content - collect_binaries
        for entry in collect_binaries:
            self._add_collect_binaries(entry)
        # Content - collect_submodules
        for entry in collect_submodules:
            self._add_collect_submodules(entry)
        # Content - collect_all
        for entry in collect_all:
            self._add_collect_all(entry)
        # Merge
        if self.content and self.content[-1] != '':
            self.content += ['']  # empty line
        self.content = '\n'.join(self.content)

    def _add_hookutil_import(self, name):
        self.content += ['from PyInstaller.utils.hooks import {0}'.format(name)]

    def _add_var(self, name, initial_value):
        self.content += ['{0} = {1}'.format(name, initial_value)]

    def _add_collect_data(self, name):
        self.content += ['datas += collect_data_files(\'{0}\')'.format(name)]

    def _add_copy_metadata(self, name):
        self.content += ['datas += copy_metadata(\'{0}\')'.format(name)]

    def _add_recursive_copy_metadata(self, name):
        self.content += ['datas += copy_metadata(\'{0}\', recursive=True)'.format(name)]

    def _add_collect_binaries(self, name):
        self.content += ['binaries += collect_dynamic_libs(\'{0}\')'.format(name)]

    def _add_collect_submodules(self, name):
        self.content += ['hiddenimports += collect_submodules(\'{0}\')'.format(name)]

    def _add_collect_all(self, name):
        self.content += [
            'tmp_ret = collect_all(\'{0}\')'.format(name),
            'datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]'
        ]


def __add_options(parser):
    """
    Add the `Makespec` options to a option-parser instance or a option group.
    """
    g = parser.add_argument_group('What to generate')
    g.add_argument(
        "-D",
        "--onedir",
        dest="onefile",
        action="store_false",
        default=None,
        help="Create a one-folder bundle containing an executable (default)",
    )
    g.add_argument(
        "-F",
        "--onefile",
        dest="onefile",
        action="store_true",
        default=None,
        help="Create a one-file bundled executable.",
    )
    g.add_argument(
        "--specpath",
        metavar="DIR",
        help="Folder to store the generated spec file (default: current directory)",
    )
    g.add_argument(
        "-n",
        "--name",
        help="Name to assign to the bundled app and spec file (default: first script's basename)",
    )
    g.add_argument(
        "--contents-directory",
        help="For onedir builds only, specify the name of the directory in which all supporting files (i.e. everything "
        "except the executable itself) will be placed in. Use \".\" to re-enable old onedir layout without contents "
        "directory.",
    )

    g = parser.add_argument_group('What to bundle, where to search')
    g.add_argument(
        '--add-data',
        action=SourceDestAction,
        dest='datas',
        help="Additional data files or directories containing data files to be added to the application. The argument "
        'value should be in form of "source:dest_dir", where source is the path to file (or directory) to be '
        "collected, dest_dir is the destination directory relative to the top-level application directory, and both "
        "paths are separated by a colon (:). To put a file in the top-level application directory, use . as a "
        "dest_dir. This option can be used multiple times."
    )
    g.add_argument(
        '--add-binary',
        action=SourceDestAction,
        dest="binaries",
        help='Additional binary files to be added to the executable. See the ``--add-data`` option for the format. '
        'This option can be used multiple times.',
    )
    g.add_argument(
        "-p",
        "--paths",
        dest="pathex",
        metavar="DIR",
        action="append",
        default=[],
        help="A path to search for imports (like using PYTHONPATH). Multiple paths are allowed, separated by ``%s``, "
        "or use this option multiple times. Equivalent to supplying the ``pathex`` argument in the spec file." %
        repr(os.pathsep),
    )
    g.add_argument(
        '--hidden-import',
        '--hiddenimport',
        action='append',
        default=[],
        metavar="MODULENAME",
        dest='hiddenimports',
        help='Name an import not visible in the code of the script(s). This option can be used multiple times.',
    )
    g.add_argument(
        '--collect-submodules',
        action="append",
        default=[],
        metavar="MODULENAME",
        dest='collect_submodules',
        help='Collect all submodules from the specified package or module. This option can be used multiple times.',
    )
    g.add_argument(
        '--collect-data',
        '--collect-datas',
        action="append",
        default=[],
        metavar="MODULENAME",
        dest='collect_data',
        help='Collect all data from the specified package or module. This option can be used multiple times.',
    )
    g.add_argument(
        '--collect-binaries',
        action="append",
        default=[],
        metavar="MODULENAME",
        dest='collect_binaries',
        help='Collect all binaries from the specified package or module. This option can be used multiple times.',
    )
    g.add_argument(
        '--collect-all',
        action="append",
        default=[],
        metavar="MODULENAME",
        dest='collect_all',
        help='Collect all submodules, data files, and binaries from the specified package or module. This option can '
        'be used multiple times.',
    )
    g.add_argument(
        '--copy-metadata',
        action="append",
        default=[],
        metavar="PACKAGENAME",
        dest='copy_metadata',
        help='Copy metadata for the specified package. This option can be used multiple times.',
    )
    g.add_argument(
        '--recursive-copy-metadata',
        action="append",
        default=[],
        metavar="PACKAGENAME",
        dest='recursive_copy_metadata',
        help='Copy metadata for the specified package and all its dependencies. This option can be used multiple '
        'times.',
    )
    g.add_argument(
        "--additional-hooks-dir",
        action="append",
        dest="hookspath",
        default=[],
        help="An additional path to search for hooks. This option can be used multiple times.",
    )
    g.add_argument(
        '--runtime-hook',
        action='append',
        dest='runtime_hooks',
        default=[],
        help='Path to a custom runtime hook file. A runtime hook is code that is bundled with the executable and is '
        'executed before any other code or module to set up special features of the runtime environment. This option '
        'can be used multiple times.',
    )
    g.add_argument(
        '--exclude-module',
        dest='excludes',
        action='append',
        default=[],
        help='Optional module or package (the Python name, not the path name) that will be ignored (as though it was '
        'not found). This option can be used multiple times.',
    )
    g.add_argument(
        '--key',
        dest='key',
        help=argparse.SUPPRESS,
        type=removed_key_option,
    )
    g.add_argument(
        '--splash',
        dest='splash',
        metavar="IMAGE_FILE",
        help="(EXPERIMENTAL) Add an splash screen with the image IMAGE_FILE to the application. The splash screen can "
        "display progress updates while unpacking.",
    )
    g.add_argument(
        '--splash-center',
        dest='splash_center',
        default=None,
        choices={'default', 'primary', 'virtual', 'active'},
        help="Splash screen centering mode. See the splash screen documentation for details.",
    )

    g = parser.add_argument_group('How to generate')
    g.add_argument(
        "-d",
        "--debug",
        # If this option is not specified, then its default value is an empty list (no debug options selected).
        default=[],
        # Note that ``nargs`` is omitted. This produces a single item not stored in a list, as opposed to a list
        # containing one item, as per `nargs <https://docs.python.org/3/library/argparse.html#nargs>`_.
        nargs=None,
        # The options specified must come from this list.
        choices=DEBUG_ALL_CHOICE + DEBUG_ARGUMENT_CHOICES,
        # Append choice, rather than storing them (which would overwrite any previous selections).
        action='append',
        # Allow newlines in the help text; see the ``_SmartFormatter`` in ``__main__.py``.
        help=(
            "R|Provide assistance with debugging a frozen\n"
            "application. This argument may be provided multiple\n"
            "times to select several of the following options.\n"
            "\n"
            "- all: All three of the following options.\n"
            "\n"
            "- imports: specify the -v option to the underlying\n"
            "  Python interpreter, causing it to print a message\n"
            "  each time a module is initialized, showing the\n"
            "  place (filename or built-in module) from which it\n"
            "  is loaded. See\n"
            "  https://docs.python.org/3/using/cmdline.html#id4.\n"
            "\n"
            "- bootloader: tell the bootloader to issue progress\n"
            "  messages while initializing and starting the\n"
            "  bundled app. Used to diagnose problems with\n"
            "  missing imports.\n"
            "\n"
            "- noarchive: instead of storing all frozen Python\n"
            "  source files as an archive inside the resulting\n"
            "  executable, store them as files in the resulting\n"
            "  output directory.\n"
            "\n"
        ),
    )
    g.add_argument(
        '--optimize',
        dest='optimize',
        metavar='LEVEL',
        type=int,
        choices={-1, 0, 1, 2},
        default=None,
        help='Bytecode optimization level used for collected python modules and scripts. For details, see the section '
        '“Bytecode Optimization Level” in PyInstaller manual.',
    )
    g.add_argument(
        '--python-option',
        dest='python_options',
        metavar='PYTHON_OPTION',
        action='append',
        default=[],
        help='Specify a command-line option to pass to the Python interpreter at runtime. Currently supports '
        '"v" (equivalent to "--debug imports"), "u", "W <warning control>", "X <xoption>", and "hash_seed=<value>". '
        'For details, see the section "Specifying Python Interpreter Options" in PyInstaller manual.',
    )
    g.add_argument(
        "-s",
        "--strip",
        action="store_true",
        help="Apply a symbol-table strip to the executable and shared libs (not recommended for Windows)",
    )
    g.add_argument(
        "--noupx",
        action="store_true",
        default=False,
        help="Do not use UPX even if it is available (works differently between Windows and *nix)",
    )
    g.add_argument(
        "--upx-exclude",
        dest="upx_exclude",
        metavar="FILE",
        action="append",
        help="Prevent a binary from being compressed when using upx. This is typically used if upx corrupts certain "
        "binaries during compression. FILE is the filename of the binary without path. This option can be used "
        "multiple times.",
    )

    g = parser.add_argument_group('Windows and macOS specific options')
    g.add_argument(
        "-c",
        "--console",
        "--nowindowed",
        dest="console",
        action="store_true",
        default=None,
        help="Open a console window for standard i/o (default). On Windows this option has no effect if the first "
        "script is a '.pyw' file.",
    )
    g.add_argument(
        "-w",
        "--windowed",
        "--noconsole",
        dest="console",
        action="store_false",
        default=None,
        help="Windows and macOS: do not provide a console window for standard i/o. On macOS this also triggers "
        "building a macOS .app bundle. On Windows this option is automatically set if the first script is a '.pyw' "
        "file. This option is ignored on *NIX systems.",
    )
    g.add_argument(
        "--hide-console",
        type=str,
        choices={'hide-early', 'hide-late', 'minimize-early', 'minimize-late'},
        default=None,
        help="Windows only: in console-enabled executable, have bootloader automatically hide or minimize the console "
        "window if the program owns the console window (i.e., was not launched from an existing console window).",
    )
    g.add_argument(
        "-i",
        "--icon",
        action='append',
        dest="icon_file",
        metavar='<FILE.ico or FILE.exe,ID or FILE.icns or Image or "NONE">',
        help="FILE.ico: apply the icon to a Windows executable. FILE.exe,ID: extract the icon with ID from an exe. "
        "FILE.icns: apply the icon to the .app bundle on macOS. If an image file is entered that isn't in the "
        "platform format (ico on Windows, icns on Mac), PyInstaller tries to use Pillow to translate the icon into "
        "the correct format (if Pillow is installed). Use \"NONE\" to not apply any icon, thereby making the OS show "
        "some default (default: apply PyInstaller's icon). This option can be used multiple times.",
    )
    g.add_argument(
        "--disable-windowed-traceback",
        dest="disable_windowed_traceback",
        action="store_true",
        default=False,
        help="Disable traceback dump of unhandled exception in windowed (noconsole) mode (Windows and macOS only), "
        "and instead display a message that this feature is disabled.",
    )

    g = parser.add_argument_group('Windows specific options')
    g.add_argument(
        "--version-file",
        dest="version_file",
        metavar="FILE",
        help="Add a version resource from FILE to the exe.",
    )
    g.add_argument(
        "--manifest",
        metavar="<FILE or XML>",
        help="Add manifest FILE or XML to the exe.",
    )
    g.add_argument(
        "-m",
        dest="shorthand_manifest",
        metavar="<FILE or XML>",
        help="Deprecated shorthand for --manifest.",
    )
    g.add_argument(
        "--no-embed-manifest",
        action=_RemovedNoEmbedManifestAction,
    )
    g.add_argument(
        "-r",
        "--resource",
        dest="resources",
        metavar="RESOURCE",
        action="append",
        default=[],
        help="Add or update a resource to a Windows executable. The RESOURCE is one to four items, "
        "FILE[,TYPE[,NAME[,LANGUAGE]]]. FILE can be a data file or an exe/dll. For data files, at least TYPE and NAME "
        "must be specified. LANGUAGE defaults to 0 or may be specified as wildcard * to update all resources of the "
        "given TYPE and NAME. For exe/dll files, all resources from FILE will be added/updated to the final executable "
        "if TYPE, NAME and LANGUAGE are omitted or specified as wildcard *. This option can be used multiple times.",
    )
    g.add_argument(
        '--uac-admin',
        dest='uac_admin',
        action="store_true",
        default=False,
        help="Using this option creates a Manifest that will request elevation upon application start.",
    )
    g.add_argument(
        '--uac-uiaccess',
        dest='uac_uiaccess',
        action="store_true",
        default=False,
        help="Using this option allows an elevated application to work with Remote Desktop.",
    )

    g = parser.add_argument_group('Windows Side-by-side Assembly searching options (advanced)')
    g.add_argument(
        "--win-private-assemblies",
        action=_RemovedWinPrivateAssembliesAction,
    )
    g.add_argument(
        "--win-no-prefer-redirects",
        action=_RemovedWinNoPreferRedirectsAction,
    )

    g = parser.add_argument_group('macOS specific options')
    g.add_argument(
        "--argv-emulation",
        dest="argv_emulation",
        action="store_true",
        default=False,
        help="Enable argv emulation for macOS app bundles. If enabled, the initial open document/URL event is "
        "processed by the bootloader and the passed file paths or URLs are appended to sys.argv.",
    )

    g.add_argument(
        '--osx-bundle-identifier',
        dest='bundle_identifier',
        help="macOS .app bundle identifier is used as the default unique program name for code signing purposes. "
        "The usual form is a hierarchical name in reverse DNS notation. For example: com.mycompany.department.appname "
        "(default: first script's basename)",
    )

    g.add_argument(
        '--target-architecture',
        '--target-arch',
        dest='target_arch',
        metavar='ARCH',
        default=None,
        help="Target architecture (macOS only; valid values: x86_64, arm64, universal2). Enables switching between "
        "universal2 and single-arch version of frozen application (provided python installation supports the target "
        "architecture). If not target architecture is not specified, the current running architecture is targeted.",
    )

    g.add_argument(
        '--codesign-identity',
        dest='codesign_identity',
        metavar='IDENTITY',
        default=None,
        help="Code signing identity (macOS only). Use the provided identity to sign collected binaries and generated "
        "executable. If signing identity is not provided, ad-hoc signing is performed instead.",
    )

    g.add_argument(
        '--osx-entitlements-file',
        dest='entitlements_file',
        metavar='FILENAME',
        default=None,
        help="Entitlements file to use when code-signing the collected binaries (macOS only).",
    )

    g = parser.add_argument_group('Rarely used special options')
    g.add_argument(
        "--runtime-tmpdir",
        dest="runtime_tmpdir",
        metavar="PATH",
        help="Where to extract libraries and support files in `onefile` mode. If this option is given, the bootloader "
        "will ignore any temp-folder location defined by the run-time OS. The ``_MEIxxxxxx``-folder will be created "
        "here. Please use this option only if you know what you are doing. Note that on POSIX systems, PyInstaller's "
        "bootloader does NOT perform shell-style environment variable expansion on the given path string. Therefore, "
        "using environment variables (e.g., ``~`` or ``$HOME``) in path will NOT work.",
    )
    g.add_argument(
        "--bootloader-ignore-signals",
        action="store_true",
        default=False,
        help="Tell the bootloader to ignore signals rather than forwarding them to the child process. Useful in "
        "situations where for example a supervisor process signals both the bootloader and the child (e.g., via a "
        "process group) to avoid signalling the child twice.",
    )


def main(
    scripts,
    name=None,
    onefile=False,
    console=True,
    debug=[],
    python_options=[],
    strip=False,
    noupx=False,
    upx_exclude=None,
    runtime_tmpdir=None,
    contents_directory=None,
    pathex=[],
    version_file=None,
    specpath=None,
    bootloader_ignore_signals=False,
    disable_windowed_traceback=False,
    datas=[],
    binaries=[],
    icon_file=None,
    manifest=None,
    resources=[],
    bundle_identifier=None,
    hiddenimports=[],
    hookspath=[],
    runtime_hooks=[],
    excludes=[],
    uac_admin=False,
    uac_uiaccess=False,
    collect_submodules=[],
    collect_binaries=[],
    collect_data=[],
    collect_all=[],
    copy_metadata=[],
    splash=None,
    recursive_copy_metadata=[],
    target_arch=None,
    codesign_identity=None,
    entitlements_file=None,
    argv_emulation=False,
    hide_console=None,
    optimize=None,
    splash_center=None,
    **_kwargs
):
    # Default values for onefile and console when not explicitly specified on command-line (indicated by None)
    if onefile is None:
        onefile = False

    if console is None:
        console = True

    # If appname is not specified - use the basename of the main script as name.
    if name is None:
        name = os.path.splitext(os.path.basename(scripts[0]))[0]

    # If specpath not specified - use default value - current working directory.
    if specpath is None:
        specpath = DEFAULT_SPECPATH
    else:
        # Expand starting tilde into user's home directory, as a work-around for tilde not being expanded by shell when
        # using `--specpath=~/path/abc` instead of `--specpath ~/path/abc` (or when the path argument is quoted).
        specpath = os.path.expanduser(specpath)
    # If cwd is the root directory of PyInstaller, generate the .spec file in ./appname/ subdirectory.
    if specpath == HOMEPATH:
        specpath = os.path.join(HOMEPATH, name)
    # Create directory tree if missing.
    if not os.path.exists(specpath):
        os.makedirs(specpath)

    # Handle additional EXE options.
    exe_options = ''
    if version_file:
        exe_options += "\n    version='%s'," % escape_win_filepath(version_file)
    if uac_admin:
        exe_options += "\n    uac_admin=True,"
    if uac_uiaccess:
        exe_options += "\n    uac_uiaccess=True,"
    if icon_file:
        # Icon file for Windows.
        # On Windows, the default icon is embedded in the bootloader executable.
        if icon_file[0] == 'NONE':
            exe_options += "\n    icon='NONE',"
        else:
            exe_options += "\n    icon=[%s]," % ','.join("'%s'" % escape_win_filepath(ic) for ic in icon_file)
        # Icon file for macOS.
        # We need to encapsulate it into apostrofes.
        icon_file = "'%s'" % icon_file[0]
    else:
        # On macOS, the default icon has to be copied into the .app bundle.
        # The the text value 'None' means - use default icon.
        icon_file = 'None'
    if contents_directory:
        exe_options += "\n    contents_directory='%s'," % (contents_directory or "_internal")
    if hide_console:
        exe_options += "\n    hide_console='%s'," % hide_console

    if bundle_identifier:
        # We need to encapsulate it into apostrofes.
        bundle_identifier = "'%s'" % bundle_identifier

    if _kwargs["shorthand_manifest"]:
        manifest = _kwargs["shorthand_manifest"]
        logger.log(
            logging.DEPRECATION, "PyInstaller v7 will remove the -m shorthand flag. Please use --manifest=%s instead",
            manifest
        )
    if manifest:
        if "<" in manifest:
            # Assume XML string
            exe_options += "\n    manifest='%s'," % manifest.replace("'", "\\'")
        else:
            # Assume filename
            exe_options += "\n    manifest='%s'," % escape_win_filepath(manifest)
    if resources:
        resources = list(map(escape_win_filepath, resources))
        exe_options += "\n    resources=%s," % repr(resources)

    hiddenimports = hiddenimports or []
    upx_exclude = upx_exclude or []

    if is_darwin and onefile and not console:
        from PyInstaller.building.osx import WINDOWED_ONEFILE_DEPRCATION
        logger.log(logging.DEPRECATION, WINDOWED_ONEFILE_DEPRCATION)

    # If file extension of the first script is '.pyw', force --windowed option.
    if is_win and os.path.splitext(scripts[0])[-1] == '.pyw':
        console = False

    # If script paths are relative, make them relative to the directory containing .spec file.
    scripts = [make_path_spec_relative(x, specpath) for x in scripts]
    # With absolute paths replace prefix with variable HOMEPATH.
    scripts = list(map(Path, scripts))

    # Translate the default of ``debug=None`` to an empty list.
    if debug is None:
        debug = []
    # Translate the ``all`` option.
    if DEBUG_ALL_CHOICE[0] in debug:
        debug = DEBUG_ARGUMENT_CHOICES

    # Create preamble (for collect_*() calls)
    preamble = Preamble(
        datas, binaries, hiddenimports, collect_data, collect_binaries, collect_submodules, collect_all, copy_metadata,
        recursive_copy_metadata
    )

    if splash:
        splash_options = ""
        if splash_center:
            splash_options = f"\n    center={splash_center!r},"
        splash_init = splashtmpl % {
            'splash_image': splash,
            'splash_options': splash_options,
        }
        splash_binaries = "\n    splash.binaries,"
        splash_target = "\n    splash,"
    else:
        splash_init = splash_binaries = splash_target = ""

    # Infer byte-code optimization level.
    opt_level = sum([opt == 'O' for opt in python_options])
    if opt_level > 2:
        logger.warning(
            "The switch '--python-option O' has been specified %d times - it should be specified at most twice!",
            opt_level,
        )
        opt_level = 2

    if optimize is None:
        if opt_level == 0:
            # Infer from running python process
            optimize = sys.flags.optimize
        else:
            # Infer from `--python-option O` switch(es).
            optimize = opt_level
    elif optimize != opt_level and opt_level != 0:
        logger.warning(
            "Mismatch between optimization level passed via --optimize switch (%d) and number of '--python-option O' "
            "switches (%d)!",
            optimize,
            opt_level,
        )

    if optimize >= 0:
        # Ensure OPTIONs passed to bootloader match the optimization settings.
        python_options += max(0, optimize - opt_level) * ['O']

    # Create OPTIONs array
    if 'imports' in debug and 'v' not in python_options:
        python_options.append('v')
    python_options_array = [(opt, None, 'OPTION') for opt in python_options]

    d = {
        'scripts': scripts,
        'pathex': pathex or [],
        'binaries': preamble.binaries,
        'datas': preamble.datas,
        'hiddenimports': preamble.hiddenimports,
        'preamble': preamble.content,
        'name': name,
        'noarchive': 'noarchive' in debug,
        'optimize': optimize,
        'options': python_options_array,
        'debug_bootloader': 'bootloader' in debug,
        'bootloader_ignore_signals': bootloader_ignore_signals,
        'strip': strip,
        'upx': not noupx,
        'upx_exclude': upx_exclude,
        'runtime_tmpdir': runtime_tmpdir,
        'exe_options': exe_options,
        # Directory with additional custom import hooks.
        'hookspath': hookspath,
        # List with custom runtime hook files.
        'runtime_hooks': runtime_hooks or [],
        # List of modules/packages to ignore.
        'excludes': excludes or [],
        # only Windows and macOS distinguish windowed and console apps
        'console': console,
        'disable_windowed_traceback': disable_windowed_traceback,
        # Icon filename. Only macOS uses this item.
        'icon': icon_file,
        # .app bundle identifier. Only macOS uses this item.
        'bundle_identifier': bundle_identifier,
        # argv emulation (macOS only)
        'argv_emulation': argv_emulation,
        # Target architecture (macOS only)
        'target_arch': target_arch,
        # Code signing identity (macOS only)
        'codesign_identity': codesign_identity,
        # Entitlements file (macOS only)
        'entitlements_file': entitlements_file,
        # splash screen
        'splash_init': splash_init,
        'splash_target': splash_target,
        'splash_binaries': splash_binaries,
    }

    # Write down .spec file to filesystem.
    specfnm = os.path.join(specpath, name + '.spec')
    with open(specfnm, 'w', encoding='utf-8') as specfile:
        if onefile:
            specfile.write(onefiletmplt % d)
            # For macOS create .app bundle.
            if is_darwin and not console:
                specfile.write(bundleexetmplt % d)
        else:
            specfile.write(onedirtmplt % d)
            # For macOS create .app bundle.
            if is_darwin and not console:
                specfile.write(bundletmplt % d)

    return specfnm
