Skip to content

Commit 291921a

Browse files
committed
Make container-engine a build (non-global) option
Lets users specify different container-engine settings for different builds. Especially useful with the create_args setting, such as 1771.
1 parent b3c4d37 commit 291921a

File tree

5 files changed

+94
-56
lines changed

5 files changed

+94
-56
lines changed

cibuildwheel/linux.py

Lines changed: 38 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
from ._compat.typing import assert_never
1212
from .architecture import Architecture
1313
from .logger import log
14-
from .oci_container import OCIContainer
15-
from .options import Options
14+
from .oci_container import OCIContainer, OCIContainerEngineConfig
15+
from .options import BuildOptions, Options
1616
from .typing import PathOrStr
1717
from .util import (
1818
AlreadyBuiltWheelError,
@@ -44,6 +44,7 @@ def path(self) -> PurePosixPath:
4444
class BuildStep:
4545
platform_configs: list[PythonConfiguration]
4646
platform_tag: str
47+
container_engine: OCIContainerEngineConfig
4748
container_image: str
4849

4950

@@ -65,8 +66,9 @@ def get_python_configurations(
6566
]
6667

6768

68-
def container_image_for_python_configuration(config: PythonConfiguration, options: Options) -> str:
69-
build_options = options.build_options(config.identifier)
69+
def container_image_for_python_configuration(
70+
config: PythonConfiguration, build_options: BuildOptions
71+
) -> str:
7072
# e.g
7173
# identifier is 'cp310-manylinux_x86_64'
7274
# platform_tag is 'manylinux_x86_64'
@@ -91,22 +93,26 @@ def get_build_steps(
9193
Groups PythonConfigurations into BuildSteps. Each BuildStep represents a
9294
separate container instance.
9395
"""
94-
steps = OrderedDict[Tuple[str, str, str], BuildStep]()
96+
steps = OrderedDict[Tuple[str, str, str, OCIContainerEngineConfig], BuildStep]()
9597

9698
for config in python_configurations:
9799
_, platform_tag = config.identifier.split("-", 1)
98100

99-
before_all = options.build_options(config.identifier).before_all
100-
container_image = container_image_for_python_configuration(config, options)
101+
build_options = options.build_options(config.identifier)
102+
103+
before_all = build_options.before_all
104+
container_image = container_image_for_python_configuration(config, build_options)
105+
container_engine = build_options.container_engine
101106

102-
step_key = (platform_tag, container_image, before_all)
107+
step_key = (platform_tag, container_image, before_all, container_engine)
103108

104109
if step_key in steps:
105110
steps[step_key].platform_configs.append(config)
106111
else:
107112
steps[step_key] = BuildStep(
108113
platform_configs=[config],
109114
platform_tag=platform_tag,
115+
container_engine=container_engine,
110116
container_image=container_image,
111117
)
112118

@@ -388,29 +394,6 @@ def build_in_container(
388394

389395

390396
def build(options: Options, tmp_path: Path) -> None: # noqa: ARG001
391-
try:
392-
# check the container engine is installed
393-
subprocess.run(
394-
[options.globals.container_engine.name, "--version"],
395-
check=True,
396-
stdout=subprocess.DEVNULL,
397-
)
398-
except subprocess.CalledProcessError:
399-
print(
400-
unwrap(
401-
f"""
402-
cibuildwheel: {options.globals.container_engine} not found. An
403-
OCI exe like Docker or Podman is required to run Linux builds.
404-
If you're building on Travis CI, add `services: [docker]` to
405-
your .travis.yml. If you're building on Circle CI in Linux,
406-
add a `setup_remote_docker` step to your .circleci/config.yml.
407-
If you're building on Cirrus CI, use `docker_builder` task.
408-
"""
409-
),
410-
file=sys.stderr,
411-
)
412-
sys.exit(2)
413-
414397
python_configurations = get_python_configurations(
415398
options.globals.build_selector, options.globals.architectures
416399
)
@@ -425,6 +408,29 @@ def build(options: Options, tmp_path: Path) -> None: # noqa: ARG001
425408
container_package_dir = container_project_path / abs_package_dir.relative_to(cwd)
426409

427410
for build_step in get_build_steps(options, python_configurations):
411+
try:
412+
# check the container engine is installed
413+
subprocess.run(
414+
[build_step.container_engine.name, "--version"],
415+
check=True,
416+
stdout=subprocess.DEVNULL,
417+
)
418+
except subprocess.CalledProcessError:
419+
print(
420+
unwrap(
421+
f"""
422+
cibuildwheel: {build_step.container_engine.name} not found. An
423+
OCI exe like Docker or Podman is required to run Linux builds.
424+
If you're building on Travis CI, add `services: [docker]` to
425+
your .travis.yml. If you're building on Circle CI in Linux,
426+
add a `setup_remote_docker` step to your .circleci/config.yml.
427+
If you're building on Cirrus CI, use `docker_builder` task.
428+
"""
429+
),
430+
file=sys.stderr,
431+
)
432+
sys.exit(2)
433+
428434
try:
429435
ids_to_build = [x.identifier for x in build_step.platform_configs]
430436
log.step(f"Starting container image {build_step.container_image}...")
@@ -435,7 +441,7 @@ def build(options: Options, tmp_path: Path) -> None: # noqa: ARG001
435441
image=build_step.container_image,
436442
enforce_32_bit=build_step.platform_tag.endswith("i686"),
437443
cwd=container_project_path,
438-
engine=options.globals.container_engine,
444+
engine=build_step.container_engine,
439445
) as container:
440446
build_in_container(
441447
options=options,

cibuildwheel/oci_container.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import typing
1212
import uuid
1313
from collections.abc import Mapping, Sequence
14-
from dataclasses import dataclass
14+
from dataclasses import dataclass, field
1515
from pathlib import Path, PurePath, PurePosixPath
1616
from types import TracebackType
1717
from typing import IO, Dict, Literal
@@ -32,7 +32,7 @@
3232
@dataclass(frozen=True)
3333
class OCIContainerEngineConfig:
3434
name: ContainerEngineName
35-
create_args: Sequence[str] = ()
35+
create_args: list[str] = field(default_factory=list)
3636
disable_host_mount: bool = False
3737

3838
@staticmethod
@@ -71,6 +71,18 @@ def options_summary(self) -> str | dict[str, str]:
7171
"disable_host_mount": str(self.disable_host_mount),
7272
}
7373

74+
def __eq__(self, value: object) -> bool:
75+
if not isinstance(value, OCIContainerEngineConfig):
76+
return False
77+
return (
78+
self.name == value.name
79+
and self.create_args == value.create_args
80+
and self.disable_host_mount == value.disable_host_mount
81+
)
82+
83+
def __hash__(self) -> int:
84+
return hash((self.name, tuple(self.create_args), self.disable_host_mount))
85+
7486

7587
DEFAULT_ENGINE = OCIContainerEngineConfig("docker")
7688

cibuildwheel/options.py

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,6 @@ class GlobalOptions:
7575
build_selector: BuildSelector
7676
test_selector: TestSelector
7777
architectures: set[Architecture]
78-
container_engine: OCIContainerEngineConfig
7978

8079

8180
@dataclasses.dataclass(frozen=True)
@@ -95,6 +94,7 @@ class BuildOptions:
9594
build_verbosity: int
9695
build_frontend: BuildFrontendConfig | None
9796
config_settings: str
97+
container_engine: OCIContainerEngineConfig
9898

9999
@property
100100
def package_dir(self) -> Path:
@@ -544,25 +544,12 @@ def globals(self) -> GlobalOptions:
544544
)
545545
test_selector = TestSelector(skip_config=test_skip)
546546

547-
container_engine_str = self.reader.get(
548-
"container-engine",
549-
table_format={"item": "{k}:{v}", "sep": "; ", "quote": shlex.quote},
550-
)
551-
552-
try:
553-
container_engine = OCIContainerEngineConfig.from_config_string(container_engine_str)
554-
except ValueError as e:
555-
msg = f"cibuildwheel: Failed to parse container config. {e}"
556-
print(msg, file=sys.stderr)
557-
sys.exit(2)
558-
559547
return GlobalOptions(
560548
package_dir=package_dir,
561549
output_dir=output_dir,
562550
build_selector=build_selector,
563551
test_selector=test_selector,
564552
architectures=architectures,
565-
container_engine=container_engine,
566553
)
567554

568555
def build_options(self, identifier: str | None) -> BuildOptions:
@@ -642,6 +629,8 @@ def build_options(self, identifier: str | None) -> BuildOptions:
642629

643630
manylinux_images: dict[str, str] = {}
644631
musllinux_images: dict[str, str] = {}
632+
container_engine: OCIContainerEngineConfig | None = None
633+
645634
if self.platform == "linux":
646635
all_pinned_container_images = _get_pinned_container_images()
647636

@@ -676,6 +665,18 @@ def build_options(self, identifier: str | None) -> BuildOptions:
676665

677666
musllinux_images[build_platform] = image
678667

668+
container_engine_str = self.reader.get(
669+
"container-engine",
670+
table_format={"item": "{k}:{v}", "sep": "; ", "quote": shlex.quote},
671+
)
672+
673+
try:
674+
container_engine = OCIContainerEngineConfig.from_config_string(container_engine_str)
675+
except ValueError as e:
676+
msg = f"cibuildwheel: Failed to parse container config. {e}"
677+
print(msg, file=sys.stderr)
678+
sys.exit(2)
679+
679680
return BuildOptions(
680681
globals=self.globals,
681682
test_command=test_command,
@@ -692,6 +693,7 @@ def build_options(self, identifier: str | None) -> BuildOptions:
692693
musllinux_images=musllinux_images or None,
693694
build_frontend=build_frontend,
694695
config_settings=config_settings,
696+
container_engine=container_engine,
695697
)
696698

697699
def check_for_invalid_configuration(self, identifiers: Iterable[str]) -> None:

unit_test/linux_build_steps_test.py

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@
55
from pprint import pprint
66

77
import cibuildwheel.linux
8-
import cibuildwheel.oci_container
8+
from cibuildwheel.oci_container import OCIContainerEngineConfig
99
from cibuildwheel.options import CommandLineArguments, Options
1010

1111

1212
def test_linux_container_split(tmp_path: Path, monkeypatch):
1313
"""
14-
Tests splitting linux builds by container image and before_all
14+
Tests splitting linux builds by container image, container engine, and before_all
1515
"""
1616

1717
args = CommandLineArguments.defaults()
@@ -28,13 +28,17 @@ def test_linux_container_split(tmp_path: Path, monkeypatch):
2828
archs = "x86_64 i686"
2929
3030
[[tool.cibuildwheel.overrides]]
31-
select = "cp{38,39,310}-*"
31+
select = "cp{37,38,39,310}-*"
3232
manylinux-x86_64-image = "other_container_image"
3333
manylinux-i686-image = "other_container_image"
3434
3535
[[tool.cibuildwheel.overrides]]
3636
select = "cp39-*"
3737
before-all = "echo 'a cp39-only command'"
38+
39+
[[tool.cibuildwheel.overrides]]
40+
select = "cp310-*"
41+
container-engine = "docker; create_args: --privileged"
3842
"""
3943
)
4044
)
@@ -55,21 +59,35 @@ def identifiers(step):
5559
def before_alls(step):
5660
return [options.build_options(c.identifier).before_all for c in step.platform_configs]
5761

62+
def container_engines(step):
63+
return [options.build_options(c.identifier).container_engine for c in step.platform_configs]
64+
5865
pprint(build_steps)
5966

67+
default_container_engine = OCIContainerEngineConfig(name="docker")
68+
6069
assert build_steps[0].container_image == "normal_container_image"
6170
assert identifiers(build_steps[0]) == [
6271
"cp36-manylinux_x86_64",
63-
"cp37-manylinux_x86_64",
6472
"cp311-manylinux_x86_64",
6573
"cp312-manylinux_x86_64",
6674
]
67-
assert before_alls(build_steps[0]) == ["", "", "", ""]
75+
assert before_alls(build_steps[0]) == [""] * 3
76+
assert container_engines(build_steps[0]) == [default_container_engine] * 3
6877

6978
assert build_steps[1].container_image == "other_container_image"
70-
assert identifiers(build_steps[1]) == ["cp38-manylinux_x86_64", "cp310-manylinux_x86_64"]
71-
assert before_alls(build_steps[1]) == ["", ""]
79+
assert identifiers(build_steps[1]) == ["cp37-manylinux_x86_64", "cp38-manylinux_x86_64"]
80+
assert before_alls(build_steps[1]) == [""] * 2
81+
assert container_engines(build_steps[1]) == [default_container_engine] * 2
7282

7383
assert build_steps[2].container_image == "other_container_image"
7484
assert identifiers(build_steps[2]) == ["cp39-manylinux_x86_64"]
7585
assert before_alls(build_steps[2]) == ["echo 'a cp39-only command'"]
86+
assert container_engines(build_steps[2]) == [default_container_engine]
87+
88+
assert build_steps[3].container_image == "other_container_image"
89+
assert identifiers(build_steps[3]) == ["cp310-manylinux_x86_64"]
90+
assert before_alls(build_steps[3]) == [""]
91+
assert container_engines(build_steps[3]) == [
92+
OCIContainerEngineConfig(name="docker", create_args=["--privileged"])
93+
]

unit_test/options_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ def test_container_engine_option(
269269
)
270270

271271
options = Options(platform="linux", command_line_arguments=args, env={})
272-
parsed_container_engine = options.globals.container_engine
272+
parsed_container_engine = options.build_options(None).container_engine
273273

274274
assert parsed_container_engine.name == result_name
275275
assert parsed_container_engine.create_args == result_create_args

0 commit comments

Comments
 (0)