Merge pull request #966 from Erotemic/new_podman_support

Podman support on Linux
This commit is contained in:
Joe Rickerby
2022-07-05 16:57:03 +01:00
committed by GitHub
25 changed files with 556 additions and 264 deletions
+13 -1
View File
@@ -47,6 +47,18 @@ jobs:
with: with:
python-version: ${{ matrix.python_version }} python-version: ${{ matrix.python_version }}
# Install podman on this CI instance for podman tests on linux
# Snippet from: https://github.com/redhat-actions/podman-login/blob/main/.github/workflows/example.yml
- name: Install latest podman
if: runner.os == 'Linux'
run: |
. /etc/os-release
echo "deb https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/xUbuntu_${VERSION_ID}/ /" | sudo tee /etc/apt/sources.list.d/devel:kubic:libcontainers:stable.list
curl -sSfL "https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/xUbuntu_${VERSION_ID}/Release.key" | sudo apt-key add -
sudo apt-get update
sudo apt-get -y upgrade
sudo apt-get -y install podman
- name: Install dependencies - name: Install dependencies
run: | run: |
python -m pip install ".[test]" python -m pip install ".[test]"
@@ -70,7 +82,7 @@ jobs:
- name: Test cibuildwheel - name: Test cibuildwheel
run: | run: |
python ./bin/run_tests.py python ./bin/run_tests.py --run-podman
test-emulated: test-emulated:
name: Test emulated cibuildwheel using qemu name: Test emulated cibuildwheel using qemu
+1
View File
@@ -61,6 +61,7 @@ repos:
- pygithub - pygithub
- rich - rich
- tomli - tomli
- tomli_w
- types-certifi - types-certifi
- types-click - types-click
- types-dataclasses - types-dataclasses
+1
View File
@@ -130,6 +130,7 @@ Options
| | [`CIBW_BEFORE_BUILD`](https://cibuildwheel.readthedocs.io/en/stable/options/#before-build) | Execute a shell command preparing each wheel's build | | | [`CIBW_BEFORE_BUILD`](https://cibuildwheel.readthedocs.io/en/stable/options/#before-build) | Execute a shell command preparing each wheel's build |
| | [`CIBW_REPAIR_WHEEL_COMMAND`](https://cibuildwheel.readthedocs.io/en/stable/options/#repair-wheel-command) | Execute a shell command to repair each built wheel | | | [`CIBW_REPAIR_WHEEL_COMMAND`](https://cibuildwheel.readthedocs.io/en/stable/options/#repair-wheel-command) | Execute a shell command to repair each built wheel |
| | [`CIBW_MANYLINUX_*_IMAGE`<br/>`CIBW_MUSLLINUX_*_IMAGE`](https://cibuildwheel.readthedocs.io/en/stable/options/#linux-image) | Specify alternative manylinux / musllinux Docker images | | | [`CIBW_MANYLINUX_*_IMAGE`<br/>`CIBW_MUSLLINUX_*_IMAGE`](https://cibuildwheel.readthedocs.io/en/stable/options/#linux-image) | Specify alternative manylinux / musllinux Docker images |
| | [`CIBW_CONTAINER_ENGINE`](https://cibuildwheel.readthedocs.io/en/stable/options/#container-engine) | Specify which container engine to use when building Linux wheels |
| | [`CIBW_DEPENDENCY_VERSIONS`](https://cibuildwheel.readthedocs.io/en/stable/options/#dependency-versions) | Specify how cibuildwheel controls the versions of the tools it uses | | | [`CIBW_DEPENDENCY_VERSIONS`](https://cibuildwheel.readthedocs.io/en/stable/options/#dependency-versions) | Specify how cibuildwheel controls the versions of the tools it uses |
| **Testing** | [`CIBW_TEST_COMMAND`](https://cibuildwheel.readthedocs.io/en/stable/options/#test-command) | Execute a shell command to test each built wheel | | **Testing** | [`CIBW_TEST_COMMAND`](https://cibuildwheel.readthedocs.io/en/stable/options/#test-command) | Execute a shell command to test each built wheel |
| | [`CIBW_BEFORE_TEST`](https://cibuildwheel.readthedocs.io/en/stable/options/#before-test) | Execute a shell command before testing each wheel | | | [`CIBW_BEFORE_TEST`](https://cibuildwheel.readthedocs.io/en/stable/options/#before-test) | Execute a shell command before testing each wheel |
+22 -8
View File
@@ -1,24 +1,35 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import argparse
import os import os
import subprocess import subprocess
import sys import sys
from pathlib import Path from pathlib import Path
if __name__ == "__main__": if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument(
"--run-podman", action="store_true", default=False, help="run podman tests (linux only)"
)
args = parser.parse_args()
# move cwd to the project root # move cwd to the project root
os.chdir(Path(__file__).resolve().parents[1]) os.chdir(Path(__file__).resolve().parents[1])
# run the unit tests # unit tests
unit_test_args = [sys.executable, "-m", "pytest", "unit_test"] unit_test_args = [sys.executable, "-m", "pytest", "unit_test"]
# run the docker unit tests only on Linux
if sys.platform.startswith("linux"): if sys.platform.startswith("linux"):
# run the docker unit tests only on Linux
unit_test_args += ["--run-docker"] unit_test_args += ["--run-docker"]
if args.run_podman:
unit_test_args += ["--run-podman"]
subprocess.run(unit_test_args, check=True) subprocess.run(unit_test_args, check=True)
# run the integration tests # integration tests
subprocess.run( integration_test_args = [
[
sys.executable, sys.executable,
"-m", "-m",
"pytest", "pytest",
@@ -28,6 +39,9 @@ if __name__ == "__main__":
"0", "0",
"--timeout=2400", "--timeout=2400",
"test", "test",
], ]
check=True,
) if sys.platform.startswith("linux") and args.run_podman:
integration_test_args += ["--run-podman"]
subprocess.run(integration_test_args, check=True)
+2 -3
View File
@@ -46,7 +46,7 @@ def main() -> None:
auto-detected platform or to run cibuildwheel on your development auto-detected platform or to run cibuildwheel on your development
machine. Specifying "macos" or "windows" only works on that machine. Specifying "macos" or "windows" only works on that
operating system, but "linux" works on all three, as long as operating system, but "linux" works on all three, as long as
Docker is installed. Default: auto. Docker/Podman is installed. Default: auto.
""", """,
) )
@@ -91,7 +91,7 @@ def main() -> None:
Path to the package that you want wheels for. Default: the working Path to the package that you want wheels for. Default: the working
directory. Can be a directory inside the working directory, or an directory. Can be a directory inside the working directory, or an
sdist. When set to a directory, the working directory is still sdist. When set to a directory, the working directory is still
considered the 'project' and is copied into the Docker container considered the 'project' and is copied into the build container
on Linux. When set to a tar.gz sdist file, --config-file on Linux. When set to a tar.gz sdist file, --config-file
and --output-dir are relative to the current directory, and other and --output-dir are relative to the current directory, and other
paths are relative to the expanded SDist directory. paths are relative to the expanded SDist directory.
@@ -208,7 +208,6 @@ def build_in_directory(args: CommandLineArguments) -> None:
sys.exit(0) sys.exit(0)
# Add CIBUILDWHEEL environment variable # Add CIBUILDWHEEL environment variable
# This needs to be passed on to the docker container in linux.py
os.environ["CIBUILDWHEEL"] = "1" os.environ["CIBUILDWHEEL"] = "1"
# Python is buffering by default when running on the CI platforms, giving problems interleaving subprocess call output with unflushed calls to 'print' # Python is buffering by default when running on the CI platforms, giving problems interleaving subprocess call output with unflushed calls to 'print'
+1 -1
View File
@@ -59,7 +59,7 @@ class Architecture(Enum):
result = {native_architecture} result = {native_architecture}
if platform == "linux" and native_architecture == Architecture.x86_64: if platform == "linux" and native_architecture == Architecture.x86_64:
# x86_64 machines can run i686 docker containers # x86_64 machines can run i686 containers
result.add(Architecture.i686) result.add(Architecture.i686)
if platform == "windows" and native_architecture == Architecture.AMD64: if platform == "windows" and native_architecture == Architecture.AMD64:
+68 -54
View File
@@ -6,8 +6,8 @@ from pathlib import Path, PurePath, PurePosixPath
from typing import Iterator, List, Set, Tuple from typing import Iterator, List, Set, Tuple
from .architecture import Architecture from .architecture import Architecture
from .docker_container import DockerContainer
from .logger import log from .logger import log
from .oci_container import OCIContainer
from .options import Options from .options import Options
from .typing import OrderedDict, PathOrStr, assert_never from .typing import OrderedDict, PathOrStr, assert_never
from .util import ( from .util import (
@@ -17,6 +17,7 @@ from .util import (
get_build_verbosity_extra_flags, get_build_verbosity_extra_flags,
prepare_command, prepare_command,
read_python_configs, read_python_configs,
unwrap,
) )
@@ -35,7 +36,7 @@ class PythonConfiguration:
class BuildStep: class BuildStep:
platform_configs: List[PythonConfiguration] platform_configs: List[PythonConfiguration]
platform_tag: str platform_tag: str
docker_image: str container_image: str
def get_python_configurations( def get_python_configurations(
@@ -57,7 +58,7 @@ def get_python_configurations(
] ]
def docker_image_for_python_configuration(config: PythonConfiguration, options: Options) -> str: def container_image_for_python_configuration(config: PythonConfiguration, options: Options) -> str:
build_options = options.build_options(config.identifier) build_options = options.build_options(config.identifier)
# e.g # e.g
# identifier is 'cp310-manylinux_x86_64' # identifier is 'cp310-manylinux_x86_64'
@@ -81,7 +82,7 @@ def get_build_steps(
) -> Iterator[BuildStep]: ) -> Iterator[BuildStep]:
""" """
Groups PythonConfigurations into BuildSteps. Each BuildStep represents a Groups PythonConfigurations into BuildSteps. Each BuildStep represents a
separate Docker container. separate container instance.
""" """
steps = OrderedDict[Tuple[str, str, str], BuildStep]() steps = OrderedDict[Tuple[str, str, str], BuildStep]()
@@ -89,32 +90,34 @@ def get_build_steps(
_, platform_tag = config.identifier.split("-", 1) _, platform_tag = config.identifier.split("-", 1)
before_all = options.build_options(config.identifier).before_all before_all = options.build_options(config.identifier).before_all
docker_image = docker_image_for_python_configuration(config, options) container_image = container_image_for_python_configuration(config, options)
step_key = (platform_tag, docker_image, before_all) step_key = (platform_tag, container_image, before_all)
if step_key in steps: if step_key in steps:
steps[step_key].platform_configs.append(config) steps[step_key].platform_configs.append(config)
else: else:
steps[step_key] = BuildStep( steps[step_key] = BuildStep(
platform_configs=[config], platform_tag=platform_tag, docker_image=docker_image platform_configs=[config],
platform_tag=platform_tag,
container_image=container_image,
) )
yield from steps.values() yield from steps.values()
def build_on_docker( def build_in_container(
*, *,
options: Options, options: Options,
platform_configs: List[PythonConfiguration], platform_configs: List[PythonConfiguration],
docker: DockerContainer, container: OCIContainer,
container_project_path: PurePath, container_project_path: PurePath,
container_package_dir: PurePath, container_package_dir: PurePath,
) -> None: ) -> None:
container_output_dir = PurePosixPath("/output") container_output_dir = PurePosixPath("/output")
log.step("Copying project into Docker...") log.step("Copying project into container...")
docker.copy_into(Path.cwd(), container_project_path) container.copy_into(Path.cwd(), container_project_path)
before_all_options_identifier = platform_configs[0].identifier before_all_options_identifier = platform_configs[0].identifier
before_all_options = options.build_options(before_all_options_identifier) before_all_options = options.build_options(before_all_options_identifier)
@@ -122,11 +125,11 @@ def build_on_docker(
if before_all_options.before_all: if before_all_options.before_all:
log.step("Running before_all...") log.step("Running before_all...")
env = docker.get_environment() env = container.get_environment()
env["PATH"] = f'/opt/python/cp38-cp38/bin:{env["PATH"]}' env["PATH"] = f'/opt/python/cp38-cp38/bin:{env["PATH"]}'
env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1" env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1"
env = before_all_options.environment.as_dictionary( env = before_all_options.environment.as_dictionary(
env, executor=docker.environment_executor env, executor=container.environment_executor
) )
before_all_prepared = prepare_command( before_all_prepared = prepare_command(
@@ -134,7 +137,7 @@ def build_on_docker(
project=container_project_path, project=container_project_path,
package=container_package_dir, package=container_package_dir,
) )
docker.call(["sh", "-c", before_all_prepared], env=env) container.call(["sh", "-c", before_all_prepared], env=env)
built_wheels: List[PurePosixPath] = [] built_wheels: List[PurePosixPath] = []
@@ -150,21 +153,21 @@ def build_on_docker(
) )
container_constraints_file = PurePath("/constraints.txt") container_constraints_file = PurePath("/constraints.txt")
docker.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...") log.step("Setting up build environment...")
env = docker.get_environment() env = container.get_environment()
# put this config's python top of the list # put this config's python top of the list
python_bin = config.path / "bin" python_bin = config.path / "bin"
env["PATH"] = f'{python_bin}:{env["PATH"]}' env["PATH"] = f'{python_bin}:{env["PATH"]}'
env = build_options.environment.as_dictionary(env, executor=docker.environment_executor) env = build_options.environment.as_dictionary(env, executor=container.environment_executor)
# check config python is still on PATH # check config python is still on PATH
which_python = docker.call(["which", "python"], env=env, capture_output=True).strip() which_python = container.call(["which", "python"], env=env, capture_output=True).strip()
if PurePosixPath(which_python) != python_bin / "python": if PurePosixPath(which_python) != python_bin / "python":
print( print(
"cibuildwheel: python available on PATH doesn't match our installed instance. If you have modified PATH, ensure that you don't overwrite cibuildwheel's entry or insert python above it.", "cibuildwheel: python available on PATH doesn't match our installed instance. If you have modified PATH, ensure that you don't overwrite cibuildwheel's entry or insert python above it.",
@@ -172,7 +175,7 @@ def build_on_docker(
) )
sys.exit(1) sys.exit(1)
which_pip = docker.call(["which", "pip"], env=env, capture_output=True).strip() which_pip = container.call(["which", "pip"], env=env, capture_output=True).strip()
if PurePosixPath(which_pip) != python_bin / "pip": if PurePosixPath(which_pip) != python_bin / "pip":
print( print(
"cibuildwheel: pip available on PATH doesn't match our installed instance. If you have modified PATH, ensure that you don't overwrite cibuildwheel's entry or insert pip above it.", "cibuildwheel: pip available on PATH doesn't match our installed instance. If you have modified PATH, ensure that you don't overwrite cibuildwheel's entry or insert pip above it.",
@@ -196,19 +199,19 @@ def build_on_docker(
project=container_project_path, project=container_project_path,
package=container_package_dir, package=container_package_dir,
) )
docker.call(["sh", "-c", before_build_prepared], env=env) container.call(["sh", "-c", before_build_prepared], env=env)
log.step("Building wheel...") log.step("Building wheel...")
temp_dir = PurePosixPath("/tmp/cibuildwheel") temp_dir = PurePosixPath("/tmp/cibuildwheel")
built_wheel_dir = temp_dir / "built_wheel" built_wheel_dir = temp_dir / "built_wheel"
docker.call(["rm", "-rf", built_wheel_dir]) container.call(["rm", "-rf", built_wheel_dir])
docker.call(["mkdir", "-p", built_wheel_dir]) container.call(["mkdir", "-p", built_wheel_dir])
verbosity_flags = get_build_verbosity_extra_flags(build_options.build_verbosity) verbosity_flags = get_build_verbosity_extra_flags(build_options.build_verbosity)
if build_options.build_frontend == "pip": if build_options.build_frontend == "pip":
docker.call( container.call(
[ [
"python", "python",
"-m", "-m",
@@ -223,7 +226,7 @@ def build_on_docker(
) )
elif build_options.build_frontend == "build": elif build_options.build_frontend == "build":
config_setting = " ".join(verbosity_flags) config_setting = " ".join(verbosity_flags)
docker.call( container.call(
[ [
"python", "python",
"-m", "-m",
@@ -238,11 +241,11 @@ def build_on_docker(
else: else:
assert_never(build_options.build_frontend) assert_never(build_options.build_frontend)
built_wheel = docker.glob(built_wheel_dir, "*.whl")[0] built_wheel = container.glob(built_wheel_dir, "*.whl")[0]
repaired_wheel_dir = temp_dir / "repaired_wheel" repaired_wheel_dir = temp_dir / "repaired_wheel"
docker.call(["rm", "-rf", repaired_wheel_dir]) container.call(["rm", "-rf", repaired_wheel_dir])
docker.call(["mkdir", "-p", repaired_wheel_dir]) container.call(["mkdir", "-p", repaired_wheel_dir])
if built_wheel.name.endswith("none-any.whl"): if built_wheel.name.endswith("none-any.whl"):
raise NonPlatformWheelError() raise NonPlatformWheelError()
@@ -252,21 +255,23 @@ def build_on_docker(
repair_command_prepared = prepare_command( repair_command_prepared = prepare_command(
build_options.repair_command, wheel=built_wheel, dest_dir=repaired_wheel_dir build_options.repair_command, wheel=built_wheel, dest_dir=repaired_wheel_dir
) )
docker.call(["sh", "-c", repair_command_prepared], env=env) container.call(["sh", "-c", repair_command_prepared], env=env)
else: else:
docker.call(["mv", built_wheel, repaired_wheel_dir]) container.call(["mv", built_wheel, repaired_wheel_dir])
repaired_wheels = docker.glob(repaired_wheel_dir, "*.whl") repaired_wheels = container.glob(repaired_wheel_dir, "*.whl")
if build_options.test_command and build_options.test_selector(config.identifier): if build_options.test_command and build_options.test_selector(config.identifier):
log.step("Testing wheel...") log.step("Testing wheel...")
# set up a virtual environment to install and test from, to make sure # set up a virtual environment to install and test from, to make sure
# there are no dependencies that were pulled in at build time. # there are no dependencies that were pulled in at build time.
docker.call(["pip", "install", "virtualenv", *dependency_constraint_flags], env=env) container.call(["pip", "install", "virtualenv", *dependency_constraint_flags], env=env)
venv_dir = PurePath(docker.call(["mktemp", "-d"], capture_output=True).strip()) / "venv" venv_dir = (
PurePath(container.call(["mktemp", "-d"], capture_output=True).strip()) / "venv"
)
docker.call(["python", "-m", "virtualenv", "--no-download", venv_dir], env=env) container.call(["python", "-m", "virtualenv", "--no-download", venv_dir], env=env)
virtualenv_env = env.copy() virtualenv_env = env.copy()
virtualenv_env["PATH"] = f"{venv_dir / 'bin'}:{virtualenv_env['PATH']}" virtualenv_env["PATH"] = f"{venv_dir / 'bin'}:{virtualenv_env['PATH']}"
@@ -277,7 +282,7 @@ def build_on_docker(
project=container_project_path, project=container_project_path,
package=container_package_dir, package=container_package_dir,
) )
docker.call(["sh", "-c", before_test_prepared], env=virtualenv_env) container.call(["sh", "-c", before_test_prepared], env=virtualenv_env)
# Install the wheel we just built # Install the wheel we just built
# Note: If auditwheel produced two wheels, it's because the earlier produced wheel # Note: If auditwheel produced two wheels, it's because the earlier produced wheel
@@ -286,14 +291,14 @@ def build_on_docker(
# different external shared libraries. so it doesn't matter which one we run the tests on. # different external shared libraries. so it doesn't matter which one we run the tests on.
# Let's just pick the first one. # Let's just pick the first one.
wheel_to_test = repaired_wheels[0] wheel_to_test = repaired_wheels[0]
docker.call( container.call(
["pip", "install", str(wheel_to_test) + build_options.test_extras], ["pip", "install", str(wheel_to_test) + build_options.test_extras],
env=virtualenv_env, env=virtualenv_env,
) )
# Install any requirements to run the tests # Install any requirements to run the tests
if build_options.test_requires: if build_options.test_requires:
docker.call(["pip", "install", *build_options.test_requires], env=virtualenv_env) container.call(["pip", "install", *build_options.test_requires], env=virtualenv_env)
# Run the tests from a different directory # Run the tests from a different directory
test_command_prepared = prepare_command( test_command_prepared = prepare_command(
@@ -301,15 +306,15 @@ def build_on_docker(
project=container_project_path, project=container_project_path,
package=container_package_dir, package=container_package_dir,
) )
docker.call(["sh", "-c", test_command_prepared], cwd="/root", env=virtualenv_env) container.call(["sh", "-c", test_command_prepared], cwd="/root", env=virtualenv_env)
# clean up test environment # clean up test environment
docker.call(["rm", "-rf", venv_dir]) container.call(["rm", "-rf", venv_dir])
# move repaired wheels to output # move repaired wheels to output
if compatible_wheel is None: if compatible_wheel is None:
docker.call(["mkdir", "-p", container_output_dir]) container.call(["mkdir", "-p", container_output_dir])
docker.call(["mv", *repaired_wheels, container_output_dir]) container.call(["mv", *repaired_wheels, container_output_dir])
built_wheels.extend( built_wheels.extend(
container_output_dir / repaired_wheel.name for repaired_wheel in repaired_wheels container_output_dir / repaired_wheel.name for repaired_wheel in repaired_wheels
) )
@@ -318,19 +323,27 @@ def build_on_docker(
log.step("Copying wheels back to host...") log.step("Copying wheels back to host...")
# copy the output back into the host # copy the output back into the host
docker.copy_out(container_output_dir, options.globals.output_dir) container.copy_out(container_output_dir, options.globals.output_dir)
log.step_end() log.step_end()
def build(options: Options, tmp_path: Path) -> None: # pylint: disable=unused-argument def build(options: Options, tmp_path: Path) -> None: # pylint: disable=unused-argument
try: try:
# check docker is installed # check the container engine is installed
subprocess.run(["docker", "--version"], check=True, stdout=subprocess.DEVNULL) subprocess.run(
[options.globals.container_engine, "--version"], check=True, stdout=subprocess.DEVNULL
)
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
print( print(
"cibuildwheel: Docker not found. Docker is required to run Linux builds. " unwrap(
"If you're building on Travis CI, add `services: [docker]` to your .travis.yml." f"""
"If you're building on Circle CI in Linux, add a `setup_remote_docker` step to your .circleci/config.yml", cibuildwheel: {options.globals.container_engine} not found. An
OCI exe like Docker or Podman is required to run Linux builds.
If you're building on Travis CI, add `services: [docker]` to
your .travis.yml. If you're building on Circle CI in Linux,
add a `setup_remote_docker` step to your .circleci/config.yml.
"""
),
file=sys.stderr, file=sys.stderr,
) )
sys.exit(2) sys.exit(2)
@@ -350,20 +363,21 @@ def build(options: Options, tmp_path: Path) -> None: # pylint: disable=unused-a
for build_step in get_build_steps(options, python_configurations): for build_step in get_build_steps(options, python_configurations):
try: try:
ids_to_build = [x.identifier for x in build_step.platform_configs] ids_to_build = [x.identifier for x in build_step.platform_configs]
log.step( log.step(f"Starting container image {build_step.container_image}...")
f"Starting Docker image {build_step.docker_image} for {', '.join(ids_to_build)}..."
)
with DockerContainer( print(f"info: This container will host the build for {', '.join(ids_to_build)}...")
docker_image=build_step.docker_image,
with OCIContainer(
image=build_step.container_image,
simulate_32_bit=build_step.platform_tag.endswith("i686"), simulate_32_bit=build_step.platform_tag.endswith("i686"),
cwd=container_project_path, cwd=container_project_path,
) as docker: engine=options.globals.container_engine,
) as container:
build_on_docker( build_in_container(
options=options, options=options,
platform_configs=build_step.platform_configs, platform_configs=build_step.platform_configs,
docker=docker, container=container,
container_project_path=container_project_path, container_project_path=container_project_path,
container_package_dir=container_package_dir, container_package_dir=container_package_dir,
) )
@@ -1,5 +1,6 @@
import io import io
import json import json
import os
import platform import platform
import shlex import shlex
import shutil import shutil
@@ -12,19 +13,32 @@ from typing import IO, Dict, List, Optional, Sequence, Type, cast
from cibuildwheel.util import CIProvider, detect_ci_provider from cibuildwheel.util import CIProvider, detect_ci_provider
from .typing import PathOrStr, PopenBytes from .typing import Literal, PathOrStr, PopenBytes
ContainerEngine = Literal["docker", "podman"]
class DockerContainer: class OCIContainer:
""" """
An object that represents a running Docker container. An object that represents a running OCI (e.g. Docker) container.
Intended for use as a context manager e.g. Intended for use as a context manager e.g.
`with DockerContainer(docker_image = 'ubuntu') as docker:` `with OCIContainer(image = 'ubuntu') as docker:`
A bash shell is running in the remote container. When `call()` is invoked, A bash shell is running in the remote container. When `call()` is invoked,
the command is relayed to the remote shell, and the results are streamed the command is relayed to the remote shell, and the results are streamed
back to cibuildwheel. back to cibuildwheel.
Example:
>>> from cibuildwheel.oci_container import * # NOQA
>>> from cibuildwheel.options import _get_pinned_container_images
>>> image = _get_pinned_container_images()['x86_64']['manylinux2014']
>>> # Test the default container
>>> with OCIContainer(image=image) as self:
... self.call(["echo", "hello world"])
... self.call(["cat", "/proc/1/cgroup"])
... print(self.get_environment())
... print(self.debug_info())
""" """
UTILITY_PYTHON = "/opt/python/cp38-cp38/bin/python" UTILITY_PYTHON = "/opt/python/cp38-cp38/bin/python"
@@ -34,19 +48,25 @@ class DockerContainer:
bash_stdout: IO[bytes] bash_stdout: IO[bytes]
def __init__( def __init__(
self, *, docker_image: str, simulate_32_bit: bool = False, cwd: Optional[PathOrStr] = None self,
*,
image: str,
simulate_32_bit: bool = False,
cwd: Optional[PathOrStr] = None,
engine: ContainerEngine = "docker",
): ):
if not docker_image: if not image:
raise ValueError("Must have a non-empty docker image to run.") raise ValueError("Must have a non-empty image to run.")
self.docker_image = docker_image self.image = image
self.simulate_32_bit = simulate_32_bit self.simulate_32_bit = simulate_32_bit
self.cwd = cwd self.cwd = cwd
self.name: Optional[str] = None self.name: Optional[str] = None
self.engine = engine
def __enter__(self) -> "OCIContainer":
def __enter__(self) -> "DockerContainer":
self.name = f"cibuildwheel-{uuid.uuid4()}" self.name = f"cibuildwheel-{uuid.uuid4()}"
cwd_args = ["-w", str(self.cwd)] if self.cwd else []
# 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
@@ -57,24 +77,25 @@ class DockerContainer:
network_args = ["--network=host"] network_args = ["--network=host"]
shell_args = ["linux32", "/bin/bash"] if self.simulate_32_bit else ["/bin/bash"] shell_args = ["linux32", "/bin/bash"] if self.simulate_32_bit else ["/bin/bash"]
subprocess.run( subprocess.run(
[ [
"docker", self.engine,
"create", "create",
"--env=CIBUILDWHEEL", "--env=CIBUILDWHEEL",
f"--name={self.name}", f"--name={self.name}",
"--interactive", "--interactive",
"--volume=/:/host", # ignored on CircleCI "--volume=/:/host", # ignored on CircleCI
*network_args, *network_args,
*cwd_args, self.image,
self.docker_image,
*shell_args, *shell_args,
], ],
check=True, check=True,
) )
self.process = subprocess.Popen( self.process = subprocess.Popen(
[ [
"docker", self.engine,
"start", "start",
"--attach", "--attach",
"--interactive", "--interactive",
@@ -89,7 +110,13 @@ class DockerContainer:
self.bash_stdout = self.process.stdout self.bash_stdout = self.process.stdout
# run a noop command to block until the container is responding # run a noop command to block until the container is responding
self.call(["/bin/true"]) self.call(["/bin/true"], cwd="/")
if self.cwd:
# Although `docker create -w` does create the working dir if it
# does not exist, podman does not. There does not seem to be a way
# to setup a workdir for a container running in podman.
self.call(["mkdir", "-p", os.fspath(self.cwd)], cwd="/")
return self return self
@@ -106,10 +133,19 @@ class DockerContainer:
self.bash_stdin.close() self.bash_stdin.close()
self.bash_stdout.close() self.bash_stdout.close()
if self.engine == "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.
# For now, this seems to work "well enough".
self.process.wait()
assert isinstance(self.name, str) assert isinstance(self.name, str)
subprocess.run( subprocess.run(
["docker", "rm", "--force", "-v", self.name], stdout=subprocess.DEVNULL, check=False [self.engine, "rm", "--force", "-v", self.name],
stdout=subprocess.DEVNULL,
check=False,
) )
self.name = None self.name = None
@@ -122,7 +158,7 @@ class DockerContainer:
if from_path.is_dir(): if from_path.is_dir():
self.call(["mkdir", "-p", to_path]) self.call(["mkdir", "-p", to_path])
subprocess.run( subprocess.run(
f"tar cf - . | docker exec -i {self.name} tar --no-same-owner -xC {shell_quote(to_path)} -f -", f"tar cf - . | {self.engine} exec -i {self.name} tar --no-same-owner -xC {shell_quote(to_path)} -f -",
shell=True, shell=True,
check=True, check=True,
cwd=from_path, cwd=from_path,
@@ -130,7 +166,7 @@ class DockerContainer:
else: else:
with subprocess.Popen( with subprocess.Popen(
[ [
"docker", self.engine,
"exec", "exec",
"-i", "-i",
str(self.name), str(self.name),
@@ -139,28 +175,47 @@ class DockerContainer:
f"cat > {shell_quote(to_path)}", f"cat > {shell_quote(to_path)}",
], ],
stdin=subprocess.PIPE, stdin=subprocess.PIPE,
) as docker: ) as exec_process:
docker.stdin = cast(IO[bytes], docker.stdin) exec_process.stdin = cast(IO[bytes], exec_process.stdin)
with open(from_path, "rb") as from_file: with open(from_path, "rb") as from_file:
shutil.copyfileobj(from_file, docker.stdin) shutil.copyfileobj(from_file, exec_process.stdin)
docker.stdin.close() exec_process.stdin.close()
docker.wait() exec_process.wait()
if docker.returncode: if exec_process.returncode:
raise subprocess.CalledProcessError(docker.returncode, docker.args, None, None) 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)
if self.engine == "podman":
subprocess.run( subprocess.run(
f"docker exec -i {self.name} tar -cC {shell_quote(from_path)} -f - . | tar -xf -", [
self.engine,
"cp",
f"{self.name}:{from_path}/.",
str(to_path),
],
check=True,
cwd=to_path,
)
elif self.engine == "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 -"
subprocess.run(
command,
shell=True, shell=True,
check=True, check=True,
cwd=to_path, cwd=to_path,
) )
else:
raise KeyError(self.engine)
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)
@@ -186,6 +241,11 @@ class DockerContainer:
cwd: Optional[PathOrStr] = None, cwd: Optional[PathOrStr] = None,
) -> str: ) -> str:
if cwd is None:
# Podman does not start the a container in a specific working dir
# so we always need to specify it when making calls.
cwd = self.cwd
chdir = f"cd {cwd}" if cwd else "" chdir = f"cd {cwd}" if cwd else ""
env_assignments = ( env_assignments = (
" ".join(f"{shlex.quote(k)}={shlex.quote(v)}" for k, v in env.items()) " ".join(f"{shlex.quote(k)}={shlex.quote(v)}" for k, v in env.items())
@@ -271,6 +331,22 @@ class DockerContainer:
# used as an EnvironmentExecutor to evaluate commands and capture output # used as an EnvironmentExecutor to evaluate commands and capture output
return self.call(command, env=environment, capture_output=True) return self.call(command, env=environment, capture_output=True)
def debug_info(self) -> str:
if self.engine == "podman":
command = f"{self.engine} info --debug"
else:
command = f"{self.engine} info"
completed = subprocess.run(
command,
shell=True,
check=True,
cwd=self.cwd,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
)
output = str(completed.stdout, encoding="utf8", errors="surrogateescape")
return output
def shell_quote(path: PurePath) -> str: def shell_quote(path: PurePath) -> str:
return shlex.quote(str(path)) return shlex.quote(os.fspath(path))
+32 -9
View File
@@ -6,7 +6,18 @@ from configparser import ConfigParser
from contextlib import contextmanager from contextlib import contextmanager
from dataclasses import asdict, dataclass from dataclasses import asdict, dataclass
from pathlib import Path from pathlib import Path
from typing import Any, Dict, Generator, List, Mapping, Optional, Set, Tuple, Union from typing import (
Any,
Dict,
Generator,
List,
Mapping,
Optional,
Set,
Tuple,
Union,
cast,
)
if sys.version_info >= (3, 11): if sys.version_info >= (3, 11):
import tomllib import tomllib
@@ -17,6 +28,7 @@ from packaging.specifiers import SpecifierSet
from .architecture import Architecture from .architecture import Architecture
from .environment import EnvironmentParseError, ParsedEnvironment, parse_environment from .environment import EnvironmentParseError, ParsedEnvironment, parse_environment
from .oci_container import ContainerEngine
from .projectfiles import get_requires_python_str from .projectfiles import get_requires_python_str
from .typing import PLATFORMS, Literal, PlatformName, TypedDict from .typing import PLATFORMS, Literal, PlatformName, TypedDict
from .util import ( from .util import (
@@ -54,6 +66,7 @@ class GlobalOptions:
build_selector: BuildSelector build_selector: BuildSelector
test_selector: TestSelector test_selector: TestSelector
architectures: Set[Architecture] architectures: Set[Architecture]
container_engine: ContainerEngine
@dataclass(frozen=True) @dataclass(frozen=True)
@@ -386,12 +399,22 @@ class Options:
archs_config_str = args.archs or self.reader.get("archs", sep=" ") archs_config_str = args.archs or self.reader.get("archs", sep=" ")
architectures = Architecture.parse_config(archs_config_str, platform=self.platform) architectures = Architecture.parse_config(archs_config_str, platform=self.platform)
container_engine_str = self.reader.get("container-engine")
if container_engine_str not in ["docker", "podman"]:
msg = f"cibuildwheel: Unrecognised container_engine '{container_engine_str}', only 'docker' and 'podman' are supported"
print(msg, file=sys.stderr)
sys.exit(2)
container_engine = cast(ContainerEngine, container_engine_str)
return GlobalOptions( return GlobalOptions(
package_dir=package_dir, package_dir=package_dir,
output_dir=output_dir, output_dir=output_dir,
build_selector=build_selector, build_selector=build_selector,
test_selector=test_selector, test_selector=test_selector,
architectures=architectures, architectures=architectures,
container_engine=container_engine,
) )
def build_options(self, identifier: Optional[str]) -> BuildOptions: def build_options(self, identifier: Optional[str]) -> BuildOptions:
@@ -466,10 +489,10 @@ class Options:
manylinux_images: Dict[str, str] = {} manylinux_images: Dict[str, str] = {}
musllinux_images: Dict[str, str] = {} musllinux_images: Dict[str, str] = {}
if self.platform == "linux": if self.platform == "linux":
all_pinned_docker_images = _get_pinned_docker_images() all_pinned_container_images = _get_pinned_container_images()
for build_platform in MANYLINUX_ARCHS: for build_platform in MANYLINUX_ARCHS:
pinned_images = all_pinned_docker_images[build_platform] pinned_images = all_pinned_container_images[build_platform]
config_value = self.reader.get( config_value = self.reader.get(
f"manylinux-{build_platform}-image", ignore_empty=True f"manylinux-{build_platform}-image", ignore_empty=True
@@ -486,7 +509,7 @@ class Options:
manylinux_images[build_platform] = image manylinux_images[build_platform] = image
for build_platform in MUSLLINUX_ARCHS: for build_platform in MUSLLINUX_ARCHS:
pinned_images = all_pinned_docker_images[build_platform] pinned_images = all_pinned_container_images[build_platform]
config_value = self.reader.get(f"musllinux-{build_platform}-image") config_value = self.reader.get(f"musllinux-{build_platform}-image")
@@ -572,7 +595,7 @@ def compute_options(
@functools.lru_cache(maxsize=None) @functools.lru_cache(maxsize=None)
def _get_pinned_docker_images() -> Mapping[str, Mapping[str, str]]: def _get_pinned_container_images() -> Mapping[str, Mapping[str, str]]:
""" """
This looks like a dict of dicts, e.g. This looks like a dict of dicts, e.g.
{ 'x86_64': {'manylinux1': '...', 'manylinux2010': '...', 'manylinux2014': '...'}, { 'x86_64': {'manylinux1': '...', 'manylinux2010': '...', 'manylinux2014': '...'},
@@ -581,10 +604,10 @@ def _get_pinned_docker_images() -> Mapping[str, Mapping[str, str]]:
... } ... }
""" """
pinned_docker_images_file = resources_dir / "pinned_docker_images.cfg" pinned_images_file = resources_dir / "pinned_docker_images.cfg"
all_pinned_docker_images = ConfigParser() all_pinned_images = ConfigParser()
all_pinned_docker_images.read(pinned_docker_images_file) all_pinned_images.read(pinned_images_file)
return all_pinned_docker_images return all_pinned_images
def deprecated_selectors(name: str, selector: str, *, error: bool = False) -> None: def deprecated_selectors(name: str, selector: str, *, error: bool = False) -> None:
+2
View File
@@ -19,6 +19,8 @@ before-test = ""
test-requires = [] test-requires = []
test-extras = [] test-extras = []
container-engine = "docker"
manylinux-x86_64-image = "manylinux2014" manylinux-x86_64-image = "manylinux2014"
manylinux-i686-image = "manylinux2014" manylinux-i686-image = "manylinux2014"
manylinux-aarch64-image = "manylinux2014" manylinux-aarch64-image = "manylinux2014"
+3 -3
View File
@@ -8,7 +8,7 @@
<!-- the docker container outline --> <!-- the docker container outline -->
<div class="grid-outline docker"> <div class="grid-outline docker">
<div class="outline"> <div class="outline">
Manylinux Docker container Manylinux container
</div> </div>
</div> </div>
<!-- the venv outlines --> <!-- the venv outlines -->
@@ -76,7 +76,7 @@
steps: [ steps: [
[ [
{ {
label: 'copy project into docker', label: 'copy project into container',
platforms: ['linux'], platforms: ['linux'],
style: 'block', style: 'block',
width: 2, width: 2,
@@ -204,7 +204,7 @@
], ],
[ [
{ {
label: 'Copy wheels out of Docker', label: 'Copy wheels out of container',
platforms: ['linux'], platforms: ['linux'],
style: 'block', style: 'block',
width: 2, width: 2,
+5 -5
View File
@@ -4,15 +4,15 @@ title: Tips and tricks
## Tips ## Tips
### Linux builds on Docker ### Linux builds in containers
Linux wheels are built in the [`manylinux`/`musllinux` docker images](https://github.com/pypa/manylinux) to provide binary compatible wheels on Linux, according to [PEP 600](https://www.python.org/dev/peps/pep-0600/) / [PEP 656](https://www.python.org/dev/peps/pep-0656/). Because of this, when building with `cibuildwheel` on Linux, a few things should be taken into account: Linux wheels are built in [`manylinux`/`musllinux` containers](https://github.com/pypa/manylinux) to provide binary compatible wheels on Linux, according to [PEP 600](https://www.python.org/dev/peps/pep-0600/) / [PEP 656](https://www.python.org/dev/peps/pep-0656/). Because of this, when building with `cibuildwheel` on Linux, a few things should be taken into account:
- Programs and libraries are not installed on the CI runner host, but rather should be installed inside of the Docker image - using `yum` for `manylinux2010` or `manylinux2014`, `apt-get` for `manylinux_2_24` and `apk` for `musllinux_1_1`, or manually. The same goes for environment variables that are potentially needed to customize the wheel building. - Programs and libraries are not installed on the CI runner host, but rather should be installed inside the container - using `yum` for `manylinux2010` or `manylinux2014`, `apt-get` for `manylinux_2_24` and `apk` for `musllinux_1_1`, or manually. The same goes for environment variables that are potentially needed to customize the wheel building.
`cibuildwheel` supports this by providing the [`CIBW_ENVIRONMENT`](options.md#environment) and [`CIBW_BEFORE_ALL`](options.md#before-all) options to setup the build environment inside the running Docker image. `cibuildwheel` supports this by providing the [`CIBW_ENVIRONMENT`](options.md#environment) and [`CIBW_BEFORE_ALL`](options.md#before-all) options to setup the build environment inside the running container.
- The project directory is mounted in the running Docker instance as `/project`, the output directory for the wheels as `/output`. In general, this is handled transparently by `cibuildwheel`. For a more finegrained level of control however, the root of the host file system is mounted as `/host`, allowing for example to access shared files, caches, etc. on the host file system. Note that `/host` is not available on CircleCI due to their Docker policies. - The project directory is mounted inside the container as `/project`, the output directory for the wheels as `/output`. In general, this is handled transparently by `cibuildwheel`. For a more finegrained level of control however, the root of the host file system is mounted as `/host`, allowing for example to access shared files, caches, etc. on the host file system. Note that `/host` is not available on CircleCI due to their Docker policies.
- Alternative Docker images can be specified with the `CIBW_MANYLINUX_*_IMAGE`/`CIBW_MUSLLINUX_*_IMAGE` options to allow for a custom, preconfigured build environment for the Linux builds. See [options](options.md#linux-image) for more details. - Alternative Docker images can be specified with the `CIBW_MANYLINUX_*_IMAGE`/`CIBW_MUSLLINUX_*_IMAGE` options to allow for a custom, preconfigured build environment for the Linux builds. See [options](options.md#linux-image) for more details.
+48 -9
View File
@@ -117,9 +117,9 @@ matches overriding earlier ones if multiple selectors match. Environment
variables always override static configuration. variables always override static configuration.
A few of the options below have special handling in overrides. A different A few of the options below have special handling in overrides. A different
`before-all` will trigger a new docker launch on Linux, and cannot be `before-all` will trigger a new container to launch on Linux, and cannot be
overridden on macOS or Windows. Overriding the image on linux will also overridden on macOS or Windows. Overriding the image on linux will also
generate new docker launches, one per image. Some commands are not supported; trigger new containers, one per image. Some commands are not supported;
`output-dir`, build/skip/test_skip selectors, and architectures cannot be `output-dir`, build/skip/test_skip selectors, and architectures cannot be
overridden. overridden.
@@ -174,7 +174,7 @@ Default: `auto`
`auto` will auto-detect platform using environment variables, such as `TRAVIS_OS_NAME`/`APPVEYOR`/`CIRCLECI`. `auto` will auto-detect platform using environment variables, such as `TRAVIS_OS_NAME`/`APPVEYOR`/`CIRCLECI`.
- For `linux`, you need Docker running, on Linux, macOS, or Windows. - For `linux`, you need [Docker or Podman](#container-engine) running, on Linux, macOS, or Windows.
- For `macos` and `windows`, you need to be running on the respective system, with a working compiler toolchain installed - Xcode Command Line tools for macOS, and MSVC for Windows. - For `macos` and `windows`, you need to be running on the respective system, with a working compiler toolchain installed - Xcode Command Line tools for macOS, and MSVC for Windows.
This option can also be set using the [command-line option](#command-line) `--platform`. This option is not available in the `pyproject.toml` config. This option can also be set using the [command-line option](#command-line) `--platform`. This option is not available in the `pyproject.toml` config.
@@ -523,7 +523,7 @@ Choose which build backend to use. Can either be "pip", which will run
A list of environment variables to set during the build. Bash syntax should be used, even on Windows. A list of environment variables to set during the build. Bash syntax should be used, even on Windows.
You must set this variable to pass variables to Linux builds (since they execute in a Docker container). It also works for the other platforms. You must set this variable to pass variables to Linux builds (since they execute in a container). It also works for the other platforms.
You can use `$PATH` syntax to insert other variables, or the `$(pwd)` syntax to insert the output of other shell commands. You can use `$PATH` syntax to insert other variables, or the `$(pwd)` syntax to insert the output of other shell commands.
@@ -645,14 +645,14 @@ To specify more than one environment variable, separate the variable names by sp
Shell command to prepare a common part of the project (e.g. build or install libraries which does not depend on the specific version of Python). Shell command to prepare a common part of the project (e.g. build or install libraries which does not depend on the specific version of Python).
This option is very useful for the Linux build, where builds take place in isolated Docker containers managed by cibuildwheel. This command will run inside the container before the wheel builds start. Note, if you're building both `x86_64` and `i686` wheels (the default), your build uses two different Docker images. In that case, this command will execute twice - once per build container. This option is very useful for the Linux build, where builds take place in isolated containers managed by cibuildwheel. This command will run inside the container before the wheel builds start. Note, if you're building both `x86_64` and `i686` wheels (the default), your build uses two different container images. In that case, this command will execute twice - once per build container.
The placeholder `{package}` can be used here; it will be replaced by the path to the package being built by cibuildwheel. The placeholder `{package}` can be used here; it will be replaced by the path to the package being built by cibuildwheel.
On Windows and macOS, the version of Python available inside `CIBW_BEFORE_ALL` is whatever is available on the host machine. On Linux, a modern Python version is available on PATH. On Windows and macOS, the version of Python available inside `CIBW_BEFORE_ALL` is whatever is available on the host machine. On Linux, a modern Python version is available on PATH.
This option has special behavior in the overrides section in `pyproject.toml`. This option has special behavior in the overrides section in `pyproject.toml`.
On linux, overriding it triggers a new docker launch. It cannot be overridden On linux, overriding it triggers a new container launch. It cannot be overridden
on macOS and Windows. on macOS and Windows.
Platform-specific environment variables also available:<br/> Platform-specific environment variables also available:<br/>
@@ -874,7 +874,7 @@ Platform-specific environment variables are also available:<br/>
<div class="link-target" id="manylinux-image"></div> <div class="link-target" id="manylinux-image"></div>
### `CIBW_MANYLINUX_*_IMAGE`, `CIBW_MUSLLINUX_*_IMAGE` {: #linux-image} ### `CIBW_MANYLINUX_*_IMAGE`, `CIBW_MUSLLINUX_*_IMAGE` {: #linux-image}
> Specify alternative manylinux / musllinux Docker images > Specify alternative manylinux / musllinux container images
The available options are (default value): The available options are (default value):
@@ -901,9 +901,9 @@ For `CIBW_MUSLLINUX_*_IMAGE`, the value of this option can either be set to `mus
If this option is blank, it will fall though to the next available definition (environment variable -> pyproject.toml -> default). If this option is blank, it will fall though to the next available definition (environment variable -> pyproject.toml -> default).
If setting a custom Docker image, you'll need to make sure it can be used in the same way as the official, default Docker images: all necessary Python and pip versions need to be present in `/opt/python/`, and the auditwheel tool needs to be present for cibuildwheel to work. Apart from that, the architecture and relevant shared system libraries need to be compatible to the relevant standard to produce valid manylinux1/manylinux2010/manylinux2014/manylinux_2_24/manylinux_2_28/musllinux_1_1 wheels (see [pypa/manylinux on GitHub](https://github.com/pypa/manylinux), [PEP 513](https://www.python.org/dev/peps/pep-0513/), [PEP 571](https://www.python.org/dev/peps/pep-0571/), [PEP 599](https://www.python.org/dev/peps/pep-0599/), [PEP 600](https://www.python.org/dev/peps/pep-0600/) and [PEP 656](https://www.python.org/dev/peps/pep-0656/) for more details). If setting a custom image, you'll need to make sure it can be used in the same way as the default images: all necessary Python and pip versions need to be present in `/opt/python/`, and the auditwheel tool needs to be present for cibuildwheel to work. Apart from that, the architecture and relevant shared system libraries need to be compatible to the relevant standard to produce valid manylinux1/manylinux2010/manylinux2014/manylinux_2_24/manylinux_2_28/musllinux_1_1 wheels (see [pypa/manylinux on GitHub](https://github.com/pypa/manylinux), [PEP 513](https://www.python.org/dev/peps/pep-0513/), [PEP 571](https://www.python.org/dev/peps/pep-0571/), [PEP 599](https://www.python.org/dev/peps/pep-0599/), [PEP 600](https://www.python.org/dev/peps/pep-0600/) and [PEP 656](https://www.python.org/dev/peps/pep-0656/) for more details).
Auditwheel detects the version of the manylinux / musllinux standard in the Docker image through the `AUDITWHEEL_PLAT` environment variable, as cibuildwheel has no way of detecting the correct `--plat` command line argument to pass to auditwheel for a custom image. If a Docker image does not correctly set this `AUDITWHEEL_PLAT` environment variable, the `CIBW_ENVIRONMENT` option can be used to do so (e.g., `CIBW_ENVIRONMENT='AUDITWHEEL_PLAT="manylinux2010_$(uname -m)"'`). Auditwheel detects the version of the manylinux / musllinux standard in the image through the `AUDITWHEEL_PLAT` environment variable, as cibuildwheel has no way of detecting the correct `--plat` command line argument to pass to auditwheel for a custom image. If a custom image does not correctly set this `AUDITWHEEL_PLAT` environment variable, the `CIBW_ENVIRONMENT` option can be used to do so (e.g., `CIBW_ENVIRONMENT='AUDITWHEEL_PLAT="manylinux2010_$(uname -m)"'`).
#### Examples #### Examples
@@ -964,6 +964,44 @@ Auditwheel detects the version of the manylinux / musllinux standard in the Dock
Like any other option, these can be placed in `[tool.cibuildwheel.linux]` Like any other option, these can be placed in `[tool.cibuildwheel.linux]`
if you prefer; they have no effect on `macos` and `windows`. if you prefer; they have no effect on `macos` and `windows`.
### `CIBW_CONTAINER_ENGINE` {: #container-engine}
> Specify which container engine to use when building Linux wheels
Options: `docker` `podman`
Default: `docker`
Set the container engine to use. Docker is the default, or you can switch to
[Podman](https://podman.io/). To use Docker, you need to have a Docker daemon
running and `docker` available on PATH. To use Podman, it needs to be
installed and `podman` available on PATH.
!!! tip
While most users will stick with Docker, Podman is available in different
contexts - for example, it can be run inside a Docker container, or without
root access. Thanks to the [OCI], images are compatible between engines, so
you can still use the regular manylinux/musllinux containers.
[OCI]: https://opencontainers.org/
#### Examples
!!! tab examples "Environment variables"
```yaml
CIBW_CONTAINER_ENGINE: podman
```
!!! tab examples "pyproject.toml"
```toml
[tool.cibuildwheel]
container-engine = "podman"
```
### `CIBW_DEPENDENCY_VERSIONS` {: #dependency-versions} ### `CIBW_DEPENDENCY_VERSIONS` {: #dependency-versions}
> Specify how cibuildwheel controls the versions of the tools it uses > Specify how cibuildwheel controls the versions of the tools it uses
@@ -1029,6 +1067,7 @@ Platform-specific environment variables are also available:<br/>
dependency-versions = "./constraints.txt" dependency-versions = "./constraints.txt"
``` ```
## Testing ## Testing
### `CIBW_TEST_COMMAND` {: #test-command} ### `CIBW_TEST_COMMAND` {: #test-command}
+1
View File
@@ -14,6 +14,7 @@ extras = {
"pytest-timeout", "pytest-timeout",
"pytest-xdist", "pytest-xdist",
"build", "build",
"tomli_w",
], ],
"bin": [ "bin": [
"click", "click",
+1 -14
View File
@@ -7,20 +7,7 @@ def pytest_addoption(parser) -> None:
parser.addoption( parser.addoption(
"--run-emulation", action="store_true", default=False, help="run emulation tests" "--run-emulation", action="store_true", default=False, help="run emulation tests"
) )
parser.addoption("--run-podman", action="store_true", default=False, help="run podman tests")
def pytest_configure(config):
config.addinivalue_line("markers", "emulation: mark test requiring qemu binfmt_misc to run")
def pytest_collection_modifyitems(config, items) -> None:
if config.getoption("--run-emulation"):
# --run-emulation given in cli: do not skip emulation tests
return
skip_emulation = pytest.mark.skip(reason="need --run-emulation option to run")
for item in items:
if "emulation" in item.keywords:
item.add_marker(skip_emulation)
@pytest.fixture( @pytest.fixture(
+4 -2
View File
@@ -17,8 +17,10 @@ def test_spam():
""" """
@pytest.mark.emulation def test(tmp_path, request):
def test(tmp_path): if not request.config.getoption("--run-emulation"):
pytest.skip("needs --run-emulation option to run")
project_dir = tmp_path / "project" project_dir = tmp_path / "project"
project_with_a_test.generate(project_dir) project_with_a_test.generate(project_dir)
+1 -1
View File
@@ -53,7 +53,7 @@ project_with_manylinux_symbols = test_projects.new_c_project(
) )
def test(manylinux_image, tmp_path): def test(manylinux_image, tmp_path):
if utils.platform != "linux": if utils.platform != "linux":
pytest.skip("the docker test is only relevant to the linux build") pytest.skip("the container image test is only relevant to the linux build")
elif platform.machine() not in ["x86_64", "i686"]: elif platform.machine() not in ["x86_64", "i686"]:
if manylinux_image in ["manylinux1", "manylinux2010"]: if manylinux_image in ["manylinux1", "manylinux2010"]:
pytest.skip("manylinux1 and 2010 doesn't exist for non-x86 architectures") pytest.skip("manylinux1 and 2010 doesn't exist for non-x86 architectures")
+38
View File
@@ -0,0 +1,38 @@
import pytest
from . import test_projects, utils
basic_project = test_projects.new_c_project()
def test(tmp_path, capfd, request):
if utils.platform != "linux":
pytest.skip("the test is only relevant to the linux build")
if not request.config.getoption("--run-podman"):
pytest.skip("needs --run-podman option to run")
project_dir = tmp_path / "project"
basic_project.generate(project_dir)
# build some musllinux and manylinux wheels (ensuring that we use two containers)
actual_wheels = utils.cibuildwheel_run(
project_dir,
add_env={
"CIBW_BUILD": "cp310-*{manylinux,musllinux}_x86_64",
"CIBW_BEFORE_ALL": "echo 'test log statement from before-all'",
"CIBW_CONTAINER_ENGINE": "podman",
},
)
# check that the expected wheels are produced
expected_wheels = [
w
for w in utils.expected_wheels("spam", "0.1.0")
if ("-cp310-" in w) and ("x86_64" in w) and ("manylinux" in w or "musllinux" in w)
]
assert set(actual_wheels) == set(expected_wheels)
# check that stdout is bring passed-though from container correctly
captured = capfd.readouterr()
assert "test log statement from before-all" in captured.out
+1 -14
View File
@@ -8,20 +8,7 @@ MOCK_PACKAGE_DIR = Path("some_package_dir")
def pytest_addoption(parser): def pytest_addoption(parser):
parser.addoption("--run-docker", action="store_true", default=False, help="run docker tests") parser.addoption("--run-docker", action="store_true", default=False, help="run docker tests")
parser.addoption("--run-podman", action="store_true", default=False, help="run podman tests")
def pytest_configure(config):
config.addinivalue_line("markers", "docker: mark test requiring docker to run")
def pytest_collection_modifyitems(config, items):
if config.getoption("--run-docker"):
# --run-docker given in cli: do not skip docker tests
return
skip_docker = pytest.mark.skip(reason="need --run-docker option to run")
for item in items:
if "docker" in item.keywords:
item.add_marker(skip_docker)
@pytest.fixture @pytest.fixture
+9 -9
View File
@@ -2,8 +2,8 @@ import textwrap
from pathlib import Path from pathlib import Path
from pprint import pprint from pprint import pprint
import cibuildwheel.docker_container
import cibuildwheel.linux import cibuildwheel.linux
import cibuildwheel.oci_container
from cibuildwheel.options import Options from cibuildwheel.options import Options
from .utils import get_default_command_line_arguments from .utils import get_default_command_line_arguments
@@ -11,7 +11,7 @@ from .utils import get_default_command_line_arguments
def test_linux_container_split(tmp_path: Path, monkeypatch): def test_linux_container_split(tmp_path: Path, monkeypatch):
""" """
Tests splitting linux builds by docker image and before_all Tests splitting linux builds by container image and before_all
""" """
args = get_default_command_line_arguments() args = get_default_command_line_arguments()
@@ -21,16 +21,16 @@ def test_linux_container_split(tmp_path: Path, monkeypatch):
textwrap.dedent( textwrap.dedent(
""" """
[tool.cibuildwheel] [tool.cibuildwheel]
manylinux-x86_64-image = "normal_docker_image" manylinux-x86_64-image = "normal_container_image"
manylinux-i686-image = "normal_docker_image" manylinux-i686-image = "normal_container_image"
build = "*-manylinux_x86_64" build = "*-manylinux_x86_64"
skip = "pp*" skip = "pp*"
archs = "x86_64 i686" archs = "x86_64 i686"
[[tool.cibuildwheel.overrides]] [[tool.cibuildwheel.overrides]]
select = "cp{38,39,310}-*" select = "cp{38,39,310}-*"
manylinux-x86_64-image = "other_docker_image" manylinux-x86_64-image = "other_container_image"
manylinux-i686-image = "other_docker_image" manylinux-i686-image = "other_container_image"
[[tool.cibuildwheel.overrides]] [[tool.cibuildwheel.overrides]]
select = "cp39-*" select = "cp39-*"
@@ -57,14 +57,14 @@ def test_linux_container_split(tmp_path: Path, monkeypatch):
pprint(build_steps) pprint(build_steps)
assert build_steps[0].docker_image == "normal_docker_image" assert build_steps[0].container_image == "normal_container_image"
assert identifiers(build_steps[0]) == ["cp36-manylinux_x86_64", "cp37-manylinux_x86_64"] assert identifiers(build_steps[0]) == ["cp36-manylinux_x86_64", "cp37-manylinux_x86_64"]
assert before_alls(build_steps[0]) == ["", ""] assert before_alls(build_steps[0]) == ["", ""]
assert build_steps[1].docker_image == "other_docker_image" assert build_steps[1].container_image == "other_container_image"
assert identifiers(build_steps[1]) == ["cp38-manylinux_x86_64", "cp310-manylinux_x86_64"] assert identifiers(build_steps[1]) == ["cp38-manylinux_x86_64", "cp310-manylinux_x86_64"]
assert before_alls(build_steps[1]) == ["", ""] assert before_alls(build_steps[1]) == ["", ""]
assert build_steps[2].docker_image == "other_docker_image" assert build_steps[2].container_image == "other_container_image"
assert identifiers(build_steps[2]) == ["cp39-manylinux_x86_64"] assert identifiers(build_steps[2]) == ["cp39-manylinux_x86_64"]
assert before_alls(build_steps[2]) == ["echo 'a cp39-only command'"] assert before_alls(build_steps[2]) == ["echo 'a cp39-only command'"]
+2 -2
View File
@@ -11,7 +11,7 @@ else:
from cibuildwheel.__main__ import main from cibuildwheel.__main__ import main
from cibuildwheel.environment import ParsedEnvironment from cibuildwheel.environment import ParsedEnvironment
from cibuildwheel.options import BuildOptions, _get_pinned_docker_images from cibuildwheel.options import BuildOptions, _get_pinned_container_images
from cibuildwheel.util import BuildSelector, resources_dir from cibuildwheel.util import BuildSelector, resources_dir
# CIBW_PLATFORM is tested in main_platform_test.py # CIBW_PLATFORM is tested in main_platform_test.py
@@ -339,6 +339,6 @@ def test_defaults(platform, intercepted_build_args):
if platform == "linux": if platform == "linux":
assert build_options.manylinux_images assert build_options.manylinux_images
pinned_images = _get_pinned_docker_images() pinned_images = _get_pinned_container_images()
default_x86_64_image = pinned_images["x86_64"][defaults["manylinux-x86_64-image"]] default_x86_64_image = pinned_images["x86_64"][defaults["manylinux-x86_64-image"]]
assert build_options.manylinux_images["x86_64"] == default_x86_64_image assert build_options.manylinux_images["x86_64"] == default_x86_64_image
@@ -1,3 +1,4 @@
import os
import platform import platform
import random import random
import shutil import shutil
@@ -6,9 +7,12 @@ import textwrap
from pathlib import Path, PurePath, PurePosixPath from pathlib import Path, PurePath, PurePosixPath
import pytest import pytest
import tomli_w
from cibuildwheel.docker_container import DockerContainer
from cibuildwheel.environment import EnvironmentAssignmentBash from cibuildwheel.environment import EnvironmentAssignmentBash
from cibuildwheel.oci_container import OCIContainer
# Test utilities
# for these tests we use manylinux2014 images, because they're available on # for these tests we use manylinux2014 images, because they're available on
# multi architectures and include python3.8 # multi architectures and include python3.8
@@ -21,23 +25,41 @@ elif pm == "ppc64le":
DEFAULT_IMAGE = "quay.io/pypa/manylinux2014_ppc64le:2020-05-17-2f8ac3b" DEFAULT_IMAGE = "quay.io/pypa/manylinux2014_ppc64le:2020-05-17-2f8ac3b"
elif pm == "s390x": elif pm == "s390x":
DEFAULT_IMAGE = "quay.io/pypa/manylinux2014_s390x:2020-05-17-2f8ac3b" DEFAULT_IMAGE = "quay.io/pypa/manylinux2014_s390x:2020-05-17-2f8ac3b"
else:
DEFAULT_IMAGE = ""
@pytest.mark.docker @pytest.fixture(params=["docker", "podman"])
def test_simple(): def container_engine(request):
with DockerContainer(docker_image=DEFAULT_IMAGE) as container: if request.param == "docker" and not request.config.getoption("--run-docker"):
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
# Tests
def test_simple(container_engine):
with OCIContainer(engine=container_engine, image=DEFAULT_IMAGE) as container:
assert container.call(["echo", "hello"], capture_output=True) == "hello\n" assert container.call(["echo", "hello"], capture_output=True) == "hello\n"
@pytest.mark.docker def test_no_lf(container_engine):
def test_no_lf(): with OCIContainer(engine=container_engine, image=DEFAULT_IMAGE) as container:
with DockerContainer(docker_image=DEFAULT_IMAGE) as container:
assert container.call(["printf", "hello"], capture_output=True) == "hello" assert container.call(["printf", "hello"], capture_output=True) == "hello"
@pytest.mark.docker def test_debug_info(container_engine):
def test_environment(): container = OCIContainer(engine=container_engine, image=DEFAULT_IMAGE)
with DockerContainer(docker_image=DEFAULT_IMAGE) as container: print(container.debug_info())
with container:
pass
def test_environment(container_engine):
with OCIContainer(engine=container_engine, image=DEFAULT_IMAGE) 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
@@ -46,20 +68,18 @@ def test_environment():
) )
@pytest.mark.docker def test_cwd(container_engine):
def test_cwd(): with OCIContainer(
with DockerContainer( engine=container_engine, image=DEFAULT_IMAGE, cwd="/cibuildwheel/working_directory"
docker_image=DEFAULT_IMAGE, 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.docker def test_container_removed(container_engine):
def test_container_removed(): with OCIContainer(engine=container_engine, image=DEFAULT_IMAGE) as container:
with DockerContainer(docker_image=DEFAULT_IMAGE) as container:
docker_containers_listing = subprocess.run( docker_containers_listing = subprocess.run(
"docker container ls", f"{container.engine} container ls",
shell=True, shell=True,
check=True, check=True,
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
@@ -70,7 +90,7 @@ def test_container_removed():
old_container_name = container.name old_container_name = container.name
docker_containers_listing = subprocess.run( docker_containers_listing = subprocess.run(
"docker container ls", f"{container.engine} container ls",
shell=True, shell=True,
check=True, check=True,
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
@@ -79,8 +99,7 @@ def test_container_removed():
assert old_container_name not in docker_containers_listing assert old_container_name not in docker_containers_listing
@pytest.mark.docker def test_large_environment(container_engine):
def test_large_environment():
# max environment variable size is 128kB # max environment variable size is 128kB
long_env_var_length = 127 * 1024 long_env_var_length = 127 * 1024
large_environment = { large_environment = {
@@ -90,7 +109,7 @@ def test_large_environment():
"d": "0" * long_env_var_length, "d": "0" * long_env_var_length,
} }
with DockerContainer(docker_image=DEFAULT_IMAGE) as container: with OCIContainer(engine=container_engine, image=DEFAULT_IMAGE) 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)
@@ -98,9 +117,8 @@ def test_large_environment():
) )
@pytest.mark.docker def test_binary_output(container_engine):
def test_binary_output(): with OCIContainer(engine=container_engine, image=DEFAULT_IMAGE) as container:
with DockerContainer(docker_image=DEFAULT_IMAGE) 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
@@ -149,9 +167,8 @@ def test_binary_output():
assert output == binary_data_string assert output == binary_data_string
@pytest.mark.docker def test_file_operation(tmp_path: Path, container_engine):
def test_file_operations(tmp_path: Path): with OCIContainer(engine=container_engine, image=DEFAULT_IMAGE) as container:
with DockerContainer(docker_image=DEFAULT_IMAGE) 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"
@@ -165,9 +182,8 @@ def test_file_operations(tmp_path: Path):
assert test_binary_data == bytes(output, encoding="utf8", errors="surrogateescape") assert test_binary_data == bytes(output, encoding="utf8", errors="surrogateescape")
@pytest.mark.docker def test_dir_operations(tmp_path: Path, container_engine):
def test_dir_operations(tmp_path: Path): with OCIContainer(engine=container_engine, image=DEFAULT_IMAGE) as container:
with DockerContainer(docker_image=DEFAULT_IMAGE) 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)
@@ -195,8 +211,86 @@ def test_dir_operations(tmp_path: Path):
assert test_binary_data == (new_test_dir / "test.dat").read_bytes() assert test_binary_data == (new_test_dir / "test.dat").read_bytes()
@pytest.mark.docker def test_environment_executor(container_engine):
def test_environment_executor(): with OCIContainer(engine=container_engine, image=DEFAULT_IMAGE) as container:
with DockerContainer(docker_image=DEFAULT_IMAGE) 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"
def test_podman_vfs(tmp_path: Path, monkeypatch, request):
# Tests podman VFS, for the podman in docker use-case
if not request.config.getoption("--run-podman"):
pytest.skip("need --run-podman option to run")
# create the VFS configuration
vfs_path = tmp_path / "podman_vfs"
vfs_path.mkdir()
# This requires that we write configuration files and point to them
# with environment variables before we run podman
# https://github.com/containers/common/blob/main/docs/containers.conf.5.md
vfs_containers_conf_data = {
"containers": {
"default_capabilities": [
"CHOWN",
"DAC_OVERRIDE",
"FOWNER",
"FSETID",
"KILL",
"NET_BIND_SERVICE",
"SETFCAP",
"SETGID",
"SETPCAP",
"SETUID",
"SYS_CHROOT",
]
},
"engine": {"cgroup_manager": "cgroupfs", "events_logger": "file"},
}
# https://github.com/containers/storage/blob/main/docs/containers-storage.conf.5.md
storage_root = vfs_path / ".local/share/containers/vfs-storage"
run_root = vfs_path / ".local/share/containers/vfs-runroot"
storage_root.mkdir(parents=True, exist_ok=True)
run_root.mkdir(parents=True, exist_ok=True)
vfs_containers_storage_conf_data = {
"storage": {
"driver": "vfs",
"graphroot": os.fspath(storage_root),
"runroot": os.fspath(run_root),
"rootless_storage_path": os.fspath(storage_root),
"options": {
# "remap-user": "containers",
"aufs": {"mountopt": "rw"},
"overlay": {"mountopt": "rw", "force_mask": "shared"},
# "vfs": {"ignore_chown_errors": "true"},
},
}
}
vfs_containers_conf_fpath = vfs_path / "temp_vfs_containers.conf"
vfs_containers_storage_conf_fpath = vfs_path / "temp_vfs_containers_storage.conf"
with open(vfs_containers_conf_fpath, "wb") as file:
tomli_w.dump(vfs_containers_conf_data, file)
with open(vfs_containers_storage_conf_fpath, "wb") as file:
tomli_w.dump(vfs_containers_storage_conf_data, file)
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:
# test running a command
assert container.call(["echo", "hello"], capture_output=True) == "hello\n"
# test copying a file into the container
(tmp_path / "some_file.txt").write_text("1234")
container.copy_into(tmp_path / "some_file.txt", PurePosixPath("some_file.txt"))
assert container.call(["cat", "some_file.txt"], capture_output=True) == "1234"
# Clean up
# When using the VFS, user is not given write permissions by default in
# new directories. As a workaround we use 'podman unshare' to delete them
# 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)
+51 -49
View File
@@ -15,7 +15,7 @@ ALL_IDS = {"cp36", "cp37", "cp38", "cp39", "cp310", "pp37", "pp38", "pp39"}
@pytest.fixture @pytest.fixture
def mock_build_docker(monkeypatch): def mock_build_container(monkeypatch):
def fail_on_call(*args, **kwargs): def fail_on_call(*args, **kwargs):
raise RuntimeError("This should never be called") raise RuntimeError("This should never be called")
@@ -35,58 +35,60 @@ def mock_build_docker(monkeypatch):
monkeypatch.setattr(subprocess, "Popen", fail_on_call) monkeypatch.setattr(subprocess, "Popen", fail_on_call)
monkeypatch.setattr(subprocess, "run", ignore_call) monkeypatch.setattr(subprocess, "run", ignore_call)
monkeypatch.setattr(util, "download", fail_on_call) monkeypatch.setattr(util, "download", fail_on_call)
monkeypatch.setattr("cibuildwheel.linux.DockerContainer", ignore_context_call) monkeypatch.setattr("cibuildwheel.linux.OCIContainer", ignore_context_call)
monkeypatch.setattr("cibuildwheel.linux.build_on_docker", mock.Mock(spec=linux.build_on_docker)) monkeypatch.setattr(
"cibuildwheel.linux.build_in_container", mock.Mock(spec=linux.build_in_container)
)
monkeypatch.setattr("cibuildwheel.util.print_new_wheels", ignore_context_call) monkeypatch.setattr("cibuildwheel.util.print_new_wheels", ignore_context_call)
def test_build_default_launches(mock_build_docker, fake_package_dir, monkeypatch): def test_build_default_launches(mock_build_container, fake_package_dir, monkeypatch):
monkeypatch.setattr(sys, "argv", ["cibuildwheel", "--platform=linux"]) monkeypatch.setattr(sys, "argv", ["cibuildwheel", "--platform=linux"])
main() main()
build_on_docker = cast(mock.Mock, linux.build_on_docker) build_in_container = cast(mock.Mock, linux.build_in_container)
assert build_on_docker.call_count == 4 assert build_in_container.call_count == 4
# In Python 3.8+, this can be simplified to [0].kwargs # In Python 3.8+, this can be simplified to [0].kwargs
kwargs = build_on_docker.call_args_list[0][1] kwargs = build_in_container.call_args_list[0][1]
assert "quay.io/pypa/manylinux2014_x86_64" in kwargs["docker"]["docker_image"] assert "quay.io/pypa/manylinux2014_x86_64" in kwargs["container"]["image"]
assert kwargs["docker"]["cwd"] == PurePosixPath("/project") assert kwargs["container"]["cwd"] == PurePosixPath("/project")
assert not kwargs["docker"]["simulate_32_bit"] assert not kwargs["container"]["simulate_32_bit"]
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}
kwargs = build_on_docker.call_args_list[1][1] kwargs = build_in_container.call_args_list[1][1]
assert "quay.io/pypa/manylinux2014_i686" in kwargs["docker"]["docker_image"] assert "quay.io/pypa/manylinux2014_i686" in kwargs["container"]["image"]
assert kwargs["docker"]["cwd"] == PurePosixPath("/project") assert kwargs["container"]["cwd"] == PurePosixPath("/project")
assert kwargs["docker"]["simulate_32_bit"] assert kwargs["container"]["simulate_32_bit"]
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}
kwargs = build_on_docker.call_args_list[2][1] kwargs = build_in_container.call_args_list[2][1]
assert "quay.io/pypa/musllinux_1_1_x86_64" in kwargs["docker"]["docker_image"] assert "quay.io/pypa/musllinux_1_1_x86_64" in kwargs["container"]["image"]
assert kwargs["docker"]["cwd"] == PurePosixPath("/project") assert kwargs["container"]["cwd"] == PurePosixPath("/project")
assert not kwargs["docker"]["simulate_32_bit"] assert not kwargs["container"]["simulate_32_bit"]
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 for x in ALL_IDS if "pp" not in x f"{x}-musllinux_x86_64" for x in ALL_IDS for x in ALL_IDS if "pp" not in x
} }
kwargs = build_on_docker.call_args_list[3][1] kwargs = build_in_container.call_args_list[3][1]
assert "quay.io/pypa/musllinux_1_1_i686" in kwargs["docker"]["docker_image"] assert "quay.io/pypa/musllinux_1_1_i686" in kwargs["container"]["image"]
assert kwargs["docker"]["cwd"] == PurePosixPath("/project") assert kwargs["container"]["cwd"] == PurePosixPath("/project")
assert kwargs["docker"]["simulate_32_bit"] assert kwargs["container"]["simulate_32_bit"]
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}
def test_build_with_override_launches(mock_build_docker, monkeypatch, tmp_path): def test_build_with_override_launches(mock_build_container, monkeypatch, tmp_path):
pkg_dir = tmp_path / "cibw_package" pkg_dir = tmp_path / "cibw_package"
pkg_dir.mkdir() pkg_dir.mkdir()
@@ -113,23 +115,23 @@ before-all = "true"
main() main()
build_on_docker = cast(mock.Mock, linux.build_on_docker) build_in_container = cast(mock.Mock, linux.build_in_container)
assert build_on_docker.call_count == 6 assert build_in_container.call_count == 6
kwargs = build_on_docker.call_args_list[0][1] kwargs = build_in_container.call_args_list[0][1]
assert "quay.io/pypa/manylinux2014_x86_64" in kwargs["docker"]["docker_image"] assert "quay.io/pypa/manylinux2014_x86_64" in kwargs["container"]["image"]
assert kwargs["docker"]["cwd"] == PurePosixPath("/project") assert kwargs["container"]["cwd"] == PurePosixPath("/project")
assert not kwargs["docker"]["simulate_32_bit"] assert not kwargs["container"]["simulate_32_bit"]
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"}
assert kwargs["options"].build_options("cp36-manylinux_x86_64").before_all == "true" assert kwargs["options"].build_options("cp36-manylinux_x86_64").before_all == "true"
kwargs = build_on_docker.call_args_list[1][1] kwargs = build_in_container.call_args_list[1][1]
assert "quay.io/pypa/manylinux2014_x86_64" in kwargs["docker"]["docker_image"] assert "quay.io/pypa/manylinux2014_x86_64" in kwargs["container"]["image"]
assert kwargs["docker"]["cwd"] == PurePosixPath("/project") assert kwargs["container"]["cwd"] == PurePosixPath("/project")
assert not kwargs["docker"]["simulate_32_bit"] assert not kwargs["container"]["simulate_32_bit"]
identifiers = {x.identifier for x in kwargs["platform_configs"]} identifiers = {x.identifier for x in kwargs["platform_configs"]}
assert identifiers == { assert identifiers == {
@@ -137,10 +139,10 @@ before-all = "true"
} }
assert kwargs["options"].build_options("cp37-manylinux_x86_64").before_all == "" assert kwargs["options"].build_options("cp37-manylinux_x86_64").before_all == ""
kwargs = build_on_docker.call_args_list[2][1] kwargs = build_in_container.call_args_list[2][1]
assert "quay.io/pypa/manylinux_2_24_x86_64" in kwargs["docker"]["docker_image"] assert "quay.io/pypa/manylinux_2_24_x86_64" in kwargs["container"]["image"]
assert kwargs["docker"]["cwd"] == PurePosixPath("/project") assert kwargs["container"]["cwd"] == PurePosixPath("/project")
assert not kwargs["docker"]["simulate_32_bit"] assert not kwargs["container"]["simulate_32_bit"]
identifiers = {x.identifier for x in kwargs["platform_configs"]} identifiers = {x.identifier for x in kwargs["platform_configs"]}
assert identifiers == { assert identifiers == {
"cp310-manylinux_x86_64", "cp310-manylinux_x86_64",
@@ -149,28 +151,28 @@ before-all = "true"
"pp39-manylinux_x86_64", "pp39-manylinux_x86_64",
} }
kwargs = build_on_docker.call_args_list[3][1] kwargs = build_in_container.call_args_list[3][1]
assert "quay.io/pypa/manylinux2014_i686" in kwargs["docker"]["docker_image"] assert "quay.io/pypa/manylinux2014_i686" in kwargs["container"]["image"]
assert kwargs["docker"]["cwd"] == PurePosixPath("/project") assert kwargs["container"]["cwd"] == PurePosixPath("/project")
assert kwargs["docker"]["simulate_32_bit"] assert kwargs["container"]["simulate_32_bit"]
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}
kwargs = build_on_docker.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["docker"]["docker_image"] assert "quay.io/pypa/musllinux_1_1_x86_64" in kwargs["container"]["image"]
assert kwargs["docker"]["cwd"] == PurePosixPath("/project") assert kwargs["container"]["cwd"] == PurePosixPath("/project")
assert not kwargs["docker"]["simulate_32_bit"] assert not kwargs["container"]["simulate_32_bit"]
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 for x in ALL_IDS if "pp" not in x f"{x}-musllinux_x86_64" for x in ALL_IDS for x in ALL_IDS if "pp" not in x
} }
kwargs = build_on_docker.call_args_list[5][1] kwargs = build_in_container.call_args_list[5][1]
assert "quay.io/pypa/musllinux_1_1_i686" in kwargs["docker"]["docker_image"] assert "quay.io/pypa/musllinux_1_1_i686" in kwargs["container"]["image"]
assert kwargs["docker"]["cwd"] == PurePosixPath("/project") assert kwargs["container"]["cwd"] == PurePosixPath("/project")
assert kwargs["docker"]["simulate_32_bit"] assert kwargs["container"]["simulate_32_bit"]
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}
+5 -5
View File
@@ -4,7 +4,7 @@ import pytest
from cibuildwheel.__main__ import get_build_identifiers from cibuildwheel.__main__ import get_build_identifiers
from cibuildwheel.environment import parse_environment from cibuildwheel.environment import parse_environment
from cibuildwheel.options import Options, _get_pinned_docker_images from cibuildwheel.options import Options, _get_pinned_container_images
from .utils import get_default_command_line_arguments from .utils import get_default_command_line_arguments
@@ -58,18 +58,18 @@ test_command: 'pyproject'
assert default_build_options.environment == parse_environment('FOO="BAR"') assert default_build_options.environment == parse_environment('FOO="BAR"')
all_pinned_docker_images = _get_pinned_docker_images() all_pinned_container_images = _get_pinned_container_images()
pinned_x86_64_docker_image = all_pinned_docker_images["x86_64"] pinned_x86_64_container_image = all_pinned_container_images["x86_64"]
local = options.build_options("cp38-manylinux_x86_64") local = options.build_options("cp38-manylinux_x86_64")
assert local.manylinux_images is not None assert local.manylinux_images is not None
assert local.test_command == "pyproject" assert local.test_command == "pyproject"
assert local.manylinux_images["x86_64"] == pinned_x86_64_docker_image["manylinux1"] assert local.manylinux_images["x86_64"] == pinned_x86_64_container_image["manylinux1"]
local = options.build_options("cp37-manylinux_x86_64") local = options.build_options("cp37-manylinux_x86_64")
assert local.manylinux_images is not None assert local.manylinux_images is not None
assert local.test_command == "pyproject-override" assert local.test_command == "pyproject-override"
assert local.manylinux_images["x86_64"] == pinned_x86_64_docker_image["manylinux2014"] assert local.manylinux_images["x86_64"] == pinned_x86_64_container_image["manylinux2014"]
def test_passthrough(tmp_path, monkeypatch): def test_passthrough(tmp_path, monkeypatch):