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

pantsbuild / pants / 23616668936

26 Mar 2026 08:33PM UTC coverage: 92.848% (-0.08%) from 92.923%
23616668936

Pull #23133

github

web-flow
Merge 51b4d6d01 into 9b3c1562e
Pull Request #23133: Add buildctl engine

325 of 386 new or added lines in 14 files covered. (84.2%)

30 existing lines in 4 files now uncovered.

91645 of 98704 relevant lines covered (92.85%)

4.05 hits per line

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

92.1
/src/python/pants/backend/docker/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
10✔
5

6
import os
10✔
7
import re
10✔
8
from abc import ABC, abstractmethod
10✔
9
from collections.abc import Callable, Iterator, Mapping, Sequence
10✔
10
from dataclasses import dataclass
10✔
11
from typing import TYPE_CHECKING, ClassVar, final
10✔
12

13
from pants.backend.docker.registries import ALL_DEFAULT_REGISTRIES
10✔
14
from pants.backend.docker.subsystems.docker_options import DockerOptions
10✔
15
from pants.base.build_environment import get_buildroot
10✔
16
from pants.core.goals.package import OutputPathField
10✔
17
from pants.core.goals.run import RestartableField
10✔
18
from pants.engine.addresses import Address
10✔
19
from pants.engine.collection import Collection
10✔
20
from pants.engine.environment import EnvironmentName
10✔
21
from pants.engine.fs import GlobMatchErrorBehavior
10✔
22
from pants.engine.rules import collect_rules, rule
10✔
23
from pants.engine.target import (
10✔
24
    COMMON_TARGET_FIELDS,
25
    AllTargets,
26
    AsyncFieldMixin,
27
    BoolField,
28
    Dependencies,
29
    DictStringToStringField,
30
    Field,
31
    InvalidFieldException,
32
    ListOfDictStringToStringField,
33
    OptionalSingleSourceField,
34
    StringField,
35
    StringSequenceField,
36
    Target,
37
    Targets,
38
)
39
from pants.engine.unions import union
10✔
40
from pants.util.docutil import bin_name, doc_url
10✔
41
from pants.util.meta import classproperty
10✔
42
from pants.util.strutil import help_text, softwrap
10✔
43

44
if TYPE_CHECKING:
45
    from pants.backend.docker.util_rules.docker_build_context import DockerBuildContext
46

47
# Common help text to be applied to each field that supports value interpolation.
48
_interpolation_help = (
10✔
49
    "{kind} may use placeholders in curly braces to be interpolated. The placeholders are derived "
50
    "from various sources, such as the Dockerfile instructions and build args."
51
)
52

53

54
class DockerImageBuildArgsField(StringSequenceField):
10✔
55
    alias = "extra_build_args"
10✔
56
    default = ()
10✔
57
    help = help_text(
10✔
58
        """
59
        Build arguments (`--build-arg`) to use when building this image.
60
        Entries are either strings in the form `ARG_NAME=value` to set an explicit value;
61
        or just `ARG_NAME` to copy the value from Pants's own environment.
62

63
        Use `[docker].build_args` to set default build args for all images.
64
        """
65
    )
66

67

68
class DockerImageContextRootField(StringField):
10✔
69
    alias = "context_root"
10✔
70
    help = help_text(
10✔
71
        """
72
        Specify which directory to use as the Docker build context root. This affects the file
73
        paths to use for the `COPY` and `ADD` instructions. For example, whether
74
        `COPY files/f.txt` should look for the file relative to the build root:
75
        `<build root>/files/f.txt` vs relative to the BUILD file:
76
        `<build root>/path_to_build_file/files/f.txt`.
77

78
        Specify the `context_root` path as `files` for relative to build root, or as `./files`
79
        for relative to the BUILD file.
80

81
        If `context_root` is not specified, it defaults to `[docker].default_context_root`.
82
        """
83
    )
84

85
    @classmethod
10✔
86
    def compute_value(cls, raw_value: str | None, address: Address) -> str | None:
10✔
87
        value_or_default = super().compute_value(raw_value, address=address)
9✔
88
        if isinstance(value_or_default, str) and value_or_default.startswith("/"):
9✔
89
            val = value_or_default.strip("/")
1✔
90
            raise InvalidFieldException(
1✔
91
                softwrap(
92
                    f"""
93
                    The `{cls.alias}` field in target {address} must be a relative path, but was
94
                    {value_or_default!r}. Use {val!r} for a path relative to the build root, or
95
                    {"./" + val!r} for a path relative to the BUILD file
96
                    (i.e. {os.path.join(address.spec_path, val)!r}).
97
                    """
98
                )
99
            )
