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

pantsbuild / pants / 19015773527

02 Nov 2025 05:33PM UTC coverage: 17.872% (-62.4%) from 80.3%
19015773527

Pull #22816

github

web-flow
Merge a12d75757 into 6c024e162
Pull Request #22816: Update Pants internal Python to 3.14

4 of 5 new or added lines in 3 files covered. (80.0%)

28452 existing lines in 683 files now uncovered.

9831 of 55007 relevant lines covered (17.87%)

0.18 hits per line

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

0.0
/src/python/pants/backend/shell/util_rules/shell_command.py
1
# Copyright 2021 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3

UNCOV
4
from __future__ import annotations
×
5

UNCOV
6
import logging
×
UNCOV
7
import os
×
UNCOV
8
import shlex
×
UNCOV
9
from dataclasses import dataclass
×
10

UNCOV
11
from pants.backend.shell.subsystems.shell_setup import ShellSetup
×
UNCOV
12
from pants.backend.shell.target_types import (
×
13
    RunShellCommandWorkdirField,
14
    ShellCommandCacheScopeField,
15
    ShellCommandCommandField,
16
    ShellCommandExecutionDependenciesField,
17
    ShellCommandExtraEnvVarsField,
18
    ShellCommandLogOutputField,
19
    ShellCommandNamedCachesField,
20
    ShellCommandOutputDirectoriesField,
21
    ShellCommandOutputFilesField,
22
    ShellCommandOutputRootDirField,
23
    ShellCommandOutputsMatchMode,
24
    ShellCommandPathEnvModifyModeField,
25
    ShellCommandRunnableDependenciesField,
26
    ShellCommandSourcesField,
27
    ShellCommandTarget,
28
    ShellCommandTimeoutField,
29
    ShellCommandToolsField,
30
    ShellCommandWorkdirField,
31
    ShellCommandWorkspaceInvalidationSourcesField,
32
)
UNCOV
33
from pants.backend.shell.util_rules.builtin import BASH_BUILTIN_COMMANDS
×
UNCOV
34
from pants.base.glob_match_error_behavior import GlobMatchErrorBehavior
×
UNCOV
35
from pants.core.environments.rules import EnvironmentNameRequest, resolve_environment_name
×
UNCOV
36
from pants.core.environments.target_types import EnvironmentTarget
×
UNCOV
37
from pants.core.goals.run import RunFieldSet, RunInSandboxBehavior, RunRequest
×
UNCOV
38
from pants.core.target_types import FileSourceField
×
UNCOV
39
from pants.core.util_rules.adhoc_process_support import (
×
40
    AdhocProcessRequest,
41
    ExtraSandboxContents,
42
    MergeExtraSandboxContents,
43
    ResolveExecutionDependenciesRequest,
44
    convert_fallible_adhoc_process_result,
45
    merge_extra_sandbox_contents,
46
    parse_relative_directory,
47
    prepare_env_vars,
48
    resolve_execution_environment,
49
)
UNCOV
50
from pants.core.util_rules.adhoc_process_support import rules as adhoc_process_support_rules
×
UNCOV
51
from pants.core.util_rules.system_binaries import (
×
52
    BashBinary,
53
    BinaryShimsRequest,
54
    create_binary_shims,
55
)
UNCOV
56
from pants.engine.environment import EnvironmentName
×
UNCOV
57
from pants.engine.fs import PathGlobs
×
UNCOV
58
from pants.engine.internals.graph import resolve_target
×
UNCOV
59
from pants.engine.internals.native_engine import EMPTY_DIGEST
×
UNCOV
60
from pants.engine.intrinsics import digest_to_snapshot
×
UNCOV
61
from pants.engine.process import Process
×
UNCOV
62
from pants.engine.rules import collect_rules, implicitly, rule
×
UNCOV
63
from pants.engine.target import (
×
64
    GeneratedSources,
65
    GenerateSourcesRequest,
66
    Target,
67
    WrappedTargetRequest,
68
)
UNCOV
69
from pants.engine.unions import UnionRule
×
UNCOV
70
from pants.util.docutil import bin_name
×
UNCOV
71
from pants.util.frozendict import FrozenDict
×
UNCOV
72
from pants.util.logging import LogLevel
×
73

UNCOV
74
logger = logging.getLogger(__name__)
×
75

76

UNCOV
77
class GenerateFilesFromShellCommandRequest(GenerateSourcesRequest):
×
UNCOV
78
    input = ShellCommandSourcesField
×
UNCOV
79
    output = FileSourceField
×
80

81

UNCOV
82
@dataclass(frozen=True)
×
UNCOV
83
class ShellCommandProcessFromTargetRequest:
×
UNCOV
84
    target: Target
×
85

86

UNCOV
87
@rule
×
UNCOV
88
async def prepare_process_request_from_target(
×
89
    request: ShellCommandProcessFromTargetRequest,
90
    shell_setup: ShellSetup.EnvironmentAware,
91
    bash: BashBinary,
92
    env_target: EnvironmentTarget,
93
) -> AdhocProcessRequest:
94
    shell_command = request.target
