# Copyright 2020 Pants project contributors (see CONTRIBUTORS.md).
# Licensed under the Apache License, Version 2.0 (see LICENSE).

from __future__ import annotations

import collections.abc
import logging
import os.path
from abc import ABC, abstractmethod
from collections.abc import Iterable, Iterator, Mapping, Sequence
from dataclasses import dataclass
from enum import Enum, StrEnum
from typing import TYPE_CHECKING, ClassVar, cast

from packaging.utils import canonicalize_name as canonicalize_project_name

from pants.backend.python.macros.python_artifact import PythonArtifact
from pants.backend.python.subsystems.setup import PythonSetup
from pants.core.environments.target_types import EnvironmentField
from pants.core.goals.generate_lockfiles import UnrecognizedResolveNamesError
from pants.core.goals.package import OutputPathField
from pants.core.goals.run import RestartableField
from pants.core.goals.test import (
    RuntimePackageDependenciesField,
    TestExtraEnvVarsField,
    TestsBatchCompatibilityTagField,
    TestSubsystem,
)
from pants.core.target_types import ResolveLikeField, ResolveLikeFieldToValueRequest
from pants.engine.addresses import Address, Addresses
from pants.engine.target import (
    COMMON_TARGET_FIELDS,
    AsyncFieldMixin,
    BoolField,
    Dependencies,
    DictStringToStringField,
    DictStringToStringSequenceField,
    Field,
    IntField,
    InvalidFieldException,
    InvalidFieldTypeException,
    InvalidTargetException,
    MultipleSourcesField,
    NestedDictStringToStringField,
    OptionalSingleSourceField,
    OverridesField,
    ScalarField,
    SingleSourceField,
    SpecialCasedDependencies,
    StringField,
    StringSequenceField,
    Target,
    TargetFilesGenerator,
    TargetFilesGeneratorSettingsRequest,
    TargetGenerator,
    TriBoolField,
    ValidNumbers,
    generate_file_based_overrides_field_help_message,
    generate_multiple_sources_field_help_message,
)
from pants.option.option_types import BoolOption
from pants.option.subsystem import Subsystem
from pants.util.docutil import bin_name, doc_url, git_url
from pants.util.frozendict import FrozenDict
from pants.util.pip_requirement import PipRequirement
from pants.util.strutil import help_text, softwrap

logger = logging.getLogger(__name__)

if TYPE_CHECKING:
    from pants.backend.python.subsystems.pytest import PyTest


# -----------------------------------------------------------------------------------------------
# Common fields
# -----------------------------------------------------------------------------------------------


class PythonSourceField(SingleSourceField):
    # Note that Python scripts often have no file ending.
    expected_file_extensions: ClassVar[tuple[str, ...]] = ("", ".py", ".pyi")


class PythonDependenciesField(Dependencies):
    pass


class PythonGeneratingSourcesBase(MultipleSourcesField):
    expected_file_extensions: ClassVar[tuple[str, ...]] = ("", ".py", ".pyi")


class InterpreterConstraintsField(StringSequenceField, AsyncFieldMixin):
    alias = "interpreter_constraints"
    help = help_text(
        f"""
        The Python interpreters this code is compatible with.

        Each element should be written in pip-style format, e.g. `CPython==2.7.*` or
        `CPython>=3.6,<4`. You can leave off `CPython` as a shorthand, e.g. `>=2.7` will be expanded
        to `CPython>=2.7`.

        Specify more than one element to OR the constraints, e.g. `['PyPy==3.7.*', 'CPython==3.7.*']`
        means either PyPy 3.7 _or_ CPython 3.7.

        If the field is not set, it will default to the option `[python].interpreter_constraints`.

        See {doc_url("docs/python/overview/interpreter-compatibility")} for how these interpreter
        constraints are merged with the constraints of dependencies.
        """
    )

    def value_or_configured_default(
        self, python_setup: PythonSetup, resolve: PythonResolveField | None
    ) -> tuple[str, ...]:
        """Return either the given `compatibility` field or the global interpreter constraints.

        If interpreter constraints are supplied by the CLI flag, return those only.
        """
        if self.value and python_setup.warn_on_python2_usage:
            # Side-step import cycle.
            from pants.backend.python.util_rules.interpreter_constraints import (
                warn_on_python2_usage_in_interpreter_constraints,
            )

            warn_on_python2_usage_in_interpreter_constraints(
                self.value,
                description_of_origin=f"the `{self.alias}` field on target at `{self.address}`",
            )
        return python_setup.compatibility_or_constraints(
            self.value,
            resolve.normalized_value(python_setup)
            if resolve and python_setup.enable_resolves
            else None,
        )