100
        return value_or_default
9✔
101

102

103
class DockerImageSourceField(OptionalSingleSourceField):
10✔
104
    none_is_valid_value = True
10✔
105
    default = "Dockerfile"
10✔
106

107
    # When the default glob value is in effect, we don't want the normal glob match error behavior
108
    # to kick in for a missing Dockerfile, in case there are `instructions` provided, in which case
109
    # we generate the Dockerfile instead. If there are no `instructions`, or there are both
110
    # `instructions` and a Dockerfile hydrated from the `source` glob, we error out with a message
111
    # to the user.
112
    default_glob_match_error_behavior = GlobMatchErrorBehavior.ignore
10✔
113

114
    help = help_text(
10✔
115
        """
116
        The Dockerfile to use when building the Docker image.
117

118
        Use the `instructions` field instead if you prefer not having the Dockerfile in your
119
        source tree.
120
        """
121
    )
122

123

124
class DockerImageInstructionsField(StringSequenceField):
10✔
125
    alias = "instructions"
10✔
126
    required = False
10✔
127
    help = help_text(
10✔
128
        """
129
        The `Dockerfile` content, typically one instruction per list item.
130

131
        Use the `source` field instead if you prefer having the Dockerfile in your source tree.
132

133
        Example:
134

135
            # example/BUILD
136
            docker_image(
137
              instructions=[
138
                "FROM base/image:1.0",
139
                "RUN echo example",
140
              ],
141
            )
142
        """
143
    )
144

145

146
class DockerImageTagsField(StringSequenceField):
10✔
147
    alias = "image_tags"
10✔
148
    default = ("latest",)
10✔
149
    help = help_text(
10✔
150
        f"""
151

152
        Any tags to apply to the Docker image name (the version is usually applied as a tag).
153

154
        {_interpolation_help.format(kind="tag")}
155

156
        See {doc_url("docs/docker/tagging-docker-images")}.
157
        """
158
    )
159

160

161
class DockerImageDependenciesField(Dependencies):
10✔
162
    supports_transitive_excludes = True
10✔
163

164

165
class DockerImageRegistriesField(StringSequenceField):
10✔
166
    alias = "registries"
10✔
167
    default = (ALL_DEFAULT_REGISTRIES,)
10✔
168
    help = help_text(
10✔
169
        """
170
        List of addresses or configured aliases to any Docker registries to use for the
171
        built image.
172

173
        The address is a domain name with optional port for your registry, and any registry
174
        aliases are prefixed with `@` for addresses in the `[docker].registries` configuration
175
        section.
176

177
        By default, all configured registries with `default = true` are used.
178

179
        Example:
180

181
            # pants.toml
182
            [docker.registries.my-registry-alias]
183
            address = "myregistrydomain:port"
184
            default = false # optional
185

186
            # example/BUILD
187
            docker_image(
188
                registries = [
189
                    "@my-registry-alias",
190
                    "myregistrydomain:port",
191
                ],
192
            )
193

194
        The above example shows two valid `registry` options: using an alias to a configured
195
        registry and the address to a registry verbatim in the BUILD file.
196
        """
197
    )
198

199

200
class DockerImageRepositoryField(StringField):
10✔
201
    alias = "repository"
10✔
202
    help = help_text(
10✔
203
        f"""
204
        The repository name for the Docker image. e.g. `"<repository>/<name>"`.
205

206
        It uses the `[docker].default_repository` by default.
207

208
        {_interpolation_help.format(kind="Repository")}
209

210
        Additional placeholders for the repository field are: `name`, `directory`,
211
        `parent_directory`, and `default_repository`.
212

213
        Registries may also configure the repository value for specific registries.
214

215
        See the documentation for `[docker].default_repository` for more information.
216
        """
217
    )
218

219

220
class DockerImageSkipPushField(BoolField):
10✔
221
    alias = "skip_push"
10✔
222
    default = False
10✔
223
    help = f"If true, do not push this image to registries when running `{bin_name()} publish`."
10✔
224

225

226
OptionValueFormatter = Callable[[str], str]
10✔
227

228

229
class ValidateOptionsMixin(Field, ABC):
10✔
230
    def validate_options(self, options: DockerOptions, context: DockerBuildContext) -> bool:
10✔
231
        """Hook method telling Pants to ignore this option in certain contexts.
232

233
        Can also be used to throw errors simply by raising an exception.
234
        """
235
        return True
4✔
236

237

238
class DockerBuildOptionsFieldMixin(ValidateOptionsMixin, ABC):
10✔
239
    docker_build_option: ClassVar[str]
10✔
240

241
    @classmethod
