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 <henryschreineriii@gmail.com> * fix: pyodide missing some logging Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com> * fix: nicer printout, nicer in-place summary Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com> * fix: use summary for everything Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com> * fix: support only one output wheel from repair Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com> * Add new Github summary format * Remove a couple of humanize uses * fix: filter ANSI codes in summary Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com> * fix: add sha256 Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com> * fix: nicer wheel/wheels depending on how many are present Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com> --------- Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com> Co-authored-by: Joe Rickerby <joerick@mac.com>
This commit is contained in:
co-authored by
Joe Rickerby
parent
a2c263d4ab
commit
d0e4aded3e
@@ -29,6 +29,7 @@ repos:
|
|||||||
additional_dependencies: &mypy-dependencies
|
additional_dependencies: &mypy-dependencies
|
||||||
- bracex
|
- bracex
|
||||||
- dependency-groups>=1.2
|
- dependency-groups>=1.2
|
||||||
|
- humanize
|
||||||
- nox>=2025.2.9
|
- nox>=2025.2.9
|
||||||
- orjson
|
- orjson
|
||||||
- packaging
|
- packaging
|
||||||
|
|||||||
@@ -7,10 +7,9 @@ import shutil
|
|||||||
import sys
|
import sys
|
||||||
import tarfile
|
import tarfile
|
||||||
import textwrap
|
import textwrap
|
||||||
import time
|
|
||||||
import traceback
|
import traceback
|
||||||
import typing
|
import typing
|
||||||
from collections.abc import Generator, Iterable, Sequence
|
from collections.abc import Iterable, Sequence
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from tempfile import mkdtemp
|
from tempfile import mkdtemp
|
||||||
from typing import Any, Literal, TextIO
|
from typing import Any, Literal, TextIO
|
||||||
@@ -286,42 +285,6 @@ def _compute_platform(args: CommandLineArguments) -> PlatformName:
|
|||||||
return _compute_platform_auto()
|
return _compute_platform_auto()
|
||||||
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
|
||||||
def print_new_wheels(msg: str, output_dir: Path) -> Generator[None, None, None]:
|
|
||||||
"""
|
|
||||||
Prints the new items in a directory upon exiting. The message to display
|
|
||||||
can include {n} for number of wheels, {s} for total number of seconds,
|
|
||||||
and/or {m} for total number of minutes. Does not print anything if this
|
|
||||||
exits via exception.
|
|
||||||
"""
|
|
||||||
|
|
||||||
start_time = time.time()
|
|
||||||
existing_contents = set(output_dir.iterdir())
|
|
||||||
yield
|
|
||||||
final_contents = set(output_dir.iterdir())
|
|
||||||
|
|
||||||
new_contents = [
|
|
||||||
FileReport(wheel.name, f"{(wheel.stat().st_size + 1023) // 1024:,d}")
|
|
||||||
for wheel in final_contents - existing_contents
|
|
||||||
]
|
|
||||||
|
|
||||||
if not new_contents:
|
|
||||||
return
|
|
||||||
|
|
||||||
max_name_len = max(len(f.name) for f in new_contents)
|
|
||||||
max_size_len = max(len(f.size) for f in new_contents)
|
|
||||||
n = len(new_contents)
|
|
||||||
s = time.time() - start_time
|
|
||||||
m = s / 60
|
|
||||||
print(
|
|
||||||
msg.format(n=n, s=s, m=m),
|
|
||||||
*sorted(
|
|
||||||
f" {f.name:<{max_name_len}s} {f.size:>{max_size_len}s} kB" for f in new_contents
|
|
||||||
),
|
|
||||||
sep="\n",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def build_in_directory(args: CommandLineArguments) -> None:
|
def build_in_directory(args: CommandLineArguments) -> None:
|
||||||
platform: PlatformName = _compute_platform(args)
|
platform: PlatformName = _compute_platform(args)
|
||||||
if platform == "pyodide" and sys.platform == "win32":
|
if platform == "pyodide" and sys.platform == "win32":
|
||||||
@@ -382,7 +345,7 @@ def build_in_directory(args: CommandLineArguments) -> None:
|
|||||||
|
|
||||||
tmp_path = Path(mkdtemp(prefix="cibw-run-")).resolve(strict=True)
|
tmp_path = Path(mkdtemp(prefix="cibw-run-")).resolve(strict=True)
|
||||||
try:
|
try:
|
||||||
with print_new_wheels("\n{n} wheels produced in {m:.0f} minutes:", output_dir):
|
with log.print_summary(options=options):
|
||||||
platform_module.build(options, tmp_path)
|
platform_module.build(options, tmp_path)
|
||||||
finally:
|
finally:
|
||||||
# avoid https://github.com/python/cpython/issues/86962 by performing
|
# avoid https://github.com/python/cpython/issues/86962 by performing
|
||||||
|
|||||||
+12
-2
@@ -4,6 +4,8 @@ from enum import Enum
|
|||||||
|
|
||||||
from .util.helpers import strtobool
|
from .util.helpers import strtobool
|
||||||
|
|
||||||
|
ANSI_CODE_REGEX = re.compile(r"(\033\[[0-9;]*m)")
|
||||||
|
|
||||||
|
|
||||||
class CIProvider(Enum):
|
class CIProvider(Enum):
|
||||||
# official support
|
# official support
|
||||||
@@ -46,7 +48,7 @@ def fix_ansi_codes_for_github_actions(text: str) -> str:
|
|||||||
Github Actions forgets the current ANSI style on every new line. This
|
Github Actions forgets the current ANSI style on every new line. This
|
||||||
function repeats the current ANSI style on every new line.
|
function repeats the current ANSI style on every new line.
|
||||||
"""
|
"""
|
||||||
ansi_code_regex = re.compile(r"(\033\[[0-9;]*m)")
|
|
||||||
ansi_codes: list[str] = []
|
ansi_codes: list[str] = []
|
||||||
output = ""
|
output = ""
|
||||||
|
|
||||||
@@ -55,7 +57,7 @@ def fix_ansi_codes_for_github_actions(text: str) -> str:
|
|||||||
output += "".join(ansi_codes) + line
|
output += "".join(ansi_codes) + line
|
||||||
|
|
||||||
# split the line at each ANSI code
|
# split the line at each ANSI code
|
||||||
parts = ansi_code_regex.split(line)
|
parts = ANSI_CODE_REGEX.split(line)
|
||||||
# if there are any ANSI codes, save them
|
# if there are any ANSI codes, save them
|
||||||
if len(parts) > 1:
|
if len(parts) > 1:
|
||||||
# iterate over the ANSI codes in this line
|
# iterate over the ANSI codes in this line
|
||||||
@@ -67,3 +69,11 @@ def fix_ansi_codes_for_github_actions(text: str) -> str:
|
|||||||
ansi_codes.append(code)
|
ansi_codes.append(code)
|
||||||
|
|
||||||
return output
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
def filter_ansi_codes(text: str, /) -> str:
|
||||||
|
"""
|
||||||
|
Remove ANSI codes from text.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return ANSI_CODE_REGEX.sub("", text)
|
||||||
|
|||||||
+154
-20
@@ -1,11 +1,24 @@
|
|||||||
import codecs
|
import codecs
|
||||||
|
import contextlib
|
||||||
|
import dataclasses
|
||||||
|
import functools
|
||||||
|
import hashlib
|
||||||
|
import io
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
import textwrap
|
||||||
import time
|
import time
|
||||||
from typing import IO, AnyStr, Final, Literal
|
from collections.abc import Generator
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import IO, TYPE_CHECKING, AnyStr, Final, Literal
|
||||||
|
|
||||||
from .ci import CIProvider, detect_ci_provider
|
import humanize
|
||||||
|
|
||||||
|
from .ci import CIProvider, detect_ci_provider, filter_ansi_codes
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .options import Options
|
||||||
|
|
||||||
FoldPattern = tuple[str, str]
|
FoldPattern = tuple[str, str]
|
||||||
DEFAULT_FOLD_PATTERN: Final[FoldPattern] = ("{name}", "")
|
DEFAULT_FOLD_PATTERN: Final[FoldPattern] = ("{name}", "")
|
||||||
@@ -69,6 +82,33 @@ class Symbols:
|
|||||||
self.error = "✕" if unicode else "failed"
|
self.error = "✕" if unicode else "failed"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass(kw_only=True, frozen=True)
|
||||||
|
class BuildInfo:
|
||||||
|
identifier: str
|
||||||
|
filename: Path | None
|
||||||
|
duration: float
|
||||||
|
|
||||||
|
@functools.cached_property
|
||||||
|
def size(self) -> str | None:
|
||||||
|
if self.filename is None:
|
||||||
|
return None
|
||||||
|
return humanize.naturalsize(self.filename.stat().st_size)
|
||||||
|
|
||||||
|
@functools.cached_property
|
||||||
|
def sha256(self) -> str | None:
|
||||||
|
if self.filename is None:
|
||||||
|
return None
|
||||||
|
with self.filename.open("rb") as f:
|
||||||
|
digest = hashlib.file_digest(f, "sha256")
|
||||||
|
return digest.hexdigest()
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
duration = humanize.naturaldelta(self.duration)
|
||||||
|
if self.filename:
|
||||||
|
return f"{self.identifier}: {self.filename.name} {self.size} in {duration}, SHA256={self.sha256}"
|
||||||
|
return f"{self.identifier}: {duration} (test only)"
|
||||||
|
|
||||||
|
|
||||||
class Logger:
|
class Logger:
|
||||||
fold_mode: Literal["azure", "github", "travis", "disabled"]
|
fold_mode: Literal["azure", "github", "travis", "disabled"]
|
||||||
colors_enabled: bool
|
colors_enabled: bool
|
||||||
@@ -77,6 +117,7 @@ class Logger:
|
|||||||
build_start_time: float | None = None
|
build_start_time: float | None = None
|
||||||
step_start_time: float | None = None
|
step_start_time: float | None = None
|
||||||
active_fold_group_name: str | None = None
|
active_fold_group_name: str | None = None
|
||||||
|
summary: list[BuildInfo]
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
if sys.platform == "win32" and hasattr(sys.stdout, "reconfigure"):
|
if sys.platform == "win32" and hasattr(sys.stdout, "reconfigure"):
|
||||||
@@ -88,25 +129,28 @@ class Logger:
|
|||||||
|
|
||||||
ci_provider = detect_ci_provider()
|
ci_provider = detect_ci_provider()
|
||||||
|
|
||||||
if ci_provider == CIProvider.azure_pipelines:
|
match ci_provider:
|
||||||
self.fold_mode = "azure"
|
case CIProvider.azure_pipelines:
|
||||||
self.colors_enabled = True
|
self.fold_mode = "azure"
|
||||||
|
self.colors_enabled = True
|
||||||
|
|
||||||
elif ci_provider == CIProvider.github_actions:
|
case CIProvider.github_actions:
|
||||||
self.fold_mode = "github"
|
self.fold_mode = "github"
|
||||||
self.colors_enabled = True
|
self.colors_enabled = True
|
||||||
|
|
||||||
elif ci_provider == CIProvider.travis_ci:
|
case CIProvider.travis_ci:
|
||||||
self.fold_mode = "travis"
|
self.fold_mode = "travis"
|
||||||
self.colors_enabled = True
|
self.colors_enabled = True
|
||||||
|
|
||||||
elif ci_provider == CIProvider.appveyor:
|
case CIProvider.appveyor:
|
||||||
self.fold_mode = "disabled"
|
self.fold_mode = "disabled"
|
||||||
self.colors_enabled = True
|
self.colors_enabled = True
|
||||||
|
|
||||||
else:
|
case _:
|
||||||
self.fold_mode = "disabled"
|
self.fold_mode = "disabled"
|
||||||
self.colors_enabled = file_supports_color(sys.stdout)
|
self.colors_enabled = file_supports_color(sys.stdout)
|
||||||
|
|
||||||
|
self.summary = []
|
||||||
|
|
||||||
def build_start(self, identifier: str) -> None:
|
def build_start(self, identifier: str) -> None:
|
||||||
self.step_end()
|
self.step_end()
|
||||||
@@ -120,7 +164,7 @@ class Logger:
|
|||||||
self.build_start_time = time.time()
|
self.build_start_time = time.time()
|
||||||
self.active_build_identifier = identifier
|
self.active_build_identifier = identifier
|
||||||
|
|
||||||
def build_end(self) -> None:
|
def build_end(self, filename: Path | None) -> None:
|
||||||
assert self.build_start_time is not None
|
assert self.build_start_time is not None
|
||||||
assert self.active_build_identifier is not None
|
assert self.active_build_identifier is not None
|
||||||
self.step_end()
|
self.step_end()
|
||||||
@@ -128,11 +172,14 @@ class Logger:
|
|||||||
c = self.colors
|
c = self.colors
|
||||||
s = self.symbols
|
s = self.symbols
|
||||||
duration = time.time() - self.build_start_time
|
duration = time.time() - self.build_start_time
|
||||||
|
duration_str = humanize.naturaldelta(duration, minimum_unit="milliseconds")
|
||||||
|
|
||||||
print()
|
print()
|
||||||
print(
|
print(f"{c.green}{s.done} {c.end}{self.active_build_identifier} finished in {duration_str}")
|
||||||
f"{c.green}{s.done} {c.end}{self.active_build_identifier} finished in {duration:.2f}s"
|
self.summary.append(
|
||||||
|
BuildInfo(identifier=self.active_build_identifier, filename=filename, duration=duration)
|
||||||
)
|
)
|
||||||
|
|
||||||
self.build_start_time = None
|
self.build_start_time = None
|
||||||
self.active_build_identifier = None
|
self.active_build_identifier = None
|
||||||
|
|
||||||
@@ -147,6 +194,7 @@ class Logger:
|
|||||||
c = self.colors
|
c = self.colors
|
||||||
s = self.symbols
|
s = self.symbols
|
||||||
duration = time.time() - self.step_start_time
|
duration = time.time() - self.step_start_time
|
||||||
|
|
||||||
if success:
|
if success:
|
||||||
print(f"{c.green}{s.done} {c.end}{duration:.2f}s".rjust(78))
|
print(f"{c.green}{s.done} {c.end}{duration:.2f}s".rjust(78))
|
||||||
else:
|
else:
|
||||||
@@ -183,6 +231,26 @@ class Logger:
|
|||||||
c = self.colors
|
c = self.colors
|
||||||
print(f"cibuildwheel: {c.bright_red}error{c.end}: {error}\n", file=sys.stderr)
|
print(f"cibuildwheel: {c.bright_red}error{c.end}: {error}\n", file=sys.stderr)
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def print_summary(self, *, options: "Options") -> Generator[None, None, None]:
|
||||||
|
start = time.time()
|
||||||
|
yield
|
||||||
|
duration = time.time() - start
|
||||||
|
if summary_path := os.environ.get("GITHUB_STEP_SUMMARY"):
|
||||||
|
github_summary = self._github_step_summary(duration=duration, options=options)
|
||||||
|
Path(summary_path).write_text(filter_ansi_codes(github_summary), encoding="utf-8")
|
||||||
|
|
||||||
|
n = len(self.summary)
|
||||||
|
s = "s" if n > 1 else ""
|
||||||
|
duration_str = humanize.naturaldelta(duration)
|
||||||
|
print()
|
||||||
|
self._start_fold_group(f"{n} wheel{s} produced in {duration_str}")
|
||||||
|
for build_info in self.summary:
|
||||||
|
print(" ", build_info)
|
||||||
|
self._end_fold_group()
|
||||||
|
|
||||||
|
self.summary = []
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def step_active(self) -> bool:
|
def step_active(self) -> bool:
|
||||||
return self.step_start_time is not None
|
return self.step_start_time is not None
|
||||||
@@ -222,6 +290,72 @@ class Logger:
|
|||||||
# lowercase, shorten
|
# lowercase, shorten
|
||||||
return identifier.lower()[:20]
|
return identifier.lower()[:20]
|
||||||
|
|
||||||
|
def _github_step_summary(self, duration: float, options: "Options") -> str:
|
||||||
|
"""
|
||||||
|
Returns the GitHub step summary, in markdown format.
|
||||||
|
"""
|
||||||
|
out = io.StringIO()
|
||||||
|
options_summary = options.summary(
|
||||||
|
identifiers=[bi.identifier for bi in self.summary], skip_unset=True
|
||||||
|
)
|
||||||
|
out.write(
|
||||||
|
textwrap.dedent("""\
|
||||||
|
### 🎡 cibuildwheel
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>
|
||||||
|
Build options
|
||||||
|
</summary>
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
{options_summary}
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
""").format(options_summary=options_summary)
|
||||||
|
)
|
||||||
|
n_wheels = len([b for b in self.summary if b.filename])
|
||||||
|
wheel_rows = "\n".join(
|
||||||
|
"<tr>"
|
||||||
|
f"<td nowrap>{'<samp>' + b.filename.name + '</samp>' if b.filename else '*Build only*'}</td>"
|
||||||
|
f"<td nowrap>{b.size or 'N/A'}</td>"
|
||||||
|
f"<td nowrap><samp>{b.identifier}</samp></td>"
|
||||||
|
f"<td nowrap>{humanize.naturaldelta(b.duration)}</td>"
|
||||||
|
f"<td nowrap><samp>{b.sha256 or 'N/A'}</samp></td>"
|
||||||
|
"</tr>"
|
||||||
|
for b in self.summary
|
||||||
|
)
|
||||||
|
out.write(
|
||||||
|
textwrap.dedent("""\
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th align="left">Wheel</th>
|
||||||
|
<th align="left">Size</th>
|
||||||
|
<th align="left">Build identifier</th>
|
||||||
|
<th align="left">Time</th>
|
||||||
|
<th align="left">SHA256</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{wheel_rows}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div align="right"><sup>{n} wheel{s} created in {duration_str}</sup></div>
|
||||||
|
""").format(
|
||||||
|
wheel_rows=wheel_rows,
|
||||||
|
n=n_wheels,
|
||||||
|
duration_str=humanize.naturaldelta(duration),
|
||||||
|
s="s" if n_wheels > 1 else "",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
out.write("\n")
|
||||||
|
out.write("---")
|
||||||
|
out.write("\n")
|
||||||
|
return out.getvalue()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def colors(self) -> Colors:
|
def colors(self) -> Colors:
|
||||||
return Colors(enabled=self.colors_enabled)
|
return Colors(enabled=self.colors_enabled)
|
||||||
|
|||||||
+20
-5
@@ -907,14 +907,18 @@ class Options:
|
|||||||
defaults=True,
|
defaults=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
def summary(self, identifiers: Iterable[str]) -> str:
|
def summary(self, identifiers: Iterable[str], skip_unset: bool = False) -> str:
|
||||||
lines = []
|
lines = []
|
||||||
global_option_names = sorted(f.name for f in dataclasses.fields(self.globals))
|
global_option_names = sorted(f.name for f in dataclasses.fields(self.globals))
|
||||||
|
|
||||||
for option_name in global_option_names:
|
for option_name in global_option_names:
|
||||||
option_value = getattr(self.globals, option_name)
|
option_value = getattr(self.globals, option_name)
|
||||||
default_value = getattr(self.defaults.globals, option_name)
|
default_value = getattr(self.defaults.globals, option_name)
|
||||||
lines.append(self.option_summary(option_name, option_value, default_value))
|
line = self.option_summary(
|
||||||
|
option_name, option_value, default_value, skip_unset=skip_unset
|
||||||
|
)
|
||||||
|
if line is not None:
|
||||||
|
lines.append(line)
|
||||||
|
|
||||||
build_options = self.build_options(identifier=None)
|
build_options = self.build_options(identifier=None)
|
||||||
build_options_defaults = self.defaults.build_options(identifier=None)
|
build_options_defaults = self.defaults.build_options(identifier=None)
|
||||||
@@ -934,9 +938,15 @@ class Options:
|
|||||||
i: getattr(build_options_for_identifier[i], option_name) for i in identifiers
|
i: getattr(build_options_for_identifier[i], option_name) for i in identifiers
|
||||||
}
|
}
|
||||||
|
|
||||||
lines.append(
|
line = self.option_summary(
|
||||||
self.option_summary(option_name, option_value, default_value, overrides=overrides)
|
option_name,
|
||||||
|
option_value,
|
||||||
|
default_value,
|
||||||
|
overrides=overrides,
|
||||||
|
skip_unset=skip_unset,
|
||||||
)
|
)
|
||||||
|
if line is not None:
|
||||||
|
lines.append(line)
|
||||||
|
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
@@ -946,7 +956,8 @@ class Options:
|
|||||||
option_value: Any,
|
option_value: Any,
|
||||||
default_value: Any,
|
default_value: Any,
|
||||||
overrides: Mapping[str, Any] | None = None,
|
overrides: Mapping[str, Any] | None = None,
|
||||||
) -> str:
|
skip_unset: bool = False,
|
||||||
|
) -> str | None:
|
||||||
"""
|
"""
|
||||||
Return a summary of the option value, including any overrides, with
|
Return a summary of the option value, including any overrides, with
|
||||||
ANSI 'dim' color if it's the default.
|
ANSI 'dim' color if it's the default.
|
||||||
@@ -960,6 +971,10 @@ class Options:
|
|||||||
overrides_value_strs = {k: v for k, v in overrides_value_strs.items() if v != value_str}
|
overrides_value_strs = {k: v for k, v in overrides_value_strs.items() if v != value_str}
|
||||||
|
|
||||||
has_been_set = (value_str != default_value_str) or overrides_value_strs
|
has_been_set = (value_str != default_value_str) or overrides_value_strs
|
||||||
|
|
||||||
|
if skip_unset and not has_been_set:
|
||||||
|
return None
|
||||||
|
|
||||||
c = log.colors
|
c = log.colors
|
||||||
|
|
||||||
result = c.gray if not has_been_set else ""
|
result = c.gray if not has_been_set else ""
|
||||||
|
|||||||
@@ -673,6 +673,7 @@ def build(options: Options, tmp_path: Path) -> None:
|
|||||||
log.step_end()
|
log.step_end()
|
||||||
|
|
||||||
# We're all done here; move it to output (overwrite existing)
|
# We're all done here; move it to output (overwrite existing)
|
||||||
|
output_wheel: Path | None = None
|
||||||
if compatible_wheel is None:
|
if compatible_wheel is None:
|
||||||
output_wheel = build_options.output_dir.joinpath(built_wheel.name)
|
output_wheel = build_options.output_dir.joinpath(built_wheel.name)
|
||||||
moved_wheel = move_file(built_wheel, output_wheel)
|
moved_wheel = move_file(built_wheel, output_wheel)
|
||||||
@@ -685,7 +686,7 @@ def build(options: Options, tmp_path: Path) -> None:
|
|||||||
# Clean up
|
# Clean up
|
||||||
shutil.rmtree(identifier_tmp_dir)
|
shutil.rmtree(identifier_tmp_dir)
|
||||||
|
|
||||||
log.build_end()
|
log.build_end(output_wheel)
|
||||||
except subprocess.CalledProcessError as error:
|
except subprocess.CalledProcessError as error:
|
||||||
msg = f"Command {error.cmd} failed with code {error.returncode}. {error.stdout or ''}"
|
msg = f"Command {error.cmd} failed with code {error.returncode}. {error.stdout or ''}"
|
||||||
raise errors.FatalError(msg) from error
|
raise errors.FatalError(msg) from error
|
||||||
|
|||||||
@@ -414,13 +414,15 @@ def build_in_container(
|
|||||||
# clean up test environment
|
# clean up test environment
|
||||||
container.call(["rm", "-rf", testing_temp_dir])
|
container.call(["rm", "-rf", testing_temp_dir])
|
||||||
|
|
||||||
# move repaired wheels to output
|
# move repaired wheel to output
|
||||||
|
output_wheel: Path | None = None
|
||||||
if compatible_wheel is None:
|
if compatible_wheel is None:
|
||||||
container.call(["mkdir", "-p", container_output_dir])
|
container.call(["mkdir", "-p", container_output_dir])
|
||||||
container.call(["mv", repaired_wheel, container_output_dir])
|
container.call(["mv", repaired_wheel, container_output_dir])
|
||||||
built_wheels.append(container_output_dir / repaired_wheel.name)
|
built_wheels.append(container_output_dir / repaired_wheel.name)
|
||||||
|
output_wheel = options.globals.output_dir / repaired_wheel.name
|
||||||
|
|
||||||
log.build_end()
|
log.build_end(output_wheel)
|
||||||
|
|
||||||
log.step("Copying wheels back to host...")
|
log.step("Copying wheels back to host...")
|
||||||
# copy the output back into the host
|
# copy the output back into the host
|
||||||
|
|||||||
@@ -732,6 +732,7 @@ def build(options: Options, tmp_path: Path) -> None:
|
|||||||
shell_with_arch(test_command_prepared, cwd=test_cwd, env=virtualenv_env)
|
shell_with_arch(test_command_prepared, cwd=test_cwd, env=virtualenv_env)
|
||||||
|
|
||||||
# we're all done here; move it to output (overwrite existing)
|
# we're all done here; move it to output (overwrite existing)
|
||||||
|
output_wheel = None
|
||||||
if compatible_wheel is None:
|
if compatible_wheel is None:
|
||||||
output_wheel = build_options.output_dir.joinpath(repaired_wheel.name)
|
output_wheel = build_options.output_dir.joinpath(repaired_wheel.name)
|
||||||
moved_wheel = move_file(repaired_wheel, output_wheel)
|
moved_wheel = move_file(repaired_wheel, output_wheel)
|
||||||
@@ -744,7 +745,7 @@ def build(options: Options, tmp_path: Path) -> None:
|
|||||||
# clean up
|
# clean up
|
||||||
shutil.rmtree(identifier_tmp_dir)
|
shutil.rmtree(identifier_tmp_dir)
|
||||||
|
|
||||||
log.build_end()
|
log.build_end(output_wheel)
|
||||||
except subprocess.CalledProcessError as error:
|
except subprocess.CalledProcessError as error:
|
||||||
msg = f"Command {error.cmd} failed with code {error.returncode}. {error.stdout or ''}"
|
msg = f"Command {error.cmd} failed with code {error.returncode}. {error.stdout or ''}"
|
||||||
raise errors.FatalError(msg) from error
|
raise errors.FatalError(msg) from error
|
||||||
|
|||||||
@@ -540,6 +540,7 @@ def build(options: Options, tmp_path: Path) -> None:
|
|||||||
shell(test_command_prepared, cwd=test_cwd, env=virtualenv_env)
|
shell(test_command_prepared, cwd=test_cwd, env=virtualenv_env)
|
||||||
|
|
||||||
# we're all done here; move it to output (overwrite existing)
|
# we're all done here; move it to output (overwrite existing)
|
||||||
|
output_wheel: Path | None = None
|
||||||
if compatible_wheel is None:
|
if compatible_wheel is None:
|
||||||
output_wheel = build_options.output_dir.joinpath(repaired_wheel.name)
|
output_wheel = build_options.output_dir.joinpath(repaired_wheel.name)
|
||||||
moved_wheel = move_file(repaired_wheel, output_wheel)
|
moved_wheel = move_file(repaired_wheel, output_wheel)
|
||||||
@@ -548,7 +549,7 @@ def build(options: Options, tmp_path: Path) -> None:
|
|||||||
f"{repaired_wheel} was moved to {moved_wheel} instead of {output_wheel}"
|
f"{repaired_wheel} was moved to {moved_wheel} instead of {output_wheel}"
|
||||||
)
|
)
|
||||||
built_wheels.append(output_wheel)
|
built_wheels.append(output_wheel)
|
||||||
log.build_end()
|
log.build_end(output_wheel)
|
||||||
|
|
||||||
except subprocess.CalledProcessError as error:
|
except subprocess.CalledProcessError as error:
|
||||||
msg = f"Command {error.cmd} failed with code {error.returncode}. {error.stdout or ''}"
|
msg = f"Command {error.cmd} failed with code {error.returncode}. {error.stdout or ''}"
|
||||||
|
|||||||
@@ -613,6 +613,7 @@ def build(options: Options, tmp_path: Path) -> None:
|
|||||||
shell(test_command_prepared, cwd=test_cwd, env=virtualenv_env)
|
shell(test_command_prepared, cwd=test_cwd, env=virtualenv_env)
|
||||||
|
|
||||||
# we're all done here; move it to output (remove if already exists)
|
# we're all done here; move it to output (remove if already exists)
|
||||||
|
output_wheel = None
|
||||||
if compatible_wheel is None:
|
if compatible_wheel is None:
|
||||||
output_wheel = build_options.output_dir.joinpath(repaired_wheel.name)
|
output_wheel = build_options.output_dir.joinpath(repaired_wheel.name)
|
||||||
moved_wheel = move_file(repaired_wheel, output_wheel)
|
moved_wheel = move_file(repaired_wheel, output_wheel)
|
||||||
@@ -627,7 +628,7 @@ def build(options: Options, tmp_path: Path) -> None:
|
|||||||
# don't want to abort a build because of that)
|
# don't want to abort a build because of that)
|
||||||
shutil.rmtree(identifier_tmp_dir, ignore_errors=True)
|
shutil.rmtree(identifier_tmp_dir, ignore_errors=True)
|
||||||
|
|
||||||
log.build_end()
|
log.build_end(output_wheel)
|
||||||
except subprocess.CalledProcessError as error:
|
except subprocess.CalledProcessError as error:
|
||||||
msg = f"Command {error.cmd} failed with code {error.returncode}. {error.stdout or ''}"
|
msg = f"Command {error.cmd} failed with code {error.returncode}. {error.stdout or ''}"
|
||||||
raise errors.FatalError(msg) from error
|
raise errors.FatalError(msg) from error
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ dependencies = [
|
|||||||
"certifi",
|
"certifi",
|
||||||
"dependency-groups>=1.2",
|
"dependency-groups>=1.2",
|
||||||
"filelock",
|
"filelock",
|
||||||
|
"humanize",
|
||||||
"packaging>=20.9",
|
"packaging>=20.9",
|
||||||
"platformdirs"
|
"platformdirs"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ from pathlib import Path
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from cibuildwheel import __main__, architecture
|
from cibuildwheel import architecture
|
||||||
|
from cibuildwheel.logger import Logger
|
||||||
from cibuildwheel.platforms import linux, macos, pyodide, windows
|
from cibuildwheel.platforms import linux, macos, pyodide, windows
|
||||||
from cibuildwheel.util import file
|
from cibuildwheel.util import file
|
||||||
|
|
||||||
@@ -58,7 +59,7 @@ def disable_print_wheels(monkeypatch):
|
|||||||
def empty_cm(*args, **kwargs):
|
def empty_cm(*args, **kwargs):
|
||||||
yield
|
yield
|
||||||
|
|
||||||
monkeypatch.setattr(__main__, "print_new_wheels", empty_cm)
|
monkeypatch.setattr(Logger, "print_summary", empty_cm)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ def mock_build_container(monkeypatch):
|
|||||||
"cibuildwheel.platforms.linux.build_in_container",
|
"cibuildwheel.platforms.linux.build_in_container",
|
||||||
mock.Mock(spec=platforms.linux.build_in_container),
|
mock.Mock(spec=platforms.linux.build_in_container),
|
||||||
)
|
)
|
||||||
monkeypatch.setattr("cibuildwheel.__main__.print_new_wheels", ignore_context_call)
|
monkeypatch.setattr("cibuildwheel.logger.Logger.print_summary", ignore_context_call)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("mock_build_container", "fake_package_dir")
|
@pytest.mark.usefixtures("mock_build_container", "fake_package_dir")
|
||||||
|
|||||||
@@ -1,34 +1,39 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from cibuildwheel.__main__ import print_new_wheels
|
from cibuildwheel.logger import BuildInfo, Logger
|
||||||
|
from cibuildwheel.options import CommandLineArguments, Options
|
||||||
|
|
||||||
|
OPTIONS_DEFAULTS = Options("linux", CommandLineArguments.defaults(), {}, defaults=True)
|
||||||
|
FILE = Path(__file__)
|
||||||
|
|
||||||
|
|
||||||
def test_printout_wheels(tmp_path, capsys):
|
def test_printout_wheels(capsys):
|
||||||
tmp_path.joinpath("example.0").touch()
|
log = Logger()
|
||||||
with print_new_wheels("TEST_MSG: {n}", tmp_path):
|
log.fold_mode = "disabled"
|
||||||
tmp_path.joinpath("example.1").write_bytes(b"0" * 1023)
|
log.colors_enabled = False
|
||||||
tmp_path.joinpath("example.2").write_bytes(b"0" * 1025)
|
|
||||||
|
with log.print_summary(options=OPTIONS_DEFAULTS):
|
||||||
|
log.summary = [
|
||||||
|
BuildInfo(identifier="id1", filename=None, duration=3),
|
||||||
|
BuildInfo(identifier="id2", filename=FILE, duration=2),
|
||||||
|
]
|
||||||
|
|
||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
assert captured.err == ""
|
assert captured.err == ""
|
||||||
|
|
||||||
assert "example.0" not in captured.out
|
assert "id1" in captured.out
|
||||||
assert "example.1 1 kB\n" in captured.out
|
assert "id2" in captured.out
|
||||||
assert "example.2 2 kB\n" in captured.out
|
assert "wheels produced in" in captured.out
|
||||||
assert "TEST_MSG:" in captured.out
|
assert "SHA256=" in captured.out
|
||||||
assert "TEST_MSG: 2\n" in captured.out
|
|
||||||
|
|
||||||
|
|
||||||
def test_no_printout_on_error(tmp_path, capsys):
|
def test_no_printout_on_error(capsys):
|
||||||
tmp_path.joinpath("example.0").touch()
|
log = Logger()
|
||||||
with pytest.raises(RuntimeError), print_new_wheels("TEST_MSG: {n}", tmp_path): # noqa: PT012
|
with pytest.raises(RuntimeError), log.print_summary(options=OPTIONS_DEFAULTS):
|
||||||
tmp_path.joinpath("example.1").touch()
|
|
||||||
raise RuntimeError()
|
raise RuntimeError()
|
||||||
|
|
||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
assert captured.err == ""
|
assert captured.err == ""
|
||||||
|
assert captured.out == ""
|
||||||
assert "example.0" not in captured.out
|
|
||||||
assert "example.1" not in captured.out
|
|
||||||
assert "example.2" not in captured.out
|
|
||||||
assert "TEST_MSG:" not in captured.out
|
|
||||||
|
|||||||
Reference in New Issue
Block a user