• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pantsbuild / pants / 20632486505

01 Jan 2026 04:21AM UTC coverage: 43.231% (-37.1%) from 80.281%
20632486505

Pull #22962

github

web-flow
Merge 08d5c63b0 into f52ab6675
Pull Request #22962: Bump the gha-deps group across 1 directory with 6 updates

26122 of 60424 relevant lines covered (43.23%)

0.86 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

73.81
/src/python/pants/core/goals/run.py
1
# Copyright 2019 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3

4
from __future__ import annotations
2✔
5

6
import logging
2✔
7
from abc import ABCMeta
2✔
8
from collections.abc import Iterable, Mapping
2✔
9
from dataclasses import dataclass
2✔
10
from enum import Enum
2✔
11
from typing import Any, ClassVar, TypeVar, final
2✔
12

13
from pants.core.environments.rules import _warn_on_non_local_environments
2✔
14
from pants.core.subsystems.debug_adapter import DebugAdapterSubsystem
2✔
15
from pants.engine.env_vars import CompleteEnvironmentVars
2✔
16
from pants.engine.environment import EnvironmentName
2✔
17
from pants.engine.fs import Digest, Workspace
2✔
18
from pants.engine.goal import Goal, GoalSubsystem
2✔
19
from pants.engine.internals.specs_rules import (
2✔
20
    AmbiguousImplementationsException,
21
    TooManyTargetsException,
22
    find_valid_field_sets_for_target_roots,
23
)
24
from pants.engine.intrinsics import run_interactive_process
2✔
25
from pants.engine.process import InteractiveProcess
2✔
26
from pants.engine.rules import Rule, _uncacheable_rule, collect_rules, goal_rule, implicitly, rule
2✔
27
from pants.engine.target import (
2✔
28
    BoolField,
29
    FieldSet,
30
    NoApplicableTargetsBehavior,
31
    Target,
32
    TargetRootsToFieldSetsRequest,
33
)
34
from pants.engine.unions import UnionMembership, UnionRule, union
2✔
35
from pants.option.global_options import GlobalOptions
2✔
36
from pants.option.option_types import ArgsListOption, BoolOption
2✔
37
from pants.util.frozendict import FrozenDict
2✔
38
from pants.util.memo import memoized
2✔
39
from pants.util.strutil import help_text, softwrap
2✔
40

41
logger = logging.getLogger(__name__)
2✔
42

43
_T = TypeVar("_T")
2✔
44

45

46
class RunInSandboxBehavior(Enum):
2✔
47
    """Defines the behavior of rules that act on a `RunFieldSet` subclass with regards to use in the
48
    sandbox.
49

50
    This is used to automatically generate rules used to fulfill `experimental_run_in_sandbox`
51
    targets.
52

53
    The behaviors are as follows:
54

55
    * `RUN_REQUEST_HERMETIC`: Use the existing `RunRequest`-generating rule, and enable cacheing.
56
       Use this if you are confident the behavior of the rule relies only on state that is
57
       captured by pants (e.g. binary paths are found using `EnvironmentVarsRequest`), and that
58
       the rule only refers to files in the sandbox.
59
    * `RUN_REQUEST_NOT_HERMETIC`: Use the existing `RunRequest`-generating rule, and do not
60
       enable cacheing. Use this if your existing rule is mostly suitable for use in the sandbox,
61
       but you cannot guarantee reproducible behavior.
62
    * `CUSTOM`: Opt to write your own rule that returns `RunInSandboxRequest`.
63
    * `NOT_SUPPORTED`: Opt out of being usable in `experimental_run_in_sandbox`. Attempting to use
64
       such a target will result in a runtime exception.
65
    """
66

67
    RUN_REQUEST_HERMETIC = 1
2✔
68
    RUN_REQUEST_NOT_HERMETIC = 2
2✔
69
    CUSTOM = 3
2✔
70
    NOT_SUPPORTED = 4
2✔
71

72

73
@union(in_scope_types=[EnvironmentName])
2✔
74
class RunFieldSet(FieldSet, metaclass=ABCMeta):
2✔
75
    """The fields necessary from a target to run a program/script."""
76

77
    supports_debug_adapter: ClassVar[bool] = False
2✔
78
    run_in_sandbox_behavior: ClassVar[RunInSandboxBehavior]
2✔
79

80
    @final
2✔
81
    @classmethod
2✔
82
    def rules(cls) -> Iterable[Rule | UnionRule]:
2✔
83
        yield UnionRule(RunFieldSet, cls)
2✔
84
        if not cls.supports_debug_adapter:
2✔
85
            yield from _unsupported_debug_adapter_rules(cls)
2✔
86
        yield from _run_in_sandbox_behavior_rule(cls)
2✔
87

88

89
class RestartableField(BoolField):
2✔
90
    alias = "restartable"
2✔
91
    default = False
2✔
92
    help = help_text(
2✔
93
        """
94
        If true, runs of this target with the `run` goal may be interrupted and
95
        restarted when its input files change.
96
        """
97
    )
98

99

100
@dataclass(frozen=True)
2✔
101
class RunRequest:
2✔
102
    digest: Digest