10✔
242
    @abstractmethod
10✔
243
    def compute_docker_build_options(
10✔
244
        cls, value, *, docker: DockerOptions, value_formatter: OptionValueFormatter
245
    ) -> Iterator[str]:
246
        """Subclasses must implement this, to turn `value` into none, one or more option values."""
247

248
    def docker_build_options(
10✔
249
        self, *, docker: DockerOptions, value_formatter: OptionValueFormatter
250
    ) -> Iterator[str]:
251
        return self.compute_docker_build_options(
4✔
252
            self.value, docker=docker, value_formatter=value_formatter
253
        )
254

255

256
class DockerBuildOptionFieldValueMixin(DockerBuildOptionsFieldMixin, ABC):
10✔
257
    """Inherit this mixin class to provide unary options (i.e. option in the form of `--flag=value`)
258
    to `docker build`."""
259

260
    @classmethod
10✔
261
    @final
10✔
262
    def compute_docker_build_options(
10✔
263
        cls, value, *, docker: DockerOptions, value_formatter: OptionValueFormatter
264
    ) -> Iterator[str]:
265
        if value is not None:
4✔
266
            yield f"{cls.docker_build_option}={value}"
4✔
267

268

269
class DockerBuildOptionFieldMultiValueMixin(StringSequenceField, DockerBuildOptionsFieldMixin, ABC):
10✔
270
    """Inherit this mixin class to provide options in the form of `--flag=value1,value2` to `docker
271
    build`."""
272

273
    @classmethod
10✔
274
    @final
10✔
275
    def compute_docker_build_options(
10✔
276
        cls,
277
        value: Sequence[str] | None,
278
        *,
279
        docker: DockerOptions,
280
        value_formatter: OptionValueFormatter,
281
    ) -> Iterator[str]:
282
        if value:
4✔
283
            yield f"{cls.docker_build_option}={','.join(value)}"
1✔
284

285

286
class DockerBuildOptionFieldMultiValueDictMixin(
10✔
287
    DictStringToStringField, DockerBuildOptionsFieldMixin, ABC
288
):
289
    """Inherit this mixin class to provide options in the form of `--flag=key1=value1,key2=value2`
290
    to `docker build`."""
291

292
    @classmethod
10✔
293
    @final
10✔
294
    def compute_docker_build_options(
10✔
295
        cls,
296
        value: Mapping[str, str] | None,
297
        *,
298
        docker: DockerOptions,
299
        value_formatter: OptionValueFormatter,
300
    ) -> Iterator[str]:
301
        if value:
4✔
302
            yield f"{cls.docker_build_option}=" + ",".join(
1✔
303
                f"{key}={value_formatter(v)}" for key, v in value.items()
304
            )
305

306

307
class DockerBuildOptionFieldListOfMultiValueDictMixin(
10✔
308
    ListOfDictStringToStringField, DockerBuildOptionsFieldMixin, ABC
309
):
310
    """Inherit this mixin class to provide multiple key-value options to docker build:
311

312
    `--flag=key1=value1,key2=value2 --flag=key3=value3,key4=value4`
313
    """
314

315
    @classmethod
10✔
316
    @final
10✔
317
    def compute_docker_build_options(
10✔
318
        cls,
319
        value: Sequence[Mapping[str, str]] | None,
320
        *,
321
        docker: DockerOptions,
322
        value_formatter: OptionValueFormatter,
323
    ) -> Iterator[str]:
324
        if value:
4✔
325
            for item in value:
1✔
326
                yield f"{cls.docker_build_option}=" + ",".join(
1✔
327
                    f"{key}={value_formatter(v)}" for key, v in item.items()
328
                )
329

330

331
class DockerBuildOptionFlagFieldMixin(BoolField, DockerBuildOptionsFieldMixin, ABC):
10✔
332
    """Inherit this mixin class to provide optional flags (i.e. add `--flag` only when the value is
333
    `True`) to `docker build`."""
334

335
    @classmethod
10✔
336
    @final
10✔
337
    def compute_docker_build_options(
10✔
338
        cls, value: bool, *, docker: DockerOptions, value_formatter: OptionValueFormatter
339
    ) -> Iterator[str]:
340
        if value:
4✔
341
            yield f"{cls.docker_build_option}"
1✔
342

343

344
class BuildctlOptionsFieldMixin(ValidateOptionsMixin, ABC):
10✔
345
    buildctl_option: ClassVar[str]
10✔
346

347
    @classmethod
10✔
348
    @abstractmethod
10✔
349
    def compute_buildctl_options(
10✔
350
        cls, value, *, docker: DockerOptions, value_formatter: OptionValueFormatter
351
    ) -> Iterator[str]:
352
        """Subclasses must implement this, to turn `value` into none, one or more option values."""
353

354
    def buildctl_options(
10✔
355
        self, *, docker: DockerOptions, value_formatter: OptionValueFormatter
356
    ) -> Iterator[str]:
NEW
357
        return self.compute_buildctl_options(
×
358
            self.value, docker=docker, value_formatter=value_formatter
359
        )
360

361

362
class DockerBuildkitPassthroughFieldMixin(
10✔
363
    BuildctlOptionsFieldMixin, DockerBuildOptionsFieldMixin, ABC
364
):
365
    @classproperty
10✔
366
    def docker_build_option(cls) -> str:
10✔
NEW
367
        return cls.buildctl_option
×
368

369
    @classmethod
10✔
370
    def compute_docker_build_options(
10✔
371
        cls, value, *, docker: DockerOptions, value_formatter: OptionValueFormatter
372
    ) -> Iterator[str]:
373
        return cls.compute_buildctl_options(value, docker=docker, value_formatter=value_formatter)
4✔
374

375

376
class BuildctlOptionFieldMultiValueDictMixin(
10✔
377
    DictStringToStringField, BuildctlOptionsFieldMixin, ABC
378
):
379
    """Inherit this mixin class to provide options in the form of `--flag=key1=value1,key2=value2`
380
    to `buildctl build`."""
381

382
    @classmethod
10✔
383
    @final
10✔
384
    def compute_buildctl_options(
10✔
385
        cls,
386
        value: Mapping[str, str] | None,
387
        *,
388
        docker: DockerOptions,
389
        value_formatter: OptionValueFormatter,
390
    ) -> Iterator[str]:
NEW
391
        if value:
×
NEW
392
            yield f"{cls.buildctl_option}=" + ",".join(
×
393
                f"{key}={value_formatter(v)}" for key, v in value.items()
394
            )
395

396

397
class BuildctlOptionFieldListOfMultiValueDictMixin(
10✔
398
    ListOfDictStringToStringField, BuildctlOptionsFieldMixin, ABC
399
):
400
    """Inherit this mixin class to provide multiple key-value options to buildctl build:
401

402
    `--flag=key1=value1,key2=value2 --flag=key3=value3,key4=value4`
403
    """
404

405
    @classmethod
10✔
406
    @final
10✔
407
    def compute_buildctl_options(
10✔
408
        cls,
409
        value: Sequence[Mapping[str, str]] | None,
410
        *,
411
        docker: DockerOptions,
412
        value_formatter: OptionValueFormatter,
413
    ) -> Iterator[str]:
NEW
414
        if value:
×
NEW
415
            for item in value:
×
NEW
416
                yield f"{cls.buildctl_option}=" + ",".join(
×
417
                    f"{key}={value_formatter(v)}" for key, v in item.items()
418
                )
419

420

421
class BuildctlOptionFieldValueMixin(BuildctlOptionsFieldMixin, ABC):
10✔
422
    """Inherit this mixin class to provide unary options (i.e. option in the form of `--flag=value`)
423
    to `buildctl build`."""
424

425
    @classmethod
10✔
426
    @final
10✔
427
    def compute_buildctl_options(
10✔
428
        cls, value, *, docker: DockerOptions, value_formatter: OptionValueFormatter
429
    ) -> Iterator[str]:
NEW
430
        if value is not None:
×
NEW
431
            yield f"{cls.buildctl_option}={value}"
×
432

433

434
class BuildctlLayeredOptionFieldValueMixin(BuildctlOptionsFieldMixin, ABC):
10✔
435
    """Inherit this mixin class to provide layered options (i.e. option in the form of `--flag
436
    suboption=value`) to `buildctl build`.
437

438
    You can override the option-value delimiter (default is `=`) by setting the
439
    `suboption_value_delimiter` class variable.
440
    """
441

442
    suboption: ClassVar[str]
10✔
443
    suboption_value_delimiter: ClassVar[str] = "="
10✔
444

445
    @classmethod
10✔
446
    @final
10✔
447
    def compute_buildctl_options(
10✔
448
        cls, value, *, docker: DockerOptions, value_formatter: OptionValueFormatter
449
    ) -> Iterator[str]:
NEW
450
        if value is not None:
×
NEW
451
            yield cls.buildctl_option
×
NEW
452
            yield f"{cls.suboption}{cls.suboption_value_delimiter}{value}"
×
453

454

455
class BuildctlOptionFieldMultiValueMixin(StringSequenceField, BuildctlOptionsFieldMixin, ABC):
10✔
456
    """Inherit this mixin class to provide options in the form of `--flag=value1,value2` to
457
    `buildctl build`."""