class PythonResolveLikeFieldToValueRequest(ResolveLikeFieldToValueRequest):
    pass


class PythonResolveField(StringField, AsyncFieldMixin, ResolveLikeField):
    alias = "resolve"
    required = False
    help = help_text(
        """
        The resolve from `[python].resolves` to use.

        If not defined, will default to `[python].default_resolve`.

        All dependencies must share the same value for their `resolve` field.
        """
    )

    def normalized_value(self, python_setup: PythonSetup) -> str:
        """Get the value after applying the default and validating that the key is recognized."""
        if not python_setup.enable_resolves:
            return "<ignore>"
        resolve = self.value or python_setup.default_resolve
        if resolve not in python_setup.resolves:
            raise UnrecognizedResolveNamesError(
                [resolve],
                python_setup.resolves.keys(),
                description_of_origin=f"the field `{self.alias}` in the target {self.address}",
            )
        return resolve

    def get_resolve_like_field_to_value_request(self) -> type[ResolveLikeFieldToValueRequest]:
        return PythonResolveLikeFieldToValueRequest


class PrefixedPythonResolveField(PythonResolveField):
    alias = "python_resolve"


class PythonRunGoalUseSandboxField(TriBoolField):
    alias = "run_goal_use_sandbox"
    help = help_text(
        """
        Whether to use a sandbox when `run`ning this target. Defaults to
        `[python].default_run_goal_use_sandbox`.

        If true, runs of this target with the `run` goal will copy the needed first-party sources
        into a temporary sandbox and run from there.

        If false, runs of this target with the `run` goal will use the in-repo sources
        directly.

        Note that this field only applies when running a target with the `run` goal. No other goals
        (such as `test`, if applicable) consult this field.

        The former mode is more hermetic, and is closer to building and running the source as it
        were packaged in a `pex_binary`. Additionally, it may be necessary if your sources depend
        transitively on "generated" files which will be materialized in the sandbox in a source
        root, but are not in-repo.

        The latter mode is similar to creating, activating, and using a virtual environment when
        running your files. It may also be necessary if the source being run writes files into the
        repo and computes their location relative to the executed files. Django's `makemigrations`
        command is an example of such a process.
        """
    )


# -----------------------------------------------------------------------------------------------
# Target generation support
# -----------------------------------------------------------------------------------------------


class PythonFilesGeneratorSettingsRequest(TargetFilesGeneratorSettingsRequest):
    pass


# -----------------------------------------------------------------------------------------------
# `pex_binary` and `pex_binaries` target
# -----------------------------------------------------------------------------------------------


# See `target_types_rules.py` for a dependency injection rule.
class PexBinaryDependenciesField(Dependencies):
    supports_transitive_excludes = True


class MainSpecification(ABC):
    @abstractmethod
    def iter_pex_args(self) -> Iterator[str]: ...

    @property
    @abstractmethod
    def spec(self) -> str: ...


