From 13cc66a21b93e6923448149da2a1aaf2059445e1 Mon Sep 17 00:00:00 2001 From: Tom Dyas Date: Thu, 13 Feb 2025 13:45:30 -0500 Subject: [PATCH 1/2] resolve plugins using direct `pex` invocations --- src/python/pants/backend/BUILD | 4 + .../pants/backend/python/goals/export.py | 2 +- .../pants/backend/python/util_rules/pex.py | 3 +- .../backend/python/util_rules/pex_cli.py | 64 +--- .../backend/python/util_rules/pex_cli_tool.py | 74 +++++ .../python/util_rules/pex_test_utils.py | 2 +- src/python/pants/init/plugin_resolver.py | 281 ++++++++++++++++-- 7 files changed, 339 insertions(+), 91 deletions(-) create mode 100644 src/python/pants/backend/python/util_rules/pex_cli_tool.py diff --git a/src/python/pants/backend/BUILD b/src/python/pants/backend/BUILD index bb5359421bf..8b8ab0d0e9f 100644 --- a/src/python/pants/backend/BUILD +++ b/src/python/pants/backend/BUILD @@ -54,8 +54,12 @@ __dependents_rules__( ( ( "[/python/util_rules/interpreter_constraints.py]", + "[/python/util_rules/pex_cli_tool.py]", "[/python/util_rules/pex_environment.py]", "[/python/util_rules/pex_requirements.py]", + "[/python/subsystems/python_native_code.py]", + "[/python/subsystems/repos.py]", + "[/python/subsystems/setup.py]", ), "src/python/pants/init/plugin_resolver.py", DEFAULT_DEPENDENTS_RULES, diff --git a/src/python/pants/backend/python/goals/export.py b/src/python/pants/backend/python/goals/export.py index d3fc01840c0..18aa1eefd60 100644 --- a/src/python/pants/backend/python/goals/export.py +++ b/src/python/pants/backend/python/goals/export.py @@ -21,7 +21,7 @@ EditableLocalDistsRequest, ) from pants.backend.python.util_rules.pex import Pex, PexRequest, VenvPex -from pants.backend.python.util_rules.pex_cli import PexPEX +from pants.backend.python.util_rules.pex_cli_tool import PexPEX from pants.backend.python.util_rules.pex_environment import PexEnvironment, PythonExecutable from pants.backend.python.util_rules.pex_requirements import EntireLockfile, Lockfile from pants.core.goals.export import ( diff --git a/src/python/pants/backend/python/util_rules/pex.py b/src/python/pants/backend/python/util_rules/pex.py index a93a8b32b8d..669eb7ae213 100644 --- a/src/python/pants/backend/python/util_rules/pex.py +++ b/src/python/pants/backend/python/util_rules/pex.py @@ -29,7 +29,8 @@ ) from pants.backend.python.util_rules import pex_cli, pex_requirements from pants.backend.python.util_rules.interpreter_constraints import InterpreterConstraints -from pants.backend.python.util_rules.pex_cli import PexCliProcess, PexPEX, maybe_log_pex_stderr +from pants.backend.python.util_rules.pex_cli import PexCliProcess, maybe_log_pex_stderr +from pants.backend.python.util_rules.pex_cli_tool import PexPEX from pants.backend.python.util_rules.pex_environment import ( CompletePexEnvironment, PexEnvironment, diff --git a/src/python/pants/backend/python/util_rules/pex_cli.py b/src/python/pants/backend/python/util_rules/pex_cli.py index 18ad2eb9af5..4c5f602a818 100644 --- a/src/python/pants/backend/python/util_rules/pex_cli.py +++ b/src/python/pants/backend/python/util_rules/pex_cli.py @@ -12,67 +12,22 @@ from pants.backend.python.subsystems.python_native_code import PythonNativeCodeSubsystem from pants.backend.python.subsystems.setup import PythonSetup from pants.backend.python.util_rules import pex_environment +from pants.backend.python.util_rules.pex_cli_tool import PexCli, PexPEX +from pants.backend.python.util_rules.pex_cli_tool import rules as pex_cli_tools_rules from pants.backend.python.util_rules.pex_environment import PexEnvironment, PexSubsystem -from pants.core.goals.resolves import ExportableTool -from pants.core.util_rules import adhoc_binaries, external_tool +from pants.core.util_rules import adhoc_binaries from pants.core.util_rules.adhoc_binaries import PythonBuildStandaloneBinary -from pants.core.util_rules.external_tool import ( - DownloadedExternalTool, - ExternalToolRequest, - TemplatedExternalTool, -) from pants.engine.fs import CreateDigest, Digest, Directory, MergeDigests from pants.engine.internals.selectors import MultiGet -from pants.engine.platform import Platform from pants.engine.process import Process, ProcessCacheScope from pants.engine.rules import Get, collect_rules, rule -from pants.engine.unions import UnionRule from pants.option.global_options import GlobalOptions, ca_certs_path_to_file_content -from pants.option.option_types import ArgsListOption from pants.util.frozendict import FrozenDict from pants.util.logging import LogLevel -from pants.util.meta import classproperty -from pants.util.strutil import softwrap logger = logging.getLogger(__name__) -class PexCli(TemplatedExternalTool): - options_scope = "pex-cli" - name = "pex" - help = "The PEX (Python EXecutable) tool (https://github.com/pex-tool/pex)." - - default_version = "v2.33.1" - default_url_template = "https://github.com/pex-tool/pex/releases/download/{version}/pex" - version_constraints = ">=2.13.0,<3.0" - - # extra args to be passed to the pex tool; note that they - # are going to apply to all invocations of the pex tool. - global_args = ArgsListOption( - example="--check=error --no-compile", - extra_help=softwrap( - """ - Note that these apply to all invocations of the pex tool, including building `pex_binary` - targets, preparing `python_test` targets to run, and generating lockfiles. - """ - ), - ) - - @classproperty - def default_known_versions(cls): - return [ - "|".join( - ( - cls.default_version, - plat, - "5ebed0e2ba875983a72b4715ee3b2ca6ae5fedbf28d738634e02e30e3bb5ed28", - "4559974", - ) - ) - for plat in ["macos_arm64", "macos_x86_64", "linux_x86_64", "linux_arm64"] - ] - - @dataclass(frozen=True) class PexCliProcess: subcommand: tuple[str, ...] @@ -120,16 +75,6 @@ def __post_init__(self) -> None: raise ValueError("`--pex-root` flag not allowed. We set its value for you.") -class PexPEX(DownloadedExternalTool): - """The Pex PEX binary.""" - - -@rule -async def download_pex_pex(pex_cli: PexCli, platform: Platform) -> PexPEX: - pex_pex = await Get(DownloadedExternalTool, ExternalToolRequest, pex_cli.get_request(platform)) - return PexPEX(digest=pex_pex.digest, exe=pex_pex.exe) - - @rule async def setup_pex_cli_process( request: PexCliProcess, @@ -238,8 +183,7 @@ def maybe_log_pex_stderr(stderr: bytes, pex_verbosity: int) -> None: def rules(): return [ *collect_rules(), - *external_tool.rules(), + *pex_cli_tools_rules(), *pex_environment.rules(), *adhoc_binaries.rules(), - UnionRule(ExportableTool, PexCli), ] diff --git a/src/python/pants/backend/python/util_rules/pex_cli_tool.py b/src/python/pants/backend/python/util_rules/pex_cli_tool.py new file mode 100644 index 00000000000..f6e1b55ef5a --- /dev/null +++ b/src/python/pants/backend/python/util_rules/pex_cli_tool.py @@ -0,0 +1,74 @@ +# Copyright 2020 Pants project contributors (see CONTRIBUTORS.md). +# Licensed under the Apache License, Version 2.0 (see LICENSE). + +from pants.core.goals.resolves import ExportableTool +from pants.core.util_rules import external_tool +from pants.core.util_rules.external_tool import ( + DownloadedExternalTool, + ExternalToolRequest, + TemplatedExternalTool, +) +from pants.engine.platform import Platform +from pants.engine.rules import Get, collect_rules, rule +from pants.engine.unions import UnionRule +from pants.option.option_types import ArgsListOption +from pants.util.meta import classproperty +from pants.util.strutil import softwrap + +# Note: These rules were separated from `pex_cli.py` so that the plugin resolution code in +# src/python/pants/init/plugin_resolver.py can rely on a downloaded `pex` tool without +# bringing in other parts of the Python backend. + + +class PexCli(TemplatedExternalTool): + options_scope = "pex-cli" + name = "pex" + help = "The PEX (Python EXecutable) tool (https://github.com/pex-tool/pex)." + + default_version = "v2.33.1" + default_url_template = "https://github.com/pex-tool/pex/releases/download/{version}/pex" + version_constraints = ">=2.13.0,<3.0" + + # extra args to be passed to the pex tool; note that they + # are going to apply to all invocations of the pex tool. + global_args = ArgsListOption( + example="--check=error --no-compile", + extra_help=softwrap( + """ + Note that these apply to all invocations of the pex tool, including building `pex_binary` + targets, preparing `python_test` targets to run, and generating lockfiles. + """ + ), + ) + + @classproperty + def default_known_versions(cls): + return [ + "|".join( + ( + cls.default_version, + plat, + "5ebed0e2ba875983a72b4715ee3b2ca6ae5fedbf28d738634e02e30e3bb5ed28", + "4559974", + ) + ) + for plat in ["macos_arm64", "macos_x86_64", "linux_x86_64", "linux_arm64"] + ] + + +class PexPEX(DownloadedExternalTool): + """The Pex PEX binary.""" + + +@rule +async def download_pex_pex(pex_cli: PexCli, platform: Platform) -> PexPEX: + pex_pex = await Get(DownloadedExternalTool, ExternalToolRequest, pex_cli.get_request(platform)) + return PexPEX(digest=pex_pex.digest, exe=pex_pex.exe) + + +def rules(): + return ( + *collect_rules(), + *external_tool.rules(), + UnionRule(ExportableTool, PexCli), + ) diff --git a/src/python/pants/backend/python/util_rules/pex_test_utils.py b/src/python/pants/backend/python/util_rules/pex_test_utils.py index eb66ab4be6e..d10f10a3416 100644 --- a/src/python/pants/backend/python/util_rules/pex_test_utils.py +++ b/src/python/pants/backend/python/util_rules/pex_test_utils.py @@ -21,7 +21,7 @@ VenvPex, VenvPexProcess, ) -from pants.backend.python.util_rules.pex_cli import PexPEX +from pants.backend.python.util_rules.pex_cli_tool import PexPEX from pants.backend.python.util_rules.pex_requirements import EntireLockfile, PexRequirements from pants.engine.fs import Digest from pants.engine.process import Process, ProcessResult diff --git a/src/python/pants/init/plugin_resolver.py b/src/python/pants/init/plugin_resolver.py index 8476b4ec532..175456b31e5 100644 --- a/src/python/pants/init/plugin_resolver.py +++ b/src/python/pants/init/plugin_resolver.py @@ -3,36 +3,151 @@ from __future__ import annotations +import json import logging +import os +import shlex import site import sys -from collections.abc import Iterable +from collections.abc import Iterable, Mapping from dataclasses import dataclass +from pathlib import PurePath +from textwrap import dedent # noqa: PNT20 from typing import cast from pkg_resources import Requirement, WorkingSet from pkg_resources import working_set as global_working_set +from pants.backend.python.subsystems.python_native_code import PythonNativeCodeSubsystem +from pants.backend.python.subsystems.repos import PythonRepos +from pants.backend.python.subsystems.setup import PythonSetup from pants.backend.python.util_rules.interpreter_constraints import InterpreterConstraints -from pants.backend.python.util_rules.pex import PexRequest, VenvPex, VenvPexProcess +from pants.backend.python.util_rules.pex_cli_tool import PexPEX +from pants.backend.python.util_rules.pex_cli_tool import rules as pex_cli_tool_rules from pants.backend.python.util_rules.pex_environment import PythonExecutable -from pants.backend.python.util_rules.pex_requirements import PexRequirements +from pants.core.subsystems.python_bootstrap import PythonBootstrap +from pants.core.util_rules.adhoc_binaries import PythonBuildStandaloneBinary from pants.core.util_rules.environments import determine_bootstrap_environment +from pants.core.util_rules.system_binaries import BashBinary from pants.engine.collection import DeduplicatedCollection -from pants.engine.env_vars import CompleteEnvironmentVars +from pants.engine.env_vars import CompleteEnvironmentVars, EnvironmentVars, EnvironmentVarsRequest from pants.engine.environment import EnvironmentName +from pants.engine.fs import CreateDigest, Digest, Directory, FileContent, MergeDigests from pants.engine.internals.selectors import Params from pants.engine.internals.session import SessionValues -from pants.engine.process import ProcessCacheScope, ProcessResult +from pants.engine.process import Process, ProcessCacheScope, ProcessResult from pants.engine.rules import Get, QueryRule, collect_rules, rule from pants.init.bootstrap_scheduler import BootstrapScheduler from pants.option.global_options import GlobalOptions from pants.option.options_bootstrapper import OptionsBootstrapper +from pants.util.frozendict import FrozenDict from pants.util.logging import LogLevel logger = logging.getLogger(__name__) +@dataclass(frozen=True) +class _Script: + path: PurePath + + @property + def argv0(self) -> str: + return f"./{self.path}" if self.path.parent == PurePath() else str(self.path) + + +@dataclass(frozen=True) +class _VenvScript: + script: _Script + content: FileContent + + +# Vendored and simplified copy of `pants.backend.python.util_rules.pex.VenvScriptWriter` so that +# plugin resolution does not have to instantiate a `CompletePexEnvironment`. +def _create_venv_script( + *, + bash: BashBinary, + script_path: PurePath, + venv_executable: PurePath, + env: Mapping[str, str], + venv_dir: PurePath, + pbs_python: PythonBuildStandaloneBinary, + pex_filename: str, +) -> _VenvScript: + env_vars = shlex.join(f"{name}={value}" for name, value in env.items()) + + target_venv_executable = shlex.quote(str(venv_executable)) + raw_execute_pex_args = [ + pbs_python.path, + f"./{pex_filename}", + ] + execute_pex_args = " ".join( + f"$(adjust_relative_paths {shlex.quote(arg)})" for arg in raw_execute_pex_args + ) + + script = dedent( + f"""\ + #!{bash.path} + set -euo pipefail + + # N.B.: This relies on BASH_SOURCE which has been available since bash-3.0, released in + # 2004. It will either contain the absolute path of the venv script or it will contain + # the relative path from the CWD to the venv script. Either way, we know the venv script + # parent directory is the sandbox root directory. + SANDBOX_ROOT="${{BASH_SOURCE%/*}}" + + function adjust_relative_paths() {{ + local value0="$1" + shift + if [ "${{value0:0:1}}" == "/" ]; then + # Don't relativize absolute paths. + echo "${{value0}}" "$@" + else + # N.B.: We convert all relative paths to paths relative to the sandbox root so + # this script works when run with a PWD set somewhere else than the sandbox + # root. + # + # There are two cases to consider. For the purposes of example, assume PWD is + # `/tmp/sandboxes/abc123/foo/bar`; i.e.: the rule API sets working_directory to + # `foo/bar`. Also assume `config/tool.yml` is the relative path in question. + # + # 1. If our BASH_SOURCE is `/tmp/sandboxes/abc123/pex_shim.sh`; so our + # SANDBOX_ROOT is `/tmp/sandboxes/abc123`, we calculate + # `/tmp/sandboxes/abc123/config/tool.yml`. + # 2. If our BASH_SOURCE is instead `../../pex_shim.sh`; so our SANDBOX_ROOT is + # `../..`, we calculate `../../config/tool.yml`. + echo "${{SANDBOX_ROOT}}/${{value0}}" "$@" + fi + }} + + export {env_vars} + export PEX_ROOT="$(adjust_relative_paths ${{PEX_ROOT}})" + + execute_pex_args="{execute_pex_args}" + target_venv_executable="$(adjust_relative_paths {target_venv_executable})" + venv_dir="$(adjust_relative_paths {shlex.quote(str(venv_dir))})" + + # Let PEX_TOOLS invocations pass through to the original PEX file since venvs don't come + # with tools support. + if [ -n "${{PEX_TOOLS:-}}" ]; then + exec ${{execute_pex_args}} "$@" + fi + + # If the seeded venv has been removed from the PEX_ROOT, we re-seed from the original + # `--venv` mode PEX file. + if [ ! -e "${{venv_dir}}" ]; then + PEX_INTERPRETER=1 ${{execute_pex_args}} -c '' + fi + + exec "${{target_venv_executable}}" "$@" + """ + ) + + return _VenvScript( + script=_Script(script_path), + content=FileContent(path=str(script_path), content=script.encode(), is_executable=True), + ) + + @dataclass(frozen=True) class PluginsRequest: # Interpreter constraints to resolve for, or None to resolve for the interpreter that Pants is @@ -51,42 +166,141 @@ class ResolvedPluginDistributions(DeduplicatedCollection[str]): @rule async def resolve_plugins( - request: PluginsRequest, global_options: GlobalOptions + request: PluginsRequest, + global_options: GlobalOptions, + pex_cli_tool: PexPEX, + python_bootstrap: PythonBootstrap, + python_setup: PythonSetup, + python_repos: PythonRepos, + python_native_code: PythonNativeCodeSubsystem.EnvironmentAware, + bash: BashBinary, + pbs_python: PythonBuildStandaloneBinary, ) -> ResolvedPluginDistributions: - """This rule resolves plugins using a VenvPex, and exposes the absolute paths of their dists. + """This rule resolves plugins directly using Pex and exposes the absolute paths of their dists. NB: This relies on the fact that PEX constructs venvs in a stable location (within the `named_caches` directory), but consequently needs to disable the process cache: see the ProcessCacheScope reference in the body. """ req_strings = sorted(global_options.plugins + request.requirements) - - requirements = PexRequirements( - req_strings_or_addrs=req_strings, - constraints_strings=(str(constraint) for constraint in request.constraints), - description_of_origin="configured Pants plugins", - ) - if not requirements: + if not req_strings: return ResolvedPluginDistributions() + existing_env = await Get(EnvironmentVars, EnvironmentVarsRequest(["PATH"])) + + _PEX_ROOT_DIRNAME = "pex_root" + PANTS_PLUGINS_PEX_FILENAME = "pants_plugins.pex" + + pex_root = PurePath(".cache") / _PEX_ROOT_DIRNAME + + tmp_digest = await Get(Digest, CreateDigest([Directory(".tmp")])) + input_digests: list[Digest] = [pex_cli_tool.digest, tmp_digest] + + append_only_caches: dict[str, str] = { + _PEX_ROOT_DIRNAME: str(pex_root), + } + append_only_caches.update(pbs_python.APPEND_ONLY_CACHES) + python: PythonExecutable | None = None if not request.interpreter_constraints: python = PythonExecutable.fingerprinted( sys.executable, ".".join(map(str, sys.version_info[:3])).encode("utf8") ) - plugins_pex = await Get( - VenvPex, - PexRequest( - output_filename="pants_plugins.pex", - internal_only=True, - python=python, - requirements=requirements, - interpreter_constraints=request.interpreter_constraints or InterpreterConstraints(), + env: dict[str, str] = { + "LANG": "en_US.UTF-8", + "PEX_IGNORE_RCFILES": "true", + "PEX_ROOT": str(pex_root), + **(python_native_code.subprocess_env_vars), + } + path_env = existing_env.get("PATH", "") + if path_env: + env["PATH"] = path_env + + if python: + env["PEX_PYTHON"] = python.path + else: + env["PEX_PYTHON_PATH"] = os.pathsep.join(python_bootstrap.interpreter_search_paths) + + args: list[str] = [ + pbs_python.path, + pex_cli_tool.exe, + "--tmpdir=.tmp", + "--jobs=1", + f"--pip-version={python_setup.pip_version}", + f"--python-path={os.pathsep.join(python_bootstrap.interpreter_search_paths)}", + f"--output-file={PANTS_PLUGINS_PEX_FILENAME}", + "--venv=prepend", + "--seed=verbose", # Seed venv into PEX_ROOT and outputs JSON blob with location of that venv. + "--no-venv-site-packages-copies", # TODO: Correct? + # An internal-only runs on a single machine, and pre-installing wheels is wasted work in + # that case (see https://github.com/pex-tool/pex/issues/2292#issuecomment-1854582647 for + # analysis). + "--no-pre-install-wheels", + "--sources-directory=source_files", + "--no-pypi", # TODO: Get index and find-links from original config option. + *(f"--index={index}" for index in python_repos.indexes), + *(f"--find-links={repo}" for repo in python_repos.find_links), + *( + [f"--manylinux={python_setup.manylinux}"] + if python_setup.manylinux + else ["--no-manylinux"] + ), + "--resolver-version=pip-2020-resolver", + "--layout=packed", + ] + + if python: + args.append(f"--python={python.path}") + append_only_caches.update(python.append_only_caches) + + if request.constraints: + constraints_file = "__constraints.txt" + constraints_content = "\n".join([str(constraint) for constraint in request.constraints]) + input_digests.append( + await Get( + Digest, + CreateDigest([FileContent(constraints_file, constraints_content.encode())]), + ) + ) + args.extend(["--constraints", constraints_file]) + + args.extend(["--", *req_strings]) + + merged_input_digest = await Get(Digest, MergeDigests(input_digests)) + + plugins_pex_result = await Get( + ProcessResult, + Process( + argv=args, + input_digest=merged_input_digest, description=f"Resolving plugins: {', '.join(req_strings)}", + append_only_caches=FrozenDict(append_only_caches), + env=env, + output_files=[PANTS_PLUGINS_PEX_FILENAME], ), ) + seed_info = json.loads(plugins_pex_result.stdout.decode()) + abs_pex_root = PurePath(seed_info["pex_root"]) + abs_pex_path = PurePath(seed_info["pex"]) + venv_rel_dir = abs_pex_path.relative_to(abs_pex_root).parent + + script_path = PurePath("pants_plugins_pex_shim.sh") + venv_dir = pex_root / venv_rel_dir + + venv_script = _create_venv_script( + bash=bash, + script_path=script_path, + venv_executable=venv_dir / "pex", + env=env, + venv_dir=venv_dir, + pbs_python=pbs_python, + pex_filename=PANTS_PLUGINS_PEX_FILENAME, + ) + + venv_script_digest = await Get(Digest, CreateDigest([venv_script.content])) + # NB: We run this Process per-restart because it (intentionally) leaks named cache # paths in a way that invalidates the Process-cache. See the method doc. cache_scope = ( @@ -95,17 +309,27 @@ async def resolve_plugins( else ProcessCacheScope.PER_RESTART_SUCCESSFUL ) - plugins_process_result = await Get( + plugins_path_input_digest = await Get( + Digest, MergeDigests([venv_script_digest, plugins_pex_result.output_digest]) + ) + + plugins_path_result = await Get( ProcessResult, - VenvPexProcess( - plugins_pex, - argv=("-c", "import os, site; print(os.linesep.join(site.getsitepackages()))"), + Process( + argv=[ + venv_script.script.argv0, + "-c", + "import os, site; print(os.linesep.join(site.getsitepackages()))", + ], + input_digest=plugins_path_input_digest, description="Extracting plugin locations", level=LogLevel.DEBUG, + append_only_caches=FrozenDict(append_only_caches), cache_scope=cache_scope, ), ) - return ResolvedPluginDistributions(plugins_process_result.stdout.decode().strip().split("\n")) + + return ResolvedPluginDistributions(plugins_path_result.stdout.decode().strip().split("\n")) class PluginResolver: @@ -170,6 +394,7 @@ def _resolve_plugins( def rules(): return [ - QueryRule(ResolvedPluginDistributions, [PluginsRequest, EnvironmentName]), *collect_rules(), + QueryRule(ResolvedPluginDistributions, (PluginsRequest, EnvironmentName)), + *pex_cli_tool_rules(), ] From c3d4b316e46c06c5d3fed415c151f7f5b782e25b Mon Sep 17 00:00:00 2001 From: Tom Dyas Date: Fri, 21 Feb 2025 01:29:30 -0500 Subject: [PATCH 2/2] put back reqs in original args position --- src/python/pants/init/plugin_resolver.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/python/pants/init/plugin_resolver.py b/src/python/pants/init/plugin_resolver.py index 175456b31e5..fff7745b5b3 100644 --- a/src/python/pants/init/plugin_resolver.py +++ b/src/python/pants/init/plugin_resolver.py @@ -238,7 +238,8 @@ async def resolve_plugins( # analysis). "--no-pre-install-wheels", "--sources-directory=source_files", - "--no-pypi", # TODO: Get index and find-links from original config option. + *req_strings, + "--no-pypi", *(f"--index={index}" for index in python_repos.indexes), *(f"--find-links={repo}" for repo in python_repos.find_links), *( @@ -265,8 +266,6 @@ async def resolve_plugins( ) args.extend(["--constraints", constraints_file]) - args.extend(["--", *req_strings]) - merged_input_digest = await Get(Digest, MergeDigests(input_digests)) plugins_pex_result = await Get(