From 65a3ee3b03e009e01d9050a131fd20bd14acd350 Mon Sep 17 00:00:00 2001 From: Tim Werner Date: Tue, 14 Apr 2026 14:07:50 +0200 Subject: [PATCH 1/7] add extra_build_options --- .../backend/docker/goals/package_image.py | 44 ++++- .../docker/goals/package_image_test.py | 184 ++++++++++++++++++ .../docker/subsystems/docker_options.py | 22 +++ .../pants/backend/docker/target_types.py | 25 +++ 4 files changed, 273 insertions(+), 2 deletions(-) diff --git a/src/python/pants/backend/docker/goals/package_image.py b/src/python/pants/backend/docker/goals/package_image.py index 801351d4972..8c7d21efaf5 100644 --- a/src/python/pants/backend/docker/goals/package_image.py +++ b/src/python/pants/backend/docker/goals/package_image.py @@ -27,6 +27,7 @@ DockerBuildOptionFieldMultiValueMixin, DockerBuildOptionFieldValueMixin, DockerBuildOptionFlagFieldMixin, + DockerImageBuildExtraOptionsField, DockerImageBuildImageOutputField, DockerImageContextRootField, DockerImageRegistriesField, @@ -323,6 +324,20 @@ class DockerInfoV1ImageTag: name: str +def _extra_options_flag_names(extra_options: tuple[str, ...]) -> frozenset[str]: + """Return the set of flag names (e.g. ``--pull``) present in *extra_options*. + + Handles both ``--flag=value`` and ``--flag value`` styles so that any structured field whose + ``docker_build_option`` class variable matches one of these names will be suppressed in favour + of the caller-supplied value, avoiding duplicate or conflicting flags on the command line. + """ + names: set[str] = set() + for opt in extra_options: + if opt.startswith("-"): + names.add(opt.split("=")[0]) + return frozenset(names) + + def get_build_options( context: DockerBuildContext, field_set: DockerPackageFieldSet, @@ -331,7 +346,23 @@ def get_build_options( global_build_no_cache_option: bool | None, use_buildx_option: bool, target: Target, + global_extra_options: tuple[str, ...] = (), ) -> Iterator[str]: + target_extra = tuple(target[DockerImageBuildExtraOptionsField].value or ()) + target_flag_names = _extra_options_flag_names(target_extra) + + # Per-target wins: drop any global entry whose flag is already covered by the per-target list. + filtered_global = tuple( + opt + for opt in global_extra_options + if opt.startswith("-") and opt.split("=")[0] not in target_flag_names + ) + + extra_options: tuple[str, ...] = (*filtered_global, *target_extra) + + # Compute the set of flag names that are provided by global and target extra options. + overridden_flags = _extra_options_flag_names(extra_options) + # Build options from target fields inheriting from DockerBuildOptionFieldMixin for field_type in target.field_types: if issubclass(field_type, DockerBuildKitOptionField): @@ -356,6 +387,11 @@ def get_build_options( DockerBuildOptionFlagFieldMixin, ), ): + + flag = getattr(field_type, "docker_build_option", None) # get the flag name if it exists such as --pull or --network, etc. + if flag and flag in overridden_flags: + continue # skip this field since its flag is already covered by extra_options + source = InterpolationContext.TextSource( address=target.address, target_alias=target.alias, field_alias=field_type.alias ) @@ -387,10 +423,13 @@ def get_build_options( ) if target_stage: - yield from ("--target", target_stage) + extra_options.extend(("--target", target_stage)) if global_build_no_cache_option: - yield "--no-cache" + extra_options.extend("--no-cache") + + # Append extra options last so that they take precedence over the structured fields above. + yield from extra_options @dataclass(frozen=True) @@ -510,6 +549,7 @@ async def get_docker_image_build_process( global_build_no_cache_option=options.build_no_cache, use_buildx_option=options.use_buildx, target=wrapped_target.target, + global_extra_options=options.build_extra_options, ) ), ) diff --git a/src/python/pants/backend/docker/goals/package_image_test.py b/src/python/pants/backend/docker/goals/package_image_test.py index 9f769c39e7e..308badeae9e 100644 --- a/src/python/pants/backend/docker/goals/package_image_test.py +++ b/src/python/pants/backend/docker/goals/package_image_test.py @@ -151,6 +151,7 @@ def _setup_docker_options(rule_runner: RuleRunner, options: dict | None) -> Dock opts.setdefault("build_hosts", None) opts.setdefault("build_verbose", False) opts.setdefault("build_no_cache", False) + opts.setdefault("build_extra_options", []) opts.setdefault("use_buildx", False) opts.setdefault("env_vars", []) opts.setdefault("suggest_renames", True) @@ -2972,3 +2973,186 @@ def test_field_set_pushes_on_package(output: dict | None, expected: bool) -> Non rule_runner.get_target(Address("", target_name="image")) ) assert field_set.pushes_on_package() is expected + +def test_global_build_extra_options(rule_runner: RuleRunner) -> None: + """Global build_extra_options from [docker] should be passed to the docker build command.""" + rule_runner.set_options( + [], + env={ + "PANTS_DOCKER_BUILD_EXTRA_OPTIONS": '["--compress"]', + }, + ) + rule_runner.write_files( + { + "docker/test/BUILD": dedent( + """\ + docker_image( + name="img1", + ) + """ + ), + } + ) + + def check_build_process(result: DockerImageBuildProcess): + assert "--compress" in result.process.argv + + assert_build_process( + rule_runner, + Address("docker/test", target_name="img1"), + build_process_assertions=check_build_process, + ) + + +def test_extra_build_options_global_and_target_merged(rule_runner: RuleRunner) -> None: + """Global options come first; per-target options come last.""" + rule_runner.set_options( + [], + env={ + "PANTS_DOCKER_BUILD_EXTRA_OPTIONS": '["--compress"]', + }, + ) + rule_runner.write_files( + { + "docker/test/BUILD": dedent( + """\ + docker_image( + name="img1", + extra_build_options=["--pull=always"], + ) + """ + ), + } + ) + + def check_build_process(result: DockerImageBuildProcess): + argv = result.process.argv + assert "--compress" in argv + assert "--pull=always" in argv + + assert_build_process( + rule_runner, + Address("docker/test", target_name="img1"), + build_process_assertions=check_build_process, + ) + + +def test_extra_build_options_overrides_pull_field(rule_runner: RuleRunner) -> None: + """When extra_build_options contains --pull, the docker_image target pull argument is suppressed.""" + rule_runner.write_files( + { + "docker/test/BUILD": dedent( + """\ + docker_image( + name="img1", + pull=True, + extra_build_options=["--pull=always"], + ) + """ + ), + } + ) + + def check_build_process(result: DockerImageBuildProcess): + argv = result.process.argv + # Only the extra_build_options value should appear; the structured field's + # --pull=True must NOT be present to avoid duplicate flags. + assert "--pull=always" in argv + assert "--pull=True" not in argv + + assert_build_process( + rule_runner, + Address("docker/test", target_name="img1"), + build_process_assertions=check_build_process, + ) + +def test_global_extra_options_overridden_by_target_extra_options( + rule_runner: RuleRunner, +) -> None: + """When both global and target extra options specify the same flag, the target wins + (it comes last) and the global does not appear.""" + rule_runner.set_options( + [], + env={ + "PANTS_DOCKER_BUILD_EXTRA_OPTIONS": '["--pull=missing"]', + }, + ) + rule_runner.write_files( + { + "docker/test/BUILD": dedent( + """\ + docker_image( + name="img1", + extra_build_options=["--pull=always"], + ) + """ + ), + } + ) + + def check_build_process(result: DockerImageBuildProcess): + argv = result.process.argv + # Both global and target specify --pull; the per-target value wins and the global + # entry is dropped at the flag-level dedup stage. + assert "--pull=always" in argv + assert "--pull=missing" not in argv # global dropped because target specifies --pull + + assert_build_process( + rule_runner, + Address("docker/test", target_name="img1"), + build_process_assertions=check_build_process, + ) + + +def test_global_build_no_cache_options_exits( + rule_runner: RuleRunner, +) -> None: + rule_runner.write_files( + { + "docker/test/BUILD": dedent( + """\ + docker_image( + name="img1", + no_cache=True, + ) + """ + ), + } + ) + + def check_build_process(result: DockerImageBuildProcess): + argv = result.process.argv + assert "--no-cache" in argv + + assert_build_process( + rule_runner, + Address("docker/test", target_name="img1"), + build_process_assertions=check_build_process, + ) + +def test_global_build_no_cache_options_overridden_by_target_extra_options( + rule_runner: RuleRunner, +) -> None: + rule_runner.write_files( + { + "docker/test/BUILD": dedent( + """\ + docker_image( + name="img1", + no_cache=False, + extra_build_options=["--no_cache"], + ) + """ + ), + } + ) + + def check_build_process(result: DockerImageBuildProcess): + argv = result.process.argv + assert "--no-cache" in argv + + assert_build_process( + rule_runner, + Address("docker/test", target_name="img1"), + build_process_assertions=check_build_process, + ) \ No newline at end of file diff --git a/src/python/pants/backend/docker/subsystems/docker_options.py b/src/python/pants/backend/docker/subsystems/docker_options.py index 1787b3a0adf..7d1342bb967 100644 --- a/src/python/pants/backend/docker/subsystems/docker_options.py +++ b/src/python/pants/backend/docker/subsystems/docker_options.py @@ -207,6 +207,24 @@ def env_vars(self) -> tuple[str, ...]: default=False, help="Do not use the Docker cache when building images.", ) + _build_extra_options = ShellStrListOption( + help=softwrap( + f""" + Global extra options to pass to the `docker build` / `podman build` command. + + These are appended after all other computed flags. If any option specified here uses a + flag that is already covered by a dedicated field (e.g. `--pull`, `--network`, + `--platform`), the dedicated field's contribution is suppressed in favour of + the value given here, to avoid duplicate or conflicting flags. + + Example: + + [{options_scope}] + build_extra_options = ["--pull=always"] + + """ + ), + ) build_verbose = BoolOption( default=False, help="Whether to log the Docker output to the console. If false, only the image ID is logged.", @@ -302,6 +320,10 @@ def env_vars(self) -> tuple[str, ...]: def build_args(self) -> tuple[str, ...]: return tuple(sorted(set(self._build_args))) + @property + def build_extra_options(self) -> tuple[str, ...]: + return tuple(self._build_extra_options) + @property def tools(self) -> tuple[str, ...]: return tuple(sorted(set(self._tools))) diff --git a/src/python/pants/backend/docker/target_types.py b/src/python/pants/backend/docker/target_types.py index 47a24701821..7067699318d 100644 --- a/src/python/pants/backend/docker/target_types.py +++ b/src/python/pants/backend/docker/target_types.py @@ -596,6 +596,30 @@ class DockerImageRunExtraArgsField(StringSequenceField): ) +class DockerImageBuildExtraOptionsField(StringSequenceField): + alias = "extra_build_options" + default = () + help = help_text( + lambda: f""" + Additional options to pass directly to the `docker build` (or `podman build`) command. + + These are appended after all other computed flags. If any option specified here uses a + flag that is already covered by a dedicated field (e.g. `--pull`, `--network`, + `--platform`), the dedicated field's contribution is suppressed in favour of + the value given here, to avoid duplicate or conflicting flags on the command line. + + Example: + + docker_image( + extra_build_options=["--pull=always", "--compress"], + ) + + Use `[{DockerOptions.options_scope}].build_extra_options` to set default options for all + images. Per-target option overrides a conflicting global one. + """ + ) + + class DockerImageTarget(Target): alias = "docker_image" core_fields = ( @@ -622,6 +646,7 @@ class DockerImageTarget(Target): DockerImageBuildImageCacheFromField, DockerImageBuildImageOutputField, DockerImageRunExtraArgsField, + DockerImageBuildExtraOptionsField, OutputPathField, RestartableField, ) From 0ea03487a7f7cf0a2977375c9127189c24072d97 Mon Sep 17 00:00:00 2001 From: Tim Werner Date: Tue, 14 Apr 2026 14:37:58 +0200 Subject: [PATCH 2/7] fix: tuple extend --- src/python/pants/backend/docker/goals/package_image.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/python/pants/backend/docker/goals/package_image.py b/src/python/pants/backend/docker/goals/package_image.py index 8c7d21efaf5..a3d8f9771da 100644 --- a/src/python/pants/backend/docker/goals/package_image.py +++ b/src/python/pants/backend/docker/goals/package_image.py @@ -423,10 +423,10 @@ def get_build_options( ) if target_stage: - extra_options.extend(("--target", target_stage)) + extra_options = extra_options + ("--target", target_stage) if global_build_no_cache_option: - extra_options.extend("--no-cache") + extra_options = extra_options + ("--no-cache",) # Append extra options last so that they take precedence over the structured fields above. yield from extra_options From 4e61f842c7fadefe1779b966e6d653e4df5162c2 Mon Sep 17 00:00:00 2001 From: Tim Werner Date: Tue, 14 Apr 2026 14:48:15 +0200 Subject: [PATCH 3/7] fix: fix test for --no-cache --- .../pants/backend/docker/goals/package_image_test.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/python/pants/backend/docker/goals/package_image_test.py b/src/python/pants/backend/docker/goals/package_image_test.py index 308badeae9e..122c6910dbb 100644 --- a/src/python/pants/backend/docker/goals/package_image_test.py +++ b/src/python/pants/backend/docker/goals/package_image_test.py @@ -3107,13 +3107,18 @@ def check_build_process(result: DockerImageBuildProcess): def test_global_build_no_cache_options_exits( rule_runner: RuleRunner, ) -> None: + rule_runner.set_options( + [], + env={ + "PANTS_DOCKER_BUILD_EXTRA_OPTIONS": '["--no-cache"]', + }, + ) rule_runner.write_files( { "docker/test/BUILD": dedent( """\ docker_image( name="img1", - no_cache=True, ) """ ), @@ -3139,8 +3144,7 @@ def test_global_build_no_cache_options_overridden_by_target_extra_options( """\ docker_image( name="img1", - no_cache=False, - extra_build_options=["--no_cache"], + extra_build_options=["--no-cache"], ) """ ), From a9faa2eac5fd341c19739072b7122e929f60ad37 Mon Sep 17 00:00:00 2001 From: Tim Werner Date: Tue, 14 Apr 2026 15:30:25 +0200 Subject: [PATCH 4/7] fix linting --- src/python/pants/backend/docker/goals/package_image.py | 7 ++++--- .../pants/backend/docker/goals/package_image_test.py | 9 ++++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/python/pants/backend/docker/goals/package_image.py b/src/python/pants/backend/docker/goals/package_image.py index a3d8f9771da..d7306d34f58 100644 --- a/src/python/pants/backend/docker/goals/package_image.py +++ b/src/python/pants/backend/docker/goals/package_image.py @@ -387,10 +387,11 @@ def get_build_options( DockerBuildOptionFlagFieldMixin, ), ): - - flag = getattr(field_type, "docker_build_option", None) # get the flag name if it exists such as --pull or --network, etc. + flag = getattr( + field_type, "docker_build_option", None + ) # get the flag name if it exists such as --pull or --network, etc. if flag and flag in overridden_flags: - continue # skip this field since its flag is already covered by extra_options + continue # skip this field since its flag is already covered by extra_options source = InterpolationContext.TextSource( address=target.address, target_alias=target.alias, field_alias=field_type.alias diff --git a/src/python/pants/backend/docker/goals/package_image_test.py b/src/python/pants/backend/docker/goals/package_image_test.py index 122c6910dbb..7610d629fbe 100644 --- a/src/python/pants/backend/docker/goals/package_image_test.py +++ b/src/python/pants/backend/docker/goals/package_image_test.py @@ -2974,6 +2974,7 @@ def test_field_set_pushes_on_package(output: dict | None, expected: bool) -> Non ) assert field_set.pushes_on_package() is expected + def test_global_build_extra_options(rule_runner: RuleRunner) -> None: """Global build_extra_options from [docker] should be passed to the docker build command.""" rule_runner.set_options( @@ -3066,6 +3067,7 @@ def check_build_process(result: DockerImageBuildProcess): build_process_assertions=check_build_process, ) + def test_global_extra_options_overridden_by_target_extra_options( rule_runner: RuleRunner, ) -> None: @@ -3127,7 +3129,7 @@ def test_global_build_no_cache_options_exits( def check_build_process(result: DockerImageBuildProcess): argv = result.process.argv - assert "--no-cache" in argv + assert "--no-cache" in argv assert_build_process( rule_runner, @@ -3135,6 +3137,7 @@ def check_build_process(result: DockerImageBuildProcess): build_process_assertions=check_build_process, ) + def test_global_build_no_cache_options_overridden_by_target_extra_options( rule_runner: RuleRunner, ) -> None: @@ -3153,10 +3156,10 @@ def test_global_build_no_cache_options_overridden_by_target_extra_options( def check_build_process(result: DockerImageBuildProcess): argv = result.process.argv - assert "--no-cache" in argv + assert "--no-cache" in argv assert_build_process( rule_runner, Address("docker/test", target_name="img1"), build_process_assertions=check_build_process, - ) \ No newline at end of file + ) From 99f5e5dbad070d5454ab0a83f56e6e3f932ecce6 Mon Sep 17 00:00:00 2001 From: Tim Werner Date: Tue, 14 Apr 2026 15:41:08 +0200 Subject: [PATCH 5/7] add docs entry --- docs/notes/2.32.x.md | 6 ++++++ src/python/pants/backend/docker/goals/package_image.py | 5 ++--- src/python/pants/backend/docker/goals/package_image_test.py | 4 ++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/docs/notes/2.32.x.md b/docs/notes/2.32.x.md index cc83589c4f1..32f84316afb 100644 --- a/docs/notes/2.32.x.md +++ b/docs/notes/2.32.x.md @@ -81,6 +81,12 @@ The option `[docker].push_on_package` can be used to prevent Docker images from Running `pants publish` on a `docker_image` target with `skip_push=True` will no longer package the `docker_image`. +The option `[docker].build_extra_options` or/and the `build_extra_options` field of `docker_image` targets can now be used to pass extra options to docker build or podman build. Make sure that the options are understood by the tool. +For example, you can now set `build_extra_options=["--no-cache"]` to do not use existing cached images for the container build. + +Fields that are already present in the docker image target are surpressed by the extra options when they conflict. +For example, if you set `build_extra_options=["--pull"]`, the `pull` field of the docker image target will be ignored. + #### Helm When `pants publish` is invoked, Pants will now skip packaging for `helm_chart` targets if either `skip_push=True` or the target has no registries. diff --git a/src/python/pants/backend/docker/goals/package_image.py b/src/python/pants/backend/docker/goals/package_image.py index d7306d34f58..9e10e8a75aa 100644 --- a/src/python/pants/backend/docker/goals/package_image.py +++ b/src/python/pants/backend/docker/goals/package_image.py @@ -423,13 +423,12 @@ def get_build_options( ) ) - if target_stage: + if target_stage and "--target" not in overridden_flags: extra_options = extra_options + ("--target", target_stage) - if global_build_no_cache_option: + if global_build_no_cache_option and "--no-cache" not in overridden_flags: extra_options = extra_options + ("--no-cache",) - # Append extra options last so that they take precedence over the structured fields above. yield from extra_options diff --git a/src/python/pants/backend/docker/goals/package_image_test.py b/src/python/pants/backend/docker/goals/package_image_test.py index 7610d629fbe..e2024c90b3f 100644 --- a/src/python/pants/backend/docker/goals/package_image_test.py +++ b/src/python/pants/backend/docker/goals/package_image_test.py @@ -3006,7 +3006,7 @@ def check_build_process(result: DockerImageBuildProcess): def test_extra_build_options_global_and_target_merged(rule_runner: RuleRunner) -> None: - """Global options come first; per-target options come last.""" + """Global options and per-target options should be merged correctly.""" rule_runner.set_options( [], env={ @@ -3072,7 +3072,7 @@ def test_global_extra_options_overridden_by_target_extra_options( rule_runner: RuleRunner, ) -> None: """When both global and target extra options specify the same flag, the target wins - (it comes last) and the global does not appear.""" + and the global does not appear.""" rule_runner.set_options( [], env={ From a08674320d1ab0d9e3c1146bc509af9aeb4ef020 Mon Sep 17 00:00:00 2001 From: Tim Werner Date: Mon, 20 Apr 2026 10:11:00 +0200 Subject: [PATCH 6/7] clenaup code and simplify tests --- .../backend/docker/goals/package_image.py | 26 +-- .../docker/goals/package_image_test.py | 169 ++++-------------- 2 files changed, 44 insertions(+), 151 deletions(-) diff --git a/src/python/pants/backend/docker/goals/package_image.py b/src/python/pants/backend/docker/goals/package_image.py index 9e10e8a75aa..efcb01e4e1f 100644 --- a/src/python/pants/backend/docker/goals/package_image.py +++ b/src/python/pants/backend/docker/goals/package_image.py @@ -325,12 +325,7 @@ class DockerInfoV1ImageTag: def _extra_options_flag_names(extra_options: tuple[str, ...]) -> frozenset[str]: - """Return the set of flag names (e.g. ``--pull``) present in *extra_options*. - - Handles both ``--flag=value`` and ``--flag value`` styles so that any structured field whose - ``docker_build_option`` class variable matches one of these names will be suppressed in favour - of the caller-supplied value, avoiding duplicate or conflicting flags on the command line. - """ + """Returns a set of flag names (e.g. --pull, -network, etc)""" names: set[str] = set() for opt in extra_options: if opt.startswith("-"): @@ -338,6 +333,17 @@ def _extra_options_flag_names(extra_options: tuple[str, ...]) -> frozenset[str]: return frozenset(names) +def _filter_global_extra_options( + global_extra_options: tuple[str, ...], target_flag_names: frozenset[str] +) -> tuple[str, ...]: + """Remove any global extra options that are included in the per-target options.""" + return tuple( + opt + for opt in global_extra_options + if opt.startswith("-") and opt.split("=")[0] not in target_flag_names + ) + + def get_build_options( context: DockerBuildContext, field_set: DockerPackageFieldSet, @@ -352,11 +358,7 @@ def get_build_options( target_flag_names = _extra_options_flag_names(target_extra) # Per-target wins: drop any global entry whose flag is already covered by the per-target list. - filtered_global = tuple( - opt - for opt in global_extra_options - if opt.startswith("-") and opt.split("=")[0] not in target_flag_names - ) + filtered_global = _filter_global_extra_options(global_extra_options, target_flag_names) extra_options: tuple[str, ...] = (*filtered_global, *target_extra) @@ -391,7 +393,7 @@ def get_build_options( field_type, "docker_build_option", None ) # get the flag name if it exists such as --pull or --network, etc. if flag and flag in overridden_flags: - continue # skip this field since its flag is already covered by extra_options + continue source = InterpolationContext.TextSource( address=target.address, target_alias=target.alias, field_alias=field_type.alias diff --git a/src/python/pants/backend/docker/goals/package_image_test.py b/src/python/pants/backend/docker/goals/package_image_test.py index e2024c90b3f..e021699733b 100644 --- a/src/python/pants/backend/docker/goals/package_image_test.py +++ b/src/python/pants/backend/docker/goals/package_image_test.py @@ -2975,20 +2975,36 @@ def test_field_set_pushes_on_package(output: dict | None, expected: bool) -> Non assert field_set.pushes_on_package() is expected -def test_global_build_extra_options(rule_runner: RuleRunner) -> None: - """Global build_extra_options from [docker] should be passed to the docker build command.""" - rule_runner.set_options( - [], - env={ - "PANTS_DOCKER_BUILD_EXTRA_OPTIONS": '["--compress"]', - }, - ) +@pytest.mark.parametrize( + ("global_options", "target_options", "expected"), + [ + (["--compress"], None, ["--compress"]), + (["--compress"], ["--pull=always"], ["--compress", "--pull=always"]), + # The `--no-cache` option is set in a different place in the code and hence requires separate tests. + (["--no-cache"], None, ["--no-cache"]), + (None, ["--no-cache"], ["--no-cache"]), + ], +) +def test_global_build_extra_options_merge( + rule_runner: RuleRunner, + global_options: list | None, + target_options: list | None, + expected: list, +) -> None: + if global_options: + rule_runner.set_options( + [], + env={ + "PANTS_DOCKER_BUILD_EXTRA_OPTIONS": f"{global_options}", + }, + ) rule_runner.write_files( { "docker/test/BUILD": dedent( - """\ + f"""\ docker_image( name="img1", + extra_build_options={target_options}, ) """ ), @@ -2996,7 +3012,8 @@ def test_global_build_extra_options(rule_runner: RuleRunner) -> None: ) def check_build_process(result: DockerImageBuildProcess): - assert "--compress" in result.process.argv + for expected_option in expected: + assert expected_option in result.process.argv assert_build_process( rule_runner, @@ -3005,41 +3022,13 @@ def check_build_process(result: DockerImageBuildProcess): ) -def test_extra_build_options_global_and_target_merged(rule_runner: RuleRunner) -> None: - """Global options and per-target options should be merged correctly.""" +def test_extra_build_options_overrides_pull_field_and_global_field(rule_runner: RuleRunner) -> None: rule_runner.set_options( [], env={ - "PANTS_DOCKER_BUILD_EXTRA_OPTIONS": '["--compress"]', + "PANTS_DOCKER_BUILD_EXTRA_OPTIONS": '["--pull=missing"]', }, ) - rule_runner.write_files( - { - "docker/test/BUILD": dedent( - """\ - docker_image( - name="img1", - extra_build_options=["--pull=always"], - ) - """ - ), - } - ) - - def check_build_process(result: DockerImageBuildProcess): - argv = result.process.argv - assert "--compress" in argv - assert "--pull=always" in argv - - assert_build_process( - rule_runner, - Address("docker/test", target_name="img1"), - build_process_assertions=check_build_process, - ) - - -def test_extra_build_options_overrides_pull_field(rule_runner: RuleRunner) -> None: - """When extra_build_options contains --pull, the docker_image target pull argument is suppressed.""" rule_runner.write_files( { "docker/test/BUILD": dedent( @@ -3056,107 +3045,9 @@ def test_extra_build_options_overrides_pull_field(rule_runner: RuleRunner) -> No def check_build_process(result: DockerImageBuildProcess): argv = result.process.argv - # Only the extra_build_options value should appear; the structured field's - # --pull=True must NOT be present to avoid duplicate flags. assert "--pull=always" in argv assert "--pull=True" not in argv - - assert_build_process( - rule_runner, - Address("docker/test", target_name="img1"), - build_process_assertions=check_build_process, - ) - - -def test_global_extra_options_overridden_by_target_extra_options( - rule_runner: RuleRunner, -) -> None: - """When both global and target extra options specify the same flag, the target wins - and the global does not appear.""" - rule_runner.set_options( - [], - env={ - "PANTS_DOCKER_BUILD_EXTRA_OPTIONS": '["--pull=missing"]', - }, - ) - rule_runner.write_files( - { - "docker/test/BUILD": dedent( - """\ - docker_image( - name="img1", - extra_build_options=["--pull=always"], - ) - """ - ), - } - ) - - def check_build_process(result: DockerImageBuildProcess): - argv = result.process.argv - # Both global and target specify --pull; the per-target value wins and the global - # entry is dropped at the flag-level dedup stage. - assert "--pull=always" in argv - assert "--pull=missing" not in argv # global dropped because target specifies --pull - - assert_build_process( - rule_runner, - Address("docker/test", target_name="img1"), - build_process_assertions=check_build_process, - ) - - -def test_global_build_no_cache_options_exits( - rule_runner: RuleRunner, -) -> None: - rule_runner.set_options( - [], - env={ - "PANTS_DOCKER_BUILD_EXTRA_OPTIONS": '["--no-cache"]', - }, - ) - rule_runner.write_files( - { - "docker/test/BUILD": dedent( - """\ - docker_image( - name="img1", - ) - """ - ), - } - ) - - def check_build_process(result: DockerImageBuildProcess): - argv = result.process.argv - assert "--no-cache" in argv - - assert_build_process( - rule_runner, - Address("docker/test", target_name="img1"), - build_process_assertions=check_build_process, - ) - - -def test_global_build_no_cache_options_overridden_by_target_extra_options( - rule_runner: RuleRunner, -) -> None: - rule_runner.write_files( - { - "docker/test/BUILD": dedent( - """\ - docker_image( - name="img1", - extra_build_options=["--no-cache"], - ) - """ - ), - } - ) - - def check_build_process(result: DockerImageBuildProcess): - argv = result.process.argv - assert "--no-cache" in argv + assert "--pull=missing" not in argv assert_build_process( rule_runner, From 0b7b298b1137557b2a3df1d49d99a3893b88f7e8 Mon Sep 17 00:00:00 2001 From: Tim Werner Date: Wed, 22 Apr 2026 09:34:39 +0200 Subject: [PATCH 7/7] move to next release note --- docs/notes/2.32.x.md | 6 ------ docs/notes/2.33.x.md | 8 ++++++++ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/notes/2.32.x.md b/docs/notes/2.32.x.md index 638577612af..07d0ec956df 100644 --- a/docs/notes/2.32.x.md +++ b/docs/notes/2.32.x.md @@ -86,12 +86,6 @@ The option `[docker].push_on_package` can be used to prevent Docker images from Running `pants publish` on a `docker_image` target with `skip_push=True` will no longer package the `docker_image`. -The option `[docker].build_extra_options` or/and the `build_extra_options` field of `docker_image` targets can now be used to pass extra options to docker build or podman build. Make sure that the options are understood by the tool. -For example, you can now set `build_extra_options=["--no-cache"]` to do not use existing cached images for the container build. - -Fields that are already present in the docker image target are surpressed by the extra options when they conflict. -For example, if you set `build_extra_options=["--pull"]`, the `pull` field of the docker image target will be ignored. - #### Helm When `pants publish` is invoked, Pants will now skip packaging for `helm_chart` targets if either `skip_push=True` or the target has no registries. diff --git a/docs/notes/2.33.x.md b/docs/notes/2.33.x.md index 8b9cb9195d3..866c58b5882 100644 --- a/docs/notes/2.33.x.md +++ b/docs/notes/2.33.x.md @@ -20,6 +20,14 @@ Thank you to [Klaviyo](https://www.klaviyo.com/) for their Platinum tier support ### Backends +#### Docker + +The option `[docker].build_extra_options` or/and the `build_extra_options` field of `docker_image` targets can now be used to pass extra options to docker build or podman build. Make sure that the options are understood by the tool. +For example, you can now set `build_extra_options=["--no-cache"]` to do not use existing cached images for the container build. + +Fields that are already present in the docker image target are surpressed by the extra options when they conflict. +For example, if you set `build_extra_options=["--pull"]`, the `pull` field of the docker image target will be ignored. + #### Helm #### JVM