458

459
    @classmethod
10✔
460
    @final
10✔
461
    def compute_buildctl_options(
10✔
462
        cls,
463
        value: Sequence[str] | None,
464
        *,
465
        docker: DockerOptions,
466
        value_formatter: OptionValueFormatter,
467
    ) -> Iterator[str]:
NEW
468
        if value:
×
NEW
469
            yield f"{cls.buildctl_option}={','.join(value)}"
×
470

471

472
class BuildctlLayeredOptionFieldMultiValueMixin(
10✔
473
    StringSequenceField, BuildctlOptionsFieldMixin, ABC
474
):
475
    """Inherit this mixin class to provide layered options in the form of `--flag
476
    suboption=value1,value2` to `buildctl build`.
477

478
    You can override the option-values delimiter (default is `=`) by setting the
479
    `suboption_value_delimiter` class variable.
480
    """
481

482
    suboption: ClassVar[str]
10✔
483
    suboption_value_delimiter: ClassVar[str] = "="
10✔
484

485
    @classmethod
10✔
486
    @final
10✔
487
    def compute_buildctl_options(
10✔
488
        cls,
489
        value: Sequence[str] | None,
490
        *,
491
        docker: DockerOptions,
492
        value_formatter: OptionValueFormatter,
493
    ) -> Iterator[str]:
NEW
494
        if value:
×
NEW
495
            yield cls.buildctl_option
×
NEW
496
            yield f"{cls.suboption}{cls.suboption_value_delimiter}{','.join(value)}"
×
497

498

499
class BuildctlOptionFlagFieldMixin(BoolField, BuildctlOptionsFieldMixin, ABC):
10✔
500
    """Inherit this mixin class to provide optional flags (i.e. add `--flag` only when the value is
501
    `True`) to `buildctl build`."""
502

503
    @classmethod
10✔
504
    @final
10✔
505
    def compute_buildctl_options(
10✔
506
        cls, value: bool, *, docker: DockerOptions, value_formatter: OptionValueFormatter
507
    ) -> Iterator[str]:
NEW
508
        if value:
×
NEW
509
            yield f"{cls.buildctl_option}"
×
510

511

512
class DockerImageTargetStageField(
10✔
513
    DockerBuildOptionFieldValueMixin, BuildctlLayeredOptionFieldValueMixin, StringField
514
):
515
    alias = "target_stage"
10✔
516
    help = help_text(
10✔
517
        """
518
        Specify target build stage, rather than building the entire `Dockerfile`.
519

520
        When using multi-stage build, you may name your stages, and can target them when building
521
        to only selectively build a certain stage. See also the `--docker-build-target-stage`
522
        option.
523

524
        Read more about [multi-stage Docker builds](https://docs.docker.com/develop/develop-images/multistage-build/#stop-at-a-specific-build-stage)
525
        """
526
    )
527
    docker_build_option = "--target"
10✔
528
    buildctl_option = "--opt"
10✔
529
    suboption = "target"
10✔
530

531
    @staticmethod
10✔
532
    def validate_options(options: DockerOptions, context: DockerBuildContext) -> bool:
10✔
533
        # Defer to global option if set and matches a stage
534
        return options.build_target_stage not in context.stages
4✔
535

536

537
class DockerImageBuildImageLabelsOptionField(
10✔
538
    DockerBuildOptionsFieldMixin,
539
    BuildctlOptionsFieldMixin,
540
    DictStringToStringField,
541
):
542
    alias = "image_labels"
10✔
543
    help = help_text(
10✔
544
        f"""
545
        Provide image metadata.
546

547
        {_interpolation_help.format(kind="Label value")}
548

549
        See [Docker labels](https://docs.docker.com/config/labels-custom-metadata/#manage-labels-on-objects)
550
        for more information.
551
        """
552
    )
553
    docker_build_option = "--label"
10✔
554
    buildctl_option = "--opt"
10✔
555

556
    @classmethod
10✔
557
    def compute_docker_build_options(
10✔
558
        cls,
559
        value: Mapping[str, str] | None,
560
        *,
561
        docker: DockerOptions,
562
        value_formatter: OptionValueFormatter,
563
    ) -> Iterator[str]:
564
        for label, v in (value or {}).items():
4✔
565
            yield cls.docker_build_option
1✔
566
            yield f"{label}={value_formatter(v)}"
1✔
567

568
    @classmethod
10✔
569
    def compute_buildctl_options(
10✔
570
        cls,
571
        value: Mapping[str, str] | None,
572
        *,
573
        docker: DockerOptions,
574
        value_formatter: OptionValueFormatter,
575
    ) -> Iterator[str]:
