Skip to content
4 changes: 3 additions & 1 deletion cibuildwheel/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,6 @@ def __init__(self, wheel_name: str) -> None:


class OCIEngineTooOldError(FatalError):
return_code = 7
def __init__(self, message: str) -> None:
super().__init__(message)
self.return_code = 7
49 changes: 38 additions & 11 deletions cibuildwheel/oci_container.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import shutil
import subprocess
import sys
import textwrap
import typing
import uuid
from collections.abc import Mapping, Sequence
Expand Down Expand Up @@ -103,25 +104,51 @@ def _check_engine_version(engine: OCIContainerEngineConfig) -> None:
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")
server_api_version = Version(version_info["Server"]["ApiVersion"])
# --platform support was introduced in 1.32 as experimental, 1.41 removed the experimental flag
version = min(client_api_version, server_api_version)
minimum_version = Version("1.41")
minimum_version_str = "20.10.0" # docker version
error_msg = textwrap.dedent(
f"""
Build failed because {engine.name} is too old.

cibuildwheel requires {engine.name}>={minimum_version_str} running API version {minimum_version}.
The API version found by cibuildwheel is {version}.
"""
)
elif engine.name == "podman":
client_api_version = Version(version_info["Client"]["APIVersion"])
# podman uses the same version string for "Version" & "ApiVersion"
# the version string is not PEP440 compliant here
def _version(version_string: str) -> Version:
for sep in ("-", "~", "^", "+"):
version_string = version_string.split(sep, maxsplit=1)[0]
return Version(version_string)

client_version = _version(version_info["Client"]["Version"])
if "Server" in version_info:
engine_api_version = Version(version_info["Server"]["APIVersion"])
server_version = _version(version_info["Server"]["Version"])
else:
engine_api_version = client_api_version
server_version = client_version
# --platform support was introduced in v3
version_supported = min(client_api_version, engine_api_version) >= Version("3")
version = min(client_version, server_version)
minimum_version = Version("3")
error_msg = textwrap.dedent(
f"""
Build failed because {engine.name} is too old.

cibuildwheel requires {engine.name}>={minimum_version}.
The version found by cibuildwheel is {version}.
"""
)
else:
assert_never(engine.name)
if not version_supported:
raise OCIEngineTooOldError() from None
if version < minimum_version:
raise OCIEngineTooOldError(error_msg) from None
except (subprocess.CalledProcessError, KeyError, InvalidVersion) as e:
raise OCIEngineTooOldError() from e
msg = f"Build failed because {engine.name} is too old or is not working properly."
raise OCIEngineTooOldError(msg) from e


class OCIContainer:
Expand Down
71 changes: 70 additions & 1 deletion unit_test/oci_container_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,21 @@
import subprocess
import sys
import textwrap
from contextlib import nullcontext
from pathlib import Path, PurePath, PurePosixPath

import pytest
import tomli_w

import cibuildwheel.oci_container
from cibuildwheel.environment import EnvironmentAssignmentBash
from cibuildwheel.oci_container import OCIContainer, OCIContainerEngineConfig, OCIPlatform
from cibuildwheel.errors import OCIEngineTooOldError
from cibuildwheel.oci_container import (
OCIContainer,
OCIContainerEngineConfig,
OCIPlatform,
_check_engine_version,
)
from cibuildwheel.util import CIProvider, detect_ci_provider

# Test utilities
Expand Down Expand Up @@ -569,3 +577,64 @@ def test_multiarch_image(container_engine, platform):
OCIPlatform.S390X: "s390x",
}
assert output_map[platform] == output.strip()


@pytest.mark.parametrize(
("engine_name", "version", "context"),
[
(
"docker",
None, # 17.12.1-ce does supports "docker version --format '{{json . }}'" so a version before that
pytest.raises(OCIEngineTooOldError),
),
(
"docker",
'{"Client":{"Version":"19.03.15","ApiVersion": "1.40"},"Server":{"ApiVersion": "1.40"}}',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've kept the "Version" info here to get a real sense of what docker version the ApiVersion relates to. It's unused & just informative.

pytest.raises(OCIEngineTooOldError),
),
(
"docker",
'{"Client":{"Version":"20.10.0","ApiVersion":"1.41"},"Server":{"ApiVersion":"1.41"}}',
nullcontext(),
),
(
"docker",
'{"Client":{"Version":"24.0.0","ApiVersion":"1.43"},"Server":{"ApiVersion":"1.43"}}',
nullcontext(),
),
(
"docker",
'{"Client":{"ApiVersion":"1.43"},"Server":{"ApiVersion":"1.30"}}',
pytest.raises(OCIEngineTooOldError),
),
(
"docker",
'{"Client":{"ApiVersion":"1.30"},"Server":{"ApiVersion":"1.43"}}',
pytest.raises(OCIEngineTooOldError),
),
("podman", '{"Client":{"Version":"5.2.0"},"Server":{"Version":"5.1.2"}}', nullcontext()),
("podman", '{"Client":{"Version":"4.9.4-rhel"}}', nullcontext()),
(
"podman",
'{"Client":{"Version":"5.2.0"},"Server":{"Version":"2.1.2"}}',
pytest.raises(OCIEngineTooOldError),
),
(
"podman",
'{"Client":{"Version":"2.2.0"},"Server":{"Version":"5.1.2"}}',
pytest.raises(OCIEngineTooOldError),
),
("podman", '{"Client":{"Version":"3.0~rc1-rhel"}}', nullcontext()),
("podman", '{"Client":{"Version":"2.1.0~rc1"}}', pytest.raises(OCIEngineTooOldError)),
],
)
def test_engine_version(engine_name, version, context, monkeypatch):
def mockcall(*args, **kwargs):
if version is None:
raise subprocess.CalledProcessError(1, " ".join(str(arg) for arg in args))
return version

monkeypatch.setattr(cibuildwheel.oci_container, "call", mockcall)
engine = OCIContainerEngineConfig.from_config_string(engine_name)
with context:
_check_engine_version(engine)