feat: adding overrides
This commit is contained in:
@@ -59,6 +59,7 @@ repos:
|
||||
- tomli
|
||||
- types-certifi
|
||||
- types-click
|
||||
- types-dataclasses
|
||||
- types-jinja2
|
||||
- types-pyyaml
|
||||
- types-requests
|
||||
|
||||
+17
-14
@@ -14,7 +14,7 @@ from cibuildwheel.architecture import Architecture, allowed_architectures_check
|
||||
from cibuildwheel.options import compute_options
|
||||
from cibuildwheel.typing import PLATFORMS, PlatformName, assert_never
|
||||
from cibuildwheel.util import (
|
||||
BuildOptions,
|
||||
AllBuildOptions,
|
||||
BuildSelector,
|
||||
Unbuffered,
|
||||
detect_ci_provider,
|
||||
@@ -148,12 +148,12 @@ def main() -> None:
|
||||
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
|
||||
)
|
||||
|
||||
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:
|
||||
@@ -161,6 +161,8 @@ def main() -> None:
|
||||
print(identifier)
|
||||
sys.exit(0)
|
||||
|
||||
build_options = AllBuildOptions(all_build_options, build_options_by_selector, identifiers)
|
||||
|
||||
# Add CIBUILDWHEEL environment variable
|
||||
# This needs to be passed on to the docker container in linux.py
|
||||
os.environ["CIBUILDWHEEL"] = "1"
|
||||
@@ -200,7 +202,7 @@ def main() -> None:
|
||||
assert_never(platform)
|
||||
|
||||
|
||||
def print_preamble(platform: str, build_options: BuildOptions) -> None:
|
||||
def print_preamble(platform: str, build_options: AllBuildOptions) -> None:
|
||||
print(
|
||||
textwrap.dedent(
|
||||
"""
|
||||
@@ -254,20 +256,21 @@ def get_build_identifiers(
|
||||
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 = []
|
||||
|
||||
# warn about deprecated {python} and {pip}
|
||||
for option_name in ["test_command", "before_build"]:
|
||||
option_value = getattr(build_options, option_name)
|
||||
for build_options in all_options.values():
|
||||
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):
|
||||
# Reminder: in an f-string, double braces means literal single brace
|
||||
msg = (
|
||||
f"{option_name}: '{{python}}' and '{{pip}}' are no longer needed, "
|
||||
"and will be removed in a future release. Simply use 'python' or 'pip' instead."
|
||||
)
|
||||
warnings.append(msg)
|
||||
if option_value and ("{python}" in option_value or "{pip}" in option_value):
|
||||
# Reminder: in an f-string, double braces means literal single brace
|
||||
msg = (
|
||||
f"{option_name}: '{{python}}' and '{{pip}}' are no longer needed, "
|
||||
"and will be removed in a future release. Simply use 'python' or 'pip' instead."
|
||||
)
|
||||
warnings.append(msg)
|
||||
|
||||
return warnings
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import dataclasses
|
||||
from typing import Dict, List, Mapping, Optional
|
||||
|
||||
import bashlex
|
||||
@@ -60,10 +61,15 @@ class EnvironmentAssignment:
|
||||
def __repr__(self) -> str:
|
||||
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:
|
||||
def __init__(self, assignments: List[EnvironmentAssignment]):
|
||||
self.assignments = assignments
|
||||
assignments: List[EnvironmentAssignment]
|
||||
|
||||
def as_dictionary(
|
||||
self,
|
||||
@@ -82,7 +88,7 @@ class ParsedEnvironment:
|
||||
return [a.as_shell_assignment() for a in self.assignments]
|
||||
|
||||
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:
|
||||
|
||||
+26
-28
@@ -9,7 +9,7 @@ from .docker_container import DockerContainer
|
||||
from .logger import log
|
||||
from .typing import PathOrStr, assert_never
|
||||
from .util import (
|
||||
BuildOptions,
|
||||
AllBuildOptions,
|
||||
BuildSelector,
|
||||
NonPlatformWheelError,
|
||||
get_build_verbosity_extra_flags,
|
||||
@@ -54,7 +54,7 @@ def get_python_configurations(
|
||||
|
||||
|
||||
def get_build_steps(
|
||||
options: BuildOptions, python_configurations: List[PythonConfiguration]
|
||||
all_options: AllBuildOptions, python_configurations: List[PythonConfiguration]
|
||||
) -> Iterator[BuildStep]:
|
||||
platforms = [
|
||||
("cp", "manylinux_x86_64", "x86_64"),
|
||||
@@ -73,15 +73,6 @@ def get_build_steps(
|
||||
]
|
||||
|
||||
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 = [
|
||||
c
|
||||
for c in python_configurations
|
||||
@@ -90,11 +81,15 @@ def get_build_steps(
|
||||
if not platform_configs:
|
||||
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(
|
||||
options: BuildOptions,
|
||||
all_options: AllBuildOptions,
|
||||
platform_configs: List[PythonConfiguration],
|
||||
docker: DockerContainer,
|
||||
container_project_path: PurePath,
|
||||
@@ -105,16 +100,16 @@ def build_on_docker(
|
||||
log.step("Copying project into Docker...")
|
||||
docker.copy_into(Path.cwd(), container_project_path)
|
||||
|
||||
if options.before_all:
|
||||
if all_options.before_all:
|
||||
log.step("Running before_all...")
|
||||
|
||||
env = docker.get_environment()
|
||||
env["PATH"] = f'/opt/python/cp38-cp38/bin:{env["PATH"]}'
|
||||
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(
|
||||
options.before_all,
|
||||
all_options.before_all,
|
||||
project=container_project_path,
|
||||
package=container_package_dir,
|
||||
)
|
||||
@@ -122,6 +117,7 @@ def build_on_docker(
|
||||
|
||||
for config in platform_configs:
|
||||
log.build_start(config.identifier)
|
||||
options = all_options[config.identifier]
|
||||
|
||||
dependency_constraint_flags: List[PathOrStr] = []
|
||||
|
||||
@@ -284,11 +280,11 @@ def build_on_docker(
|
||||
|
||||
log.step("Copying wheels back to 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()
|
||||
|
||||
|
||||
def build(options: BuildOptions) -> None:
|
||||
def build(all_options: AllBuildOptions) -> None:
|
||||
try:
|
||||
# check docker is installed
|
||||
subprocess.run(["docker", "--version"], check=True, stdout=subprocess.DEVNULL)
|
||||
@@ -301,19 +297,19 @@ def build(options: BuildOptions) -> None:
|
||||
)
|
||||
sys.exit(2)
|
||||
|
||||
assert options.manylinux_images is not None
|
||||
assert options.musllinux_images is not None
|
||||
python_configurations = get_python_configurations(options.build_selector, options.architectures)
|
||||
python_configurations = get_python_configurations(
|
||||
all_options.build_selector, all_options.architectures
|
||||
)
|
||||
|
||||
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:
|
||||
raise Exception("package_dir must be inside the working directory")
|
||||
|
||||
container_project_path = PurePath("/project")
|
||||
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:
|
||||
log.step(f"Starting Docker image {build_step.docker_image}...")
|
||||
|
||||
@@ -324,7 +320,7 @@ def build(options: BuildOptions) -> None:
|
||||
) as docker:
|
||||
|
||||
build_on_docker(
|
||||
options,
|
||||
all_options,
|
||||
build_step.platform_configs,
|
||||
docker,
|
||||
container_project_path,
|
||||
@@ -335,7 +331,7 @@ def build(options: BuildOptions) -> None:
|
||||
log.step_end_with_error(
|
||||
f"Command {error.cmd} failed with code {error.returncode}. {error.stdout}"
|
||||
)
|
||||
troubleshoot(options, error)
|
||||
troubleshoot(all_options, error)
|
||||
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)
|
||||
|
||||
|
||||
def troubleshoot(options: BuildOptions, error: Exception) -> None:
|
||||
def troubleshoot(all_options: AllBuildOptions, error: Exception) -> None:
|
||||
|
||||
if isinstance(error, subprocess.CalledProcessError) and (
|
||||
error.cmd[0:4] == ["python", "-m", "pip", "wheel"]
|
||||
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
|
||||
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:
|
||||
print(
|
||||
|
||||
@@ -14,8 +14,8 @@ from .environment import ParsedEnvironment
|
||||
from .logger import log
|
||||
from .typing import Literal, PathOrStr, assert_never
|
||||
from .util import (
|
||||
AllBuildOptions,
|
||||
BuildFrontend,
|
||||
BuildOptions,
|
||||
BuildSelector,
|
||||
NonPlatformWheelError,
|
||||
download,
|
||||
@@ -343,26 +343,29 @@ def setup_python(
|
||||
return env
|
||||
|
||||
|
||||
def build(options: BuildOptions) -> None:
|
||||
def build(all_options: AllBuildOptions) -> None:
|
||||
temp_dir = Path(tempfile.mkdtemp(prefix="cibuildwheel"))
|
||||
built_wheel_dir = temp_dir / "built_wheel"
|
||||
repaired_wheel_dir = temp_dir / "repaired_wheel"
|
||||
|
||||
all_options.check_build_selectors()
|
||||
|
||||
try:
|
||||
if options.before_all:
|
||||
if all_options.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")
|
||||
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)
|
||||
|
||||
python_configurations = get_python_configurations(
|
||||
options.build_selector, options.architectures
|
||||
all_options.build_selector, all_options.architectures
|
||||
)
|
||||
|
||||
for config in python_configurations:
|
||||
options = all_options[config.identifier]
|
||||
log.build_start(config.identifier)
|
||||
|
||||
config_is_arm64 = config.identifier.endswith("arm64")
|
||||
|
||||
+70
-12
@@ -1,9 +1,10 @@
|
||||
import copy
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
from configparser import ConfigParser
|
||||
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
|
||||
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)
|
||||
|
||||
|
||||
T = TypeVar("T", bound="ConfigOptions")
|
||||
|
||||
|
||||
class ConfigOptions:
|
||||
"""
|
||||
Gets options from the environment, config or defaults, optionally scoped
|
||||
@@ -117,12 +121,27 @@ class ConfigOptions:
|
||||
self.config_options = config_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:
|
||||
"""
|
||||
Returns True if an option with this name is allowed in the
|
||||
[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
|
||||
|
||||
@@ -151,6 +170,14 @@ class ConfigOptions:
|
||||
|
||||
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__(
|
||||
self,
|
||||
name: str,
|
||||
@@ -179,9 +206,11 @@ class ConfigOptions:
|
||||
|
||||
# get the option from the environment, then the config file, then finally the default.
|
||||
# platform-specific options are preferred, if they're allowed.
|
||||
empty: Dict[str, Any] = {}
|
||||
result = _dig_first(
|
||||
(os.environ if env_plat else {}, plat_envvar), # type: ignore[arg-type]
|
||||
(os.environ, envvar),
|
||||
(self.overrides.get(self.current_override, empty), name),
|
||||
(self.config_platform_options, name),
|
||||
(self.config_options, name),
|
||||
(self.default_platform_options, name),
|
||||
@@ -210,7 +239,7 @@ def compute_options(
|
||||
config_file: Optional[str],
|
||||
args_archs: Optional[str],
|
||||
prerelease_pythons: bool,
|
||||
) -> BuildOptions:
|
||||
) -> Tuple[BuildOptions, Dict[str, BuildOptions]]:
|
||||
"""
|
||||
Compute the options from the environment and configuration file.
|
||||
"""
|
||||
@@ -263,11 +292,25 @@ def compute_options(
|
||||
)
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
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(
|
||||
options: ConfigOptions,
|
||||
args_archs: Optional[str],
|
||||
@@ -337,14 +380,7 @@ def _compute_single_options(
|
||||
manylinux_images: Dict[str, str] = {}
|
||||
musllinux_images: Dict[str, str] = {}
|
||||
if platform == "linux":
|
||||
pinned_docker_images_file = resources_dir / "pinned_docker_images.cfg"
|
||||
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': '...' }
|
||||
# ... }
|
||||
all_pinned_docker_images = _get_pinned_docker_images()
|
||||
|
||||
for build_platform in MANYLINUX_ARCHS:
|
||||
pinned_images = all_pinned_docker_images[build_platform]
|
||||
@@ -359,6 +395,7 @@ def _compute_single_options(
|
||||
else:
|
||||
image = config_value
|
||||
|
||||
assert image is not None
|
||||
manylinux_images[build_platform] = image
|
||||
|
||||
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:
|
||||
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}"
|
||||
|
||||
@@ -12,14 +12,16 @@ else:
|
||||
__all__ = (
|
||||
"Final",
|
||||
"Literal",
|
||||
"TypedDict",
|
||||
"Set",
|
||||
"Union",
|
||||
"PopenBytes",
|
||||
"PLATFORMS",
|
||||
"PathOrStr",
|
||||
"PlatformName",
|
||||
"Protocol",
|
||||
"PLATFORMS",
|
||||
"PopenBytes",
|
||||
"Protocol",
|
||||
"Set",
|
||||
"TypedDict",
|
||||
"Union",
|
||||
"assert_never",
|
||||
)
|
||||
|
||||
|
||||
+129
-2
@@ -1,4 +1,5 @@
|
||||
import contextlib
|
||||
import dataclasses
|
||||
import fnmatch
|
||||
import itertools
|
||||
import os
|
||||
@@ -9,10 +10,23 @@ import sys
|
||||
import textwrap
|
||||
import time
|
||||
import urllib.request
|
||||
from collections import defaultdict
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
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 certifi
|
||||
@@ -22,7 +36,7 @@ from packaging.version import Version
|
||||
|
||||
from .architecture import Architecture
|
||||
from .environment import ParsedEnvironment
|
||||
from .typing import Literal, PathOrStr, PlatformName
|
||||
from .typing import Literal, PathOrStr, PlatformName, Protocol
|
||||
|
||||
resources_dir = Path(__file__).parent / "resources"
|
||||
|
||||
@@ -250,6 +264,119 @@ class BuildOptions(NamedTuple):
|
||||
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):
|
||||
def __init__(self) -> None:
|
||||
message = textwrap.dedent(
|
||||
|
||||
@@ -12,8 +12,8 @@ from .environment import ParsedEnvironment
|
||||
from .logger import log
|
||||
from .typing import PathOrStr, assert_never
|
||||
from .util import (
|
||||
AllBuildOptions,
|
||||
BuildFrontend,
|
||||
BuildOptions,
|
||||
BuildSelector,
|
||||
NonPlatformWheelError,
|
||||
download,
|
||||
@@ -246,25 +246,28 @@ def setup_python(
|
||||
return env
|
||||
|
||||
|
||||
def build(options: BuildOptions) -> None:
|
||||
def build(all_options: AllBuildOptions) -> None:
|
||||
temp_dir = Path(tempfile.mkdtemp(prefix="cibuildwheel"))
|
||||
built_wheel_dir = temp_dir / "built_wheel"
|
||||
repaired_wheel_dir = temp_dir / "repaired_wheel"
|
||||
|
||||
all_options.check_build_selectors()
|
||||
|
||||
try:
|
||||
if options.before_all:
|
||||
if all_options.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(
|
||||
options.before_all, project=".", package=options.package_dir
|
||||
all_options.before_all, project=".", package=all_options.package_dir
|
||||
)
|
||||
shell(before_all_prepared, env=env)
|
||||
|
||||
python_configurations = get_python_configurations(
|
||||
options.build_selector, options.architectures
|
||||
all_options.build_selector, all_options.architectures
|
||||
)
|
||||
|
||||
for config in python_configurations:
|
||||
options = all_options[config.identifier]
|
||||
log.build_start(config.identifier)
|
||||
|
||||
dependency_constraint_flags: Sequence[PathOrStr] = []
|
||||
|
||||
@@ -36,6 +36,7 @@ install_requires =
|
||||
certifi
|
||||
packaging
|
||||
tomli
|
||||
dataclasses;python_version < '3.7'
|
||||
typing_extensions;python_version < '3.8'
|
||||
python_requires = >=3.6
|
||||
include_package_data = True
|
||||
|
||||
@@ -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"]
|
||||
@@ -18,13 +18,13 @@ def test_output_dir(platform, intercepted_build_args, monkeypatch):
|
||||
|
||||
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):
|
||||
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])
|
||||
@@ -37,7 +37,7 @@ def test_output_dir_argument(also_set_environment, platform, intercepted_build_a
|
||||
|
||||
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):
|
||||
@@ -49,7 +49,7 @@ def test_build_selector(platform, intercepted_build_args, monkeypatch, allow_emp
|
||||
|
||||
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 intercepted_build_selector("build24-this")
|
||||
assert not intercepted_build_selector("skip65-that")
|
||||
@@ -98,9 +98,12 @@ def test_manylinux_images(
|
||||
main()
|
||||
|
||||
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:
|
||||
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):
|
||||
@@ -129,7 +132,7 @@ def test_repair_command(
|
||||
main()
|
||||
|
||||
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(
|
||||
@@ -147,7 +150,7 @@ def test_environment(environment, platform_specific, platform, intercepted_build
|
||||
|
||||
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 intercepted_environment.as_dictionary(prev_environment={}) == environment
|
||||
|
||||
@@ -166,7 +169,10 @@ def test_test_requires(
|
||||
|
||||
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"])
|
||||
@@ -181,7 +187,7 @@ def test_test_extras(test_extras, platform_specific, platform, intercepted_build
|
||||
|
||||
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 ""
|
||||
)
|
||||
|
||||
@@ -200,7 +206,7 @@ def test_test_command(
|
||||
|
||||
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"])
|
||||
@@ -217,7 +223,7 @@ def test_before_build(
|
||||
|
||||
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])
|
||||
@@ -235,7 +241,9 @@ def test_build_verbosity(
|
||||
main()
|
||||
|
||||
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(
|
||||
@@ -286,4 +294,4 @@ def test_before_all(before_all, platform_specific, platform, intercepted_build_a
|
||||
|
||||
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 "")
|
||||
|
||||
@@ -246,3 +246,48 @@ def test_dig_first(ignore_empty):
|
||||
(d4, "other"),
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user