@dataclass(frozen=True)
class EntryPoint(MainSpecification):
    module: str
    function: str | None = None

    @classmethod
    def parse(cls, value: str, provenance: str | None = None) -> EntryPoint:
        given = f"entry point {provenance}" if provenance else "entry point"
        entry_point = value.strip()
        if not entry_point:
            raise ValueError(
                softwrap(
                    f"""
                    The {given} cannot be blank. It must indicate a Python module by name or path
                    and an optional nullary function in that module separated by a colon, i.e.:
                    module_name_or_path(':'function_name)?
                    """
                )
            )
        module_or_path, sep, func = entry_point.partition(":")
        if not module_or_path:
            raise ValueError(f"The {given} must specify a module; given: {value!r}")
        if ":" in func:
            raise ValueError(
                softwrap(
                    f"""
                    The {given} can only contain one colon separating the entry point's module from
                    the entry point function in that module; given: {value!r}
                    """
                )
            )
        if sep and not func:
            logger.warning(
                softwrap(
                    f"""
                    Assuming no entry point function and stripping trailing ':' from the {given}:
                    {value!r}. Consider deleting it to make it clear no entry point function is
                    intended.
                    """
                )
            )
        return cls(module=module_or_path, function=func if func else None)

    def __post_init__(self):
        if ":" in self.module:
            raise ValueError(
                softwrap(
                    f"""
                    The `:` character is not valid in a module name. Given an entry point module of
                    {self.module}. Did you mean to use EntryPoint.parse?
                    """
                )
            )
        if self.function and ":" in self.function:
            raise ValueError(
                softwrap(
                    f"""
                    The `:` character is not valid in a function name. Given an entry point function
                    of {self.function}.
                    """
                )
            )

    def iter_pex_args(self) -> Iterator[str]:
        yield "--entry-point"
        yield self.spec

    @property
    def spec(self) -> str:
        return self.module if self.function is None else f"{self.module}:{self.function}"


@dataclass(frozen=True)
class ConsoleScript(MainSpecification):
    name: str

    def iter_pex_args(self) -> Iterator[str]:
        yield "--console-script"
        yield self.name

    @property
    def spec(self) -> str:
        return self.name


@dataclass(frozen=True)
class Executable(MainSpecification):
    executable: str

    @classmethod
    def create(cls, address: Address, filename: str) -> Executable:
        # spec_path is relative to the workspace. The rule is responsible for
        # stripping the source root as needed.
        return cls(os.path.join(address.spec_path, filename).lstrip(os.path.sep))

    def iter_pex_args(self) -> Iterator[str]:
        yield "--executable"
        # We do NOT yield self.executable or self.spec
        # as the path needs additional processing in the rule graph.
        # see: build_pex in util_rules/pex

    @property
    def spec(self) -> str:
        return self.executable


class EntryPointField(AsyncFieldMixin, Field):
    alias = "entry_point"
    default = None
    help = help_text(
        """
        Set the entry point, i.e. what gets run when executing `./my_app.pex`, to a module.

        You can specify a full module like `'path.to.module'` and `'path.to.module:func'`, or use a
        shorthand to specify a file name, using the same syntax as the `sources` field:

          1) `'app.py'`, Pants will convert into the module `path.to.app`;
          2) `'app.py:func'`, Pants will convert into `path.to.app:func`.

        You may only set one of: this field, or the `script` field, or the `executable` field.
        Leave off all three fields to have no entry point.
        """
    )
    value: EntryPoint | None

    @classmethod
    def compute_value(cls, raw_value: str | None, address: Address) -> EntryPoint | None:
        value = super().compute_value(raw_value, address)
        if value is None:
            return None
        if not isinstance(value, str):
            raise InvalidFieldTypeException(address, cls.alias, value, expected_type="a string")
        try:
            return EntryPoint.parse(value, provenance=f"for {address}")
        except ValueError as e:
            raise InvalidFieldException(str(e))


class PexEntryPointField(EntryPointField):
    # Specialist subclass for use with `PexBinary` targets.
    pass


# See `target_types_rules.py` for the `ResolvePexEntryPointRequest -> ResolvedPexEntryPoint` rule.
@dataclass(frozen=True)
class ResolvedPexEntryPoint:
    val: EntryPoint | None
    file_name_used: bool


@dataclass(frozen=True)
class ResolvePexEntryPointRequest:
    """Determine the `entry_point` for a `pex_binary` after applying all syntactic sugar."""

    entry_point_field: EntryPointField


