feat: adding overrides

This commit is contained in:
Henry Schreiner
2021-10-16 21:34:02 -04:00
parent f511961b40
commit cd07697fe5
13 changed files with 405 additions and 89 deletions
+1
View File
@@ -59,6 +59,7 @@ repos:
- tomli - tomli
- types-certifi - types-certifi
- types-click - types-click
- types-dataclasses
- types-jinja2 - types-jinja2
- types-pyyaml - types-pyyaml
- types-requests - types-requests
+17 -14
View File
@@ -14,7 +14,7 @@ from cibuildwheel.architecture import Architecture, allowed_architectures_check
from cibuildwheel.options import compute_options from cibuildwheel.options import compute_options
from cibuildwheel.typing import PLATFORMS, PlatformName, assert_never from cibuildwheel.typing import PLATFORMS, PlatformName, assert_never
from cibuildwheel.util import ( from cibuildwheel.util import (
BuildOptions, AllBuildOptions,
BuildSelector, BuildSelector,
Unbuffered, Unbuffered,
detect_ci_provider, detect_ci_provider,
@@ -148,12 +148,12 @@ def main() -> None:
else os.environ.get("CIBW_OUTPUT_DIR", "wheelhouse") else os.environ.get("CIBW_OUTPUT_DIR", "wheelhouse")
) )
build_options = compute_options( all_build_options, build_options_by_selector = compute_options(
platform, package_dir, output_dir, args.config_file, args.archs, args.prerelease_pythons platform, package_dir, output_dir, args.config_file, args.archs, args.prerelease_pythons
) )
identifiers = get_build_identifiers( identifiers = get_build_identifiers(
platform, build_options.build_selector, build_options.architectures platform, all_build_options.build_selector, all_build_options.architectures
) )
if args.print_build_identifiers: if args.print_build_identifiers:
@@ -161,6 +161,8 @@ def main() -> None:
print(identifier) print(identifier)
sys.exit(0) sys.exit(0)
build_options = AllBuildOptions(all_build_options, build_options_by_selector, identifiers)
# Add CIBUILDWHEEL environment variable # Add CIBUILDWHEEL environment variable
# This needs to be passed on to the docker container in linux.py # This needs to be passed on to the docker container in linux.py
os.environ["CIBUILDWHEEL"] = "1" os.environ["CIBUILDWHEEL"] = "1"
@@ -200,7 +202,7 @@ def main() -> None:
assert_never(platform) assert_never(platform)
def print_preamble(platform: str, build_options: BuildOptions) -> None: def print_preamble(platform: str, build_options: AllBuildOptions) -> None:
print( print(
textwrap.dedent( textwrap.dedent(
""" """
@@ -254,20 +256,21 @@ def get_build_identifiers(
return [config.identifier for config in python_configurations] return [config.identifier for config in python_configurations]
def detect_warnings(platform: str, build_options: BuildOptions) -> List[str]: def detect_warnings(platform: str, all_options: AllBuildOptions) -> List[str]:
warnings = [] warnings = []
# warn about deprecated {python} and {pip} # warn about deprecated {python} and {pip}
for option_name in ["test_command", "before_build"]: for build_options in all_options.values():
option_value = getattr(build_options, option_name) for option_name in ["test_command", "before_build"]:
option_value = getattr(build_options, option_name)
if option_value and ("{python}" in option_value or "{pip}" in option_value): if option_value and ("{python}" in option_value or "{pip}" in option_value):
# Reminder: in an f-string, double braces means literal single brace # Reminder: in an f-string, double braces means literal single brace
msg = ( msg = (
f"{option_name}: '{{python}}' and '{{pip}}' are no longer needed, " f"{option_name}: '{{python}}' and '{{pip}}' are no longer needed, "
"and will be removed in a future release. Simply use 'python' or 'pip' instead." "and will be removed in a future release. Simply use 'python' or 'pip' instead."
) )
warnings.append(msg) warnings.append(msg)
return warnings return warnings
+9 -3
View File
@@ -1,3 +1,4 @@
import dataclasses
from typing import Dict, List, Mapping, Optional from typing import Dict, List, Mapping, Optional
import bashlex import bashlex
@@ -60,10 +61,15 @@ class EnvironmentAssignment:
def __repr__(self) -> str: def __repr__(self) -> str:
return f"{self.name}={self.value}" return f"{self.name}={self.value}"
def __eq__(self, other: object) -> bool:
if isinstance(other, EnvironmentAssignment):
return self.name == other.name and self.value == other.value
return False
@dataclasses.dataclass
class ParsedEnvironment: class ParsedEnvironment:
def __init__(self, assignments: List[EnvironmentAssignment]): assignments: List[EnvironmentAssignment]
self.assignments = assignments
def as_dictionary( def as_dictionary(
self, self,
@@ -82,7 +88,7 @@ class ParsedEnvironment:
return [a.as_shell_assignment() for a in self.assignments] return [a.as_shell_assignment() for a in self.assignments]
def __repr__(self) -> str: def __repr__(self) -> str:
return f"ParsedEnvironment({[repr(a) for a in self.assignments]!r})" return f"{self.__class__.__name__}({[repr(a) for a in self.assignments]!r})"
def parse_environment(env_string: str) -> ParsedEnvironment: def parse_environment(env_string: str) -> ParsedEnvironment:
+26 -28
View File
@@ -9,7 +9,7 @@ from .docker_container import DockerContainer
from .logger import log from .logger import log
from .typing import PathOrStr, assert_never from .typing import PathOrStr, assert_never
from .util import ( from .util import (
BuildOptions, AllBuildOptions,
BuildSelector, BuildSelector,
NonPlatformWheelError, NonPlatformWheelError,
get_build_verbosity_extra_flags, get_build_verbosity_extra_flags,
@@ -54,7 +54,7 @@ def get_python_configurations(
def get_build_steps( def get_build_steps(
options: BuildOptions, python_configurations: List[PythonConfiguration] all_options: AllBuildOptions, python_configurations: List[PythonConfiguration]
) -> Iterator[BuildStep]: ) -> Iterator[BuildStep]:
platforms = [ platforms = [
("cp", "manylinux_x86_64", "x86_64"), ("cp", "manylinux_x86_64", "x86_64"),
@@ -73,15 +73,6 @@ def get_build_steps(
] ]
for implementation, platform_tag, platform_arch in platforms: for implementation, platform_tag, platform_arch in platforms:
assert options.manylinux_images is not None
assert options.musllinux_images is not None
docker_image = (
options.manylinux_images[platform_arch]
if platform_tag.startswith("manylinux")
else options.musllinux_images[platform_arch]
)
platform_configs = [ platform_configs = [
c c
for c in python_configurations for c in python_configurations
@@ -90,11 +81,15 @@ def get_build_steps(
if not platform_configs: if not platform_configs:
continue continue
yield BuildStep(platform_configs, platform_tag, docker_image) for local_configs, docker_image in all_options.produce_image_batches(
platform_configs, platform_tag, platform_arch
):
# TODO: Validate that the options are not invalid for these selectors
yield BuildStep(local_configs, platform_tag, docker_image)
def build_on_docker( def build_on_docker(
options: BuildOptions, all_options: AllBuildOptions,
platform_configs: List[PythonConfiguration], platform_configs: List[PythonConfiguration],
docker: DockerContainer, docker: DockerContainer,
container_project_path: PurePath, container_project_path: PurePath,
@@ -105,16 +100,16 @@ def build_on_docker(
log.step("Copying project into Docker...") log.step("Copying project into Docker...")
docker.copy_into(Path.cwd(), container_project_path) docker.copy_into(Path.cwd(), container_project_path)
if options.before_all: if all_options.before_all:
log.step("Running before_all...") log.step("Running before_all...")
env = docker.get_environment() env = docker.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 = options.environment.as_dictionary(env, executor=docker.environment_executor) env = all_options.environment.as_dictionary(env, executor=docker.environment_executor)
before_all_prepared = prepare_command( before_all_prepared = prepare_command(
options.before_all, all_options.before_all,
project=container_project_path, project=container_project_path,
package=container_package_dir, package=container_package_dir,
) )
@@ -122,6 +117,7 @@ def build_on_docker(
for config in platform_configs: for config in platform_configs:
log.build_start(config.identifier) log.build_start(config.identifier)
options = all_options[config.identifier]
dependency_constraint_flags: List[PathOrStr] = [] dependency_constraint_flags: List[PathOrStr] = []
@@ -284,11 +280,11 @@ 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.output_dir) docker.copy_out(container_output_dir, all_options.output_dir)
log.step_end() log.step_end()
def build(options: BuildOptions) -> None: def build(all_options: AllBuildOptions) -> None:
try: try:
# check docker is installed # check docker is installed
subprocess.run(["docker", "--version"], check=True, stdout=subprocess.DEVNULL) subprocess.run(["docker", "--version"], check=True, stdout=subprocess.DEVNULL)
@@ -301,19 +297,19 @@ def build(options: BuildOptions) -> None:
) )
sys.exit(2) sys.exit(2)
assert options.manylinux_images is not None python_configurations = get_python_configurations(
assert options.musllinux_images is not None all_options.build_selector, all_options.architectures
python_configurations = get_python_configurations(options.build_selector, options.architectures) )
cwd = Path.cwd() cwd = Path.cwd()
abs_package_dir = options.package_dir.resolve() abs_package_dir = all_options.package_dir.resolve()
if cwd != abs_package_dir and cwd not in abs_package_dir.parents: if cwd != abs_package_dir and cwd not in abs_package_dir.parents:
raise Exception("package_dir must be inside the working directory") raise Exception("package_dir must be inside the working directory")
container_project_path = PurePath("/project") container_project_path = PurePath("/project")
container_package_dir = container_project_path / abs_package_dir.relative_to(cwd) container_package_dir = container_project_path / abs_package_dir.relative_to(cwd)
for build_step in get_build_steps(options, python_configurations): for build_step in get_build_steps(all_options, python_configurations):
try: try:
log.step(f"Starting Docker image {build_step.docker_image}...") log.step(f"Starting Docker image {build_step.docker_image}...")
@@ -324,7 +320,7 @@ def build(options: BuildOptions) -> None:
) as docker: ) as docker:
build_on_docker( build_on_docker(
options, all_options,
build_step.platform_configs, build_step.platform_configs,
docker, docker,
container_project_path, container_project_path,
@@ -335,7 +331,7 @@ def build(options: BuildOptions) -> None:
log.step_end_with_error( log.step_end_with_error(
f"Command {error.cmd} failed with code {error.returncode}. {error.stdout}" f"Command {error.cmd} failed with code {error.returncode}. {error.stdout}"
) )
troubleshoot(options, error) troubleshoot(all_options, error)
sys.exit(1) sys.exit(1)
@@ -346,16 +342,18 @@ def _matches_prepared_command(error_cmd: List[str], command_template: str) -> bo
return error_cmd[2].startswith(command_prefix) return error_cmd[2].startswith(command_prefix)
def troubleshoot(options: BuildOptions, error: Exception) -> None: def troubleshoot(all_options: AllBuildOptions, error: Exception) -> None:
if isinstance(error, subprocess.CalledProcessError) and ( if isinstance(error, subprocess.CalledProcessError) and (
error.cmd[0:4] == ["python", "-m", "pip", "wheel"] error.cmd[0:4] == ["python", "-m", "pip", "wheel"]
or error.cmd[0:3] == ["python", "-m", "build"] or error.cmd[0:3] == ["python", "-m", "build"]
or _matches_prepared_command(error.cmd, options.repair_command) or _matches_prepared_command(
error.cmd, all_options.general_build_options.repair_command
) # TODO
): ):
# the wheel build step failed # the wheel build step failed
print("Checking for common errors...") print("Checking for common errors...")
so_files = list(options.package_dir.glob("**/*.so")) so_files = list(all_options.package_dir.glob("**/*.so"))
if so_files: if so_files:
print( print(
+9 -6
View File
@@ -14,8 +14,8 @@ from .environment import ParsedEnvironment
from .logger import log from .logger import log
from .typing import Literal, PathOrStr, assert_never from .typing import Literal, PathOrStr, assert_never
from .util import ( from .util import (
AllBuildOptions,
BuildFrontend, BuildFrontend,
BuildOptions,
BuildSelector, BuildSelector,
NonPlatformWheelError, NonPlatformWheelError,
download, download,
@@ -343,26 +343,29 @@ def setup_python(
return env return env
def build(options: BuildOptions) -> None: def build(all_options: AllBuildOptions) -> None:
temp_dir = Path(tempfile.mkdtemp(prefix="cibuildwheel")) temp_dir = Path(tempfile.mkdtemp(prefix="cibuildwheel"))
built_wheel_dir = temp_dir / "built_wheel" built_wheel_dir = temp_dir / "built_wheel"
repaired_wheel_dir = temp_dir / "repaired_wheel" repaired_wheel_dir = temp_dir / "repaired_wheel"
all_options.check_build_selectors()
try: try:
if options.before_all: if all_options.before_all:
log.step("Running before_all...") log.step("Running before_all...")
env = options.environment.as_dictionary(prev_environment=os.environ) env = all_options.environment.as_dictionary(prev_environment=os.environ)
env.setdefault("MACOSX_DEPLOYMENT_TARGET", "10.9") env.setdefault("MACOSX_DEPLOYMENT_TARGET", "10.9")
before_all_prepared = prepare_command( before_all_prepared = prepare_command(
options.before_all, project=".", package=options.package_dir all_options.before_all, project=".", package=all_options.package_dir
) )
call([before_all_prepared], shell=True, env=env) call([before_all_prepared], shell=True, env=env)
python_configurations = get_python_configurations( python_configurations = get_python_configurations(
options.build_selector, options.architectures all_options.build_selector, all_options.architectures
) )
for config in python_configurations: for config in python_configurations:
options = all_options[config.identifier]
log.build_start(config.identifier) log.build_start(config.identifier)
config_is_arm64 = config.identifier.endswith("arm64") config_is_arm64 = config.identifier.endswith("arm64")
+70 -12
View File
@@ -1,9 +1,10 @@
import copy
import os import os
import sys import sys
import traceback import traceback
from configparser import ConfigParser from configparser import ConfigParser
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, Mapping, Optional, Set, Tuple, Union from typing import Any, Dict, List, Mapping, Optional, Set, Tuple, TypeVar, Union
import tomli import tomli
from packaging.specifiers import SpecifierSet from packaging.specifiers import SpecifierSet
@@ -58,6 +59,9 @@ def _dig_first(*pairs: Tuple[Mapping[str, Setting], str], ignore_empty: bool = F
raise KeyError(key) raise KeyError(key)
T = TypeVar("T", bound="ConfigOptions")
class ConfigOptions: class ConfigOptions:
""" """
Gets options from the environment, config or defaults, optionally scoped Gets options from the environment, config or defaults, optionally scoped
@@ -117,12 +121,27 @@ class ConfigOptions:
self.config_options = config_options self.config_options = config_options
self.config_platform_options = config_platform_options self.config_platform_options = config_platform_options
self.overrides: Dict[str, Dict[str, Any]] = {}
self.current_override: str = "*"
overrides = self.config_options.get("overrides")
if overrides is not None:
if not isinstance(overrides, list):
raise ConfigOptionError('"tool.cibuildwheel.overrides" must be a list')
for override in overrides:
selector = override.pop("select")
if isinstance(selector, list):
selector = " ".join(selector)
if selector in {"", "*"}:
raise ConfigOptionError("select all must not be used in an override")
self.overrides[selector.strip()] = override
def _is_valid_global_option(self, name: str) -> bool: def _is_valid_global_option(self, name: str) -> bool:
""" """
Returns True if an option with this name is allowed in the Returns True if an option with this name is allowed in the
[tool.cibuildwheel] section of a config file. [tool.cibuildwheel] section of a config file.
""" """
allowed_option_names = self.default_options.keys() | PLATFORMS allowed_option_names = self.default_options.keys() | PLATFORMS | {"overrides"}
return name in allowed_option_names return name in allowed_option_names
@@ -151,6 +170,14 @@ class ConfigOptions:
return global_options, platform_options return global_options, platform_options
def override(self: T, selector: str) -> T:
"""
Start an override scope.
"""
other = copy.copy(self)
other.current_override = selector
return other
def __call__( def __call__(
self, self,
name: str, name: str,
@@ -179,9 +206,11 @@ class ConfigOptions:
# get the option from the environment, then the config file, then finally the default. # get the option from the environment, then the config file, then finally the default.
# platform-specific options are preferred, if they're allowed. # platform-specific options are preferred, if they're allowed.
empty: Dict[str, Any] = {}
result = _dig_first( result = _dig_first(
(os.environ if env_plat else {}, plat_envvar), # type: ignore[arg-type] (os.environ if env_plat else {}, plat_envvar), # type: ignore[arg-type]
(os.environ, envvar), (os.environ, envvar),
(self.overrides.get(self.current_override, empty), name),
(self.config_platform_options, name), (self.config_platform_options, name),
(self.config_options, name), (self.config_options, name),
(self.default_platform_options, name), (self.default_platform_options, name),
@@ -210,7 +239,7 @@ def compute_options(
config_file: Optional[str], config_file: Optional[str],
args_archs: Optional[str], args_archs: Optional[str],
prerelease_pythons: bool, prerelease_pythons: bool,
) -> BuildOptions: ) -> Tuple[BuildOptions, Dict[str, BuildOptions]]:
""" """
Compute the options from the environment and configuration file. Compute the options from the environment and configuration file.
""" """
@@ -263,11 +292,25 @@ def compute_options(
) )
test_selector = TestSelector(skip_config=test_skip) test_selector = TestSelector(skip_config=test_skip)
return _compute_single_options( return _compute_all_options(
options, args_archs, build_selector, test_selector, platform, package_dir, output_dir options, args_archs, build_selector, test_selector, platform, package_dir, output_dir
) )
def _get_pinned_docker_images() -> Mapping[str, Mapping[str, str]]:
"""
This looks like a dict of dicts, e.g.
{ 'x86_64': {'manylinux1': '...', 'manylinux2010': '...', 'manylinux2014': '...'},
'i686': {'manylinux1': '...', 'manylinux2010': '...', 'manylinux2014': '...'},
'pypy_x86_64': {'manylinux2010': '...' }
... }
"""
pinned_docker_images_file = resources_dir / "pinned_docker_images.cfg"
all_pinned_docker_images = ConfigParser()
all_pinned_docker_images.read(pinned_docker_images_file)
return all_pinned_docker_images
def _compute_single_options( def _compute_single_options(
options: ConfigOptions, options: ConfigOptions,
args_archs: Optional[str], args_archs: Optional[str],
@@ -337,14 +380,7 @@ def _compute_single_options(
manylinux_images: Dict[str, str] = {} manylinux_images: Dict[str, str] = {}
musllinux_images: Dict[str, str] = {} musllinux_images: Dict[str, str] = {}
if platform == "linux": if platform == "linux":
pinned_docker_images_file = resources_dir / "pinned_docker_images.cfg" all_pinned_docker_images = _get_pinned_docker_images()
all_pinned_docker_images = ConfigParser()
all_pinned_docker_images.read(pinned_docker_images_file)
# all_pinned_docker_images looks like a dict of dicts, e.g.
# { 'x86_64': {'manylinux1': '...', 'manylinux2010': '...', 'manylinux2014': '...'},
# 'i686': {'manylinux1': '...', 'manylinux2010': '...', 'manylinux2014': '...'},
# 'pypy_x86_64': {'manylinux2010': '...' }
# ... }
for build_platform in MANYLINUX_ARCHS: for build_platform in MANYLINUX_ARCHS:
pinned_images = all_pinned_docker_images[build_platform] pinned_images = all_pinned_docker_images[build_platform]
@@ -359,6 +395,7 @@ def _compute_single_options(
else: else:
image = config_value image = config_value
assert image is not None
manylinux_images[build_platform] = image manylinux_images[build_platform] = image
for build_platform in MUSLLINUX_ARCHS: for build_platform in MUSLLINUX_ARCHS:
@@ -397,6 +434,27 @@ def _compute_single_options(
) )
def _compute_all_options(
options: ConfigOptions,
args_archs: Optional[str],
build_selector: BuildSelector,
test_selector: TestSelector,
platform: PlatformName,
package_dir: Path,
output_dir: Path,
) -> Tuple[BuildOptions, Dict[str, BuildOptions]]:
args = (args_archs, build_selector, test_selector, platform, package_dir, output_dir)
general_build_options = _compute_single_options(options, *args)
selectors = options.overrides.keys()
build_options_by_selector = {
s: _compute_single_options(options.override(s), *args) for s in selectors
}
return general_build_options, build_options_by_selector
def deprecated_selectors(name: str, selector: str, *, error: bool = False) -> None: def deprecated_selectors(name: str, selector: str, *, error: bool = False) -> None:
if "p2" in selector or "p35" in selector: if "p2" in selector or "p35" in selector:
msg = f"cibuildwheel 2.x no longer supports Python < 3.6. Please use the 1.x series or update {name}" msg = f"cibuildwheel 2.x no longer supports Python < 3.6. Please use the 1.x series or update {name}"
+6 -4
View File
@@ -12,14 +12,16 @@ else:
__all__ = ( __all__ = (
"Final", "Final",
"Literal", "Literal",
"TypedDict", "PLATFORMS",
"Set",
"Union",
"PopenBytes",
"PathOrStr", "PathOrStr",
"PlatformName", "PlatformName",
"Protocol", "Protocol",
"PLATFORMS", "PLATFORMS",
"PopenBytes",
"Protocol",
"Set",
"TypedDict",
"Union",
"assert_never", "assert_never",
) )
+129 -2
View File
@@ -1,4 +1,5 @@
import contextlib import contextlib
import dataclasses
import fnmatch import fnmatch
import itertools import itertools
import os import os
@@ -9,10 +10,23 @@ import sys
import textwrap import textwrap
import time import time
import urllib.request import urllib.request
from collections import defaultdict
from enum import Enum from enum import Enum
from pathlib import Path from pathlib import Path
from time import sleep from time import sleep
from typing import Dict, Iterator, List, NamedTuple, Optional, Set from typing import (
Counter,
Dict,
Iterable,
Iterator,
List,
Mapping,
NamedTuple,
Optional,
Set,
Tuple,
TypeVar,
)
import bracex import bracex
import certifi import certifi
@@ -22,7 +36,7 @@ from packaging.version import Version
from .architecture import Architecture from .architecture import Architecture
from .environment import ParsedEnvironment from .environment import ParsedEnvironment
from .typing import Literal, PathOrStr, PlatformName from .typing import Literal, PathOrStr, PlatformName, Protocol
resources_dir = Path(__file__).parent / "resources" resources_dir = Path(__file__).parent / "resources"
@@ -250,6 +264,119 @@ class BuildOptions(NamedTuple):
return "\n".join(res) return "\n".join(res)
class SimpleConfig(Protocol):
@property
def identifier(self) -> str:
...
SC = TypeVar("SC", bound=SimpleConfig)
T = TypeVar("T", bound="AllBuildOptions")
@dataclasses.dataclass
class AllBuildOptions:
general_build_options: BuildOptions
build_options_by_selector: Dict[str, BuildOptions]
identifiers: List[str]
def __getitem__(self, identifier: str) -> BuildOptions:
for sel in self.build_options_by_selector:
bs = BuildSelector(build_config=sel, skip_config="")
if bs(identifier):
return self.build_options_by_selector[sel]
return self.general_build_options
def values(self) -> Iterable[BuildOptions]:
return itertools.chain(
[self.general_build_options], self.build_options_by_selector.values()
)
# These values are not overridable in some cases
@property
def package_dir(self) -> Path:
return self.general_build_options.package_dir
@property
def build_selector(self) -> BuildSelector:
return self.general_build_options.build_selector
@property
def output_dir(self) -> Path:
return self.general_build_options.output_dir
@property
def architectures(self) -> Set[Architecture]:
return self.general_build_options.architectures
@property
def environment(self) -> ParsedEnvironment:
return self.general_build_options.environment
@property
def before_all(self) -> str:
return self.general_build_options.before_all
def produce_image_batches(
self,
configurations: List[SC],
platform_tag: str,
platform_arch: str,
) -> Iterator[Tuple[List[SC], str]]:
docker_images: Mapping[str, List[SC]] = defaultdict(list)
for config in configurations:
build_options = self[config.identifier]
images = (
build_options.manylinux_images
if platform_tag.startswith("manylinux")
else build_options.musllinux_images
)
assert images is not None
docker_images[images[platform_arch]].append(config)
for image, configs in docker_images.items():
# TODO: check for colisions for identifiers in configs
# Some settings (before-all) are not overridable in the same image
yield configs, image
def check_build_selectors(self) -> None:
hits = Counter[str]()
for sel in self.build_options_by_selector:
bs = BuildSelector(build_config=sel, skip_config="")
hits += Counter(i for i in self.identifiers if bs(i))
non_unique_identifers = {idnt for idnt, count in hits.items() if count > 1}
if non_unique_identifers:
msg = "cibuildwheel: error, the windows/macOS selectors must match uniquely"
print(msg, file=sys.stderr)
for sel in self.build_options_by_selector:
bs = BuildSelector(build_config=sel, skip_config="")
for i in non_unique_identifers:
if bs(i):
print(f" {sel}: {i} (nonunique match)")
sys.exit(1)
def __str__(self) -> str:
results = []
for option in sorted(self.general_build_options._asdict().keys()):
variations = {
key: value._asdict()[option]
for key, value in self.build_options_by_selector.items()
}
variations["*"] = self.general_build_options._asdict()[option]
if len({repr(v) for v in variations.values()}) == 1:
results.append(f"{option}: {variations['*']!r}")
else:
results.append(f"{option}:")
for key, value in sorted(variations.items()):
results.append(f" {key}: {value!r}")
return "\n".join(results)
class NonPlatformWheelError(Exception): class NonPlatformWheelError(Exception):
def __init__(self) -> None: def __init__(self) -> None:
message = textwrap.dedent( message = textwrap.dedent(
+9 -6
View File
@@ -12,8 +12,8 @@ from .environment import ParsedEnvironment
from .logger import log from .logger import log
from .typing import PathOrStr, assert_never from .typing import PathOrStr, assert_never
from .util import ( from .util import (
AllBuildOptions,
BuildFrontend, BuildFrontend,
BuildOptions,
BuildSelector, BuildSelector,
NonPlatformWheelError, NonPlatformWheelError,
download, download,
@@ -246,25 +246,28 @@ def setup_python(
return env return env
def build(options: BuildOptions) -> None: def build(all_options: AllBuildOptions) -> None:
temp_dir = Path(tempfile.mkdtemp(prefix="cibuildwheel")) temp_dir = Path(tempfile.mkdtemp(prefix="cibuildwheel"))
built_wheel_dir = temp_dir / "built_wheel" built_wheel_dir = temp_dir / "built_wheel"
repaired_wheel_dir = temp_dir / "repaired_wheel" repaired_wheel_dir = temp_dir / "repaired_wheel"
all_options.check_build_selectors()
try: try:
if options.before_all: if all_options.before_all:
log.step("Running before_all...") log.step("Running before_all...")
env = options.environment.as_dictionary(prev_environment=os.environ) env = all_options.environment.as_dictionary(prev_environment=os.environ)
before_all_prepared = prepare_command( before_all_prepared = prepare_command(
options.before_all, project=".", package=options.package_dir all_options.before_all, project=".", package=all_options.package_dir
) )
shell(before_all_prepared, env=env) shell(before_all_prepared, env=env)
python_configurations = get_python_configurations( python_configurations = get_python_configurations(
options.build_selector, options.architectures all_options.build_selector, all_options.architectures
) )
for config in python_configurations: for config in python_configurations:
options = all_options[config.identifier]
log.build_start(config.identifier) log.build_start(config.identifier)
dependency_constraint_flags: Sequence[PathOrStr] = [] dependency_constraint_flags: Sequence[PathOrStr] = []
+1
View File
@@ -36,6 +36,7 @@ install_requires =
certifi certifi
packaging packaging
tomli tomli
dataclasses;python_version < '3.7'
typing_extensions;python_version < '3.8' typing_extensions;python_version < '3.8'
python_requires = >=3.6 python_requires = >=3.6
include_package_data = True include_package_data = True
+61
View File
@@ -0,0 +1,61 @@
from pathlib import Path
from cibuildwheel.__main__ import get_build_identifiers
from cibuildwheel.environment import parse_environment
from cibuildwheel.options import _get_pinned_docker_images, compute_options
from cibuildwheel.util import AllBuildOptions
PYPROJECT_1 = """
[tool.cibuildwheel]
build = ["cp38*", "cp37*"]
environment = {FOO="BAR"}
test-command = "pyproject"
manylinux-x86_64-image = "manylinux1"
[tool.cibuildwheel.macos]
test-requires = "else"
[[tool.cibuildwheel.overrides]]
select = "cp37*"
test-command = "pyproject-override"
manylinux-x86_64-image = "manylinux2014"
"""
def test_all_build_options_1(tmp_path):
with tmp_path.joinpath("pyproject.toml").open("w") as f:
f.write(PYPROJECT_1)
all_build_options, build_options_by_selector = compute_options(
"linux", tmp_path, Path("dist"), None, None, False
)
identifiers = get_build_identifiers(
"linux", all_build_options.build_selector, all_build_options.architectures
)
build_options = AllBuildOptions(all_build_options, build_options_by_selector, identifiers)
override_display = """\
test_command:
*: 'pyproject'
cp37*: 'pyproject-override'"""
assert override_display in str(build_options)
assert build_options.environment == parse_environment('FOO="BAR"')
all_pinned_docker_images = _get_pinned_docker_images()
pinned_x86_64_docker_image = all_pinned_docker_images["x86_64"]
local = build_options["cp38-manylinux_x86_64"]
assert local.manylinux_images is not None
assert local.test_command == "pyproject"
assert local.manylinux_images["x86_64"] == pinned_x86_64_docker_image["manylinux1"]
local = build_options["cp37-manylinux_x86_64"]
assert local.manylinux_images is not None
assert local.test_command == "pyproject-override"
assert local.manylinux_images["x86_64"] == pinned_x86_64_docker_image["manylinux2014"]
+22 -14
View File
@@ -18,13 +18,13 @@ def test_output_dir(platform, intercepted_build_args, monkeypatch):
main() main()
assert intercepted_build_args.args[0].output_dir == OUTPUT_DIR assert intercepted_build_args.args[0].general_build_options.output_dir == OUTPUT_DIR
def test_output_dir_default(platform, intercepted_build_args, monkeypatch): def test_output_dir_default(platform, intercepted_build_args, monkeypatch):
main() main()
assert intercepted_build_args.args[0].output_dir == Path("wheelhouse") assert intercepted_build_args.args[0].general_build_options.output_dir == Path("wheelhouse")
@pytest.mark.parametrize("also_set_environment", [False, True]) @pytest.mark.parametrize("also_set_environment", [False, True])
@@ -37,7 +37,7 @@ def test_output_dir_argument(also_set_environment, platform, intercepted_build_a
main() main()
assert intercepted_build_args.args[0].output_dir == OUTPUT_DIR assert intercepted_build_args.args[0].general_build_options.output_dir == OUTPUT_DIR
def test_build_selector(platform, intercepted_build_args, monkeypatch, allow_empty): def test_build_selector(platform, intercepted_build_args, monkeypatch, allow_empty):
@@ -49,7 +49,7 @@ def test_build_selector(platform, intercepted_build_args, monkeypatch, allow_emp
main() main()
intercepted_build_selector = intercepted_build_args.args[0].build_selector intercepted_build_selector = intercepted_build_args.args[0].general_build_options.build_selector
assert isinstance(intercepted_build_selector, BuildSelector) assert isinstance(intercepted_build_selector, BuildSelector)
assert intercepted_build_selector("build24-this") assert intercepted_build_selector("build24-this")
assert not intercepted_build_selector("skip65-that") assert not intercepted_build_selector("skip65-that")
@@ -98,9 +98,12 @@ def test_manylinux_images(
main() main()
if platform == "linux": if platform == "linux":
assert fnmatch(intercepted_build_args.args[0].manylinux_images[architecture], full_image) assert fnmatch(
intercepted_build_args.args[0].general_build_options.manylinux_images[architecture],
full_image,
)
else: else:
assert intercepted_build_args.args[0].manylinux_images is None assert intercepted_build_args.args[0].general_build_options.manylinux_images is None
def get_default_repair_command(platform): def get_default_repair_command(platform):
@@ -129,7 +132,7 @@ def test_repair_command(
main() main()
expected_repair = repair_command or get_default_repair_command(platform) expected_repair = repair_command or get_default_repair_command(platform)
assert intercepted_build_args.args[0].repair_command == expected_repair assert intercepted_build_args.args[0].general_build_options.repair_command == expected_repair
@pytest.mark.parametrize( @pytest.mark.parametrize(
@@ -147,7 +150,7 @@ def test_environment(environment, platform_specific, platform, intercepted_build
main() main()
intercepted_environment = intercepted_build_args.args[0].environment intercepted_environment = intercepted_build_args.args[0].general_build_options.environment
assert isinstance(intercepted_environment, ParsedEnvironment) assert isinstance(intercepted_environment, ParsedEnvironment)
assert intercepted_environment.as_dictionary(prev_environment={}) == environment assert intercepted_environment.as_dictionary(prev_environment={}) == environment
@@ -166,7 +169,10 @@ def test_test_requires(
main() main()
assert intercepted_build_args.args[0].test_requires == (test_requires or "").split() assert (
intercepted_build_args.args[0].general_build_options.test_requires
== (test_requires or "").split()
)
@pytest.mark.parametrize("test_extras", [None, "extras"]) @pytest.mark.parametrize("test_extras", [None, "extras"])
@@ -181,7 +187,7 @@ def test_test_extras(test_extras, platform_specific, platform, intercepted_build
main() main()
assert intercepted_build_args.args[0].test_extras == ( assert intercepted_build_args.args[0].general_build_options.test_extras == (
"[" + test_extras + "]" if test_extras else "" "[" + test_extras + "]" if test_extras else ""
) )
@@ -200,7 +206,7 @@ def test_test_command(
main() main()
assert intercepted_build_args.args[0].test_command == (test_command or "") assert intercepted_build_args.args[0].general_build_options.test_command == (test_command or "")
@pytest.mark.parametrize("before_build", [None, "before --build"]) @pytest.mark.parametrize("before_build", [None, "before --build"])
@@ -217,7 +223,7 @@ def test_before_build(
main() main()
assert intercepted_build_args.args[0].before_build == (before_build or "") assert intercepted_build_args.args[0].general_build_options.before_build == (before_build or "")
@pytest.mark.parametrize("build_verbosity", [None, 0, 2, -2, 4, -4]) @pytest.mark.parametrize("build_verbosity", [None, 0, 2, -2, 4, -4])
@@ -235,7 +241,9 @@ def test_build_verbosity(
main() main()
expected_verbosity = max(-3, min(3, int(build_verbosity or 0))) expected_verbosity = max(-3, min(3, int(build_verbosity or 0)))
assert intercepted_build_args.args[0].build_verbosity == expected_verbosity assert (
intercepted_build_args.args[0].general_build_options.build_verbosity == expected_verbosity
)
@pytest.mark.parametrize( @pytest.mark.parametrize(
@@ -286,4 +294,4 @@ def test_before_all(before_all, platform_specific, platform, intercepted_build_a
main() main()
assert intercepted_build_args.args[0].before_all == (before_all or "") assert intercepted_build_args.args[0].general_build_options.before_all == (before_all or "")
+45
View File
@@ -246,3 +246,48 @@ def test_dig_first(ignore_empty):
(d4, "other"), (d4, "other"),
ignore_empty=ignore_empty, ignore_empty=ignore_empty,
) )
PYPROJECT_2 = """
[tool.cibuildwheel]
build = ["cp38*", "cp37*"]
environment = {FOO="BAR"}
test-command = "pyproject"
manylinux-x86_64-image = "manylinux1"
[tool.cibuildwheel.macos]
test-requires = "else"
[[tool.cibuildwheel.overrides]]
select = "cp37*"
test-command = "pyproject-override"
manylinux-x86_64-image = "manylinux2014"
"""
def test_pyproject_2(tmp_path, platform):
with tmp_path.joinpath("pyproject.toml").open("w") as f:
f.write(PYPROJECT_2)
options = ConfigOptions(tmp_path, platform=platform)
assert options("test-command") == "pyproject"
assert options.override("random")("test-command") == "pyproject"
assert options.override("cp37*")("test-command") == "pyproject-override"
def test_overrides_not_a_list(tmp_path, platform):
with tmp_path.joinpath("pyproject.toml").open("w") as f:
f.write(
"""\
[tool.cibuildwheel]
build = ["cp38*", "cp37*"]
[tool.cibuildwheel.overrides]
select = "cp37*"
test-command = "pyproject-override"
"""
)
with pytest.raises(ConfigOptionError):
ConfigOptions(tmp_path, platform=platform)