×
95

96
    description = f"the `{shell_command.alias}` at `{shell_command.address}`"
×
97

98
    working_directory = shell_command[ShellCommandWorkdirField].value
×
99
    assert working_directory is not None, "working_directory should always be a string"
×
100

101
    command = shell_command[ShellCommandCommandField].value
×
102
    if not command:
×
103
        raise ValueError(f"Missing `command` line in `{description}.")
×
104

105
    execution_environment = await resolve_execution_environment(
×
106
        ResolveExecutionDependenciesRequest(
107
            shell_command.address,
108
            shell_command.get(ShellCommandExecutionDependenciesField).value,
109
            shell_command.get(ShellCommandRunnableDependenciesField).value,
110
        ),
111
        **implicitly(),
112
    )
113

114
    dependencies_digest = execution_environment.digest
×
115

116
    output_files = shell_command.get(ShellCommandOutputFilesField).value or ()
×
117
    output_directories = shell_command.get(ShellCommandOutputDirectoriesField).value or ()
×
118

119
    # Resolve the `tools` field into a digest
120
    tools = shell_command.get(ShellCommandToolsField, default_raw_value=()).value or ()
×
121
    tools = tuple(tool for tool in tools if tool not in BASH_BUILTIN_COMMANDS)
×
122
    resolved_tools = await create_binary_shims(
×
123
        BinaryShimsRequest.for_binaries(
124
            *tools,
125
            rationale=f"execute {description}",
126
            search_path=shell_setup.executable_search_path,
127
        ),
128
        bash,
129
    )
130

131
    runnable_dependencies = execution_environment.runnable_dependencies
×
132
    extra_sandbox_contents: list[ExtraSandboxContents] = []
×
133
    extra_sandbox_contents.append(
×
134
        ExtraSandboxContents(
135
            digest=EMPTY_DIGEST,
136
            paths=(resolved_tools.path_component,),
137
            immutable_input_digests=FrozenDict(resolved_tools.immutable_input_digests or {}),
138
            append_only_caches=FrozenDict(),
139
            extra_env=FrozenDict(),
140
        )
141
    )
142

143
    if runnable_dependencies:
×
144
        extra_sandbox_contents.append(
×
145
            ExtraSandboxContents(
146
                digest=EMPTY_DIGEST,
147
                paths=(f"{{chroot}}/{runnable_dependencies.path_component}",),
148
                immutable_input_digests=runnable_dependencies.immutable_input_digests,
149
                append_only_caches=runnable_dependencies.append_only_caches,
150
                extra_env=runnable_dependencies.extra_env,
151
            )
152
        )
153

154
    merged_extras = await merge_extra_sandbox_contents(
×
155
        MergeExtraSandboxContents(tuple(extra_sandbox_contents))
156
    )
157

158
    env_vars = await prepare_env_vars(
×
159
        merged_extras.extra_env,
160
        shell_command.get(ShellCommandExtraEnvVarsField).value or (),
161
        extra_paths=merged_extras.paths,
162
        path_env_modify_mode=shell_command.get(ShellCommandPathEnvModifyModeField).enum_value,
163
        description_of_origin=f"`{ShellCommandExtraEnvVarsField.alias}` for `shell_command` target at `{shell_command.address}`",
164
    )
165

166
    append_only_caches = {
×
167
        **merged_extras.append_only_caches,
168
        **(shell_command.get(ShellCommandNamedCachesField).value or {}),
169
    }
170

171
    cache_scope = env_target.default_cache_scope
×
172
    maybe_override_cache_scope = shell_command.get(ShellCommandCacheScopeField).enum_value
×
173
    if maybe_override_cache_scope is not None:
×
174
        cache_scope = maybe_override_cache_scope
×
175

176
    workspace_invalidation_globs: PathGlobs | None = None
×
177
    workspace_invalidation_sources = (
×
178
        shell_command.get(ShellCommandWorkspaceInvalidationSourcesField).value or ()
179
    )
180
    if workspace_invalidation_sources:
×
181
        spec_path = shell_command.address.spec_path
×
182
        workspace_invalidation_globs = PathGlobs(
×
183
            globs=(os.path.join(spec_path, glob) for glob in workspace_invalidation_sources),
184
            glob_match_error_behavior=GlobMatchErrorBehavior.error,
185
            description_of_origin=f"`{ShellCommandWorkspaceInvalidationSourcesField.alias}` for `shell_command` target at `{shell_command.address}`",
186
        )
187

188
    outputs_match_mode = shell_command.get(ShellCommandOutputsMatchMode).enum_value
×
189