class PexScriptField(Field):
    alias = "script"
    default = None
    help = help_text(
        """
        Set the entry point, i.e. what gets run when executing `./my_app.pex`, to a script or
        console_script as defined by any of the distributions in the PEX.

        You may only set one of: this field, or the `entry_point` field, or the `executable` field.
        Leave off all three fields to have no entry point.
        """
    )
    value: ConsoleScript | None

    @classmethod
    def compute_value(cls, raw_value: str | None, address: Address) -> ConsoleScript | None:
        value = super().compute_value(raw_value, address)
        if value is None:
            return None
        if not isinstance(value, str):
            raise InvalidFieldTypeException(address, cls.alias, value, expected_type="a string")
        return ConsoleScript(value)


class PexExecutableField(Field):
    alias = "executable"
    default = None
    help = help_text(
        """
        Set the entry point, i.e. what gets run when executing `./my_app.pex`, to an execuatble
        local python script. This executable python script is typically something that cannot
        be imported so it cannot be used via `script` or `entry_point`.

        You may only set one of: this field, or the `entry_point` field, or the `script` field.
        Leave off all three fields to have no entry point.
        """
    )
    value: Executable | None

    @classmethod
    def compute_value(cls, raw_value: str | None, address: Address) -> Executable | None:
        value = super().compute_value(raw_value, address)
        if value is None:
            return None
        if not isinstance(value, str):
            raise InvalidFieldTypeException(address, cls.alias, value, expected_type="a string")
        return Executable.create(address, value)


class PexArgsField(StringSequenceField):
    alias: ClassVar[str] = "args"
    help = help_text(
        lambda: f"""
        Freeze these command-line args into the PEX. Allows you to run generic entry points
        on specific arguments without creating a shim file.

        This is different to `{PexExtraBuildArgsField.alias}`: `{PexArgsField.alias}`
        records arguments used by the packaged PEX when executed,
        `{PexExtraBuildArgsField.alias}` passes arguments to the process that does the
        packaging.
        """
    )


class PexExtraBuildArgsField(StringSequenceField):
    alias: ClassVar[str] = "extra_build_args"
    default = ()
    help = help_text(
        lambda: f"""
        Extra arguments to pass to the `pex` invocation used to build this PEX. These are
        passed after all other arguments. This can be used to pass extra options that
        Pants doesn't have built-in support for.

        This is different to `{PexArgsField.alias}`: `{PexArgsField.alias}` records
        arguments used by the packaged PEX when executed, `{PexExtraBuildArgsField.alias}`
        passes arguments to the process that does the packaging.
        """
    )


class PexCheckField(StringField):
    alias = "check"
    valid_choices = ("none", "warn", "error")
    expected_type = str
    default = "warn"
    help = help_text(
        """
        Check that the built PEX is valid. Currently this only
        applies to `--layout zipapp` where the PEX zip is
        tested for importability of its `__main__` module by
        the Python zipimport module. This check will fail for
        PEX zips that use ZIP64 extensions since the Python
        zipimport zipimporter only works with 32 bit zips. The
        check no-ops for all other layouts.
        """
    )


class PexEnvField(DictStringToStringField):
    alias = "env"
    help = help_text(
        """
        Freeze these environment variables into the PEX. Allows you to run generic entry points
        on a specific environment without creating a shim file.
        """
    )


class PexCompletePlatformsField(SpecialCasedDependencies):
    alias = "complete_platforms"
    help = help_text(
        f"""
        The platforms the built PEX should be compatible with.

        There must be built wheels available for all of the foreign platforms, rather than sdists.

        You can give a list of multiple complete platforms to create a multiplatform PEX,
        meaning that the PEX will be executable in all of the supported environments.

        Complete platforms should be addresses of `file` or `resource` targets that point to files that contain
        complete platform JSON as described by Pex
        (https://pex.readthedocs.io/en/latest/buildingpex.html#complete-platform).

        See {doc_url("docs/python/overview/pex#generating-the-complete_platforms-file")} for details on how to create this file.
        """
    )