NEW
576
        for label, v in (value or {}).items():
×
NEW
577
            yield cls.buildctl_option
×
NEW
578
            yield f"label:{label}={value_formatter(v)}"
×
579

580

581
class DockerImageBuildImageExtraHostsField(DockerBuildOptionsFieldMixin, DictStringToStringField):
10✔
582
    alias = "extra_build_hosts"
10✔
583
    help = help_text(
10✔
584
        """
585
        Extra hosts entries to be added to a container's `/etc/hosts` file.
586

587
        Use `[docker].build_hosts` to set default host entries for all images.
588
        """
589
    )
590
    docker_build_option = "--add-host"
10✔
591

592
    @classmethod
10✔
593
    def compute_docker_build_options(
10✔
594
        cls,
595
        value: Mapping[str, str] | None,
596
        *,
597
        docker: DockerOptions,
598
        value_formatter: OptionValueFormatter,
599
    ) -> Iterator[str]:
600
        if value:
4✔
601
            merged_values = {**docker.build_hosts, **value}
1✔
602
            for label, v in merged_values.items():
1✔
603
                yield cls.docker_build_option
1✔
604
                yield f"{label}:{value_formatter(v)}"
1✔
605

606

607
class DockerImageBuildImageCacheToField(
10✔
608
    DockerBuildOptionFieldMultiValueDictMixin,
609
    BuildctlOptionFieldMultiValueDictMixin,
610
    DictStringToStringField,
611
):
612
    alias = "cache_to"
10✔
613
    help = help_text(
10✔
614
        f"""
615
        Export image build cache to an external cache destination.
616

617
        Note that Docker [supports](https://docs.docker.com/build/cache/backends/#multiple-caches)
618
        multiple cache sources - Pants will pass these as multiple `--cache_from` arguments to the
619
        Docker CLI. Docker will only use the first cache hit (i.e. the image exists) in the build.
620

621
        If you're using the legacy builder, this option is not supported.
622

623
        Example:
624

625
            docker_image(
626
                name="example-local-cache-backend",
627
                cache_to={{
628
                    "type": "local",
629
                    "dest": "/tmp/docker-cache/example"
630
                }},
631
                cache_from=[{{
632
                    "type": "local",
633
                    "src": "/tmp/docker-cache/example"
634
                }}]
635
            )
636

637
        {_interpolation_help.format(kind="Values")}
638
        """
639
    )
640
    docker_build_option = "--cache-to"
10✔
641
    buildctl_option = "--export-cache"
10✔
642

643
    @staticmethod
10✔
644
    def validate_options(options: DockerOptions, context: DockerBuildContext) -> bool:
10✔
645
        return not options.build_no_cache
4✔
646

647

648
class DockerImageBuildImageCacheFromField(
10✔
649
    DockerBuildOptionFieldListOfMultiValueDictMixin,
650
    BuildctlOptionFieldListOfMultiValueDictMixin,
651
    ListOfDictStringToStringField,
652
):
653
    alias = "cache_from"
10✔
654
    help = help_text(
10✔
655
        f"""
656
        Use external cache sources when building the image.
657

658
        If you're using the legacy builder, this option is not supported.
659

660
        Example:
661

662
            docker_image(
663
                name="example-local-cache-backend",
664
                cache_to={{
665
                    "type": "local",
666
                    "dest": "/tmp/docker-cache/primary"
667
                }},
668
                cache_from=[
669
                    {{
670
                        "type": "local",
671
                        "src": "/tmp/docker-cache/primary"
672
                    }},
673
                    {{
674
                        "type": "local",
675
                        "src": "/tmp/docker-cache/secondary"
676
                    }}
677
                ]
678
            )
679

680
        {_interpolation_help.format(kind="Values")}
681
        """
682
    )
683
    docker_build_option = "--cache-from"
10✔
684
    buildctl_option = "--import-cache"
10✔
685

686
    @staticmethod
10✔
687
    def validate_options(options: DockerOptions, context: DockerBuildContext) -> bool:
10✔
688
        return not options.build_no_cache
4✔
689

690

691
class DockerImageBuildImageOutputField(DictStringToStringField):
10✔
692
    alias = "output"
10✔
693
    help = help_text(
10✔
694
        f"""
695
        Sets the export action for the build result.
696

697
        If you're using the legacy builder, this option is not supported.
698

699
        When using `pants publish` to publish Docker images to a registry, the output type
700
        must be 'docker', as `publish` expects that the built images exist in the local
701
        image store.
702

703
        {_interpolation_help.format(kind="Values")}
704
        """
705
    )