190
    return AdhocProcessRequest(
×
191
        description=description,
192
        address=shell_command.address,
193
        working_directory=working_directory,
194
        root_output_directory=shell_command.get(ShellCommandOutputRootDirField).value or "",
195
        argv=(bash.path, "-c", command, shell_command.address.spec),
196
        timeout=shell_command.get(ShellCommandTimeoutField).value,
197
        input_digest=dependencies_digest,
198
        output_files=output_files,
199
        output_directories=output_directories,
200
        append_only_caches=FrozenDict(append_only_caches),
201
        env_vars=env_vars,
202
        immutable_input_digests=FrozenDict.frozen(merged_extras.immutable_input_digests),
203
        log_on_process_errors=_LOG_ON_PROCESS_ERRORS,
204
        log_output=shell_command[ShellCommandLogOutputField].value,
205
        capture_stdout_file=None,
206
        capture_stderr_file=None,
207
        workspace_invalidation_globs=workspace_invalidation_globs,
208
        cache_scope=cache_scope,
209
        use_working_directory_as_base_for_output_captures=env_target.use_working_directory_as_base_for_output_captures,
210
        outputs_match_error_behavior=outputs_match_mode.glob_match_error_behavior,
211
        outputs_match_conjunction=outputs_match_mode.glob_expansion_conjunction,
212
    )
213

214

UNCOV
215
class RunShellCommand(RunFieldSet):
×
UNCOV
216
    required_fields = (
×
217
        ShellCommandCommandField,
218
        RunShellCommandWorkdirField,
219
    )
UNCOV
220
    run_in_sandbox_behavior = RunInSandboxBehavior.NOT_SUPPORTED
×
221

222

UNCOV
223
@rule(desc="Running shell command", level=LogLevel.DEBUG)
×
UNCOV
224
async def shell_command_in_sandbox(
×
225
    request: GenerateFilesFromShellCommandRequest,
226
) -> GeneratedSources:
227
    shell_command = request.protocol_target
×
228
    environment_name = await resolve_environment_name(
×
229
        EnvironmentNameRequest.from_target(shell_command),
230
        **implicitly(),
231
    )
232

233
    adhoc_result = await convert_fallible_adhoc_process_result(
×
234
        **implicitly(
235
            {
236
                environment_name: EnvironmentName,
237
                ShellCommandProcessFromTargetRequest(
238
                    shell_command
239
                ): ShellCommandProcessFromTargetRequest,
240
            }
241
        )
242
    )
243

244
    output = await digest_to_snapshot(adhoc_result.adjusted_digest)
×
245
    return GeneratedSources(output)
×
246

247

UNCOV
248
async def _interactive_shell_command(
×
249
    shell_command: Target,
250
    bash: BashBinary,
251
) -> Process:
252
    description = f"the `{shell_command.alias}` at `{shell_command.address}`"
×
253
    working_directory = shell_command[RunShellCommandWorkdirField].value
×
254

255
    if working_directory is None:
×
256
        raise ValueError("Working directory must be not be `None` for interactive processes.")
×
257

258
    command = shell_command[ShellCommandCommandField].value
×
259
    if not command:
×
260
        raise ValueError(f"Missing `command` line in `{description}.")
×
261

262
    command_env = {
×
263
        "CHROOT": "{chroot}",
264
    }
265

266
    execution_environment = await resolve_execution_environment(
×
267
        ResolveExecutionDependenciesRequest(
268
            shell_command.address,
269
            shell_command.get(ShellCommandExecutionDependenciesField).value,
270
            shell_command.get(ShellCommandRunnableDependenciesField).value,
271
        ),
272
        bash,
273
    )
274
    dependencies_digest = execution_environment.digest
×
275

276
    relpath = parse_relative_directory(working_directory, shell_command.address)
×
277
    boot_script = f"cd {shlex.quote(relpath)}; " if relpath != "" else ""
×
278

279
    return Process(
×
280
        argv=(
281
            bash.path,
282
            "-c",
283
            boot_script + command,
284
            f"{bin_name()} run {shell_command.address.spec} --",
285
        ),
286
        description=f"Running {description}",
287
        env=command_env,
288
        input_digest=dependencies_digest,
289
    )
290

291

UNCOV
292
@rule
×
UNCOV
293
async def run_shell_command_request(bash: BashBinary, shell_command: RunShellCommand) -> RunRequest:
×
294
    wrapped_tgt = await resolve_target(
×
295
        WrappedTargetRequest(shell_command.address, description_of_origin="<infallible>"),
296
        **implicitly(),
297
    )
298
    process = await _interactive_shell_command(wrapped_tgt.target, bash)
×
299
    return RunRequest(
×
300
        digest=process.input_digest,
301
        args=process.argv,
302
        extra_env=process.env,
303
    )
304

305

UNCOV
306
def rules():
×
UNCOV
307
    return [
×
308
        *collect_rules(),
309
        *adhoc_process_support_rules(),
310
        UnionRule(GenerateSourcesRequest, GenerateFilesFromShellCommandRequest),
311
        *RunShellCommand.rules(),
312
    ]
313

314

UNCOV
315
_LOG_ON_PROCESS_ERRORS = FrozenDict(
×
316
    {
317
        127: (
318
            f"`{ShellCommandTarget.alias}` requires the names of any external commands used by this "
319
            f"shell command to be specified in the `{ShellCommandToolsField.alias}` field. If "
320
            f"`bash` cannot find a tool, add it to the `{ShellCommandToolsField.alias}` field."
321
        )
322
    }
323
)
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

© 2025 Coveralls, Inc