2✔
103
    # Values in args and in env can contain the format specifier "{chroot}", which will
104
    # be substituted with the (absolute) chroot path.
105
    args: tuple[str, ...]
2✔
106
    extra_env: FrozenDict[str, str]
2✔
107
    immutable_input_digests: Mapping[str, Digest] | None = None
2✔
108
    append_only_caches: Mapping[str, str] | None = None
2✔
109

110
    def __init__(
2✔
111
        self,
112
        *,
113
        digest: Digest,
114
        args: Iterable[str],
115
        extra_env: Mapping[str, str] | None = None,
116
        immutable_input_digests: Mapping[str, Digest] | None = None,
117
        append_only_caches: Mapping[str, str] | None = None,
118
    ) -> None:
119
        object.__setattr__(self, "digest", digest)
×
120
        object.__setattr__(self, "args", tuple(args))
×
121
        object.__setattr__(self, "extra_env", FrozenDict(extra_env or {}))
×
122
        object.__setattr__(
×
123
            self, "immutable_input_digests", FrozenDict(immutable_input_digests or {})
124
        )
125
        object.__setattr__(self, "append_only_caches", FrozenDict(append_only_caches or {}))
×
126

127
    def to_run_in_sandbox_request(self) -> RunInSandboxRequest:
2✔
128
        return RunInSandboxRequest(
×
129
            args=self.args,
130
            digest=self.digest,
131
            extra_env=self.extra_env,
132
            immutable_input_digests=self.immutable_input_digests,
133
            append_only_caches=self.append_only_caches,
134
        )
135

136

137
class RunDebugAdapterRequest(RunRequest):
2✔
138
    """Like RunRequest, but launches the process using the relevant Debug Adapter server.
139

140
    The process should be launched waiting for the client to connect.
141
    """
142

143

144
class RunInSandboxRequest(RunRequest):
2✔
145
    """A run request that launches the process in the sandbox for use as part of a build rule.
146

147
    The arguments and environment should only use values relative to the build root (or prefixed
148
    with `{chroot}`), or refer to binaries that were fetched with `BinaryPathRequest`.
149

150
    Presently, implementors can opt to use the existing as not guaranteeing hermeticity, which will
151
    internally mark the rule as uncacheable. In such a case, non-safe APIs can be used, however,
152
    this behavior can result in poorer performance, and only exists as a stop-gap while
153
    implementors work to make sure their `RunRequest`-generating rules can be used in a hermetic
154
    context, or writing new custom rules. (See the Plugin Upgrade Guide for details).
155
    """
156

157

158
@rule(polymorphic=True)
2✔
159
async def generate_run_request(run_field_set: RunFieldSet, env_name: EnvironmentName) -> RunRequest:
2✔
160
    raise NotImplementedError()
×
161

162

163
@rule(polymorphic=True)
2✔
164
async def generate_run_debug_adapter_request(
2✔
165
    run_field_set: RunFieldSet, env_name: EnvironmentName
166
) -> RunDebugAdapterRequest:
167
    raise NotImplementedError()
×
168

169

170
@rule(polymorphic=True)
2✔
171
async def generate_run_in_sandbox_request(
2✔
172
    run_field_set: RunFieldSet, env_name: EnvironmentName
173
) -> RunInSandboxRequest:
174
    raise NotImplementedError()
×
175

176

177
class RunSubsystem(GoalSubsystem):
2✔
178
    name = "run"
2✔
179
    help = help_text(
2✔
180
        """
181
        Runs a binary target.
182

183
        This goal propagates the return code of the underlying executable.
184

185
        If your application can safely be restarted while it is running, you can pass
186
        `restartable=True` on your binary target (for supported types), and the `run` goal
187
        will automatically restart them as all relevant files change. This can be particularly
188
        useful for server applications.
189
        """
190
    )
191

192
    @classmethod
2✔
193
    def activated(cls, union_membership: UnionMembership) -> bool:
2✔
194
        return RunFieldSet in union_membership
×
195

196
    args = ArgsListOption(
2✔
197
        example="val1 val2 --debug",
198
        tool_name="the executed target",
199
        passthrough=True,
200
    )
201
    # See also `test.py`'s same option
202
    debug_adapter = BoolOption(
2✔
203
        default=False,
204
        help=softwrap(
205
            """
206
            Run the interactive process using a Debug Adapter
207
            (https://microsoft.github.io/debug-adapter-protocol/) for the language if supported.
208

209
            The interactive process used will be immediately blocked waiting for a client before
210
            continuing.
211
            """
212
        ),
213
    )
214

215

216
class Run(Goal):
2✔
217
    subsystem_cls = RunSubsystem
2✔
218
    environment_behavior = Goal.EnvironmentBehavior.LOCAL_ONLY
2✔
219

220

221
async def _find_what_to_run(
2✔
222
    goal_description: str,
223
) -> tuple[RunFieldSet, Target]:
224
    targets_to_valid_field_sets = await find_valid_field_sets_for_target_roots(
×
225
        TargetRootsToFieldSetsRequest(
226
            RunFieldSet,
227
            goal_description=goal_description,
228
            no_applicable_targets_behavior=NoApplicableTargetsBehavior.error,
229
        ),
230
        **implicitly(),
231
    )
