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

pantsbuild / pants / 20857535614

09 Jan 2026 03:56PM UTC coverage: 43.231% (-37.0%) from 80.269%
20857535614

Pull #22992

github

web-flow
Merge c58d9435e into 3782956e6
Pull Request #22992: feat(engine): log elapsed time for completed workunits

26137 of 60459 relevant lines covered (43.23%)

0.86 hits per line

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

90.06
/src/python/pants/backend/shell/target_types.py
1
# Copyright 2021 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 re
2✔
7
from enum import Enum
2✔
8

9
from pants.backend.adhoc.target_types import (
2✔
10
    AdhocToolCacheScopeField,
11
    AdhocToolDependenciesField,
12
    AdhocToolExecutionDependenciesField,
13
    AdhocToolExtraEnvVarsField,
14
    AdhocToolLogOutputField,
15
    AdhocToolNamedCachesField,
16
    AdhocToolOutputDependenciesField,
17
    AdhocToolOutputDirectoriesField,
18
    AdhocToolOutputFilesField,
19
    AdhocToolOutputRootDirField,
20
    AdhocToolOutputsMatchMode,
21
    AdhocToolPathEnvModifyModeField,
22
    AdhocToolRunnableDependenciesField,
23
    AdhocToolTimeoutField,
24
    AdhocToolWorkdirField,
25
    AdhocToolWorkspaceInvalidationSourcesField,
26
)
27
from pants.backend.shell.subsystems.shell_setup import ShellSetup
2✔
28
from pants.core.environments.target_types import EnvironmentField
2✔
29
from pants.core.goals.package import OutputPathField
2✔
30
from pants.core.goals.test import RuntimePackageDependenciesField, TestTimeoutField
2✔
31
from pants.core.util_rules.system_binaries import BinaryPathTest
2✔
32
from pants.engine.rules import collect_rules, rule
2✔
33
from pants.engine.target import (
2✔
34
    COMMON_TARGET_FIELDS,
35
    BoolField,
36
    MultipleSourcesField,
37
    OverridesField,
38
    SingleSourceField,
39
    StringField,
40
    StringSequenceField,
41
    Target,
42
    TargetFilesGenerator,
43
    TargetFilesGeneratorSettings,
44
    TargetFilesGeneratorSettingsRequest,
45
    generate_file_based_overrides_field_help_message,
46
    generate_multiple_sources_field_help_message,
47
)
48
from pants.engine.unions import UnionRule
2✔
49
from pants.util.enums import match
2✔
50
from pants.util.strutil import help_text
2✔
51

52

53
class ShellDependenciesField(AdhocToolDependenciesField):
2✔
54
    pass
2✔
55

56

57
class ShellSourceField(SingleSourceField):
2✔
58
    # Normally, we would add `expected_file_extensions = ('.sh',)`, but Bash scripts don't need a
59
    # file extension, so we don't use this.
60
    uses_source_roots = False
2✔
61

62

63
class ShellGeneratingSourcesBase(MultipleSourcesField):
2✔
64
    uses_source_roots = False
2✔
65

66

67
class ShellGeneratorSettingsRequest(TargetFilesGeneratorSettingsRequest):
2✔
68
    pass
2✔
69

70

71
@rule
2✔
72
async def generator_settings(
2✔
73
    _: ShellGeneratorSettingsRequest,
74
    shell_setup: ShellSetup,
75
) -> TargetFilesGeneratorSettings:
76
    return TargetFilesGeneratorSettings(
×
77
        add_dependencies_on_all_siblings=not shell_setup.dependency_inference
78
    )
79

80

81
# -----------------------------------------------------------------------------------------------
82
# `shunit2_test` target
83
# -----------------------------------------------------------------------------------------------
84

85

86
class Shunit2Shell(Enum):
2✔
87
    sh = "sh"
2✔
88
    bash = "bash"
2✔
89
    dash = "dash"
2✔
90
    ksh = "ksh"
2✔
91
    pdksh = "pdksh"
2✔
92
    zsh = "zsh"