706

707

708
class DockerImageBuildSecretsOptionField(
10✔
709
    AsyncFieldMixin,
710
    ValidateOptionsMixin,
711
    DictStringToStringField,
712
):
713
    alias = "secrets"
10✔
714
    help = help_text(
10✔
715
        """
716
        Secret files to expose to the build (only if BuildKit enabled).
717

718
        Secrets may use absolute paths, or paths relative to your build root, or the BUILD file
719
        if prefixed with `./`. Paths to your home directory will be automatically expanded.
720
        The id should be valid as used by the Docker build `--secret` option.
721
        See [Docker secrets](https://docs.docker.com/engine/swarm/secrets/) for more
722
        information.
723

724
        Example:
725

726
            docker_image(
727
                secrets={
728
                    "mysecret": "/var/secrets/some-secret",
729
                    "repo-secret": "src/proj/secrets/some-secret",
730
                    "home-dir-secret": "~/.config/some-secret",
731
                    "target-secret": "./secrets/some-secret",
732
                }
733
            )
734
        """
735
    )
736

737
    buildctl_option = "--secret"
10✔
738
    docker_build_option = "--secret"
10✔
739

740
    def buildctl_options(
10✔
741
        self, *, docker: DockerOptions, value_formatter: OptionValueFormatter
742
    ) -> Iterator[str]:
743
        # os.path.join() discards preceding parts if encountering an abs path, e.g. if the secret
744
        # `path` is an absolute path, the `buildroot` and `spec_path` will not be considered.  Also,
745
        # an empty path part is ignored.
746
        for secret, path in (self.value or {}).items():
5✔
747
            full_path = os.path.join(
2✔
748
                get_buildroot(),
749
                self.address.spec_path if re.match(r"\.{1,2}/", path) else "",
750
                os.path.expanduser(path),
751
            )
752
            yield self.buildctl_option
2✔
753
            yield f"id={secret},src={os.path.normpath(full_path)}"
2✔
754

755
    def docker_build_options(
10✔
756
        self, *, docker: DockerOptions, value_formatter: OptionValueFormatter
757
    ) -> Iterator[str]:
758
        return self.buildctl_options(docker=docker, value_formatter=value_formatter)
4✔
759

760

761
class DockerImageBuildSSHOptionField(
10✔
762
    DockerBuildkitPassthroughFieldMixin,
763
    StringSequenceField,
764
):
765
    alias = "ssh"
10✔
766
    default = ()
10✔
767
    help = help_text(
10✔
768
        """
769
        SSH agent socket or keys to expose to the build (only if BuildKit enabled)
770
        (format: `default|<id>[=<socket>|<key>[,<key>]]`)
771

772
        The exposed agent and/or keys can then be used in your `Dockerfile` by mounting them in
773
        your `RUN` instructions:
774

775
            RUN --mount=type=ssh ...
776

777
        See [Docker documentation](https://docs.docker.com/develop/develop-images/build_enhancements/#using-ssh-to-access-private-data-in-builds)
778
        for more information.
779
        """
780
    )
781

782
    buildctl_option = "--ssh"
10✔
783

784
    @classmethod
10✔
785
    def compute_buildctl_options(
10✔
786
        cls, value: Sequence[str], *, docker: DockerOptions, value_formatter: OptionValueFormatter
787
    ) -> Iterator[str]:
788
        for v in value:
4✔
789
            yield cls.buildctl_option
1✔
790
            yield v
1✔
791

792

793
class DockerImageBuildPullOptionField(DockerBuildOptionFieldValueMixin, BoolField):
10✔
794
    alias = "pull"
10✔
795
    default = False
10✔
796
    help = help_text(
10✔
797
        """
798
        If true, then docker will always attempt to pull a newer version of the image.
799

800
        NOTE: This option cannot be used on images that build off of "transitive" base images
801
        referenced by address (i.e. `FROM path/to/your/base/Dockerfile`).
802
        """
803
    )
804
    docker_build_option = "--pull"
10✔
805

806

807
class DockerImageBuildSquashOptionField(DockerBuildOptionFlagFieldMixin):
10✔
808
    alias = "squash"
10✔
809
    default = False
10✔
810
    help = help_text(
10✔
811
        """
812
        If true, then docker will squash newly built layers into a single new layer.
813

814
        Note that this option is only supported on a Docker daemon with experimental features enabled.
815
        """
816
    )
817
    docker_build_option = "--squash"
10✔
818

819

820
class DockerImageBuildNetworkOptionField(DockerBuildOptionFieldValueMixin, StringField):
10✔
821
    alias = "build_network"
10✔
822
    default = None
