Skip to content

Commit d0e4ade

Browse files
henryiiijoerick
andauthored
feat: add summary to Action (#2469)
* chore: add summary to Action * refactor: new summary table * fix: fixup tests and formatting Signed-off-by: Henry Schreiner <[email protected]> * fix: pyodide missing some logging Signed-off-by: Henry Schreiner <[email protected]> * fix: nicer printout, nicer in-place summary Signed-off-by: Henry Schreiner <[email protected]> * fix: use summary for everything Signed-off-by: Henry Schreiner <[email protected]> * fix: support only one output wheel from repair Signed-off-by: Henry Schreiner <[email protected]> * Add new Github summary format * Remove a couple of humanize uses * fix: filter ANSI codes in summary Signed-off-by: Henry Schreiner <[email protected]> * fix: add sha256 Signed-off-by: Henry Schreiner <[email protected]> * fix: nicer wheel/wheels depending on how many are present Signed-off-by: Henry Schreiner <[email protected]> --------- Signed-off-by: Henry Schreiner <[email protected]> Co-authored-by: Joe Rickerby <[email protected]>
1 parent a2c263d commit d0e4ade

File tree

14 files changed

+231
-95
lines changed

14 files changed

+231
-95
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ repos:
2929
additional_dependencies: &mypy-dependencies
3030
- bracex
3131
- dependency-groups>=1.2
32+
- humanize
3233
- nox>=2025.2.9
3334
- orjson
3435
- packaging

cibuildwheel/__main__.py

Lines changed: 2 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,9 @@
77
import sys
88
import tarfile
99
import textwrap
10-
import time
1110
import traceback
1211
import typing
13-
from collections.abc import Generator, Iterable, Sequence
12+
from collections.abc import Iterable, Sequence
1413
from pathlib import Path
1514
from tempfile import mkdtemp
1615
from typing import Any, Literal, TextIO
@@ -286,42 +285,6 @@ def _compute_platform(args: CommandLineArguments) -> PlatformName:
286285
return _compute_platform_auto()
287286

288287

289-
@contextlib.contextmanager
290-
def print_new_wheels(msg: str, output_dir: Path) -> Generator[None, None, None]:
291-
"""
292-
Prints the new items in a directory upon exiting. The message to display
293-
can include {n} for number of wheels, {s} for total number of seconds,
294-
and/or {m} for total number of minutes. Does not print anything if this
295-
exits via exception.
296-
"""
297-
298-
start_time = time.time()
299-
existing_contents = set(output_dir.iterdir())
300-
yield
301-
final_contents = set(output_dir.iterdir())
302-
303-
new_contents = [
304-
FileReport(wheel.name, f"{(wheel.stat().st_size + 1023) // 1024:,d}")
305-
for wheel in final_contents - existing_contents
306-
]
307-
308-
if not new_contents:
309-
return
310-
311-
max_name_len = max(len(f.name) for f in new_contents)
312-
max_size_len = max(len(f.size) for f in new_contents)
313-
n = len(new_contents)
314-
s = time.time() - start_time
315-
m = s / 60
316-
print(
317-
msg.format(n=n, s=s, m=m),
318-
*sorted(
319-
f" {f.name:<{max_name_len}s} {f.size:>{max_size_len}s} kB" for f in new_contents
320-
),
321-
sep="\n",
322-
)
323-
324-
325288
def build_in_directory(args: CommandLineArguments) -> None:
326289
platform: PlatformName = _compute_platform(args)
327290
if platform == "pyodide" and sys.platform == "win32":
@@ -382,7 +345,7 @@ def build_in_directory(args: CommandLineArguments) -> None:
382345

383346
tmp_path = Path(mkdtemp(prefix="cibw-run-")).resolve(strict=True)
384347
try:
385-
with print_new_wheels("\n{n} wheels produced in {m:.0f} minutes:", output_dir):
348+
with log.print_summary(options=options):
386349
platform_module.build(options, tmp_path)
387350
finally:
388351
# avoid https://github.com/python/cpython/issues/86962 by performing

cibuildwheel/ci.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
from .util.helpers import strtobool
66

7+
ANSI_CODE_REGEX = re.compile(r"(\033\[[0-9;]*m)")
8+
79

810
class CIProvider(Enum):
911
# official support
@@ -46,7 +48,7 @@ def fix_ansi_codes_for_github_actions(text: str) -> str:
4648
Github Actions forgets the current ANSI style on every new line. This
4749
function repeats the current ANSI style on every new line.
4850
"""
49-
ansi_code_regex = re.compile(r"(\033\[[0-9;]*m)")
51+
5052
ansi_codes: list[str] = []
5153
output = ""
5254

@@ -55,7 +57,7 @@ def fix_ansi_codes_for_github_actions(text: str) -> str:
5557
output += "".join(ansi_codes) + line
5658

5759
# split the line at each ANSI code
58-
parts = ansi_code_regex.split(line)
60+
parts = ANSI_CODE_REGEX.split(line)
5961
# if there are any ANSI codes, save them
6062
if len(parts) > 1:
6163
# iterate over the ANSI codes in this line
@@ -67,3 +69,11 @@ def fix_ansi_codes_for_github_actions(text: str) -> str:
6769
ansi_codes.append(code)
6870

6971
return output
72+
73+
74+
def filter_ansi_codes(text: str, /) -> str:
75+
"""
76+
Remove ANSI codes from text.
77+
"""
78+
79+
return ANSI_CODE_REGEX.sub("", text)

cibuildwheel/logger.py

Lines changed: 154 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,24 @@
11
import codecs
2+
import contextlib
3+
import dataclasses
4+
import functools
5+
import hashlib
6+
import io
27
import os
38
import re
49
import sys
10+
import textwrap
511
import time
6-
from typing import IO, AnyStr, Final, Literal
12+
from collections.abc import Generator
13+
from pathlib import Path
14+
from typing import IO, TYPE_CHECKING, AnyStr, Final, Literal
715

8-
from .ci import CIProvider, detect_ci_provider
16+
import humanize
17+
18+
from .ci import CIProvider, detect_ci_provider, filter_ansi_codes
19+
20+
if TYPE_CHECKING:
21+
from .options import Options
922

1023
FoldPattern = tuple[str, str]
1124
DEFAULT_FOLD_PATTERN: Final[FoldPattern] = ("{name}", "")
@@ -69,6 +82,33 @@ def __init__(self, *, unicode: bool) -> None:
6982
self.error = "✕" if unicode else "failed"
7083

7184

85+
@dataclasses.dataclass(kw_only=True, frozen=True)
86+
class BuildInfo:
87+
identifier: str
88+
filename: Path | None
89+
duration: float
90+
91+
@functools.cached_property
92+
def size(self) -> str | None:
93+
if self.filename is None:
94+
return None
95+
return humanize.naturalsize(self.filename.stat().st_size)
96+
97+
@functools.cached_property
98+
def sha256(self) -> str | None:
99+
if self.filename is None:
100+
return None
101+
with self.filename.open("rb") as f:
102+
digest = hashlib.file_digest(f, "sha256")
103+
return digest.hexdigest()
104+
105+
def __str__(self) -> str:
106+
duration = humanize.naturaldelta(self.duration)
107+
if self.filename:
108+
return f"{self.identifier}: {self.filename.name} {self.size} in {duration}, SHA256={self.sha256}"
109+
return f"{self.identifier}: {duration} (test only)"
110+
111+
72112
class Logger:
73113
fold_mode: Literal["azure", "github", "travis", "disabled"]
74114
colors_enabled: bool
@@ -77,6 +117,7 @@ class Logger:
77117
build_start_time: float | None = None
78118
step_start_time: float | None = None
79119
active_fold_group_name: str | None = None
120+
summary: list[BuildInfo]
80121

81122
def __init__(self) -> None:
82123
if sys.platform == "win32" and hasattr(sys.stdout, "reconfigure"):
@@ -88,25 +129,28 @@ def __init__(self) -> None:
88129

89130
ci_provider = detect_ci_provider()
90131

91-
if ci_provider == CIProvider.azure_pipelines:
92-
self.fold_mode = "azure"
93-
self.colors_enabled = True
132+
match ci_provider:
133+
case CIProvider.azure_pipelines:
134+
self.fold_mode = "azure"
135+
self.colors_enabled = True
94136

95-
elif ci_provider == CIProvider.github_actions:
96-
self.fold_mode = "github"
97-
self.colors_enabled = True
137+
case CIProvider.github_actions:
138+
self.fold_mode = "github"
139+
self.colors_enabled = True
98140

99-
elif ci_provider == CIProvider.travis_ci:
100-
self.fold_mode = "travis"
101-
self.colors_enabled = True
141+
case CIProvider.travis_ci:
142+
self.fold_mode = "travis"
143+
self.colors_enabled = True
102144

103-
elif ci_provider == CIProvider.appveyor:
104-
self.fold_mode = "disabled"
105-
self.colors_enabled = True
145+
case CIProvider.appveyor:
146+
self.fold_mode = "disabled"
147+
self.colors_enabled = True
106148

107-
else:
108-
self.fold_mode = "disabled"
109-
self.colors_enabled = file_supports_color(sys.stdout)
149+
case _:
150+
self.fold_mode = "disabled"
151+
self.colors_enabled = file_supports_color(sys.stdout)
152+
153+
self.summary = []
110154

111155
def build_start(self, identifier: str) -> None:
112156
self.step_end()
@@ -120,19 +164,22 @@ def build_start(self, identifier: str) -> None:
120164
self.build_start_time = time.time()
121165
self.active_build_identifier = identifier
122166

123-
def build_end(self) -> None:
167+
def build_end(self, filename: Path | None) -> None:
124168
assert self.build_start_time is not None
125169
assert self.active_build_identifier is not None
126170
self.step_end()
127171

128172
c = self.colors
129173
s = self.symbols
130174
duration = time.time() - self.build_start_time
175+
duration_str = humanize.naturaldelta(duration, minimum_unit="milliseconds")
131176

132177
print()
133-
print(
134-
f"{c.green}{s.done} {c.end}{self.active_build_identifier} finished in {duration:.2f}s"
178+
print(f"{c.green}{s.done} {c.end}{self.active_build_identifier} finished in {duration_str}")
179+
self.summary.append(
180+
BuildInfo(identifier=self.active_build_identifier, filename=filename, duration=duration)
135181
)
182+
136183
self.build_start_time = None
137184
self.active_build_identifier = None
138185

@@ -147,6 +194,7 @@ def step_end(self, success: bool = True) -> None:
147194
c = self.colors
148195
s = self.symbols
149196
duration = time.time() - self.step_start_time
197+
150198
if success:
151199
print(f"{c.green}{s.done} {c.end}{duration:.2f}s".rjust(78))
152200
else:
@@ -183,6 +231,26 @@ def error(self, error: BaseException | str) -> None:
183231
c = self.colors
184232
print(f"cibuildwheel: {c.bright_red}error{c.end}: {error}\n", file=sys.stderr)
185233

234+
@contextlib.contextmanager
235+
def print_summary(self, *, options: "Options") -> Generator[None, None, None]:
236+
start = time.time()
237+
yield
238+
duration = time.time() - start
239+
if summary_path := os.environ.get("GITHUB_STEP_SUMMARY"):
240+
github_summary = self._github_step_summary(duration=duration, options=options)
241+
Path(summary_path).write_text(filter_ansi_codes(github_summary), encoding="utf-8")
242+
243+
n = len(self.summary)
244+
s = "s" if n > 1 else ""
245+
duration_str = humanize.naturaldelta(duration)
246+
print()
247+
self._start_fold_group(f"{n} wheel{s} produced in {duration_str}")
248+
for build_info in self.summary:
249+
print(" ", build_info)
250+
self._end_fold_group()
251+
252+
self.summary = []
253+
186254
@property
187255
def step_active(self) -> bool:
188256
return self.step_start_time is not None
@@ -222,6 +290,72 @@ def _fold_group_identifier(name: str) -> str:
222290
# lowercase, shorten
223291
return identifier.lower()[:20]
224292

293+
def _github_step_summary(self, duration: float, options: "Options") -> str:
294+
"""
295+
Returns the GitHub step summary, in markdown format.
296+
"""
297+
out = io.StringIO()
298+
options_summary = options.summary(
299+
identifiers=[bi.identifier for bi in self.summary], skip_unset=True
300+
)
301+
out.write(
302+
textwrap.dedent("""\
303+
### 🎡 cibuildwheel
304+
305+
<details>
306+
<summary>
307+
Build options
308+
</summary>
309+
310+
```yaml
311+
{options_summary}
312+
```
313+
314+
</details>
315+
316+
""").format(options_summary=options_summary)
317+
)
318+
n_wheels = len([b for b in self.summary if b.filename])
319+
wheel_rows = "\n".join(
320+
"<tr>"
321+
f"<td nowrap>{'<samp>' + b.filename.name + '</samp>' if b.filename else '*Build only*'}</td>"
322+
f"<td nowrap>{b.size or 'N/A'}</td>"
323+
f"<td nowrap><samp>{b.identifier}</samp></td>"
324+
f"<td nowrap>{humanize.naturaldelta(b.duration)}</td>"
325+
f"<td nowrap><samp>{b.sha256 or 'N/A'}</samp></td>"
326+
"</tr>"
327+
for b in self.summary
328+
)
329+
out.write(
330+
textwrap.dedent("""\
331+
<table>
332+
<thead>
333+
<tr>
334+
<th align="left">Wheel</th>
335+
<th align="left">Size</th>
336+
<th align="left">Build identifier</th>
337+
<th align="left">Time</th>
338+
<th align="left">SHA256</th>
339+
</tr>
340+
</thead>
341+
<tbody>
342+
{wheel_rows}
343+
</tbody>
344+
</table>
345+
<div align="right"><sup>{n} wheel{s} created in {duration_str}</sup></div>
346+
""").format(
347+
wheel_rows=wheel_rows,
348+
n=n_wheels,
349+
duration_str=humanize.naturaldelta(duration),
350+
s="s" if n_wheels > 1 else "",
351+
)
352+
)
353+
354+
out.write("\n")
355+
out.write("---")
356+
out.write("\n")
357+
return out.getvalue()
358+
225359
@property
226360
def colors(self) -> Colors:
227361
return Colors(enabled=self.colors_enabled)

0 commit comments

Comments
 (0)