2✔
93

94
    @classmethod
2✔
95
    def parse_shebang(cls, shebang: bytes) -> Shunit2Shell | None:
2✔
96
        if not shebang:
×
97
            return None
×
98
        first_line = shebang.splitlines()[0]
×
99
        matches = re.match(rb"^#! *[/\w]*/(?P<program>\w+) *(?P<arg>\w*)", first_line)
×
100
        if not matches:
×
101
            return None
×
102
        program = matches.group("program")
×
103
        if program == b"env":
×
104
            program = matches.group("arg")
×
105
        try:
×
106
            return cls(program.decode())
×
107
        except ValueError:
×
108
            return None
×
109

110
    @property
2✔
111
    def binary_path_test(self) -> BinaryPathTest | None:
2✔
112
        arg = match(
×
113
            self,
114
            {
115
                self.sh: None,
116
                self.bash: "--version",
117
                self.dash: None,
118
                self.ksh: "--version",
119
                self.pdksh: None,
120
                self.zsh: "--version",
121
            },
122
        )
123
        if not arg:
×
124
            return None
×
125
        return BinaryPathTest((arg,))
×
126

127

128
class Shunit2TestDependenciesField(ShellDependenciesField):
2✔
129
    supports_transitive_excludes = True
2✔
130

131

132
class Shunit2TestTimeoutField(TestTimeoutField):
2✔
133
    pass
2✔
134

135

136
class SkipShunit2TestsField(BoolField):
2✔
137
    alias = "skip_tests"
2✔
138
    default = False
2✔
139
    help = "If true, don't run this target's tests."
2✔
140

141

142
class Shunit2TestSourceField(ShellSourceField):
2✔
143
    pass
2✔
144

145

146
class Shunit2ShellField(StringField):
2✔
147
    alias = "shell"
2✔
148
    valid_choices = Shunit2Shell
2✔
149
    help = "Which shell to run the tests with. If unspecified, Pants will look for a shebang line."
2✔
150

151

152
class Shunit2TestTarget(Target):
2✔
153
    alias = "shunit2_test"
2✔
154
    core_fields = (
2✔
155
        *COMMON_TARGET_FIELDS,
156
        Shunit2TestSourceField,
157
        Shunit2TestDependenciesField,
158
        Shunit2TestTimeoutField,
159
        SkipShunit2TestsField,
160
        Shunit2ShellField,
161
        RuntimePackageDependenciesField,
162
    )
163
    help = help_text(
2✔
164
        f"""
165
        A single test file for Bourne-based shell scripts using the shunit2 test framework.
166

167
        To use, add tests to your file per https://github.com/kward/shunit2/. Specify the shell
168
        to run with by either setting the field `{Shunit2ShellField.alias}` or including a
169
        shebang. To test the same file with multiple shells, create multiple `shunit2_tests`
170
        targets, one for each shell.
171

172
        Pants will automatically download the `shunit2` bash script and add
173
        `source ./shunit2` to your test for you. If you already have `source ./shunit2`,
174
        Pants will overwrite it to use the correct relative path.
175
        """
176
    )
177

178

179
# -----------------------------------------------------------------------------------------------
180
# `shunit2_tests` target generator
181
# -----------------------------------------------------------------------------------------------
182

183

184
class Shunit2TestsGeneratorSourcesField(ShellGeneratingSourcesBase):
2✔
185
    default = ("*_test.sh", "test_*.sh", "tests.sh")
2✔
186
    help = generate_multiple_sources_field_help_message(
2✔
187
        "Example: `sources=['test.sh', 'test_*.sh', '!test_ignore.sh']`"
188
    )
189

190

191
class Shunit2TestsOverrideField(OverridesField):
2✔
192
    help = generate_file_based_overrides_field_help_message(
2✔
193
        Shunit2TestTarget.alias,
194
        """
195
        overrides={
196
            "foo_test.sh": {"timeout": 120},
197
            "bar_test.sh": {"timeout": 200},
198
            ("foo_test.sh", "bar_test.sh"): {"tags": ["slow_tests"]},
199
        }
200
        """,
201
    )
