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 if [ "$(uname -s)" == "Darwin" ]; then
sudo softwareupdate --install-rosetta --agree-to-license sudo softwareupdate --install-rosetta --agree-to-license
else
docker run --rm --privileged docker.io/tonistiigi/binfmt:latest --install all
fi fi
$PYTHON --version $PYTHON --version
+2
View File
@@ -17,6 +17,7 @@ linux_x86_task:
memory: 8G memory: 8G
install_pre_requirements_script: install_pre_requirements_script:
- docker run --rm --privileged docker.io/tonistiigi/binfmt:latest --install all
- apt install -y python3-venv python-is-python3 - apt install -y python3-venv python-is-python3
<<: *RUN_TESTS <<: *RUN_TESTS
@@ -30,6 +31,7 @@ linux_aarch64_task:
memory: 4G memory: 4G
install_pre_requirements_script: install_pre_requirements_script:
- docker run --rm --privileged docker.io/tonistiigi/binfmt:latest --install all
- apt install -y python3-venv python-is-python3 - apt install -y python3-venv python-is-python3
<<: *RUN_TESTS <<: *RUN_TESTS
+5 -3
View File
@@ -59,6 +59,11 @@ jobs:
docker system prune -a -f docker system prune -a -f
df -h df -h
# for oci_container unit tests
- name: Set up QEMU
if: runner.os == 'Linux'
uses: docker/setup-qemu-action@v3
- name: Install dependencies - name: Install dependencies
run: | run: |
uv pip install --system ".[test]" uv pip install --system ".[test]"
@@ -157,10 +162,7 @@ jobs:
run: python -m pip install ".[test,uv]" run: python -m pip install ".[test,uv]"
- name: Set up QEMU - name: Set up QEMU
id: qemu
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v3
with:
platforms: all
- name: Run the emulation tests - name: Run the emulation tests
run: pytest --run-emulation ${{ matrix.arch }} test/test_emulation.py 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 PYTEST_ADDOPTS: -k "unit_test or test_0_basic" --suppress-no-test-exit-code
script: script:
- curl -sSL https://get.docker.com/ | sh - 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 -m pip install -e ".[dev]" pytest-custom-exit-code
- python ./bin/run_tests.py - python ./bin/run_tests.py
+10 -1
View File
@@ -20,6 +20,14 @@ jobs:
group: edge group: edge
virt: vm virt: vm
env: PYTHON=python 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 - name: Linux | ppc64le | Python 3.9
python: 3.9 python: 3.9
@@ -48,8 +56,9 @@ jobs:
env: PYTHON=python env: PYTHON=python
install: 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 -U pip
- $PYTHON -m pip install -e ".[dev]" pytest-custom-exit-code - $PYTHON -m pip install -e ".[test]" pytest-custom-exit-code
script: | script: |
# travis_wait disable the output while waiting # 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-'))) { 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' $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: install:
- python -m pip install -U pip - python -m pip install -U pip
+1
View File
@@ -13,6 +13,7 @@ jobs:
inputs: inputs:
versionSpec: '3.8' versionSpec: '3.8'
- bash: | - bash: |
docker run --rm --privileged docker.io/tonistiigi/binfmt:latest --install all
python -m pip install -e ".[dev]" python -m pip install -e ".[dev]"
python ./bin/run_tests.py python ./bin/run_tests.py
+1 -1
View File
@@ -28,7 +28,7 @@ if __name__ == "__main__":
# unit tests # unit tests
unit_test_args = [sys.executable, "-m", "pytest", "unit_test"] 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 # run the docker unit tests only on Linux
unit_test_args += ["--run-docker"] unit_test_args += ["--run-docker"]
+3 -1
View File
@@ -64,7 +64,9 @@ class Architecture(Enum):
if arch_str == "auto": if arch_str == "auto":
result |= Architecture.auto_archs(platform=platform) result |= Architecture.auto_archs(platform=platform)
elif arch_str == "native": 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": elif arch_str == "all":
result |= Architecture.all_archs(platform=platform) result |= Architecture.all_archs(platform=platform)
elif arch_str == "auto64": elif arch_str == "auto64":
+4
View File
@@ -58,3 +58,7 @@ class AlreadyBuiltWheelError(FatalError):
) )
super().__init__(message) super().__init__(message)
self.return_code = 6 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 ._compat.typing import assert_never
from .architecture import Architecture from .architecture import Architecture
from .logger import log from .logger import log
from .oci_container import OCIContainer, OCIContainerEngineConfig from .oci_container import OCIContainer, OCIContainerEngineConfig, OCIPlatform
from .options import BuildOptions, Options from .options import BuildOptions, Options
from .typing import PathOrStr from .typing import PathOrStr
from .util import ( from .util import (
@@ -29,6 +29,14 @@ from .util import (
unwrap, 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) @dataclass(frozen=True)
class PythonConfiguration: class PythonConfiguration:
@@ -196,6 +204,8 @@ def build_in_container(
dependency_constraint_flags: list[PathOrStr] = [] dependency_constraint_flags: list[PathOrStr] = []
log.step("Setting up build environment...")
if build_options.dependency_constraints: if build_options.dependency_constraints:
constraints_file = build_options.dependency_constraints.get_for_python_version( constraints_file = build_options.dependency_constraints.get_for_python_version(
config.version config.version
@@ -205,8 +215,6 @@ def build_in_container(
container.copy_into(constraints_file, container_constraints_file) container.copy_into(constraints_file, container_constraints_file)
dependency_constraint_flags = ["-c", container_constraints_file] dependency_constraint_flags = ["-c", container_constraints_file]
log.step("Setting up build environment...")
env = container.get_environment() env = container.get_environment()
env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1" env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1"
env["PIP_ROOT_USER_ACTION"] = "ignore" 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}...") log.step(f"Starting container image {build_step.container_image}...")
print(f"info: This container will host the build for {', '.join(ids_to_build)}...") 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( with OCIContainer(
image=build_step.container_image, 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, cwd=container_project_path,
engine=build_step.container_engine, engine=build_step.container_engine,
) as container: ) as container:
+100 -68
View File
@@ -5,18 +5,22 @@ import json
import os import os
import platform import platform
import shlex import shlex
import shutil
import subprocess import subprocess
import sys import sys
import typing import typing
import uuid import uuid
from collections.abc import Mapping, Sequence from collections.abc import Mapping, Sequence
from dataclasses import dataclass, field from dataclasses import dataclass, field
from enum import Enum
from pathlib import Path, PurePath, PurePosixPath from pathlib import Path, PurePath, PurePosixPath
from types import TracebackType from types import TracebackType
from typing import IO, Dict, Literal 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 .typing import PathOrStr, PopenBytes
from .util import ( from .util import (
CIProvider, CIProvider,
@@ -29,6 +33,15 @@ from .util import (
ContainerEngineName = Literal["docker", "podman"] 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) @dataclass(frozen=True)
class OCIContainerEngineConfig: class OCIContainerEngineConfig:
name: ContainerEngineName name: ContainerEngineName
@@ -56,6 +69,15 @@ class OCIContainerEngineConfig:
disable_host_mount = ( disable_host_mount = (
strtobool(disable_host_mount_options[-1]) if disable_host_mount_options else False 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( return OCIContainerEngineConfig(
name=name, create_args=tuple(create_args), disable_host_mount=disable_host_mount name=name, create_args=tuple(create_args), disable_host_mount=disable_host_mount
@@ -75,6 +97,32 @@ class OCIContainerEngineConfig:
DEFAULT_ENGINE = OCIContainerEngineConfig("docker") 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: class OCIContainer:
""" """
An object that represents a running OCI (e.g. Docker) container. An object that represents a running OCI (e.g. Docker) container.
@@ -108,7 +156,7 @@ class OCIContainer:
self, self,
*, *,
image: str, image: str,
enforce_32_bit: bool = False, oci_platform: OCIPlatform,
cwd: PathOrStr | None = None, cwd: PathOrStr | None = None,
engine: OCIContainerEngineConfig = DEFAULT_ENGINE, engine: OCIContainerEngineConfig = DEFAULT_ENGINE,
): ):
@@ -117,14 +165,41 @@ class OCIContainer:
raise ValueError(msg) raise ValueError(msg)
self.image = image self.image = image
self.enforce_32_bit = enforce_32_bit self.oci_platform = oci_platform
self.cwd = cwd self.cwd = cwd
self.name: str | None = None self.name: str | None = None
self.engine = engine 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: def __enter__(self) -> Self:
self.name = f"cibuildwheel-{uuid.uuid4()}" self.name = f"cibuildwheel-{uuid.uuid4()}"
_check_engine_version(self.engine)
# work-around for Travis-CI PPC64le Docker runs since 2021: # work-around for Travis-CI PPC64le Docker runs since 2021:
# this avoids network splits # this avoids network splits
# https://github.com/pypa/cibuildwheel/issues/904 # https://github.com/pypa/cibuildwheel/issues/904
@@ -133,14 +208,26 @@ class OCIContainer:
if detect_ci_provider() == CIProvider.travis_ci and platform.machine() == "ppc64le": if detect_ci_provider() == CIProvider.travis_ci and platform.machine() == "ppc64le":
network_args = ["--network=host"] network_args = ["--network=host"]
platform_args = self._get_platform_args()
simulate_32_bit = False 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 # 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 # or the image entrypoint takes care of enforcing this, then we don't need to
# simulate this # simulate this
container_machine = call( run_cmd = [self.engine.name, "run", "--rm"]
self.engine.name, "run", "--rm", self.image, "uname", "-m", capture_stdout=True ctr_cmd = ["uname", "-m"]
).strip() 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" simulate_32_bit = container_machine != "i686"
shell_args = ["linux32", "/bin/bash"] if simulate_32_bit else ["/bin/bash"] shell_args = ["linux32", "/bin/bash"] if simulate_32_bit else ["/bin/bash"]
@@ -155,6 +242,7 @@ class OCIContainer:
"--interactive", "--interactive",
*(["--volume=/:/host"] if not self.engine.disable_host_mount else []), *(["--volume=/:/host"] if not self.engine.disable_host_mount else []),
*network_args, *network_args,
*platform_args,
*self.engine.create_args, *self.engine.create_args,
self.image, self.image,
*shell_args, *shell_args,
@@ -221,73 +309,17 @@ class OCIContainer:
self.name = None self.name = None
def copy_into(self, from_path: Path, to_path: PurePath) -> 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(): if from_path.is_dir():
self.call(["mkdir", "-p", to_path]) self.call(["mkdir", "-p", to_path])
subprocess.run( call(self.engine.name, "cp", f"{from_path}/.", f"{self.name}:{to_path}")
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,
)
else: else:
exec_process: subprocess.Popen[bytes] self.call(["mkdir", "-p", to_path.parent])
with subprocess.Popen( call(self.engine.name, "cp", from_path, f"{self.name}:{to_path}")
[
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
)
def copy_out(self, from_path: PurePath, to_path: Path) -> None: def copy_out(self, from_path: PurePath, to_path: Path) -> None:
# note: we assume from_path is a dir # note: we assume from_path is a dir
to_path.mkdir(parents=True, exist_ok=True) to_path.mkdir(parents=True, exist_ok=True)
call(self.engine.name, "cp", f"{self.name}:{from_path}/.", to_path)
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)
def glob(self, path: PurePosixPath, pattern: str) -> list[PurePosixPath]: def glob(self, path: PurePosixPath, pattern: str) -> list[PurePosixPath]:
glob_pattern = path.joinpath(pattern) 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( actual_wheels = utils.cibuildwheel_run(
project_dir, project_dir,
add_env={ add_env={
"CIBW_ARCHS": "x86_64", "CIBW_ARCHS": "native",
"CIBW_BEFORE_ALL": "echo 'test log statement from before-all'", "CIBW_BEFORE_ALL": "echo 'test log statement from before-all'",
"CIBW_CONTAINER_ENGINE": "podman", "CIBW_CONTAINER_ENGINE": "podman",
}, },
@@ -29,9 +29,7 @@ def test_podman(tmp_path, capfd, request):
) )
# check that the expected wheels are produced # check that the expected wheels are produced
expected_wheels = [ expected_wheels = utils.expected_wheels("spam", "0.1.0", single_python=True, single_arch=True)
w for w in utils.expected_wheels("spam", "0.1.0", single_python=True) if "x86_64" in w
]
assert set(actual_wheels) == set(expected_wheels) assert set(actual_wheels) == set(expected_wheels)
# check that stdout is bring passed-though from container correctly # check that stdout is bring passed-though from container correctly
+140 -26
View File
@@ -6,6 +6,7 @@ import platform
import random import random
import shutil import shutil
import subprocess import subprocess
import sys
import textwrap import textwrap
from pathlib import Path, PurePath, PurePosixPath from pathlib import Path, PurePath, PurePosixPath
@@ -13,7 +14,7 @@ import pytest
import tomli_w import tomli_w
from cibuildwheel.environment import EnvironmentAssignmentBash 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 from cibuildwheel.util import CIProvider, detect_ci_provider
# Test utilities # Test utilities
@@ -28,6 +29,14 @@ elif pm in {"aarch64", "arm64"}:
DEFAULT_IMAGE = DEFAULT_IMAGE_TEMPLATE.format(machine="aarch64") DEFAULT_IMAGE = DEFAULT_IMAGE_TEMPLATE.format(machine="aarch64")
else: else:
DEFAULT_IMAGE = "" 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") PODMAN = OCIContainerEngineConfig(name="podman")
@@ -63,24 +72,32 @@ def container_engine(request):
def test_simple(container_engine): 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" assert container.call(["echo", "hello"], capture_output=True) == "hello\n"
def test_no_lf(container_engine): 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" assert container.call(["printf", "hello"], capture_output=True) == "hello"
def test_debug_info(container_engine): 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()) print(container.debug_info())
with container: with container:
pass pass
def test_environment(container_engine): 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 ( assert (
container.call( container.call(
["sh", "-c", "echo $TEST_VAR"], env={"TEST_VAR": "1"}, capture_output=True ["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): def test_environment_pass(container_engine, monkeypatch):
monkeypatch.setenv("CIBUILDWHEEL", "1") monkeypatch.setenv("CIBUILDWHEEL", "1")
monkeypatch.setenv("SOURCE_DATE_EPOCH", "1489957071") 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 $CIBUILDWHEEL"], capture_output=True) == "1\n"
assert ( assert (
container.call(["sh", "-c", "echo $SOURCE_DATE_EPOCH"], capture_output=True) 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): def test_cwd(container_engine):
with OCIContainer( 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: ) as container:
assert container.call(["pwd"], capture_output=True) == "/cibuildwheel/working_directory\n" assert container.call(["pwd"], capture_output=True) == "/cibuildwheel/working_directory\n"
assert container.call(["pwd"], capture_output=True, cwd="/opt") == "/opt\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): 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( docker_containers_listing = subprocess.run(
f"{container.engine.name} container ls", f"{container.engine.name} container ls",
shell=True, shell=True,
@@ -141,7 +169,9 @@ def test_large_environment(container_engine):
"d": "0" * long_env_var_length, "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 # check the length of d
assert ( assert (
container.call(["sh", "-c", "echo ${#d}"], env=large_environment, capture_output=True) 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): 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 # note: the below embedded snippets are in python2
# check that we can pass though arbitrary binary data without erroring # 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): 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 copying a file in
test_binary_data = bytes(random.randrange(256) for _ in range(1000)) test_binary_data = bytes(random.randrange(256) for _ in range(1000))
original_test_file = tmp_path / "test.dat" 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): 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)) test_binary_data = bytes(random.randrange(256) for _ in range(1000))
original_test_file = tmp_path / "test.dat" original_test_file = tmp_path / "test.dat"
original_test_file.write_bytes(test_binary_data) 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): 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)") assignment = EnvironmentAssignmentBash("TEST=$(echo 42)")
assert assignment.evaluated_value({}, container.environment_executor) == "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): def test_podman_vfs(tmp_path: Path, monkeypatch, container_engine):
if container_engine.name != "podman": if container_engine.name != "podman":
pytest.skip("only runs with 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 # create the VFS configuration
vfs_path = tmp_path / "podman_vfs" 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_CONF", str(vfs_containers_conf_fpath))
monkeypatch.setenv("CONTAINERS_STORAGE_CONF", str(vfs_containers_storage_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 # test running a command
assert container.call(["echo", "hello"], capture_output=True) == "hello\n" 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( with OCIContainer(
engine=container_engine, engine=container_engine,
image=DEFAULT_IMAGE, image=DEFAULT_IMAGE,
oci_platform=DEFAULT_OCI_PLATFORM,
) as container: ) as container:
assert container.call(["cat", "/test_mount/test_file.txt"], capture_output=True) == "1234" 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", "docker",
("--some-option=value; with; semicolons", "--another-option"), ("--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) engine_config = OCIContainerEngineConfig.from_config_string(config)
assert engine_config.name == name assert engine_config.name == name
assert engine_config.create_args == create_args 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.skipif(pm != "x86_64", reason="Only runs on x86_64")
@pytest.mark.parametrize( def test_enforce_32_bit(container_engine):
("image", "shell_args"), with OCIContainer(
[ engine=container_engine,
(DEFAULT_IMAGE_TEMPLATE.format(machine="i686"), ["/bin/bash"]), image=DEFAULT_IMAGE_TEMPLATE.format(machine="i686"),
(DEFAULT_IMAGE_TEMPLATE.format(machine="x86_64"), ["linux32", "/bin/bash"]), oci_platform=OCIPlatform.i386,
], ) as container:
)
def test_enforce_32_bit(container_engine, image, shell_args):
with OCIContainer(engine=container_engine, image=image, enforce_32_bit=True) as container:
assert container.call(["uname", "-m"], capture_output=True).strip() == "i686" assert container.call(["uname", "-m"], capture_output=True).strip() == "i686"
container_args = subprocess.run( container_args = subprocess.run(
f"{container.engine.name} inspect -f '{{{{json .Args }}}}' {container.name}", 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, stdout=subprocess.PIPE,
text=True, text=True,
).stdout ).stdout
assert json.loads(container_args) == shell_args assert json.loads(container_args) == ["/bin/bash"]
@pytest.mark.parametrize( @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): 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}: if detect_ci_provider() in {CIProvider.circle_ci, CIProvider.gitlab}:
pytest.skip("Skipping test because docker on this platform does not support host mounts") 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)) engine = OCIContainerEngineConfig.from_config_string(config.format(name=container_engine.name))
sentinel_file = tmp_path / "sentinel" sentinel_file = tmp_path / "sentinel"
sentinel_file.write_text("12345") 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) host_mount_path = "/host" + str(sentinel_file)
if should_have_host_mount: if should_have_host_mount:
assert container.call(["cat", host_mount_path], capture_output=True) == "12345" assert container.call(["cat", host_mount_path], capture_output=True) == "12345"
else: else:
with pytest.raises(subprocess.CalledProcessError): with pytest.raises(subprocess.CalledProcessError):
container.call(["cat", host_mount_path], capture_output=True) 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 import linux, util
from cibuildwheel.__main__ import main from cibuildwheel.__main__ import main
from cibuildwheel.oci_container import OCIPlatform
ALL_IDS = { ALL_IDS = {
"cp36", "cp36",
@@ -73,7 +74,7 @@ def test_build_default_launches(monkeypatch):
kwargs = build_in_container.call_args_list[0][1] kwargs = build_in_container.call_args_list[0][1]
assert "quay.io/pypa/manylinux2014_x86_64" in kwargs["container"]["image"] assert "quay.io/pypa/manylinux2014_x86_64" in kwargs["container"]["image"]
assert kwargs["container"]["cwd"] == PurePosixPath("/project") 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"]} identifiers = {x.identifier for x in kwargs["platform_configs"]}
assert identifiers == {f"{x}-manylinux_x86_64" for x in ALL_IDS} 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] kwargs = build_in_container.call_args_list[1][1]
assert "quay.io/pypa/manylinux2014_i686" in kwargs["container"]["image"] assert "quay.io/pypa/manylinux2014_i686" in kwargs["container"]["image"]
assert kwargs["container"]["cwd"] == PurePosixPath("/project") 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"]} identifiers = {x.identifier for x in kwargs["platform_configs"]}
assert identifiers == {f"{x}-manylinux_i686" for x in ALL_IDS} 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] kwargs = build_in_container.call_args_list[2][1]
assert "quay.io/pypa/musllinux_1_2_x86_64" in kwargs["container"]["image"] assert "quay.io/pypa/musllinux_1_2_x86_64" in kwargs["container"]["image"]
assert kwargs["container"]["cwd"] == PurePosixPath("/project") 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"]} identifiers = {x.identifier for x in kwargs["platform_configs"]}
assert identifiers == { assert identifiers == {
@@ -99,7 +100,7 @@ def test_build_default_launches(monkeypatch):
kwargs = build_in_container.call_args_list[3][1] kwargs = build_in_container.call_args_list[3][1]
assert "quay.io/pypa/musllinux_1_2_i686" in kwargs["container"]["image"] assert "quay.io/pypa/musllinux_1_2_i686" in kwargs["container"]["image"]
assert kwargs["container"]["cwd"] == PurePosixPath("/project") 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"]} 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} 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] kwargs = build_in_container.call_args_list[0][1]
assert "quay.io/pypa/manylinux2014_x86_64" in kwargs["container"]["image"] assert "quay.io/pypa/manylinux2014_x86_64" in kwargs["container"]["image"]
assert kwargs["container"]["cwd"] == PurePosixPath("/project") 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"]} identifiers = {x.identifier for x in kwargs["platform_configs"]}
assert identifiers == {"cp36-manylinux_x86_64"} assert identifiers == {"cp36-manylinux_x86_64"}
@@ -151,7 +152,7 @@ before-all = "true"
kwargs = build_in_container.call_args_list[1][1] kwargs = build_in_container.call_args_list[1][1]
assert "quay.io/pypa/manylinux2014_x86_64" in kwargs["container"]["image"] assert "quay.io/pypa/manylinux2014_x86_64" in kwargs["container"]["image"]
assert kwargs["container"]["cwd"] == PurePosixPath("/project") 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"]} identifiers = {x.identifier for x in kwargs["platform_configs"]}
assert identifiers == { assert identifiers == {
@@ -164,7 +165,7 @@ before-all = "true"
kwargs = build_in_container.call_args_list[2][1] kwargs = build_in_container.call_args_list[2][1]
assert "quay.io/pypa/manylinux_2_28_x86_64" in kwargs["container"]["image"] assert "quay.io/pypa/manylinux_2_28_x86_64" in kwargs["container"]["image"]
assert kwargs["container"]["cwd"] == PurePosixPath("/project") 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"]} identifiers = {x.identifier for x in kwargs["platform_configs"]}
assert identifiers == { assert identifiers == {
f"{x}-manylinux_x86_64" f"{x}-manylinux_x86_64"
@@ -174,7 +175,7 @@ before-all = "true"
kwargs = build_in_container.call_args_list[3][1] kwargs = build_in_container.call_args_list[3][1]
assert "quay.io/pypa/manylinux2014_i686" in kwargs["container"]["image"] assert "quay.io/pypa/manylinux2014_i686" in kwargs["container"]["image"]
assert kwargs["container"]["cwd"] == PurePosixPath("/project") 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"]} identifiers = {x.identifier for x in kwargs["platform_configs"]}
assert identifiers == {f"{x}-manylinux_i686" for x in ALL_IDS} 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] kwargs = build_in_container.call_args_list[4][1]
assert "quay.io/pypa/musllinux_1_1_x86_64" in kwargs["container"]["image"] assert "quay.io/pypa/musllinux_1_1_x86_64" in kwargs["container"]["image"]
assert kwargs["container"]["cwd"] == PurePosixPath("/project") 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"]} identifiers = {x.identifier for x in kwargs["platform_configs"]}
assert identifiers == { assert identifiers == {
@@ -192,7 +193,7 @@ before-all = "true"
kwargs = build_in_container.call_args_list[5][1] kwargs = build_in_container.call_args_list[5][1]
assert "quay.io/pypa/musllinux_1_2_x86_64" in kwargs["container"]["image"] assert "quay.io/pypa/musllinux_1_2_x86_64" in kwargs["container"]["image"]
assert kwargs["container"]["cwd"] == PurePosixPath("/project") 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"]} identifiers = {x.identifier for x in kwargs["platform_configs"]}
assert identifiers == { assert identifiers == {
f"{x}-musllinux_x86_64" for x in ALL_IDS - {"cp36", "cp37", "cp38", "cp39"} if "pp" not in x 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] kwargs = build_in_container.call_args_list[6][1]
assert "quay.io/pypa/musllinux_1_2_i686" in kwargs["container"]["image"] assert "quay.io/pypa/musllinux_1_2_i686" in kwargs["container"]["image"]
assert kwargs["container"]["cwd"] == PurePosixPath("/project") 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"]} 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} assert identifiers == {f"{x}-musllinux_i686" for x in ALL_IDS if "pp" not in x}