Skip to content

Commit b605f8f

Browse files
authored
chore: refactor cibuildwheel.utils (pypa#2252)
* chore: refactor cibuildwheel.utils * rework build_frontend extra flags * ci(fix): use tonistiigi/binfmt:qemu-v8.1.5 image for qemu
1 parent 318a963 commit b605f8f

39 files changed

+1188
-1187
lines changed

.gitignore

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,7 @@ celerybeat-schedule
8484

8585
# virtualenv
8686
.venv
87-
venv/
88-
venv3/
89-
venv2/
87+
venv*/
9088
ENV/
9189
env/
9290
env2/
@@ -112,8 +110,5 @@ all_known_setup.yaml
112110
# mkdocs
113111
site/
114112

115-
# Virtual environments
116-
venv*
117-
118113
# PyCharm
119114
.idea/

cibuildwheel/__main__.py

Lines changed: 67 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@
88
import sys
99
import tarfile
1010
import textwrap
11+
import time
1112
import traceback
1213
import typing
13-
from collections.abc import Iterable, Sequence, Set
14+
from collections.abc import Generator, Iterable, Sequence, Set
1415
from pathlib import Path
1516
from tempfile import mkdtemp
16-
from typing import Protocol, assert_never
17+
from typing import Any, Protocol, TextIO, assert_never
1718

1819
import cibuildwheel
1920
import cibuildwheel.linux
@@ -23,26 +24,43 @@
2324
import cibuildwheel.windows
2425
from cibuildwheel import errors
2526
from cibuildwheel.architecture import Architecture, allowed_architectures_check
27+
from cibuildwheel.ci import CIProvider, detect_ci_provider, fix_ansi_codes_for_github_actions
2628
from cibuildwheel.logger import log
2729
from cibuildwheel.options import CommandLineArguments, Options, compute_options
30+
from cibuildwheel.selector import BuildSelector, EnableGroup
2831
from cibuildwheel.typing import PLATFORMS, GenericPythonConfiguration, PlatformName
29-
from cibuildwheel.util import (
30-
CIBW_CACHE_PATH,
31-
BuildSelector,
32-
CIProvider,
33-
EnableGroup,
34-
Unbuffered,
35-
detect_ci_provider,
36-
fix_ansi_codes_for_github_actions,
37-
strtobool,
38-
)
32+
from cibuildwheel.util.file import CIBW_CACHE_PATH
33+
from cibuildwheel.util.helpers import strtobool
3934

4035

4136
@dataclasses.dataclass
4237
class GlobalOptions:
4338
print_traceback_on_error: bool = True # decides what happens when errors are hit.
4439

4540

41+
@dataclasses.dataclass(frozen=True)
42+
class FileReport:
43+
name: str
44+
size: str
45+
46+
47+
# Taken from https://stackoverflow.com/a/107717
48+
class Unbuffered:
49+
def __init__(self, stream: TextIO) -> None:
50+
self.stream = stream
51+
52+
def write(self, data: str) -> None:
53+
self.stream.write(data)
54+
self.stream.flush()
55+
56+
def writelines(self, data: Iterable[str]) -> None:
57+
self.stream.writelines(data)
58+
self.stream.flush()
59+
60+
def __getattr__(self, attr: str) -> Any:
61+
return getattr(self.stream, attr)
62+
63+
4664
def main() -> None:
4765
global_options = GlobalOptions()
4866
try:
@@ -288,6 +306,42 @@ def get_platform_module(platform: PlatformName) -> PlatformModule:
288306
assert_never(platform)
289307

290308

309+
@contextlib.contextmanager
310+
def print_new_wheels(msg: str, output_dir: Path) -> Generator[None, None, None]:
311+
"""
312+
Prints the new items in a directory upon exiting. The message to display
313+
can include {n} for number of wheels, {s} for total number of seconds,
314+
and/or {m} for total number of minutes. Does not print anything if this
315+
exits via exception.
316+
"""
317+
318+
start_time = time.time()
319+
existing_contents = set(output_dir.iterdir())
320+
yield
321+
final_contents = set(output_dir.iterdir())
322+
323+
new_contents = [
324+
FileReport(wheel.name, f"{(wheel.stat().st_size + 1023) // 1024:,d}")
325+
for wheel in final_contents - existing_contents
326+
]
327+
328+
if not new_contents:
329+
return
330+
331+
max_name_len = max(len(f.name) for f in new_contents)
332+
max_size_len = max(len(f.size) for f in new_contents)
333+
n = len(new_contents)
334+
s = time.time() - start_time
335+
m = s / 60
336+
print(
337+
msg.format(n=n, s=s, m=m),
338+
*sorted(
339+
f" {f.name:<{max_name_len}s} {f.size:>{max_size_len}s} kB" for f in new_contents
340+
),
341+
sep="\n",
342+
)
343+
344+
291345
def build_in_directory(args: CommandLineArguments) -> None:
292346
platform: PlatformName = _compute_platform(args)
293347
if platform == "pyodide" and sys.platform == "win32":
@@ -350,9 +404,7 @@ def build_in_directory(args: CommandLineArguments) -> None:
350404

351405
tmp_path = Path(mkdtemp(prefix="cibw-run-")).resolve(strict=True)
352406
try:
353-
with cibuildwheel.util.print_new_wheels(
354-
"\n{n} wheels produced in {m:.0f} minutes:", output_dir
355-
):
407+
with print_new_wheels("\n{n} wheels produced in {m:.0f} minutes:", output_dir):
356408
platform_module.build(options, tmp_path)
357409
finally:
358410
# avoid https://github.com/python/cpython/issues/86962 by performing

cibuildwheel/ci.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
from __future__ import annotations
2+
3+
import os
4+
import re
5+
from enum import Enum
6+
7+
from .util.helpers import strtobool
8+
9+
10+
class CIProvider(Enum):
11+
travis_ci = "travis"
12+
appveyor = "appveyor"
13+
circle_ci = "circle_ci"
14+
azure_pipelines = "azure_pipelines"
15+
github_actions = "github_actions"
16+
gitlab = "gitlab"
17+
cirrus_ci = "cirrus_ci"
18+
other = "other"
19+
20+
21+
def detect_ci_provider() -> CIProvider | None:
22+
if "TRAVIS" in os.environ:
23+
return CIProvider.travis_ci
24+
elif "APPVEYOR" in os.environ:
25+
return CIProvider.appveyor
26+
elif "CIRCLECI" in os.environ:
27+
return CIProvider.circle_ci
28+
elif "AZURE_HTTP_USER_AGENT" in os.environ:
29+
return CIProvider.azure_pipelines
30+
elif "GITHUB_ACTIONS" in os.environ:
31+
return CIProvider.github_actions
32+
elif "GITLAB_CI" in os.environ:
33+
return CIProvider.gitlab
34+
elif "CIRRUS_CI" in os.environ:
35+
return CIProvider.cirrus_ci
36+
elif strtobool(os.environ.get("CI", "false")):
37+
return CIProvider.other
38+
else:
39+
return None
40+
41+
42+
def fix_ansi_codes_for_github_actions(text: str) -> str:
43+
"""
44+
Github Actions forgets the current ANSI style on every new line. This
45+
function repeats the current ANSI style on every new line.
46+
"""
47+
ansi_code_regex = re.compile(r"(\033\[[0-9;]*m)")
48+
ansi_codes: list[str] = []
49+
output = ""
50+
51+
for line in text.splitlines(keepends=True):
52+
# add the current ANSI codes to the beginning of the line
53+
output += "".join(ansi_codes) + line
54+
55+
# split the line at each ANSI code
56+
parts = ansi_code_regex.split(line)
57+
# if there are any ANSI codes, save them
58+
if len(parts) > 1:
59+
# iterate over the ANSI codes in this line
60+
for code in parts[1::2]:
61+
if code == "\033[0m":
62+
# reset the list of ANSI codes when the clear code is found
63+
ansi_codes = []
64+
else:
65+
ansi_codes.append(code)
66+
67+
return output

cibuildwheel/frontend.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
from __future__ import annotations
2+
3+
import shlex
4+
import typing
5+
from collections.abc import Sequence
6+
from dataclasses import dataclass
7+
from typing import Literal
8+
9+
from .logger import log
10+
from .util.helpers import parse_key_value_string
11+
12+
BuildFrontendName = Literal["pip", "build", "build[uv]"]
13+
14+
15+
@dataclass(frozen=True)
16+
class BuildFrontendConfig:
17+
name: BuildFrontendName
18+
args: Sequence[str] = ()
19+
20+
@staticmethod
21+
def from_config_string(config_string: str) -> BuildFrontendConfig:
22+
config_dict = parse_key_value_string(config_string, ["name"], ["args"])
23+
name = " ".join(config_dict["name"])
24+
if name not in {"pip", "build", "build[uv]"}:
25+
msg = f"Unrecognised build frontend {name!r}, only 'pip', 'build', and 'build[uv]' are supported"
26+
raise ValueError(msg)
27+
28+
name = typing.cast(BuildFrontendName, name)
29+
30+
args = config_dict.get("args") or []
31+
return BuildFrontendConfig(name=name, args=args)
32+
33+
def options_summary(self) -> str | dict[str, str]:
34+
if not self.args:
35+
return self.name
36+
else:
37+
return {"name": self.name, "args": repr(self.args)}
38+
39+
40+
def _get_verbosity_flags(level: int, frontend: BuildFrontendName) -> list[str]:
41+
if frontend == "pip":
42+
if level > 0:
43+
return ["-" + level * "v"]
44+
if level < 0:
45+
return ["-" + -level * "q"]
46+
elif not 0 <= level < 2:
47+
msg = f"build_verbosity {level} is not supported for build frontend. Ignoring."
48+
log.warning(msg)
49+
return []
50+
51+
52+
def _split_config_settings(config_settings: str, frontend: BuildFrontendName) -> list[str]:
53+
config_settings_list = shlex.split(config_settings)
54+
s = "s" if frontend == "pip" else ""
55+
return [f"--config-setting{s}={setting}" for setting in config_settings_list]
56+
57+
58+
def get_build_frontend_extra_flags(
59+
build_frontend: BuildFrontendConfig, verbosity_level: int, config_settings: str
60+
) -> list[str]:
61+
return [
62+
*_split_config_settings(config_settings, build_frontend.name),
63+
*build_frontend.args,
64+
*_get_verbosity_flags(verbosity_level, build_frontend.name),
65+
]

cibuildwheel/linux.py

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,16 @@
1414

1515
from . import errors
1616
from .architecture import Architecture
17+
from .frontend import BuildFrontendConfig, get_build_frontend_extra_flags
1718
from .logger import log
1819
from .oci_container import OCIContainer, OCIContainerEngineConfig, OCIPlatform
1920
from .options import BuildOptions, Options
21+
from .selector import BuildSelector
2022
from .typing import PathOrStr
21-
from .util import (
22-
BuildFrontendConfig,
23-
BuildSelector,
24-
copy_test_sources,
25-
find_compatible_wheel,
26-
get_build_verbosity_extra_flags,
27-
prepare_command,
28-
read_python_configs,
29-
split_config_settings,
30-
unwrap,
31-
)
23+
from .util import resources
24+
from .util.file import copy_test_sources
25+
from .util.helpers import prepare_command, unwrap
26+
from .util.packaging import find_compatible_wheel
3227

3328
ARCHITECTURE_OCI_PLATFORM_MAP = {
3429
Architecture.x86_64: OCIPlatform.AMD64,
@@ -63,7 +58,7 @@ def get_python_configurations(
6358
build_selector: BuildSelector,
6459
architectures: Set[Architecture],
6560
) -> list[PythonConfiguration]:
66-
full_python_configs = read_python_configs("linux")
61+
full_python_configs = resources.read_python_configs("linux")
6762

6863
python_configurations = [PythonConfiguration(**item) for item in full_python_configs]
6964

@@ -275,11 +270,11 @@ def build_in_container(
275270
container.call(["rm", "-rf", built_wheel_dir])
276271
container.call(["mkdir", "-p", built_wheel_dir])
277272

278-
extra_flags = split_config_settings(build_options.config_settings, build_frontend.name)
279-
extra_flags += build_frontend.args
273+
extra_flags = get_build_frontend_extra_flags(
274+
build_frontend, build_options.build_verbosity, build_options.config_settings
275+
)
280276

281277
if build_frontend.name == "pip":
282-
extra_flags += get_build_verbosity_extra_flags(build_options.build_verbosity)
283278
container.call(
284279
[
285280
"python",
@@ -294,9 +289,6 @@ def build_in_container(
294289
env=env,
295290
)
296291
elif build_frontend.name == "build" or build_frontend.name == "build[uv]":
297-
if not 0 <= build_options.build_verbosity < 2:
298-
msg = f"build_verbosity {build_options.build_verbosity} is not supported for build frontend. Ignoring."
299-
log.warning(msg)
300292
if use_uv and "--no-isolation" not in extra_flags and "-n" not in extra_flags:
301293
extra_flags += ["--installer=uv"]
302294
container.call(

cibuildwheel/logger.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import time
88
from typing import IO, AnyStr, Final
99

10-
from .util import CIProvider, detect_ci_provider
10+
from .ci import CIProvider, detect_ci_provider
1111

1212
FoldPattern = tuple[str, str]
1313
DEFAULT_FOLD_PATTERN: Final[FoldPattern] = ("{name}", "")

0 commit comments

Comments
 (0)