202

203

204
class Shunit2TestsGeneratorTarget(TargetFilesGenerator):
2✔
205
    alias = "shunit2_tests"
2✔
206
    core_fields = (
2✔
207
        *COMMON_TARGET_FIELDS,
208
        Shunit2TestsGeneratorSourcesField,
209
        Shunit2TestsOverrideField,
210
    )
211
    generated_target_cls = Shunit2TestTarget
2✔
212
    copied_fields = COMMON_TARGET_FIELDS
2✔
213
    moved_fields = (
2✔
214
        Shunit2TestDependenciesField,
215
        Shunit2TestTimeoutField,
216
        SkipShunit2TestsField,
217
        Shunit2ShellField,
218
        RuntimePackageDependenciesField,
219
    )
220
    help = "Generate a `shunit2_test` target for each file in the `sources` field."
2✔
221

222

223
# -----------------------------------------------------------------------------------------------
224
# `shell_source` and `shell_sources` targets
225
# -----------------------------------------------------------------------------------------------
226

227

228
class ShellSourceTarget(Target):
2✔
229
    alias = "shell_source"
2✔
230
    core_fields = (*COMMON_TARGET_FIELDS, ShellDependenciesField, ShellSourceField)
2✔
231
    help = "A single Bourne-based shell script, e.g. a Bash script."
2✔
232

233

234
class ShellSourcesGeneratingSourcesField(ShellGeneratingSourcesBase):
2✔
235
    default = ("*.sh",) + tuple(f"!{pat}" for pat in Shunit2TestsGeneratorSourcesField.default)
2✔
236
    help = generate_multiple_sources_field_help_message(
2✔
237
        "Example: `sources=['example.sh', 'new_*.sh', '!old_ignore.sh']`"
238
    )
239

240

241
class ShellSourcesOverridesField(OverridesField):
2✔
242
    help = generate_file_based_overrides_field_help_message(
2✔
243
        ShellSourceTarget.alias,
244
        """
245
        overrides={
246
            "foo.sh": {"skip_shellcheck": True]},
247
            "bar.sh": {"skip_shfmt": True]},
248
            ("foo.sh", "bar.sh"): {"tags": ["linter_disabled"]},
249
        }
250
        """,
251
    )
252

253

254
class ShellSourcesGeneratorTarget(TargetFilesGenerator):
2✔
255
    alias = "shell_sources"
2✔
256
    core_fields = (
2✔
257
        *COMMON_TARGET_FIELDS,
258
        ShellSourcesGeneratingSourcesField,
259
        ShellSourcesOverridesField,
260
    )
261
    generated_target_cls = ShellSourceTarget
2✔
262
    copied_fields = COMMON_TARGET_FIELDS
2✔
263
    moved_fields = (ShellDependenciesField,)
2✔
264
    help = "Generate a `shell_source` target for each file in the `sources` field."
2✔
265

266

267
# -----------------------------------------------------------------------------------------------
268
# `shell_command` target
269
# -----------------------------------------------------------------------------------------------
270

271

272
class ShellCommandCommandFieldBase(StringField):
2✔
273
    alias = "command"
2✔
274
    required = True
2✔
275
    help = help_text(
2✔
276
        """
277
        Shell command to execute.
278

279
        The command is executed as `'bash -c <command>'` by default. If you want to invoke a binary
280
        use `exec -a $0 <binary> <args>` as the command so that the binary gets the correct `argv[0]`
281
        set.
282
        """
283
    )
284

285

286
class ShellCommandCommandField(ShellCommandCommandFieldBase):
2✔
287
    pass
2✔
288

289

290
class ShellCommandOutputFilesField(AdhocToolOutputFilesField):
2✔
291
    pass
2✔
292

293

294
class ShellCommandOutputDirectoriesField(AdhocToolOutputDirectoriesField):
2✔
295
    pass
2✔
296

297

298
class ShellCommandOutputDependenciesField(AdhocToolOutputDependenciesField):
2✔
299
    pass