class PexInheritPathField(StringField):
    alias = "inherit_path"
    valid_choices = ("false", "fallback", "prefer")
    help = help_text(
        """
        Whether to inherit the `sys.path` (aka PYTHONPATH) of the environment that the binary runs in.

        Use `false` to not inherit `sys.path`; use `fallback` to inherit `sys.path` after packaged
        dependencies; and use `prefer` to inherit `sys.path` before packaged dependencies.
        """
    )

    # TODO(#9388): deprecate allowing this to be a `bool`.
    @classmethod
    def compute_value(cls, raw_value: str | bool | None, address: Address) -> str | None:
        if isinstance(raw_value, bool):
            return "prefer" if raw_value else "false"
        return super().compute_value(raw_value, address)


class PexStripEnvField(BoolField):
    alias = "strip_pex_env"
    default = True
    help = help_text(
        """
        Whether or not to strip the PEX runtime environment of `PEX*` environment variables.

        Most applications have no need for the `PEX*` environment variables that are used to
        control PEX startup; so these variables are scrubbed from the environment by Pex before
        transferring control to the application by default. This prevents any subprocesses that
        happen to execute other PEX files from inheriting these control knob values since most
        would be undesired; e.g.: PEX_MODULE or PEX_PATH.
        """
    )


class PexIgnoreErrorsField(BoolField):
    alias = "ignore_errors"
    default = False
    help = "Should PEX ignore errors when it cannot resolve dependencies?"


class PexShBootField(BoolField):
    alias = "sh_boot"
    default = False
    help = help_text(
        """
        Should PEX create a modified ZIPAPP that uses `/bin/sh` to boot?

        If you know the machines that the PEX will be distributed to have
        POSIX compliant `/bin/sh` (almost all do, see:
        https://pubs.opengroup.org/onlinepubs/9699919799/utilities/sh.html);
        then this is probably the way you want your PEX to boot. Instead of
        launching via a Python shebang, the PEX will launch via a `#!/bin/sh`
        shebang that executes a small script embedded in the head of the PEX
        ZIPAPP that performs initial interpreter selection and re-execution of
        the underlying PEX in a way that is often more robust than a Python
        shebang and always faster on 2nd and subsequent runs since the sh
        script has a constant overhead of O(1ms) whereas the Python overhead
        to perform the same interpreter selection and re-execution is
        O(100ms).
        """
    )


class PexShebangField(StringField):
    alias = "shebang"
    help = help_text(
        """
        Set the generated PEX to use this shebang, rather than the default of PEX choosing a
        shebang based on the interpreter constraints.

        This influences the behavior of running `./result.pex`. You can ignore the shebang by
        instead running `/path/to/python_interpreter ./result.pex`.
        """
    )


class PexEmitWarningsField(TriBoolField):
    alias = "emit_warnings"
    help = help_text(
        """
        Whether or not to emit PEX warnings at runtime.

        The default is determined by the option `emit_warnings` in the `[pex-binary-defaults]` scope.
        """
    )

    def value_or_global_default(self, pex_binary_defaults: PexBinaryDefaults) -> bool:
        if self.value is None:
            return pex_binary_defaults.emit_warnings

        return self.value


class PexExecutionMode(Enum):
    ZIPAPP = "zipapp"
    VENV = "venv"


class PexExecutionModeField(StringField):
    alias = "execution_mode"
    valid_choices = PexExecutionMode
    expected_type = str
    default = PexExecutionMode.ZIPAPP.value
    help = help_text(
        f"""
        The mode the generated PEX file will run in.

        The traditional PEX file runs in a modified {PexExecutionMode.ZIPAPP.value!r} mode (See:
        https://www.python.org/dev/peps/pep-0441/) where zipped internal code and dependencies
        are first unpacked to disk. This mode achieves the fastest cold start times and may, for
        example be the best choice for cloud lambda functions.

        The fastest execution mode in the steady state is {PexExecutionMode.VENV.value!r}, which
        generates a virtual environment from the PEX file on first run, but then achieves near
        native virtual environment start times. This mode also benefits from a traditional virtual
        environment `sys.path`, giving maximum compatibility with stdlib and third party APIs.
        """
    )


class PexLayout(Enum):
    ZIPAPP = "zipapp"
    PACKED = "packed"
    LOOSE = "loose"