10✔
823
    help = help_text(
10✔
824
        """
825
        Sets the networking mode for the run commands during build.
826
        Supported standard values are: bridge, host, none, and container:<name|id>.
827
        Any other value is taken as a custom network's name to which the container should connect to.
828
        """
829
    )
830
    docker_build_option = "--network"
10✔
831

832

833
class DockerImageBuildPlatformOptionField(
10✔
834
    DockerBuildOptionFieldMultiValueMixin,
835
    BuildctlLayeredOptionFieldMultiValueMixin,
836
    StringSequenceField,
837
):
838
    alias = "build_platform"
10✔
839
    default = None
10✔
840
    help = help_text(
10✔
841
        """
842
        Set the target platform(s) for the build.
843
        """
844
    )
845
    docker_build_option = "--platform"
10✔
846
    buildctl_option = "--opt"
10✔
847
    suboption = "platform"
10✔
848

849

850
class DockerImageRunExtraArgsField(StringSequenceField):
10✔
851
    alias: ClassVar[str] = "extra_run_args"
10✔
852
    default = ()
10✔
853
    help = help_text(
10✔
854
        lambda: f"Extra arguments to pass into the invocation of `docker run`. These are in addition to those at the `[{DockerOptions.options_scope}].run_args`"
855
    )
856

857

858
class DockerImageTarget(Target):
10✔
859
    alias = "docker_image"
10✔
860
    core_fields = (
10✔
861
        *COMMON_TARGET_FIELDS,
862
        DockerImageBuildArgsField,
863
        DockerImageDependenciesField,
864
        DockerImageSourceField,
865
        DockerImageInstructionsField,
866
        DockerImageContextRootField,
867
        DockerImageTagsField,
868
        DockerImageRegistriesField,
869
        DockerImageRepositoryField,
870
        DockerImageBuildImageLabelsOptionField,
871
        DockerImageBuildImageExtraHostsField,
872
        DockerImageBuildSecretsOptionField,
873
        DockerImageBuildSSHOptionField,
874
        DockerImageSkipPushField,
875
        DockerImageTargetStageField,
876
        DockerImageBuildPullOptionField,
877
        DockerImageBuildSquashOptionField,
878
        DockerImageBuildNetworkOptionField,
879
        DockerImageBuildPlatformOptionField,
880
        DockerImageBuildImageCacheToField,
881
        DockerImageBuildImageCacheFromField,
882
        DockerImageBuildImageOutputField,
883
        DockerImageRunExtraArgsField,
884
        OutputPathField,
885
        RestartableField,
886
    )
887
    help = help_text(
888
        """
889
        The `docker_image` target describes how to build and tag a Docker image.
890

891
        Any dependencies, as inferred or explicitly specified, will be included in the Docker
892
        build context, after being packaged if applicable.
893

894
        By default, it will use a Dockerfile from the same directory as the BUILD file this target
895
        is defined in. Point at another file with the `source` field, or use the `instructions`
896
        field to have the Dockerfile contents verbatim directly in the BUILD file.
897

898
        Dependencies on upstream/base images defined by another `docker_image` are inferred if
899
        referenced by a build argument with a default value of the target address.
900

901
        Example:
902

903
            # src/docker/downstream/Dockerfile
904
            ARG BASE=src/docker/upstream:image
905
            FROM $BASE
906
            ...
907

908
        """
909
    )
910

911

912
@union(in_scope_types=[EnvironmentName])
10✔
913
@dataclass(frozen=True)
10✔
914
class DockerImageTagsRequest:
10✔
915
    """A request to provide additional image tags."""
916

917
    target: Target
10✔
918

919
    @classmethod
10✔
920
    def is_applicable(cls, target: Target) -> bool:
10✔
921
        """Whether to provide additional tags for this target or not."""
922
        return True
1✔
923

924

925
class DockerImageTags(Collection[str]):
10✔
926
    """Additional image tags to apply to built Docker images."""
927

928

929
@rule(polymorphic=True)
10✔
930
async def get_docker_image_tags(
10✔
931
    req: DockerImageTagsRequest, env_name: EnvironmentName
932
) -> DockerImageTags:
933
    raise NotImplementedError()
×
934

935

936
class AllDockerImageTargets(Targets):
10✔
937
    pass
10✔
938

939

940
@rule
10✔
941
async def all_docker_targets(all_targets: AllTargets) -> AllDockerImageTargets:
10✔
942
    return AllDockerImageTargets(
6✔
943
        [tgt for tgt in all_targets if tgt.has_field(DockerImageSourceField)]
944
    )
945

946

947
def rules():
10✔
948
    return collect_rules()
6✔
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