2✔
300

301

302
class ShellCommandExecutionDependenciesField(AdhocToolExecutionDependenciesField):
2✔
303
    pass
2✔
304

305

306
class RunShellCommandExecutionDependenciesField(ShellCommandExecutionDependenciesField):
2✔
307
    help = help_text(
2✔
308
        lambda: f"""
309
        The execution dependencies for this command.
310

311
        Dependencies specified here are those required to make the command complete successfully
312
        (e.g. file inputs, packages compiled from other targets, etc), but NOT required to make
313
        the outputs of the command useful.
314

315
        See also `{RunShellCommandRunnableDependenciesField.alias}`.
316
        """
317
    )
318

319

320
class ShellCommandRunnableDependenciesField(AdhocToolRunnableDependenciesField):
2✔
321
    pass
2✔
322

323

324
class RunShellCommandRunnableDependenciesField(ShellCommandRunnableDependenciesField):
2✔
325
    help = help_text(
2✔
326
        lambda: f"""
327
        The runnable dependencies for this command.
328

329
        Dependencies specified here are those required to exist on the `PATH` to make the command
330
        complete successfully (interpreters specified in a `#!` command, etc). Note that these
331
        dependencies will be made available on the `PATH` with the name of the target.
332

333
        See also `{RunShellCommandExecutionDependenciesField.alias}`.
334
        """
335
    )
336

337

338
class ShellCommandSourcesField(MultipleSourcesField):
2✔
339
    # We solely register this field for codegen to work.
340
    alias = "_sources"
2✔
341
    uses_source_roots = False
2✔
342
    expected_num_files = 0
2✔
343

344

345
class ShellCommandTimeoutField(AdhocToolTimeoutField):
2✔
346
    pass
2✔
347

348

349
class ShellCommandToolsField(StringSequenceField):
2✔
350
    alias = "tools"
2✔
351
    default = ()
2✔
352
    help = help_text(
2✔
353
        """
354
        Specify required executable tools that might be used.
355

356
        Only the tools explicitly provided will be available on the search PATH,
357
        and these tools must be found on the paths provided by
358
        `[shell-setup].executable_search_paths` (which defaults to the system PATH).
359
        """
360
    )
361

362

363
class ShellCommandExtraEnvVarsField(AdhocToolExtraEnvVarsField):
2✔
364
    pass
2✔
365

366

367
class ShellCommandLogOutputField(AdhocToolLogOutputField):
2✔
368
    pass
2✔
369

370

371
class ShellCommandWorkdirField(AdhocToolWorkdirField):
2✔
372
    pass
2✔
373

374

375
class RunShellCommandWorkdirField(AdhocToolWorkdirField):
2✔
376
    pass
2✔
377

378

379
class ShellCommandOutputRootDirField(AdhocToolOutputRootDirField):
2✔
380
    pass
2✔
381

382

383
class ShellCommandTestDependenciesField(ShellCommandExecutionDependenciesField):
2✔
384
    pass
2✔
385

386

387
class ShellCommandNamedCachesField(AdhocToolNamedCachesField):
2✔
388
    pass
2✔
389

390

391
class ShellCommandWorkspaceInvalidationSourcesField(AdhocToolWorkspaceInvalidationSourcesField):
2✔
392
    pass
2✔
393

394

395
class ShellCommandPathEnvModifyModeField(AdhocToolPathEnvModifyModeField):
2✔
396
    pass
2✔
397

398

399
class ShellCommandOutputsMatchMode(AdhocToolOutputsMatchMode):
2✔
400
    pass
2✔
401

402

403
class ShellCommandCacheScopeField(AdhocToolCacheScopeField):
2✔
404
    pass
2✔
405

406

407
class SkipShellCommandTestsField(BoolField):
2✔
408
    alias = "skip_tests"
2✔
409
    default = False
2✔
410
    help = "If true, don't run this tests for target."
2✔
411

412

413
class SkipShellCommandPackageField(BoolField):
2✔
414
    alias = "skip_package"