class PexLayoutField(StringField):
    alias = "layout"
    valid_choices = PexLayout
    expected_type = str
    default = PexLayout.ZIPAPP.value
    help = help_text(
        f"""
        The layout used for the PEX binary.

        By default, a PEX is created as a single file zipapp, but either a packed or loose directory
        tree based layout can be chosen instead.

        A packed layout PEX is an executable directory structure designed to have
        cache-friendly characteristics for syncing incremental updates to PEXed applications over
        a network. At the top level of the packed directory tree there is an executable
        `__main__.py` script. The directory can also be executed by passing its path to a Python
        executable; e.g: `python packed-pex-dir/`. The Pex bootstrap code and all dependency code
        are packed into individual zip files for efficient caching and syncing.

        A loose layout PEX is similar to a packed PEX, except that neither the Pex bootstrap code
        nor the dependency code are packed into zip files, but are instead present as collections of
        loose files in the directory tree providing different caching and syncing tradeoffs.

        Both zipapp and packed layouts install themselves in the `$PEX_ROOT` as loose apps by
        default before executing, but these layouts compose with
        `{PexExecutionModeField.alias}='{PexExecutionMode.ZIPAPP.value}'` as well.
        """
    )


class PexIncludeRequirementsField(BoolField):
    alias = "include_requirements"
    default = True
    help = help_text(
        """
        Whether to include the third party requirements the binary depends on in the
        packaged PEX file.
        """
    )


class PexIncludeSourcesField(BoolField):
    alias = "include_sources"
    default = True
    help = help_text(
        """
        Whether to include your first party sources the binary uses in the packaged PEX file.
        """
    )


class PexIncludeToolsField(BoolField):
    alias = "include_tools"
    default = False
    help = help_text(
        """
        Whether to include Pex tools in the PEX bootstrap code.

        With tools included, the generated PEX file can be executed with `PEX_TOOLS=1 <pex file> --help`
        to gain access to all the available tools.
        """
    )


class PexVenvSitePackagesCopies(BoolField):
    alias = "venv_site_packages_copies"
    default = False
    help = help_text(
        """
        If execution_mode is venv, populate the venv site packages using hard links or copies of resolved PEX dependencies instead of symlinks.

        This can be used to work around problems with tools or libraries that are confused by symlinked source files.
        """
    )


class PexVenvHermeticScripts(BoolField):
    alias = "venv_hermetic_scripts"
    default = True
    help = help_text(
        """
        If execution_mode is "venv", emit a hermetic venv `pex` script and hermetic console scripts.

        The venv `pex` script and the venv console scripts are constructed to be hermetic by
        default; Python is executed with `-sE` to restrict the `sys.path` to the PEX venv contents
        only. Setting this field to `False` elides the Python `-sE` restrictions and can be used to
        interoperate with frameworks that use `PYTHONPATH` manipulation to run code.
        """
    )


class PexScieField(StringField):
    alias = "scie"
    valid_choices = ("lazy", "eager")
    default = None
    help = help_text(
        """
        Create one or more native executable scies from your PEX that include
        a portable CPython interpreter along with your PEX making for a truly
        hermetic PEX that can run on machines with no Python installed at
        all. If your PEX has multiple targets then one PEX scie will be made
        for each platform, selecting the latest compatible portable CPython or
        PyPy interpreter as appropriate. Note that only Python>=3.8 is
        supported. If you'd like to explicitly control the target platforms or
        the exact portable CPython selected, see `scie_platform`,
        `scie_pbs_release` and `scie_python_version`.  Specifying `lazy` will
        fetch the portable CPython interpreter just in time on first boot of
        the PEX scie on a given machine if needed. Specifying `eager` will
        embed the portable CPython interpreter in your PEX scie making for a
        larger file, but requiring no internet access to boot. See
        https://science.scie.app for further details.

        This field must be set for any other `scie_*` fields to take effect.

        NOTE: `pants run` will always run the "regular" PEX, use `package` to
        create scie PEXs.  """
    )


