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

pantsbuild / pants / 18697429970

21 Oct 2025 08:57PM UTC coverage: 80.262% (+0.01%) from 80.25%
18697429970

push

github

web-flow
PBS: scrape release metadata through 20251014 (#22766)

NOTE: Includes Python Pi!

77770 of 96895 relevant lines covered (80.26%)

3.35 hits per line

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

96.05
/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
11✔
5

6
import re
11✔
7
from enum import Enum
11✔
8

9
from pants.backend.adhoc.target_types import (
11✔
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
11✔
28
from pants.core.environments.target_types import EnvironmentField
11✔
29
from pants.core.goals.package import OutputPathField
11✔
30
from pants.core.goals.test import RuntimePackageDependenciesField, TestTimeoutField
11✔
31
from pants.core.util_rules.system_binaries import BinaryPathTest
11✔
32
from pants.engine.rules import collect_rules, rule
11✔
33
from pants.engine.target import (
11✔
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
11✔
49
from pants.util.enums import match
11✔
50
from pants.util.strutil import help_text
11✔
51

52

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

56

57
class ShellSourceField(SingleSourceField):
11✔
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
11✔
61

62

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

66

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

70

71
@rule
11✔
72
async def generator_settings(
11✔
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):
11✔
87
    sh = "sh"
11✔
88
    bash = "bash"
11✔
89
    dash = "dash"
11✔
90
    ksh = "ksh"
11✔
91
    pdksh = "pdksh"
11✔
92
    zsh = "zsh"
11✔
93

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

110
    @property
11✔
111
    def binary_path_test(self) -> BinaryPathTest | None:
11✔
112
        arg = match(  # type: ignore[misc]
×
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):
11✔
129
    supports_transitive_excludes = True
11✔
130

131

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

135

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

141

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

145

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

151

152
class Shunit2TestTarget(Target):
11✔
153
    alias = "shunit2_test"
11✔
154
    core_fields = (
11✔
155
        *COMMON_TARGET_FIELDS,
156
        Shunit2TestSourceField,
157
        Shunit2TestDependenciesField,
158
        Shunit2TestTimeoutField,
159
        SkipShunit2TestsField,
160
        Shunit2ShellField,
161
        RuntimePackageDependenciesField,
162
    )
163
    help = help_text(
11✔
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):
11✔
185
    default = ("*_test.sh", "test_*.sh", "tests.sh")
11✔
186
    help = generate_multiple_sources_field_help_message(
11✔
187
        "Example: `sources=['test.sh', 'test_*.sh', '!test_ignore.sh']`"
188
    )
189

190

191
class Shunit2TestsOverrideField(OverridesField):
11✔
192
    help = generate_file_based_overrides_field_help_message(
11✔
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):
11✔
205
    alias = "shunit2_tests"
11✔
206
    core_fields = (
11✔
207
        *COMMON_TARGET_FIELDS,
208
        Shunit2TestsGeneratorSourcesField,
209
        Shunit2TestsOverrideField,
210
    )
211
    generated_target_cls = Shunit2TestTarget
11✔
212
    copied_fields = COMMON_TARGET_FIELDS
11✔
213
    moved_fields = (
11✔
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."
11✔
221

222

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

227

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

233

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

240

241
class ShellSourcesOverridesField(OverridesField):
11✔
242
    help = generate_file_based_overrides_field_help_message(
11✔
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):
11✔
255
    alias = "shell_sources"
11✔
256
    core_fields = (
11✔
257
        *COMMON_TARGET_FIELDS,
258
        ShellSourcesGeneratingSourcesField,
259
        ShellSourcesOverridesField,
260
    )
261
    generated_target_cls = ShellSourceTarget
11✔
262
    copied_fields = COMMON_TARGET_FIELDS
11✔
263
    moved_fields = (ShellDependenciesField,)
11✔
264
    help = "Generate a `shell_source` target for each file in the `sources` field."
11✔
265

266

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

271

272
class ShellCommandCommandField(StringField):
11✔
273
    alias = "command"
11✔
274
    required = True
11✔
275
    help = help_text(
11✔
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 ShellCommandOutputFilesField(AdhocToolOutputFilesField):
11✔
287
    pass
11✔
288

289

290
class ShellCommandOutputDirectoriesField(AdhocToolOutputDirectoriesField):
11✔
291
    pass
11✔
292

293

294
class ShellCommandOutputDependenciesField(AdhocToolOutputDependenciesField):
11✔
295
    pass
11✔
296

297

298
class ShellCommandExecutionDependenciesField(AdhocToolExecutionDependenciesField):
11✔
299
    pass
11✔
300

301

302
class RunShellCommandExecutionDependenciesField(ShellCommandExecutionDependenciesField):
11✔
303
    help = help_text(
11✔
304
        lambda: f"""
305
        The execution dependencies for this command.
306

307
        Dependencies specified here are those required to make the command complete successfully
308
        (e.g. file inputs, packages compiled from other targets, etc), but NOT required to make
309
        the outputs of the command useful.
310

311
        See also `{RunShellCommandRunnableDependenciesField.alias}`.
312
        """
313
    )
314

315

316
class ShellCommandRunnableDependenciesField(AdhocToolRunnableDependenciesField):
11✔
317
    pass
11✔
318

319

320
class RunShellCommandRunnableDependenciesField(ShellCommandRunnableDependenciesField):
11✔
321
    help = help_text(
11✔
322
        lambda: f"""
323
        The runnable dependencies for this command.
324

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

329
        See also `{RunShellCommandExecutionDependenciesField.alias}`.
330
        """
331
    )
332

333

334
class ShellCommandSourcesField(MultipleSourcesField):
11✔
335
    # We solely register this field for codegen to work.
336
    alias = "_sources"
11✔
337
    uses_source_roots = False
11✔
338
    expected_num_files = 0
11✔
339

340

341
class ShellCommandTimeoutField(AdhocToolTimeoutField):
11✔
342
    pass
11✔
343

344

345
class ShellCommandToolsField(StringSequenceField):
11✔
346
    alias = "tools"
11✔
347
    default = ()
11✔
348
    help = help_text(
11✔
349
        """
350
        Specify required executable tools that might be used.
351

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

358

359
class ShellCommandExtraEnvVarsField(AdhocToolExtraEnvVarsField):
11✔
360
    pass
11✔
361

362

363
class ShellCommandLogOutputField(AdhocToolLogOutputField):
11✔
364
    pass
11✔
365

366

367
class ShellCommandWorkdirField(AdhocToolWorkdirField):
11✔
368
    pass
11✔
369

370

371
class RunShellCommandWorkdirField(AdhocToolWorkdirField):
11✔
372
    pass
11✔
373

374

375
class ShellCommandOutputRootDirField(AdhocToolOutputRootDirField):
11✔
376
    pass
11✔
377

378

379
class ShellCommandTestDependenciesField(ShellCommandExecutionDependenciesField):
11✔
380
    pass
11✔
381

382

383
class ShellCommandNamedCachesField(AdhocToolNamedCachesField):
11✔
384
    pass
11✔
385

386

387
class ShellCommandWorkspaceInvalidationSourcesField(AdhocToolWorkspaceInvalidationSourcesField):
11✔
388
    pass
11✔
389

390

391
class ShellCommandPathEnvModifyModeField(AdhocToolPathEnvModifyModeField):
11✔
392
    pass
11✔
393

394

395
class ShellCommandOutputsMatchMode(AdhocToolOutputsMatchMode):
11✔
396
    pass
11✔
397

398

399
class ShellCommandCacheScopeField(AdhocToolCacheScopeField):
11✔
400
    pass
11✔
401

402

403
class SkipShellCommandTestsField(BoolField):
11✔
404
    alias = "skip_tests"
11✔
405
    default = False
11✔
406
    help = "If true, don't run this tests for target."
11✔
407

408

409
class SkipShellCommandPackageField(BoolField):
11✔
410
    alias = "skip_package"
11✔
411
    default = False
11✔
412
    help = "If true, don't run this package for target."
11✔
413

414

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

443
        Example BUILD file:
444

445
            shell_command(
446
                command="./my-script.sh --flag",
447
                tools=["tar", "curl", "cat", "bash", "env"],
448
                execution_dependencies=[":scripts"],
449
                output_files=["logs/my-script.log"],
450
                output_directories=["results"],
451
            )
452

453
            shell_sources(name="scripts")
454

455
        Remember to add this target to the dependencies of each consumer, such as your
456
        `python_tests` or `docker_image`. When relevant, Pants will run your `command` and
457
        insert the `outputs` into that consumer's context.
458

459
        The command may be retried and/or cancelled, so ensure that it is idempotent.
460
        """
461
    )
462

463

464
class ShellCommandRunTarget(Target):
11✔
465
    alias = "run_shell_command"
11✔
466
    core_fields = (
11✔
467
        *COMMON_TARGET_FIELDS,
468
        RunShellCommandExecutionDependenciesField,
469
        RunShellCommandRunnableDependenciesField,
470
        ShellCommandCommandField,
471
        RunShellCommandWorkdirField,
472
    )
473
    help = help_text(
11✔
474
        """
475
        Run a script in the workspace, with all dependencies packaged/copied into a chroot.
476

477
        Example BUILD file:
478

479
            run_shell_command(
480
                command="./scripts/my-script.sh --data-files-dir={chroot}",
481
                execution_dependencies=["src/project/files:data"],
482
            )
483

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

487
        In contrast to the `shell_command`, in addition to `workdir` you only have
488
        the `command` and `execution_dependencies` fields as the `tools` you are going to use are
489
        already on the PATH which is inherited from the Pants environment. Also, the `outputs` does
490
        not apply, as any output files produced will end up directly in your project tree.
491
        """
492
    )
493

494

495
class ShellCommandTestTarget(Target):
11✔
496
    alias = "test_shell_command"
11✔
497

498
    core_fields = (
11✔
499
        *COMMON_TARGET_FIELDS,
500
        ShellCommandTestDependenciesField,
501
        ShellCommandRunnableDependenciesField,
502
        ShellCommandCommandField,
503
        ShellCommandLogOutputField,
504
        ShellCommandSourcesField,
505
        ShellCommandTimeoutField,
506
        ShellCommandToolsField,
507
        ShellCommandExtraEnvVarsField,
508
        ShellCommandPathEnvModifyModeField,
509
        EnvironmentField,
510
        SkipShellCommandTestsField,
511
        ShellCommandWorkdirField,
512
        ShellCommandOutputFilesField,
513
        ShellCommandOutputDirectoriesField,
514
        ShellCommandOutputRootDirField,
515
        ShellCommandOutputsMatchMode,
516
    )
517
    help = help_text(
11✔
518
        """
519
        Run a script as a test via the `test` goal, with all dependencies packaged/copied available in the chroot.
520

521
        Example BUILD file:
522

523
            test_shell_command(
524
                name="test",
525
                tools=["test"],
526
                command="test -r $CHROOT/some-data-file.txt",
527
                execution_dependencies=["src/project/files:data"],
528
            )
529

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

533
        In contrast to the `run_shell_command`, this target is intended to run shell commands as tests
534
        and will only run them via the `test` goal.
535
        """
536
    )
537

538

539
class ShellCommandPackageDependenciesField(ShellCommandExecutionDependenciesField):
11✔
540
    pass
11✔
541

542

543
class ShellCommandPackageTarget(Target):
11✔
544
    alias = "package_shell_command"
11✔
545

546
    core_fields = (
11✔
547
        *COMMON_TARGET_FIELDS,
548
        ShellCommandPackageDependenciesField,
549
        ShellCommandRunnableDependenciesField,
550
        ShellCommandCommandField,
551
        ShellCommandLogOutputField,
552
        ShellCommandSourcesField,
553
        ShellCommandTimeoutField,
554
        ShellCommandToolsField,
555
        ShellCommandExtraEnvVarsField,
556
        ShellCommandPathEnvModifyModeField,
557
        ShellCommandNamedCachesField,
558
        ShellCommandWorkspaceInvalidationSourcesField,
559
        ShellCommandCacheScopeField,
560
        EnvironmentField,
561
        SkipShellCommandPackageField,
562
        ShellCommandWorkdirField,
563
        ShellCommandOutputFilesField,
564
        ShellCommandOutputDirectoriesField,
565
        ShellCommandOutputRootDirField,
566
        ShellCommandOutputsMatchMode,
567
        ShellCommandOutputDependenciesField,
568
        OutputPathField,
569
    )
570

571
    help = help_text(
11✔
572
        """
573
        Run a script to produce distributable outputs via the `package` goal.
574

575
        Example BUILD file:
576

577
            experimental_package_shell_command(
578
                name="build-rust-app",
579
                tools=["cargo"],
580
                command="cargo build --release",
581
                output_files=["target/release/binary"],
582
            )
583

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

587
        The outputs specified via `output_files` and `output_directories` will be captured and
588
        made available for other Pants targets to depend on. They will also be copied to the path
589
        specified via the `output_path` field (relative to the dist directory) when running
590
        `pants package`.
591

592
        This target is experimental and its behavior may change in future versions.
593
        """
594
    )
595

596

597
def rules():
11✔
598
    return [
7✔
599
        *collect_rules(),
600
        UnionRule(TargetFilesGeneratorSettingsRequest, ShellGeneratorSettingsRequest),
601
    ]
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