2✔
415
    default = False
2✔
416
    help = "If true, don't run this package for target."
2✔
417

418

419
class ShellCommandTarget(Target):
2✔
420
    alias = "shell_command"
2✔
421
    core_fields = (
2✔
422
        *COMMON_TARGET_FIELDS,
423
        ShellCommandOutputDependenciesField,
424
        ShellCommandExecutionDependenciesField,
425
        ShellCommandRunnableDependenciesField,
426
        ShellCommandCommandField,
427
        ShellCommandLogOutputField,
428
        ShellCommandOutputFilesField,
429
        ShellCommandOutputDirectoriesField,
430
        ShellCommandSourcesField,
431
        ShellCommandTimeoutField,
432
        ShellCommandToolsField,
433
        ShellCommandExtraEnvVarsField,
434
        ShellCommandWorkdirField,
435
        ShellCommandNamedCachesField,
436
        ShellCommandOutputRootDirField,
437
        ShellCommandWorkspaceInvalidationSourcesField,
438
        ShellCommandPathEnvModifyModeField,
439
        ShellCommandOutputsMatchMode,
440
        ShellCommandCacheScopeField,
441
        EnvironmentField,
442
    )
443
    help = help_text(
2✔
444
        """
445
        Execute any external tool for its side effects, or run it interactively.
446

447
        This target provides hermetic execution with explicit tool dependencies via the
448
        `tools` field. It can be used for:
449

450
        - Code generation (produces output files consumed by other targets)
451
        - Running scripts interactively with explicit dependencies (via `pants run`)
452
        - Build-time hermetic execution (via `pants experimental_run_in_sandbox`)
453

454
        Example BUILD file:
455

456
            shell_command(
457
                command="./my-script.sh --flag",
458
                tools=["tar", "curl", "cat", "bash", "env"],
459
                execution_dependencies=[":scripts"],
460
                output_files=["logs/my-script.log"],
461
                output_directories=["results"],
462
            )
463

464
            shell_sources(name="scripts")
465

466
        When used as a dependency of other targets (e.g., `python_tests` or `docker_image`),
467
        Pants will run your `command` and insert the `outputs` into that consumer's context.
468

469
        When used with `pants run :target`, the command runs interactively in the workspace
470
        with all dependencies and tools available.
471

472
        The command may be retried and/or cancelled, so ensure that it is idempotent.
473

474
        For simpler, workspace-oriented scripts that use system PATH tools, consider
475
        `run_shell_command` instead.
476
        """
477
    )
478

479

480
class RunShellCommandCommandField(ShellCommandCommandFieldBase):
2✔
481
    pass
2✔
482

483

484
class ShellCommandRunTarget(Target):
2✔
485
    alias = "run_shell_command"
2✔
486
    core_fields = (
2✔
487
        *COMMON_TARGET_FIELDS,
488
        RunShellCommandExecutionDependenciesField,
489
        RunShellCommandRunnableDependenciesField,
490
        RunShellCommandCommandField,
491
        RunShellCommandWorkdirField,
492
    )
493
    help = help_text(
2✔
494
        """
495
        Run a script in the workspace with dependencies packaged into a chroot.
496

497
        This target is designed for quick, workspace-oriented interactive scripts that use
498
        tools from the system PATH.
499

500
        Example BUILD file:
501

502
            run_shell_command(
503
                command="./scripts/my-script.sh --data-files-dir={chroot}",
504
                execution_dependencies=["src/project/files:data"],
505
            )
506

507
        The `command` may use either `{chroot}` on the command line, or the `$CHROOT`
508
        environment variable to get the root directory for where any dependencies are located.
509

510
        In contrast to `shell_command`, this target:
511
        - Uses tools from the system PATH (not explicit `tools` field)
512
        - Does not support `output_files` (outputs go directly to workspace)
513
        - Is simpler to use for quick workspace scripts
514

515
        For more hermetic execution with explicit tool dependencies, consider using
516
        `shell_command` instead, which provides better reproducibility and caching.
517
        """
518
    )
519

520

521
class ShellCommandTestTarget(Target):
2✔
522
    alias = "test_shell_command"
2✔
523

524
    core_fields = (
2✔
525
        *COMMON_TARGET_FIELDS,
526
        ShellCommandTestDependenciesField,
527
        ShellCommandRunnableDependenciesField,
528
        ShellCommandCommandField,
529
        ShellCommandLogOutputField,
530
        ShellCommandSourcesField,
531
        ShellCommandTimeoutField,
532
        ShellCommandToolsField,
533
        ShellCommandExtraEnvVarsField,
534
        ShellCommandPathEnvModifyModeField,
535
        EnvironmentField,
536
        SkipShellCommandTestsField,
537
        ShellCommandWorkdirField,
538
        ShellCommandOutputFilesField,
539
        ShellCommandOutputDirectoriesField,
540
        ShellCommandOutputRootDirField,
541
        ShellCommandOutputsMatchMode,
542
        ShellCommandCacheScopeField,
543
    )
544
    help = help_text(
2✔
545
        """
546
        Run a script as a test via the `test` goal, with all dependencies packaged/copied available in the chroot.
547

548
        Example BUILD file:
549

550
            test_shell_command(
551
                name="test",
552
                tools=["test"],
553
                command="test -r $CHROOT/some-data-file.txt",
554
                execution_dependencies=["src/project/files:data"],
555
            )
556

557
        The `command` may use the `{chroot}` marker on the command line or in environment variables
558
        to get the root directory where any dependencies are materialized during execution.
559

560
        In contrast to the `run_shell_command`, this target is intended to run shell commands as tests
561
        and will only run them via the `test` goal.
562
        """
563
    )
564

565

566
class ShellCommandPackageDependenciesField(ShellCommandExecutionDependenciesField):
2✔
567
    pass
2✔
568

569

570
class ShellCommandPackageTarget(Target):
2✔
571
    alias = "package_shell_command"
2✔
572

573
    core_fields = (
2✔
574
        *COMMON_TARGET_FIELDS,
575
        ShellCommandPackageDependenciesField,
576
        ShellCommandRunnableDependenciesField,
577
        ShellCommandCommandField,
578
        ShellCommandLogOutputField,
579
        ShellCommandSourcesField,
580
        ShellCommandTimeoutField,
581
        ShellCommandToolsField,
582
        ShellCommandExtraEnvVarsField,
583
        ShellCommandPathEnvModifyModeField,
584
        ShellCommandNamedCachesField,
585
        ShellCommandWorkspaceInvalidationSourcesField,
586
        ShellCommandCacheScopeField,
587
        EnvironmentField,
588
        SkipShellCommandPackageField,
589
        ShellCommandWorkdirField,
590
        ShellCommandOutputFilesField,
591
        ShellCommandOutputDirectoriesField,
592
        ShellCommandOutputRootDirField,
593
        ShellCommandOutputsMatchMode,
594
        ShellCommandOutputDependenciesField,
595
        OutputPathField,
596
    )
597

598
    help = help_text(
2✔
599
        """
600
        Run a script to produce distributable outputs via the `package` goal.
601

602
        Example BUILD file:
603

604
            package_shell_command(
605
                name="build-rust-app",
606
                tools=["cargo"],
607
                command="cargo build --release",
608
                output_files=["target/release/binary"],
609
            )
610

611
        The `command` may use the `{chroot}` marker on the command line or in environment variables
612
        to get the root directory where any dependencies are materialized during execution.
613

614
        The outputs specified via `output_files` and `output_directories` will be captured and
615
        made available for other Pants targets to depend on. They will also be copied to the path
616
        specified via the `output_path` field (relative to the dist directory) when running
617
        `pants package`.
618

619
        This target is experimental and its behavior may change in future versions.
620
        """
621
    )
622

623

624
def rules():
2✔
625
    return [
2✔
626
        *collect_rules(),
627
        UnionRule(TargetFilesGeneratorSettingsRequest, ShellGeneratorSettingsRequest),
628
    ]
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