Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion bin/generate_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,24 @@
dependency-versions:
default: pinned
description: Specify how cibuildwheel controls the versions of the tools it uses
type: string
oneOf:
- enum: [pinned, latest]
- type: string
description: Path to a file containing dependency versions, or inline package specifications, starting with "packages:"
not:
enum: [pinned, latest]
- type: object
additionalProperties: false
properties:
file:
type: string
- type: object
additionalProperties: false
properties:
packages:
type: array
items:
type: string
enable:
description: Enable or disable certain builds.
oneOf:
Expand Down
20 changes: 11 additions & 9 deletions cibuildwheel/linux.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ def build_in_container(
container: OCIContainer,
container_project_path: PurePath,
container_package_dir: PurePath,
local_tmp_dir: Path,
) -> None:
container_output_dir = PurePosixPath("/output")

Expand Down Expand Up @@ -199,22 +200,22 @@ def build_in_container(

for config in platform_configs:
log.build_start(config.identifier)
local_identifier_tmp_dir = local_tmp_dir / config.identifier
build_options = options.build_options(config.identifier)
build_frontend = build_options.build_frontend or BuildFrontendConfig("pip")
use_uv = build_frontend.name == "build[uv]"
pip = ["uv", "pip"] if use_uv else ["pip"]

dependency_constraint_flags: list[PathOrStr] = []

log.step("Setting up build environment...")

if build_options.dependency_constraints:
constraints_file = build_options.dependency_constraints.get_for_python_version(
config.version
)
dependency_constraint_flags: list[PathOrStr] = []
local_constraints_file = build_options.dependency_constraints.get_for_python_version(
version=config.version,
tmp_dir=local_identifier_tmp_dir,
)
if local_constraints_file:
container_constraints_file = PurePosixPath("/constraints.txt")

container.copy_into(constraints_file, container_constraints_file)
container.copy_into(local_constraints_file, container_constraints_file)
dependency_constraint_flags = ["-c", container_constraints_file]

env = container.get_environment()
Expand Down Expand Up @@ -426,7 +427,7 @@ def build_in_container(
log.step_end()


def build(options: Options, tmp_path: Path) -> None: # noqa: ARG001
def build(options: Options, tmp_path: Path) -> None:
python_configurations = get_python_configurations(
options.globals.build_selector, options.globals.architectures
)
Expand Down Expand Up @@ -480,6 +481,7 @@ def build(options: Options, tmp_path: Path) -> None: # noqa: ARG001
container=container,
container_project_path=container_project_path,
container_package_dir=container_package_dir,
local_tmp_dir=tmp_path,
)

except subprocess.CalledProcessError as error:
Expand Down
19 changes: 8 additions & 11 deletions cibuildwheel/macos.py
Original file line number Diff line number Diff line change
Expand Up @@ -421,12 +421,12 @@ def build(options: Options, tmp_path: Path) -> None:
config_is_arm64 = config.identifier.endswith("arm64")
config_is_universal2 = config.identifier.endswith("universal2")

dependency_constraint_flags: Sequence[PathOrStr] = []
if build_options.dependency_constraints:
dependency_constraint_flags = [
"-c",
build_options.dependency_constraints.get_for_python_version(config.version),
]
constraints_path = build_options.dependency_constraints.get_for_python_version(
version=config.version, tmp_dir=identifier_tmp_dir
)
dependency_constraint_flags: Sequence[PathOrStr] = (
["-c", constraints_path] if constraints_path else []
)

base_python, env = setup_python(
identifier_tmp_dir / "build",
Expand Down Expand Up @@ -463,12 +463,9 @@ def build(options: Options, tmp_path: Path) -> None:
build_env = env.copy()
if not use_uv:
build_env["VIRTUALENV_PIP"] = pip_version
if build_options.dependency_constraints:
constraint_path = build_options.dependency_constraints.get_for_python_version(
config.version
)
if constraints_path:
combine_constraints(
build_env, constraint_path, identifier_tmp_dir if use_uv else None
build_env, constraints_path, identifier_tmp_dir if use_uv else None
)

if build_frontend.name == "pip":
Expand Down
22 changes: 12 additions & 10 deletions cibuildwheel/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ class BuildOptions:
repair_command: str
manylinux_images: dict[str, str] | None
musllinux_images: dict[str, str] | None
dependency_constraints: DependencyConstraints | None
dependency_constraints: DependencyConstraints
test_command: str | None
before_test: str | None
test_sources: list[str]
Expand Down Expand Up @@ -687,7 +687,6 @@ def build_options(self, identifier: str | None) -> BuildOptions:
"config-settings", option_format=ShlexTableFormat(sep=" ", pair_sep="=")
)

dependency_versions = self.reader.get("dependency-versions")
test_command = self.reader.get("test-command", option_format=ListFormat(sep=" && "))
before_test = self.reader.get("before-test", option_format=ListFormat(sep=" && "))
test_sources = shlex.split(
Expand Down Expand Up @@ -733,15 +732,18 @@ def build_options(self, identifier: str | None) -> BuildOptions:
with contextlib.suppress(KeyError):
environment.add(env_var_name, self.env[env_var_name], prepend=True)

if dependency_versions == "pinned":
dependency_constraints: DependencyConstraints | None = (
DependencyConstraints.with_defaults()
dependency_versions_str = self.reader.get(
"dependency-versions",
env_plat=True,
option_format=ShlexTableFormat(sep="; ", pair_sep=":", allow_merge=False),
)
try:
dependency_constraints = DependencyConstraints.from_config_string(
dependency_versions_str
)
elif dependency_versions == "latest":
dependency_constraints = None
else:
dependency_versions_path = Path(dependency_versions)
dependency_constraints = DependencyConstraints(dependency_versions_path)
except (ValueError, OSError) as e:
msg = f"Failed to parse dependency versions. {e}"
raise errors.ConfigurationError(msg) from e

if test_extras:
test_extras = f"[{test_extras}]"
Expand Down
14 changes: 7 additions & 7 deletions cibuildwheel/pyodide.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,12 +265,12 @@ def build(options: Options, tmp_path: Path) -> None:
built_wheel_dir.mkdir()
repaired_wheel_dir.mkdir()

dependency_constraint_flags: Sequence[PathOrStr] = []
if build_options.dependency_constraints:
constraints_path = build_options.dependency_constraints.get_for_python_version(
config.version, variant="pyodide"
)
dependency_constraint_flags = ["-c", constraints_path]
constraints_path = build_options.dependency_constraints.get_for_python_version(
version=config.version, variant="pyodide", tmp_dir=identifier_tmp_dir
)
dependency_constraint_flags: Sequence[PathOrStr] = (
["-c", constraints_path] if constraints_path else []
)

env = setup_python(
identifier_tmp_dir / "build",
Expand Down Expand Up @@ -319,7 +319,7 @@ def build(options: Options, tmp_path: Path) -> None:
)

build_env = env.copy()
if build_options.dependency_constraints:
if constraints_path:
combine_constraints(build_env, constraints_path, identifier_tmp_dir)
build_env["VIRTUALENV_PIP"] = pip_version
call(
Expand Down
40 changes: 39 additions & 1 deletion cibuildwheel/resources/cibuildwheel.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,45 @@
"dependency-versions": {
"default": "pinned",
"description": "Specify how cibuildwheel controls the versions of the tools it uses",
"type": "string",
"oneOf": [
{
"enum": [
"pinned",
"latest"
]
},
{
"type": "string",
"description": "Path to a file containing dependency versions, or inline package specifications, starting with \"packages:\"",
"not": {
"enum": [
"pinned",
"latest"
]
}
},
{
"type": "object",
"additionalProperties": false,
"properties": {
"file": {
"type": "string"
}
}
},
{
"type": "object",
"additionalProperties": false,
"properties": {
"packages": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
],
"title": "CIBW_DEPENDENCY_VERSIONS"
},
"enable": {
Expand Down
118 changes: 91 additions & 27 deletions cibuildwheel/util/packaging.py
Original file line number Diff line number Diff line change
@@ -1,52 +1,116 @@
import shlex
from collections.abc import Mapping, MutableMapping, Sequence
from dataclasses import dataclass, field
from pathlib import Path, PurePath
from typing import Any, Literal, Self, TypeVar

from packaging.utils import parse_wheel_filename

from . import resources
from .cmd import call
from .helpers import parse_key_value_string, unwrap


@dataclass()
class DependencyConstraints:
def __init__(self, base_file_path: Path):
assert base_file_path.exists()
self.base_file_path = base_file_path.resolve()
base_file_path: Path | None = None
packages: list[str] = field(default_factory=list)

def __post_init__(self) -> None:
if self.packages and self.base_file_path is not None:
msg = "Cannot specify both a file and packages in the dependency constraints"
raise ValueError(msg)

if self.base_file_path is not None:
if not self.base_file_path.exists():
msg = f"Dependency constraints file not found: {self.base_file_path}"
raise FileNotFoundError(msg)
self.base_file_path = self.base_file_path.resolve()

@classmethod
def with_defaults(cls) -> Self:
def pinned(cls) -> Self:
return cls(base_file_path=resources.CONSTRAINTS)

def get_for_python_version(
self, version: str, *, variant: Literal["python", "pyodide"] = "python"
) -> Path:
version_parts = version.split(".")

# try to find a version-specific dependency file e.g. if
# ./constraints.txt is the base, look for ./constraints-python36.txt
specific_stem = self.base_file_path.stem + f"-{variant}{version_parts[0]}{version_parts[1]}"
specific_name = specific_stem + self.base_file_path.suffix
specific_file_path = self.base_file_path.with_name(specific_name)

if specific_file_path.exists():
return specific_file_path
else:
return self.base_file_path
@classmethod
def latest(cls) -> Self:
return cls()

def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.base_file_path!r})"
@classmethod
def from_config_string(cls, config_string: str) -> Self:
if config_string == "pinned":
return cls.pinned()

if config_string == "latest" or not config_string:
return cls.latest()

if config_string.startswith(("file:", "packages:")):
# we only do the table-style parsing if it looks like a table,
# because this option used to be only a file path. We don't want
# to break existing configurations, whose file paths might include
# special characters like ':' or ' ', which would require quoting
# if they were to be passed as a parse_key_value_string positional
# argument.
return cls.from_table_style_config_string(config_string)

def __eq__(self, o: object) -> bool:
if not isinstance(o, DependencyConstraints):
return False
return cls(base_file_path=Path(config_string))

@classmethod
def from_table_style_config_string(cls, config_string: str) -> Self:
config_dict = parse_key_value_string(config_string, kw_arg_names=["file", "packages"])
files = config_dict.get("file")
packages = config_dict.get("packages") or []

return self.base_file_path == o.base_file_path
if files and packages:
msg = "Cannot specify both a file and packages in dependency-versions"
raise ValueError(msg)

if files:
if len(files) > 1:
msg = unwrap("""
Only one file can be specified in dependency-versions.
If you intended to pass only one, perhaps you need to quote the path?
""")
raise ValueError(msg)

return cls(base_file_path=Path(files[0]))

return cls(packages=packages)

def get_for_python_version(
self, *, version: str, variant: Literal["python", "pyodide"] = "python", tmp_dir: Path
) -> Path | None:
if self.packages:
constraint_file = tmp_dir / "constraints.txt"
constraint_file.write_text("\n".join(self.packages))
return constraint_file

if self.base_file_path is not None:
version_parts = version.split(".")

# try to find a version-specific dependency file e.g. if
# ./constraints.txt is the base, look for ./constraints-python36.txt
specific_stem = (
self.base_file_path.stem + f"-{variant}{version_parts[0]}{version_parts[1]}"
)
specific_name = specific_stem + self.base_file_path.suffix
specific_file_path = self.base_file_path.with_name(specific_name)

if specific_file_path.exists():
return specific_file_path
else:
return self.base_file_path

return None

def options_summary(self) -> Any:
if self == DependencyConstraints.with_defaults():
if self == DependencyConstraints.pinned():
return "pinned"
else:
elif self.packages:
return {"packages": " ".join(shlex.quote(p) for p in self.packages)}
elif self.base_file_path is not None:
return self.base_file_path.name
else:
return "latest"


def get_pip_version(env: Mapping[str, str]) -> str:
Expand Down
Loading
Loading