232
    mapping = targets_to_valid_field_sets.mapping
×
233

234
    if len(mapping) > 1:
×
235
        raise TooManyTargetsException(mapping, goal_description=goal_description)
×
236

237
    target, field_sets = next(iter(mapping.items()))
×
238
    if len(field_sets) > 1:
×
239
        raise AmbiguousImplementationsException(
×
240
            target,
241
            field_sets,
242
            goal_description=goal_description,
243
        )
244

245
    return field_sets[0], target
×
246

247

248
@goal_rule
2✔
249
async def run(
2✔
250
    run_subsystem: RunSubsystem,
251
    debug_adapter: DebugAdapterSubsystem,
252
    global_options: GlobalOptions,
253
    workspace: Workspace,  # Needed to enable side-effecting.
254
    complete_env: CompleteEnvironmentVars,
255
) -> Run:
256
    field_set, target = await _find_what_to_run("the `run` goal")
×
257

258
    await _warn_on_non_local_environments((target,), "the `run` goal")
×
259

260
    request = await (
×
261
        generate_run_request(**implicitly({field_set: RunFieldSet}))
262
        if not run_subsystem.debug_adapter
263
        else generate_run_debug_adapter_request(**implicitly({field_set: RunFieldSet}))
264
    )
265
    restartable = target.get(RestartableField).value
×
266
    if run_subsystem.debug_adapter:
×
267
        logger.info(
×
268
            softwrap(
269
                f"""
270
                Launching debug adapter at '{debug_adapter.host}:{debug_adapter.port}',
271
                which will wait for a client connection...
272
                """
273
            )
274
        )
275

276
    result = await run_interactive_process(
×
277
        InteractiveProcess(
278
            argv=(*request.args, *run_subsystem.args),
279
            env={**complete_env, **request.extra_env},
280
            input_digest=request.digest,
281
            run_in_workspace=True,
282
            restartable=restartable,
283
            keep_sandboxes=global_options.keep_sandboxes,
284
            immutable_input_digests=request.immutable_input_digests,
285
            append_only_caches=request.append_only_caches,
286
        )
287
    )
288

289
    return Run(result.exit_code)
×
290

291

292
@memoized
2✔
293
def _unsupported_debug_adapter_rules(cls: type[RunFieldSet]) -> Iterable:
2✔
294
    """Returns a rule that implements DebugAdapterRequest by raising an error."""
295

296
    @rule(canonical_name_suffix=cls.__name__, _param_type_overrides={"request": cls})
2✔
297
    async def get_run_debug_adapter_request(request: RunFieldSet) -> RunDebugAdapterRequest:
2✔
298
        raise NotImplementedError(
×
299
            "Running this target type with a debug adapter is not yet supported."
300
        )
301

302
    return collect_rules(locals())
2✔
303

304

305
async def _run_request(request: RunFieldSet) -> RunInSandboxRequest:
2✔
306
    run_req = await generate_run_request(**implicitly({request: RunFieldSet}))
×
307
    return run_req.to_run_in_sandbox_request()
×
308

309

310
@memoized
2✔
311
def _run_in_sandbox_behavior_rule(cls: type[RunFieldSet]) -> Iterable:
2✔
312
    """Returns a default rule that helps fulfil `experimental_run_in_sandbox` targets.
313

314
    If `RunInSandboxBehavior.CUSTOM` is specified, rule implementors must write a rule that returns
315
    a `RunInSandboxRequest`.
316
    """
317

318
    @rule(canonical_name_suffix=cls.__name__, _param_type_overrides={"request": cls})
2✔
319
    async def not_supported(request: RunFieldSet) -> RunInSandboxRequest:
2✔
320
        raise NotImplementedError(
×
321
            "Running this target type within the sandbox is not yet supported."
322
        )
323

324
    @rule(canonical_name_suffix=cls.__name__, _param_type_overrides={"request": cls})
2✔
325
    async def run_request_hermetic(request: RunFieldSet) -> RunInSandboxRequest:
2✔
326
        return await _run_request(request)
×
327

328
    @_uncacheable_rule(canonical_name_suffix=cls.__name__, _param_type_overrides={"request": cls})
2✔
329
    async def run_request_not_hermetic(request: RunFieldSet) -> RunInSandboxRequest:
2✔
330
        return await _run_request(request)
×
331

332
    default_rules: dict[RunInSandboxBehavior, list[Any]] = {
2✔
333
        RunInSandboxBehavior.NOT_SUPPORTED: [not_supported],
334
        RunInSandboxBehavior.RUN_REQUEST_HERMETIC: [run_request_hermetic],
335
        RunInSandboxBehavior.RUN_REQUEST_NOT_HERMETIC: [run_request_not_hermetic],
336
        RunInSandboxBehavior.CUSTOM: [],
337
    }
338

339
    return collect_rules(
2✔
340
        {_rule.__name__: _rule for _rule in default_rules[cls.run_in_sandbox_behavior]}
341
    )
342

343

344
def rules():
2✔
345
    return collect_rules()
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc