fix: enforce minimum version of docker/podman (#1961)

* fix: enforce minimum version of docker/podman

This allows to always pass `--platform` to the OCI engine
thus fixing issues with multiarch images.

* Allow older versions with warnings

* Upgrade docker on Travis CI

* fix: use `docker cp` instead of `tar`

* Enforce docker>=24.0

* move log to include container.copy_into

* fix: travis-ci, only update docker on aarch64

* skip test_multiarch_image on s390x / ppc64le

* skip flaky test

* chore: only install test deps on Travis CI

* fix: do not try to pull images tagged `cibw_local`

* use "--pull=never" for local images

* Use docker image inspect to check if an image needs to be pulled
This commit is contained in:
Matthieu Darbois
2024-09-12 15:32:43 -04:00
committed by GitHub
parent fd11286290
commit 0787a44d99
15 changed files with 299 additions and 119 deletions
+2
View File
@@ -4,6 +4,8 @@ set -o xtrace
if [ "$(uname -s)" == "Darwin" ]; then
sudo softwareupdate --install-rosetta --agree-to-license
else
docker run --rm --privileged docker.io/tonistiigi/binfmt:latest --install all
fi
$PYTHON --version
+2
View File
@@ -17,6 +17,7 @@ linux_x86_task:
memory: 8G
install_pre_requirements_script:
- docker run --rm --privileged docker.io/tonistiigi/binfmt:latest --install all
- apt install -y python3-venv python-is-python3
<<: *RUN_TESTS
@@ -30,6 +31,7 @@ linux_aarch64_task:
memory: 4G
install_pre_requirements_script:
- docker run --rm --privileged docker.io/tonistiigi/binfmt:latest --install all
- apt install -y python3-venv python-is-python3
<<: *RUN_TESTS
+5 -3
View File
@@ -59,6 +59,11 @@ jobs:
docker system prune -a -f
df -h
# for oci_container unit tests
- name: Set up QEMU
if: runner.os == 'Linux'
uses: docker/setup-qemu-action@v3
- name: Install dependencies
run: |
uv pip install --system ".[test]"
@@ -157,10 +162,7 @@ jobs:
run: python -m pip install ".[test,uv]"
- name: Set up QEMU
id: qemu
uses: docker/setup-qemu-action@v3
with:
platforms: all
- name: Run the emulation tests
run: pytest --run-emulation ${{ matrix.arch }} test/test_emulation.py
+1
View File
@@ -15,6 +15,7 @@ linux:
PYTEST_ADDOPTS: -k "unit_test or test_0_basic" --suppress-no-test-exit-code
script:
- curl -sSL https://get.docker.com/ | sh
- docker run --rm --privileged docker.io/tonistiigi/binfmt:latest --install all
- python -m pip install -e ".[dev]" pytest-custom-exit-code
- python ./bin/run_tests.py
+10 -1
View File
@@ -20,6 +20,14 @@ jobs:
group: edge
virt: vm
env: PYTHON=python
# docker is outdated in the arm64-graviton2 vm focal image (19.x)
# we need to upgrade to get >= 24.0
addons:
apt:
sources:
- sourceline: 'deb https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable'
packages:
- docker-ce docker-ce-cli containerd.io
- name: Linux | ppc64le | Python 3.9
python: 3.9
@@ -48,8 +56,9 @@ jobs:
env: PYTHON=python
install:
- if [ "${TRAVIS_OS_NAME}" == "linux" ]; then docker run --rm --privileged docker.io/tonistiigi/binfmt:latest --install all; fi
- $PYTHON -m pip install -U pip
- $PYTHON -m pip install -e ".[dev]" pytest-custom-exit-code
- $PYTHON -m pip install -e ".[test]" pytest-custom-exit-code
script: |
# travis_wait disable the output while waiting
+3
View File
@@ -17,6 +17,9 @@ init:
if (-not ($BRANCH -eq 'main' -or $BRANCH.ToLower().StartsWith('appveyor-'))) {
$env:PYTEST_ADDOPTS = '-k "unit_test or test_0_basic" --suppress-no-test-exit-code'
}
if ($IsLinux) {
docker run --rm --privileged docker.io/tonistiigi/binfmt:latest --install all
}
install:
- python -m pip install -U pip
+1
View File
@@ -13,6 +13,7 @@ jobs:
inputs:
versionSpec: '3.8'
- bash: |
docker run --rm --privileged docker.io/tonistiigi/binfmt:latest --install all
python -m pip install -e ".[dev]"
python ./bin/run_tests.py
+1 -1
View File
@@ -28,7 +28,7 @@ if __name__ == "__main__":
# unit tests
unit_test_args = [sys.executable, "-m", "pytest", "unit_test"]
if sys.platform.startswith("linux"):
if sys.platform.startswith("linux") and os.environ.get("CIBW_PLATFORM", "linux") == "linux":
# run the docker unit tests only on Linux
unit_test_args += ["--run-docker"]
+3 -1
View File
@@ -64,7 +64,9 @@ class Architecture(Enum):
if arch_str == "auto":
result |= Architecture.auto_archs(platform=platform)
elif arch_str == "native":
result.add(Architecture(platform_module.machine()))
native_arch = Architecture.native_arch(platform=platform)
if native_arch:
result.add(native_arch)
elif arch_str == "all":
result |= Architecture.all_archs(platform=platform)
elif arch_str == "auto64":
+4
View File
@@ -58,3 +58,7 @@ class AlreadyBuiltWheelError(FatalError):
)
super().__init__(message)
self.return_code = 6
class OCIEngineTooOldError(FatalError):
return_code = 7
+13 -4
View File
@@ -14,7 +14,7 @@ from . import errors
from ._compat.typing import assert_never
from .architecture import Architecture
from .logger import log
from .oci_container import OCIContainer, OCIContainerEngineConfig
from .oci_container import OCIContainer, OCIContainerEngineConfig, OCIPlatform
from .options import BuildOptions, Options
from .typing import PathOrStr
from .util import (
@@ -29,6 +29,14 @@ from .util import (
unwrap,
)
ARCHITECTURE_OCI_PLATFORM_MAP = {
Architecture.x86_64: OCIPlatform.AMD64,
Architecture.i686: OCIPlatform.i386,
Architecture.aarch64: OCIPlatform.ARM64,
Architecture.ppc64le: OCIPlatform.PPC64LE,
Architecture.s390x: OCIPlatform.S390X,
}
@dataclass(frozen=True)
class PythonConfiguration:
@@ -196,6 +204,8 @@ def build_in_container(
dependency_constraint_flags: list[PathOrStr] = []
log.step("Setting up build environment...")
if build_options.dependency_constraints:
constraints_file = build_options.dependency_constraints.get_for_python_version(
config.version
@@ -205,8 +215,6 @@ def build_in_container(
container.copy_into(constraints_file, container_constraints_file)
dependency_constraint_flags = ["-c", container_constraints_file]
log.step("Setting up build environment...")
env = container.get_environment()
env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1"
env["PIP_ROOT_USER_ACTION"] = "ignore"
@@ -446,10 +454,11 @@ def build(options: Options, tmp_path: Path) -> None: # noqa: ARG001
log.step(f"Starting container image {build_step.container_image}...")
print(f"info: This container will host the build for {', '.join(ids_to_build)}...")
architecture = Architecture(build_step.platform_tag.split("_", 1)[1])
with OCIContainer(
image=build_step.container_image,
enforce_32_bit=build_step.platform_tag.endswith("i686"),
oci_platform=ARCHITECTURE_OCI_PLATFORM_MAP[architecture],
cwd=container_project_path,
engine=build_step.container_engine,
) as container:
+100 -68
View File
@@ -5,18 +5,22 @@ import json
import os
import platform
import shlex
import shutil
import subprocess
import sys
import typing
import uuid
from collections.abc import Mapping, Sequence
from dataclasses import dataclass, field
from enum import Enum
from pathlib import Path, PurePath, PurePosixPath
from types import TracebackType
from typing import IO, Dict, Literal
from ._compat.typing import Self
from packaging.version import InvalidVersion, Version
from ._compat.typing import Self, assert_never
from .errors import OCIEngineTooOldError
from .logger import log
from .typing import PathOrStr, PopenBytes
from .util import (
CIProvider,
@@ -29,6 +33,15 @@ from .util import (
ContainerEngineName = Literal["docker", "podman"]
# Order of the enum matters for tests. 386 shall appear before amd64.
class OCIPlatform(Enum):
i386 = "linux/386"
AMD64 = "linux/amd64"
ARM64 = "linux/arm64"
PPC64LE = "linux/ppc64le"
S390X = "linux/s390x"
@dataclass(frozen=True)
class OCIContainerEngineConfig:
name: ContainerEngineName
@@ -56,6 +69,15 @@ class OCIContainerEngineConfig:
disable_host_mount = (
strtobool(disable_host_mount_options[-1]) if disable_host_mount_options else False
)
if "--platform" in create_args or any(arg.startswith("--platform=") for arg in create_args):
msg = "Using '--platform' in 'container-engine::create_args' is deprecated. It will be ignored."
log.warning(msg)
if "--platform" in create_args:
index = create_args.index("--platform")
create_args.pop(index)
create_args.pop(index)
else:
create_args = [arg for arg in create_args if not arg.startswith("--platform=")]
return OCIContainerEngineConfig(
name=name, create_args=tuple(create_args), disable_host_mount=disable_host_mount
@@ -75,6 +97,32 @@ class OCIContainerEngineConfig:
DEFAULT_ENGINE = OCIContainerEngineConfig("docker")
def _check_engine_version(engine: OCIContainerEngineConfig) -> None:
try:
version_string = call(engine.name, "version", "-f", "{{json .}}", capture_stdout=True)
version_info = json.loads(version_string.strip())
if engine.name == "docker":
# --platform support was introduced in 1.32 as experimental
# docker cp, as used by cibuildwheel, has been fixed in v24 => API 1.43, https://github.com/moby/moby/issues/38995
client_api_version = Version(version_info["Client"]["ApiVersion"])
engine_api_version = Version(version_info["Server"]["ApiVersion"])
version_supported = min(client_api_version, engine_api_version) >= Version("1.43")
elif engine.name == "podman":
client_api_version = Version(version_info["Client"]["APIVersion"])
if "Server" in version_info:
engine_api_version = Version(version_info["Server"]["APIVersion"])
else:
engine_api_version = client_api_version
# --platform support was introduced in v3
version_supported = min(client_api_version, engine_api_version) >= Version("3")
else:
assert_never(engine.name)
if not version_supported:
raise OCIEngineTooOldError() from None
except (subprocess.CalledProcessError, KeyError, InvalidVersion) as e:
raise OCIEngineTooOldError() from e
class OCIContainer:
"""
An object that represents a running OCI (e.g. Docker) container.
@@ -108,7 +156,7 @@ class OCIContainer:
self,
*,
image: str,
enforce_32_bit: bool = False,
oci_platform: OCIPlatform,
cwd: PathOrStr | None = None,
engine: OCIContainerEngineConfig = DEFAULT_ENGINE,
):
@@ -117,14 +165,41 @@ class OCIContainer:
raise ValueError(msg)
self.image = image
self.enforce_32_bit = enforce_32_bit
self.oci_platform = oci_platform
self.cwd = cwd
self.name: str | None = None
self.engine = engine
def _get_platform_args(self, *, oci_platform: OCIPlatform | None = None) -> tuple[str, str]:
if oci_platform is None:
oci_platform = self.oci_platform
# we need '--pull=always' otherwise some images with the wrong platform get re-used (e.g. 386 image for amd64)
# c.f. https://github.com/moby/moby/issues/48197#issuecomment-2282802313
pull = "always"
try:
image_platform = call(
self.engine.name,
"image",
"inspect",
self.image,
"--format",
"{{.Os}}/{{.Architecture}}",
capture_stdout=True,
).strip()
if image_platform == oci_platform.value:
# in case the correct image is already present, don't pull
# this allows to run local only images
pull = "never"
except subprocess.CalledProcessError:
pass
return f"--platform={oci_platform.value}", f"--pull={pull}"
def __enter__(self) -> Self:
self.name = f"cibuildwheel-{uuid.uuid4()}"
_check_engine_version(self.engine)
# work-around for Travis-CI PPC64le Docker runs since 2021:
# this avoids network splits
# https://github.com/pypa/cibuildwheel/issues/904
@@ -133,14 +208,26 @@ class OCIContainer:
if detect_ci_provider() == CIProvider.travis_ci and platform.machine() == "ppc64le":
network_args = ["--network=host"]
platform_args = self._get_platform_args()
simulate_32_bit = False
if self.enforce_32_bit:
if self.oci_platform == OCIPlatform.i386:
# If the architecture running the image is already the right one
# or the image entrypoint takes care of enforcing this, then we don't need to
# simulate this
container_machine = call(
self.engine.name, "run", "--rm", self.image, "uname", "-m", capture_stdout=True
).strip()
run_cmd = [self.engine.name, "run", "--rm"]
ctr_cmd = ["uname", "-m"]
try:
container_machine = call(
*run_cmd, *platform_args, self.image, *ctr_cmd, capture_stdout=True
).strip()
except subprocess.CalledProcessError:
# The image might have been built with amd64 architecture
# Let's try that
platform_args = self._get_platform_args(oci_platform=OCIPlatform.AMD64)
container_machine = call(
*run_cmd, *platform_args, self.image, *ctr_cmd, capture_stdout=True
).strip()
simulate_32_bit = container_machine != "i686"
shell_args = ["linux32", "/bin/bash"] if simulate_32_bit else ["/bin/bash"]
@@ -155,6 +242,7 @@ class OCIContainer:
"--interactive",
*(["--volume=/:/host"] if not self.engine.disable_host_mount else []),
*network_args,
*platform_args,
*self.engine.create_args,
self.image,
*shell_args,
@@ -221,73 +309,17 @@ class OCIContainer:
self.name = None
def copy_into(self, from_path: Path, to_path: PurePath) -> None:
# `docker cp` causes 'no space left on device' error when
# a container is running and the host filesystem is
# mounted. https://github.com/moby/moby/issues/38995
# Use `docker exec` instead.
if from_path.is_dir():
self.call(["mkdir", "-p", to_path])
subprocess.run(
f"tar cf - . | {self.engine.name} exec -i {self.name} tar --no-same-owner -xC {shell_quote(to_path)} -f -",
shell=True,
check=True,
cwd=from_path,
)
call(self.engine.name, "cp", f"{from_path}/.", f"{self.name}:{to_path}")
else:
exec_process: subprocess.Popen[bytes]
with subprocess.Popen(
[
self.engine.name,
"exec",
"-i",
str(self.name),
"sh",
"-c",
f"cat > {shell_quote(to_path)}",
],
stdin=subprocess.PIPE,
) as exec_process:
assert exec_process.stdin
with open(from_path, "rb") as from_file:
# Bug in mypy, https://github.com/python/mypy/issues/15031
shutil.copyfileobj(from_file, exec_process.stdin) # type: ignore[misc]
exec_process.stdin.close()
exec_process.wait()
if exec_process.returncode:
raise subprocess.CalledProcessError(
exec_process.returncode, exec_process.args, None, None
)
self.call(["mkdir", "-p", to_path.parent])
call(self.engine.name, "cp", from_path, f"{self.name}:{to_path}")
def copy_out(self, from_path: PurePath, to_path: Path) -> None:
# note: we assume from_path is a dir
to_path.mkdir(parents=True, exist_ok=True)
if self.engine.name == "podman":
subprocess.run(
[
self.engine.name,
"cp",
f"{self.name}:{from_path}/.",
str(to_path),
],
check=True,
cwd=to_path,
)
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.name} exec -i {self.name} tar -cC {shell_quote(from_path)} -f - . | tar -xf -"
subprocess.run(
command,
shell=True,
check=True,
cwd=to_path,
)
else:
raise KeyError(self.engine.name)
call(self.engine.name, "cp", f"{self.name}:{from_path}/.", to_path)
def glob(self, path: PurePosixPath, pattern: str) -> list[PurePosixPath]:
glob_pattern = path.joinpath(pattern)
+2 -4
View File
@@ -21,7 +21,7 @@ def test_podman(tmp_path, capfd, request):
actual_wheels = utils.cibuildwheel_run(
project_dir,
add_env={
"CIBW_ARCHS": "x86_64",
"CIBW_ARCHS": "native",
"CIBW_BEFORE_ALL": "echo 'test log statement from before-all'",
"CIBW_CONTAINER_ENGINE": "podman",
},
@@ -29,9 +29,7 @@ def test_podman(tmp_path, capfd, request):
)
# check that the expected wheels are produced
expected_wheels = [
w for w in utils.expected_wheels("spam", "0.1.0", single_python=True) if "x86_64" in w
]
expected_wheels = utils.expected_wheels("spam", "0.1.0", single_python=True, single_arch=True)
assert set(actual_wheels) == set(expected_wheels)
# check that stdout is bring passed-though from container correctly
+140 -26
View File
@@ -6,6 +6,7 @@ import platform
import random
import shutil
import subprocess
import sys
import textwrap
from pathlib import Path, PurePath, PurePosixPath
@@ -13,7 +14,7 @@ import pytest
import tomli_w
from cibuildwheel.environment import EnvironmentAssignmentBash
from cibuildwheel.oci_container import OCIContainer, OCIContainerEngineConfig
from cibuildwheel.oci_container import OCIContainer, OCIContainerEngineConfig, OCIPlatform
from cibuildwheel.util import CIProvider, detect_ci_provider
# Test utilities
@@ -28,6 +29,14 @@ elif pm in {"aarch64", "arm64"}:
DEFAULT_IMAGE = DEFAULT_IMAGE_TEMPLATE.format(machine="aarch64")
else:
DEFAULT_IMAGE = ""
DEFAULT_OCI_PLATFORM = {
"AMD64": OCIPlatform.AMD64,
"x86_64": OCIPlatform.AMD64,
"ppc64le": OCIPlatform.PPC64LE,
"s390x": OCIPlatform.S390X,
"aarch64": OCIPlatform.ARM64,
"arm64": OCIPlatform.ARM64,
}[pm]
PODMAN = OCIContainerEngineConfig(name="podman")
@@ -63,24 +72,32 @@ def container_engine(request):
def test_simple(container_engine):
with OCIContainer(engine=container_engine, image=DEFAULT_IMAGE) as container:
with OCIContainer(
engine=container_engine, image=DEFAULT_IMAGE, oci_platform=DEFAULT_OCI_PLATFORM
) as container:
assert container.call(["echo", "hello"], capture_output=True) == "hello\n"
def test_no_lf(container_engine):
with OCIContainer(engine=container_engine, image=DEFAULT_IMAGE) as container:
with OCIContainer(
engine=container_engine, image=DEFAULT_IMAGE, oci_platform=DEFAULT_OCI_PLATFORM
) as container:
assert container.call(["printf", "hello"], capture_output=True) == "hello"
def test_debug_info(container_engine):
container = OCIContainer(engine=container_engine, image=DEFAULT_IMAGE)
container = OCIContainer(
engine=container_engine, image=DEFAULT_IMAGE, oci_platform=DEFAULT_OCI_PLATFORM
)
print(container.debug_info())
with container:
pass
def test_environment(container_engine):
with OCIContainer(engine=container_engine, image=DEFAULT_IMAGE) as container:
with OCIContainer(
engine=container_engine, image=DEFAULT_IMAGE, oci_platform=DEFAULT_OCI_PLATFORM
) as container:
assert (
container.call(
["sh", "-c", "echo $TEST_VAR"], env={"TEST_VAR": "1"}, capture_output=True
@@ -92,7 +109,9 @@ def test_environment(container_engine):
def test_environment_pass(container_engine, monkeypatch):
monkeypatch.setenv("CIBUILDWHEEL", "1")
monkeypatch.setenv("SOURCE_DATE_EPOCH", "1489957071")
with OCIContainer(engine=container_engine, image=DEFAULT_IMAGE) as container:
with OCIContainer(
engine=container_engine, image=DEFAULT_IMAGE, oci_platform=DEFAULT_OCI_PLATFORM
) as container:
assert container.call(["sh", "-c", "echo $CIBUILDWHEEL"], capture_output=True) == "1\n"
assert (
container.call(["sh", "-c", "echo $SOURCE_DATE_EPOCH"], capture_output=True)
@@ -102,14 +121,23 @@ def test_environment_pass(container_engine, monkeypatch):
def test_cwd(container_engine):
with OCIContainer(
engine=container_engine, image=DEFAULT_IMAGE, cwd="/cibuildwheel/working_directory"
engine=container_engine,
image=DEFAULT_IMAGE,
oci_platform=DEFAULT_OCI_PLATFORM,
cwd="/cibuildwheel/working_directory",
) as container:
assert container.call(["pwd"], capture_output=True) == "/cibuildwheel/working_directory\n"
assert container.call(["pwd"], capture_output=True, cwd="/opt") == "/opt\n"
@pytest.mark.skipif(
pm == "s390x" and detect_ci_provider() == CIProvider.travis_ci,
reason="test is flaky on this platform, see https://github.com/pypa/cibuildwheel/pull/1961#issuecomment-2334678966",
)
def test_container_removed(container_engine):
with OCIContainer(engine=container_engine, image=DEFAULT_IMAGE) as container:
with OCIContainer(
engine=container_engine, image=DEFAULT_IMAGE, oci_platform=DEFAULT_OCI_PLATFORM
) as container:
docker_containers_listing = subprocess.run(
f"{container.engine.name} container ls",
shell=True,
@@ -141,7 +169,9 @@ def test_large_environment(container_engine):
"d": "0" * long_env_var_length,
}
with OCIContainer(engine=container_engine, image=DEFAULT_IMAGE) as container:
with OCIContainer(
engine=container_engine, image=DEFAULT_IMAGE, oci_platform=DEFAULT_OCI_PLATFORM
) as container:
# check the length of d
assert (
container.call(["sh", "-c", "echo ${#d}"], env=large_environment, capture_output=True)
@@ -150,7 +180,9 @@ def test_large_environment(container_engine):
def test_binary_output(container_engine):
with OCIContainer(engine=container_engine, image=DEFAULT_IMAGE) as container:
with OCIContainer(
engine=container_engine, image=DEFAULT_IMAGE, oci_platform=DEFAULT_OCI_PLATFORM
) as container:
# note: the below embedded snippets are in python2
# check that we can pass though arbitrary binary data without erroring
@@ -200,7 +232,9 @@ def test_binary_output(container_engine):
def test_file_operation(tmp_path: Path, container_engine):
with OCIContainer(engine=container_engine, image=DEFAULT_IMAGE) as container:
with OCIContainer(
engine=container_engine, image=DEFAULT_IMAGE, oci_platform=DEFAULT_OCI_PLATFORM
) as container:
# test copying a file in
test_binary_data = bytes(random.randrange(256) for _ in range(1000))
original_test_file = tmp_path / "test.dat"
@@ -215,7 +249,9 @@ def test_file_operation(tmp_path: Path, container_engine):
def test_dir_operations(tmp_path: Path, container_engine):
with OCIContainer(engine=container_engine, image=DEFAULT_IMAGE) as container:
with OCIContainer(
engine=container_engine, image=DEFAULT_IMAGE, oci_platform=DEFAULT_OCI_PLATFORM
) as container:
test_binary_data = bytes(random.randrange(256) for _ in range(1000))
original_test_file = tmp_path / "test.dat"
original_test_file.write_bytes(test_binary_data)
@@ -244,7 +280,9 @@ def test_dir_operations(tmp_path: Path, container_engine):
def test_environment_executor(container_engine):
with OCIContainer(engine=container_engine, image=DEFAULT_IMAGE) as container:
with OCIContainer(
engine=container_engine, image=DEFAULT_IMAGE, oci_platform=DEFAULT_OCI_PLATFORM
) as container:
assignment = EnvironmentAssignmentBash("TEST=$(echo 42)")
assert assignment.evaluated_value({}, container.environment_executor) == "42"
@@ -252,6 +290,8 @@ def test_environment_executor(container_engine):
def test_podman_vfs(tmp_path: Path, monkeypatch, container_engine):
if container_engine.name != "podman":
pytest.skip("only runs with podman")
if sys.platform.startswith("darwin"):
pytest.skip("Skipping test because podman on this platform does not support vfs")
# create the VFS configuration
vfs_path = tmp_path / "podman_vfs"
@@ -309,7 +349,9 @@ def test_podman_vfs(tmp_path: Path, monkeypatch, container_engine):
monkeypatch.setenv("CONTAINERS_CONF", str(vfs_containers_conf_fpath))
monkeypatch.setenv("CONTAINERS_STORAGE_CONF", str(vfs_containers_storage_conf_fpath))
with OCIContainer(engine=PODMAN, image=DEFAULT_IMAGE) as container:
with OCIContainer(
engine=PODMAN, image=DEFAULT_IMAGE, oci_platform=DEFAULT_OCI_PLATFORM
) as container:
# test running a command
assert container.call(["echo", "hello"], capture_output=True) == "hello\n"
@@ -346,6 +388,7 @@ def test_create_args_volume(tmp_path: Path, container_engine):
with OCIContainer(
engine=container_engine,
image=DEFAULT_IMAGE,
oci_platform=DEFAULT_OCI_PLATFORM,
) as container:
assert container.call(["cat", "/test_mount/test_file.txt"], capture_output=True) == "1234"
@@ -388,24 +431,47 @@ def test_create_args_volume(tmp_path: Path, container_engine):
"docker",
("--some-option=value; with; semicolons", "--another-option"),
),
(
"docker; create_args: --platform=linux/amd64",
"docker",
(),
),
(
"podman; create_args: --platform=linux/amd64",
"podman",
(),
),
(
"docker; create_args: --platform linux/amd64",
"docker",
(),
),
(
"podman; create_args: --platform linux/amd64",
"podman",
(),
),
],
)
def test_parse_engine_config(config, name, create_args):
def test_parse_engine_config(config, name, create_args, capsys):
engine_config = OCIContainerEngineConfig.from_config_string(config)
assert engine_config.name == name
assert engine_config.create_args == create_args
if "--platform" in config:
captured = capsys.readouterr()
assert (
"Using '--platform' in 'container-engine::create_args' is deprecated. It will be ignored."
in captured.err
)
@pytest.mark.skipif(pm != "x86_64", reason="Only runs on x86_64")
@pytest.mark.parametrize(
("image", "shell_args"),
[
(DEFAULT_IMAGE_TEMPLATE.format(machine="i686"), ["/bin/bash"]),
(DEFAULT_IMAGE_TEMPLATE.format(machine="x86_64"), ["linux32", "/bin/bash"]),
],
)
def test_enforce_32_bit(container_engine, image, shell_args):
with OCIContainer(engine=container_engine, image=image, enforce_32_bit=True) as container:
def test_enforce_32_bit(container_engine):
with OCIContainer(
engine=container_engine,
image=DEFAULT_IMAGE_TEMPLATE.format(machine="i686"),
oci_platform=OCIPlatform.i386,
) as container:
assert container.call(["uname", "-m"], capture_output=True).strip() == "i686"
container_args = subprocess.run(
f"{container.engine.name} inspect -f '{{{{json .Args }}}}' {container.name}",
@@ -414,7 +480,7 @@ def test_enforce_32_bit(container_engine, image, shell_args):
stdout=subprocess.PIPE,
text=True,
).stdout
assert json.loads(container_args) == shell_args
assert json.loads(container_args) == ["/bin/bash"]
@pytest.mark.parametrize(
@@ -428,16 +494,64 @@ def test_enforce_32_bit(container_engine, image, shell_args):
def test_disable_host_mount(tmp_path: Path, container_engine, config, should_have_host_mount):
if detect_ci_provider() in {CIProvider.circle_ci, CIProvider.gitlab}:
pytest.skip("Skipping test because docker on this platform does not support host mounts")
if sys.platform.startswith("darwin"):
pytest.skip("Skipping test because docker on this platform does not support host mounts")
engine = OCIContainerEngineConfig.from_config_string(config.format(name=container_engine.name))
sentinel_file = tmp_path / "sentinel"
sentinel_file.write_text("12345")
with OCIContainer(engine=engine, image=DEFAULT_IMAGE) as container:
with OCIContainer(
engine=engine, image=DEFAULT_IMAGE, oci_platform=DEFAULT_OCI_PLATFORM
) as container:
host_mount_path = "/host" + str(sentinel_file)
if should_have_host_mount:
assert container.call(["cat", host_mount_path], capture_output=True) == "12345"
else:
with pytest.raises(subprocess.CalledProcessError):
container.call(["cat", host_mount_path], capture_output=True)
def test_local_image(container_engine):
local_image = f"cibw_test_{container_engine.name}_local:latest"
subprocess.run(
[container_engine.name, "pull", f"--platform={DEFAULT_OCI_PLATFORM.value}", DEFAULT_IMAGE],
check=True,
)
subprocess.run([container_engine.name, "image", "tag", DEFAULT_IMAGE, local_image], check=True)
with OCIContainer(
engine=container_engine, image=local_image, oci_platform=DEFAULT_OCI_PLATFORM
):
pass
@pytest.mark.parametrize("platform", list(OCIPlatform))
def test_multiarch_image(container_engine, platform):
if (
detect_ci_provider() in {CIProvider.travis_ci}
and pm in {"s390x", "ppc64le"}
and platform != DEFAULT_OCI_PLATFORM
):
pytest.skip("Skipping test because docker on this platform does not support QEMU")
with OCIContainer(
engine=container_engine, image="debian:12-slim", oci_platform=platform
) as container:
output = container.call(["uname", "-m"], capture_output=True)
output_map = {
OCIPlatform.i386: "i686",
OCIPlatform.AMD64: "x86_64",
OCIPlatform.ARM64: "aarch64",
OCIPlatform.PPC64LE: "ppc64le",
OCIPlatform.S390X: "s390x",
}
assert output_map[platform] == output.strip()
output = container.call(["dpkg", "--print-architecture"], capture_output=True)
output_map = {
OCIPlatform.i386: "i386",
OCIPlatform.AMD64: "amd64",
OCIPlatform.ARM64: "arm64",
OCIPlatform.PPC64LE: "ppc64el",
OCIPlatform.S390X: "s390x",
}
assert output_map[platform] == output.strip()
+12 -11
View File
@@ -12,6 +12,7 @@ import pytest
from cibuildwheel import linux, util
from cibuildwheel.__main__ import main
from cibuildwheel.oci_container import OCIPlatform
ALL_IDS = {
"cp36",
@@ -73,7 +74,7 @@ def test_build_default_launches(monkeypatch):
kwargs = build_in_container.call_args_list[0][1]
assert "quay.io/pypa/manylinux2014_x86_64" in kwargs["container"]["image"]
assert kwargs["container"]["cwd"] == PurePosixPath("/project")
assert not kwargs["container"]["enforce_32_bit"]
assert kwargs["container"]["oci_platform"] == OCIPlatform.AMD64
identifiers = {x.identifier for x in kwargs["platform_configs"]}
assert identifiers == {f"{x}-manylinux_x86_64" for x in ALL_IDS}
@@ -81,7 +82,7 @@ def test_build_default_launches(monkeypatch):
kwargs = build_in_container.call_args_list[1][1]
assert "quay.io/pypa/manylinux2014_i686" in kwargs["container"]["image"]
assert kwargs["container"]["cwd"] == PurePosixPath("/project")
assert kwargs["container"]["enforce_32_bit"]
assert kwargs["container"]["oci_platform"] == OCIPlatform.i386
identifiers = {x.identifier for x in kwargs["platform_configs"]}
assert identifiers == {f"{x}-manylinux_i686" for x in ALL_IDS}
@@ -89,7 +90,7 @@ def test_build_default_launches(monkeypatch):
kwargs = build_in_container.call_args_list[2][1]
assert "quay.io/pypa/musllinux_1_2_x86_64" in kwargs["container"]["image"]
assert kwargs["container"]["cwd"] == PurePosixPath("/project")
assert not kwargs["container"]["enforce_32_bit"]
assert kwargs["container"]["oci_platform"] == OCIPlatform.AMD64
identifiers = {x.identifier for x in kwargs["platform_configs"]}
assert identifiers == {
@@ -99,7 +100,7 @@ def test_build_default_launches(monkeypatch):
kwargs = build_in_container.call_args_list[3][1]
assert "quay.io/pypa/musllinux_1_2_i686" in kwargs["container"]["image"]
assert kwargs["container"]["cwd"] == PurePosixPath("/project")
assert kwargs["container"]["enforce_32_bit"]
assert kwargs["container"]["oci_platform"] == OCIPlatform.i386
identifiers = {x.identifier for x in kwargs["platform_configs"]}
assert identifiers == {f"{x}-musllinux_i686" for x in ALL_IDS if "pp" not in x}
@@ -142,7 +143,7 @@ before-all = "true"
kwargs = build_in_container.call_args_list[0][1]
assert "quay.io/pypa/manylinux2014_x86_64" in kwargs["container"]["image"]
assert kwargs["container"]["cwd"] == PurePosixPath("/project")
assert not kwargs["container"]["enforce_32_bit"]
assert kwargs["container"]["oci_platform"] == OCIPlatform.AMD64
identifiers = {x.identifier for x in kwargs["platform_configs"]}
assert identifiers == {"cp36-manylinux_x86_64"}
@@ -151,7 +152,7 @@ before-all = "true"
kwargs = build_in_container.call_args_list[1][1]
assert "quay.io/pypa/manylinux2014_x86_64" in kwargs["container"]["image"]
assert kwargs["container"]["cwd"] == PurePosixPath("/project")
assert not kwargs["container"]["enforce_32_bit"]
assert kwargs["container"]["oci_platform"] == OCIPlatform.AMD64
identifiers = {x.identifier for x in kwargs["platform_configs"]}
assert identifiers == {
@@ -164,7 +165,7 @@ before-all = "true"
kwargs = build_in_container.call_args_list[2][1]
assert "quay.io/pypa/manylinux_2_28_x86_64" in kwargs["container"]["image"]
assert kwargs["container"]["cwd"] == PurePosixPath("/project")
assert not kwargs["container"]["enforce_32_bit"]
assert kwargs["container"]["oci_platform"] == OCIPlatform.AMD64
identifiers = {x.identifier for x in kwargs["platform_configs"]}
assert identifiers == {
f"{x}-manylinux_x86_64"
@@ -174,7 +175,7 @@ before-all = "true"
kwargs = build_in_container.call_args_list[3][1]
assert "quay.io/pypa/manylinux2014_i686" in kwargs["container"]["image"]
assert kwargs["container"]["cwd"] == PurePosixPath("/project")
assert kwargs["container"]["enforce_32_bit"]
assert kwargs["container"]["oci_platform"] == OCIPlatform.i386
identifiers = {x.identifier for x in kwargs["platform_configs"]}
assert identifiers == {f"{x}-manylinux_i686" for x in ALL_IDS}
@@ -182,7 +183,7 @@ before-all = "true"
kwargs = build_in_container.call_args_list[4][1]
assert "quay.io/pypa/musllinux_1_1_x86_64" in kwargs["container"]["image"]
assert kwargs["container"]["cwd"] == PurePosixPath("/project")
assert not kwargs["container"]["enforce_32_bit"]
assert kwargs["container"]["oci_platform"] == OCIPlatform.AMD64
identifiers = {x.identifier for x in kwargs["platform_configs"]}
assert identifiers == {
@@ -192,7 +193,7 @@ before-all = "true"
kwargs = build_in_container.call_args_list[5][1]
assert "quay.io/pypa/musllinux_1_2_x86_64" in kwargs["container"]["image"]
assert kwargs["container"]["cwd"] == PurePosixPath("/project")
assert not kwargs["container"]["enforce_32_bit"]
assert kwargs["container"]["oci_platform"] == OCIPlatform.AMD64
identifiers = {x.identifier for x in kwargs["platform_configs"]}
assert identifiers == {
f"{x}-musllinux_x86_64" for x in ALL_IDS - {"cp36", "cp37", "cp38", "cp39"} if "pp" not in x
@@ -201,7 +202,7 @@ before-all = "true"
kwargs = build_in_container.call_args_list[6][1]
assert "quay.io/pypa/musllinux_1_2_i686" in kwargs["container"]["image"]
assert kwargs["container"]["cwd"] == PurePosixPath("/project")
assert kwargs["container"]["enforce_32_bit"]
assert kwargs["container"]["oci_platform"] == OCIPlatform.i386
identifiers = {x.identifier for x in kwargs["platform_configs"]}
assert identifiers == {f"{x}-musllinux_i686" for x in ALL_IDS if "pp" not in x}