diff --git a/cibuildwheel/linux.py b/cibuildwheel/linux.py index 6fd393dc..44bafc55 100644 --- a/cibuildwheel/linux.py +++ b/cibuildwheel/linux.py @@ -379,7 +379,9 @@ def build(options: Options, tmp_path: Path) -> None: # noqa: ARG001 try: # check the container engine is installed subprocess.run( - [options.globals.container_engine, "--version"], check=True, stdout=subprocess.DEVNULL + [options.globals.container_engine.name, "--version"], + check=True, + stdout=subprocess.DEVNULL, ) except subprocess.CalledProcessError: print( diff --git a/cibuildwheel/oci_container.py b/cibuildwheel/oci_container.py index b2f6fe4a..1cbabc0d 100644 --- a/cibuildwheel/oci_container.py +++ b/cibuildwheel/oci_container.py @@ -11,6 +11,7 @@ import sys import typing import uuid from collections.abc import Mapping, Sequence +from dataclasses import dataclass from pathlib import Path, PurePath, PurePosixPath from types import TracebackType from typing import IO, Dict @@ -19,7 +20,16 @@ from ._compat.typing import Literal from .typing import PathOrStr, PopenBytes from .util import CIProvider, detect_ci_provider -ContainerEngine = Literal["docker", "podman"] +ContainerEngineName = Literal["docker", "podman"] + + +@dataclass(frozen=True) +class OCIContainerEngineConfig: + name: ContainerEngineName + create_args: Sequence[str] = () + + +DEFAULT_ENGINE = OCIContainerEngineConfig("docker") class OCIContainer: @@ -57,7 +67,7 @@ class OCIContainer: image: str, simulate_32_bit: bool = False, cwd: PathOrStr | None = None, - engine: ContainerEngine = "docker", + engine: OCIContainerEngineConfig = DEFAULT_ENGINE, ): if not image: msg = "Must have a non-empty image to run." @@ -84,13 +94,14 @@ class OCIContainer: subprocess.run( [ - self.engine, + self.engine.name, "create", "--env=CIBUILDWHEEL", f"--name={self.name}", "--interactive", "--volume=/:/host", # ignored on CircleCI *network_args, + *self.engine.create_args, self.image, *shell_args, ], @@ -99,7 +110,7 @@ class OCIContainer: self.process = subprocess.Popen( [ - self.engine, + self.engine.name, "start", "--attach", "--interactive", @@ -137,7 +148,7 @@ class OCIContainer: self.bash_stdin.close() self.bash_stdout.close() - if self.engine == "podman": + if self.engine.name == "podman": # This works around what seems to be a race condition in the podman # backend. The full reason is not understood. See PR #966 for a # discussion on possible causes and attempts to remove this line. @@ -147,7 +158,7 @@ class OCIContainer: assert isinstance(self.name, str) subprocess.run( - [self.engine, "rm", "--force", "-v", self.name], + [self.engine.name, "rm", "--force", "-v", self.name], stdout=subprocess.DEVNULL, check=False, ) @@ -171,7 +182,7 @@ class OCIContainer: exec_process: subprocess.Popen[bytes] with subprocess.Popen( [ - self.engine, + self.engine.name, "exec", "-i", str(self.name), @@ -198,10 +209,10 @@ class OCIContainer: # note: we assume from_path is a dir to_path.mkdir(parents=True, exist_ok=True) - if self.engine == "podman": + if self.engine.name == "podman": subprocess.run( [ - self.engine, + self.engine.name, "cp", f"{self.name}:{from_path}/.", str(to_path), @@ -209,10 +220,10 @@ class OCIContainer: check=True, cwd=to_path, ) - elif self.engine == "docker": + elif self.engine.name == "docker": # There is a bug in docker that prevents a simple 'cp' invocation # from working https://github.com/moby/moby/issues/38995 - command = f"{self.engine} exec -i {self.name} tar -cC {shell_quote(from_path)} -f - . | tar -xf -" + command = f"{self.engine.name} exec -i {self.name} tar -cC {shell_quote(from_path)} -f - . | tar -xf -" subprocess.run( command, shell=True, @@ -220,7 +231,7 @@ class OCIContainer: cwd=to_path, ) else: - raise KeyError(self.engine) + raise KeyError(self.engine.name) def glob(self, path: PurePosixPath, pattern: str) -> list[PurePosixPath]: glob_pattern = path.joinpath(pattern) @@ -338,10 +349,10 @@ class OCIContainer: return self.call(command, env=environment, capture_output=True) def debug_info(self) -> str: - if self.engine == "podman": - command = f"{self.engine} info --debug" + if self.engine.name == "podman": + command = f"{self.engine.name} info --debug" else: - command = f"{self.engine} info" + command = f"{self.engine.name} info" completed = subprocess.run( command, shell=True, diff --git a/cibuildwheel/options.py b/cibuildwheel/options.py index 736f13a7..50cb91cd 100644 --- a/cibuildwheel/options.py +++ b/cibuildwheel/options.py @@ -22,7 +22,7 @@ from ._compat.typing import Literal, NotRequired, TypedDict from .architecture import Architecture from .environment import EnvironmentParseError, ParsedEnvironment, parse_environment from .logger import log -from .oci_container import ContainerEngine +from .oci_container import OCIContainerEngineConfig from .projectfiles import get_requires_python_str from .typing import PLATFORMS, PlatformName from .util import ( @@ -75,7 +75,7 @@ class GlobalOptions: build_selector: BuildSelector test_selector: TestSelector architectures: set[Architecture] - container_engine: ContainerEngine + container_engine: OCIContainerEngineConfig @dataclasses.dataclass(frozen=True) diff --git a/unit_test/oci_container_test.py b/unit_test/oci_container_test.py index 30739db7..e0fbcb50 100644 --- a/unit_test/oci_container_test.py +++ b/unit_test/oci_container_test.py @@ -12,7 +12,7 @@ import pytest import tomli_w from cibuildwheel.environment import EnvironmentAssignmentBash -from cibuildwheel.oci_container import OCIContainer +from cibuildwheel.oci_container import OCIContainer, OCIContainerEngineConfig # Test utilities @@ -21,7 +21,7 @@ from cibuildwheel.oci_container import OCIContainer pm = platform.machine() if pm == "x86_64": DEFAULT_IMAGE = "quay.io/pypa/manylinux2014_x86_64:2020-05-17-2f8ac3b" -elif pm == "aarch64": +elif pm in ["aarch64", "arm64"]: DEFAULT_IMAGE = "quay.io/pypa/manylinux2014_aarch64:2020-05-17-2f8ac3b" elif pm == "ppc64le": DEFAULT_IMAGE = "quay.io/pypa/manylinux2014_ppc64le:2020-05-17-2f8ac3b" @@ -37,7 +37,7 @@ def container_engine(request): pytest.skip("need --run-docker option to run") if request.param == "podman" and not request.config.getoption("--run-podman"): pytest.skip("need --run-podman option to run") - return request.param + return OCIContainerEngineConfig(name=request.param) # Tests @@ -296,3 +296,18 @@ def test_podman_vfs(tmp_path: Path, monkeypatch, request): # as UID 0. The reason why permission errors occur on podman is documented # in https://podman.io/blogs/2018/10/03/podman-remove-content-homedir.html subprocess.run(["podman", "unshare", "rm", "-rf", vfs_path], check=True) + + +def test_create_args(tmp_path: Path): + test_mount_dir = tmp_path / "test_mount" + test_mount_dir.mkdir() + (test_mount_dir / "test_file.txt").write_text("1234") + container_engine = OCIContainerEngineConfig( + name="docker", create_args=[f"--volume={test_mount_dir}:/test_mount"] + ) + + with OCIContainer( + engine=container_engine, + image=DEFAULT_IMAGE, + ) as container: + assert container.call(["cat", "/test_mount/test_file.txt"], capture_output=True) == "1234"