* chore: lazy imports Signed-off-by: Henry Schreiner <henryfs@princeton.edu> * chore: bump flake8-lazy, rerun Signed-off-by: Henry Schreiner <henryfs@princeton.edu> * chore: include android Signed-off-by: Henry Schreiner <henryfs@princeton.edu> * fix: after rebase Assisted-by: ClaudeCode:claude-sonnet-4.6 Signed-off-by: Henry Schreiner <henryfs@princeton.edu> * chore: autofix __lazy_modules__ Signed-off-by: Henry Schreiner <henryfs@princeton.edu> * chore: avoid touching resources Signed-off-by: Henry Schreiner <henryfs@princeton.edu> * chore: bump flake8-lazy to v0.8.2, rerun Assisted-by: ClaudeCode:claude-sonnet-4-6 --------- Signed-off-by: Henry Schreiner <henryfs@princeton.edu>
200 lines
7.1 KiB
Python
200 lines
7.1 KiB
Python
from __future__ import annotations
|
|
|
|
__lazy_modules__ = {
|
|
"cibuildwheel.util.cmd",
|
|
"cibuildwheel.util.helpers",
|
|
"packaging",
|
|
"packaging.utils",
|
|
"shlex",
|
|
}
|
|
|
|
import shlex
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path, PurePath
|
|
from typing import TypeVar
|
|
|
|
from packaging.utils import parse_wheel_filename
|
|
|
|
from cibuildwheel.util import resources
|
|
from cibuildwheel.util.cmd import call
|
|
from cibuildwheel.util.helpers import parse_key_value_string, unwrap
|
|
|
|
TYPE_CHECKING = False
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Mapping, Sequence
|
|
from typing import Literal, Self
|
|
|
|
|
|
@dataclass(kw_only=True)
|
|
class DependencyConstraints:
|
|
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 pinned(cls) -> Self:
|
|
return cls(base_file_path=resources.CONSTRAINTS)
|
|
|
|
@classmethod
|
|
def latest(cls) -> Self:
|
|
return cls()
|
|
|
|
@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)
|
|
|
|
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 []
|
|
|
|
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) -> str | dict[str, str]:
|
|
if self == DependencyConstraints.pinned():
|
|
return "pinned"
|
|
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:
|
|
versions_output_text = call(
|
|
"python", "-m", "pip", "freeze", "--all", capture_stdout=True, env=env
|
|
)
|
|
(pip_version,) = (
|
|
version[5:]
|
|
for version in versions_output_text.strip().splitlines()
|
|
if version.startswith("pip==")
|
|
)
|
|
return pip_version
|
|
|
|
|
|
T = TypeVar("T", bound=PurePath)
|
|
|
|
|
|
def find_compatible_wheel(wheels: Sequence[T], identifier: str) -> T | None:
|
|
"""
|
|
Finds a wheel with an abi3 or a none ABI tag in `wheels` compatible with the Python interpreter
|
|
specified by `identifier` that is previously built.
|
|
"""
|
|
|
|
interpreter, platform = identifier.split("-", 1)
|
|
interpreter = interpreter.split("_")[0]
|
|
free_threaded = interpreter.endswith("t")
|
|
if free_threaded:
|
|
interpreter = interpreter[:-1]
|
|
for wheel in wheels:
|
|
_, _, _, tags = parse_wheel_filename(wheel.name)
|
|
for tag in tags:
|
|
if tag.abi == "abi3" and not free_threaded:
|
|
# ABI3 wheels must start with cp3 for impl and tag
|
|
if not (interpreter.startswith("cp3") and tag.interpreter.startswith("cp3")):
|
|
continue
|
|
elif tag.abi == "none":
|
|
# CPythonless wheels must include py3 tag
|
|
if tag.interpreter[:3] != "py3":
|
|
continue
|
|
else:
|
|
# Other types of wheels are not detected, this is looking for previously built wheels.
|
|
continue
|
|
|
|
if tag.interpreter != "py3" and int(tag.interpreter[3:]) > int(interpreter[3:]):
|
|
# If a minor version number is given, it has to be lower than the current one.
|
|
continue
|
|
|
|
if platform.startswith(("manylinux", "musllinux", "macosx", "android", "ios")):
|
|
# On these platforms the wheel tag includes a platform version number, which we
|
|
# should ignore.
|
|
os_, arch = platform.split("_", 1)
|
|
if not tag.platform.startswith(os_):
|
|
continue
|
|
if not tag.platform.endswith(f"_{arch}"):
|
|
continue
|
|
elif platform.startswith("pyodide"):
|
|
# each Pyodide version has its own platform tag
|
|
continue
|
|
# Windows should exactly match
|
|
elif tag.platform != platform:
|
|
continue
|
|
|
|
# If all the filters above pass, then the wheel is a previously built compatible wheel.
|
|
return wheel
|
|
|
|
return None
|
|
|
|
|
|
def is_abi3_wheel(wheel_name: str) -> bool:
|
|
"""Check if a wheel uses the abi3 stable ABI based on its filename."""
|
|
_, _, _, tags = parse_wheel_filename(wheel_name)
|
|
return any(tag.abi == "abi3" for tag in tags)
|