class ScieNameStyle(StrEnum):
    DYNAMIC = "dynamic"
    PLATFORM_PARENT_DIR = "platform-parent-dir"
    PLATFORM_FILE_SUFFIX = "platform-file-suffix"


class PexScieNameStyleField(StringField):
    alias = "scie_name_style"
    valid_choices = ScieNameStyle
    expected_type = str
    default = ScieNameStyle.DYNAMIC
    help = help_text(
        """
        Control how the output file translates to a scie name. By default
        (`dynamic`), the platform is used as a file suffix only when needed
        for disambiguation when targeting a local platform.  Specifying
        `platform-file-suffix` forces the scie target platform name to be
        added as a suffix of the output filename; Specifying
        `platform-parent-dir` places the scie in a sub- directory with the
        name of the platform it targets."""
    )


class PexScieBusyBox(StringField):
    alias = "scie_busybox"
    default = None
    help = help_text(
        """
        Make the PEX scie a BusyBox over the specified entry points. The entry
        points can either be console scripts or entry point specifiers. To
        select all console scripts in all distributions contained in the PEX,
        use `@`. To just pick all the console scripts from a particular
        project name's distributions in the PEX, use `@<project name>`; e.g.:
        `@ansible-core`. To exclude all the console scripts from a project,
        prefix with a `!`; e.g.: `@,!@ansible-core` selects all console
        scripts except those provided by the `ansible- core` project. To
        select an individual console script, just use its name or prefix the
        name with `!` to exclude that individual console script. To specify an
        arbitrary entry point in a module contained within one of the
        distributions in the PEX, use a string of the form
        `<name>=<module>(:<function>)`; e.g.: 'run- baz=foo.bar:baz' to
        execute the `baz` function in the `foo.bar` module as the entry point
        named `run-baz`.

        A BusyBox scie has no default entrypoint; instead, when run, it
        inspects argv0; if that matches one of its embedded entry points, it
        runs that entry point; if not, it lists all available entrypoints for
        you to pick from. To run a given entry point, you specify it as the
        first argument and all other arguments after that are forwarded to
        that entry point. BusyBox PEX scies allow you to install all their
        contained entry points into a given directory.  For more information,
        run `SCIE=help <your PEX scie>` and review the `install` command help.

        NOTE: This is only available for formal Python entry points
        <https://packaging.python.org/en/latest/specifications/entry-points/>
        and not the informal use by the `pex_binary` field `entry_point` to
        run first party files.
        """
    )


class PexScieBusyboxPexEntrypointEnvPassthrough(TriBoolField):
    alias = "scie_busybox_pex_entrypoint_env_passthrough"
    required = False
    default = None
    help = help_text(
        """ When creating a busybox, allow overriding the primary entrypoint
        at runtime via PEX_INTERPRETER, PEX_SCRIPT and PEX_MODULE. Note that
        when using the `venv` execution mode this adds modest startup overhead
        on the order of 10ms.  """
    )


class PexSciePlatformField(StringSequenceField):
    alias = "scie_platform"
    valid_choices = (
        "current",
        "linux-aarch64",
        "linux-armv7l",
        "linux-powerpc64",
        "linux-riscv64",
        "linux-s390x",
        "linux-x86_64",
        "macos-aarch64",
        "macos-x86_64",
    )
    expected_type = str
    help = help_text(
        """ The platform to produce the native PEX scie executable for.  You
        can use a value of `current` to select the current platform. If left
        unspecified, the platforms implied by the targets selected to build
        the PEX with are used. Those targets are influenced by the current
        interpreter running Pex as well as use of `complete_platforms` and
        `interpreter_constraints`. Note that, in general, `scie_platform`
        should only be used to select a subset of the platforms implied by the
        targets selected via other options.  """
    )


class PexSciePbsReleaseField(StringField):
    alias = "scie_pbs_release"
    default = None
    help = help_text(
        """ The Python Standalone Builds release to use when a CPython
        interpreter distribution is needed for the PEX scie. Currently,
        releases are dates of the form YYYYMMDD, e.g.: '20240713'. See their
        GitHub releases page at
