mirror of
https://github.com/vercel-labs/fx.git
synced 2026-10-29 02:35:31 +08:00
Run the ReleaseSafe size comparison on the four supported native platforms. Parse ELF section reports on Linux while retaining Mach-O attribution on macOS.
313 lines
11 KiB
Python
313 lines
11 KiB
Python
from __future__ import annotations
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import pathlib
|
|
import re
|
|
from collections.abc import Sequence
|
|
|
|
|
|
MIB = 1_048_576
|
|
DEFAULT_WARNING_BYTES = 52_429
|
|
SHA_PATTERN = re.compile(r"[0-9a-f]{40}")
|
|
TARGET_PATTERN = re.compile(r"[A-Za-z0-9_.-]+")
|
|
SEGMENT_PATTERN = re.compile(r"^Segment\s+(\S+):\s+(\d+)")
|
|
SECTION_PATTERN = re.compile(r"^Section\s+(\S+):\s+(\d+)")
|
|
ELF_SECTION_PATTERN = re.compile(r"^(\S+)\s+(\d+)\s+(?:0x)?[0-9A-Fa-f]+$")
|
|
|
|
|
|
class BinarySizeError(RuntimeError):
|
|
pass
|
|
|
|
|
|
def sha256_file(path: pathlib.Path) -> str:
|
|
digest = hashlib.sha256()
|
|
with path.open("rb") as stream:
|
|
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
|
digest.update(chunk)
|
|
return digest.hexdigest()
|
|
|
|
|
|
def artifact_evidence(path: pathlib.Path, source_sha: str) -> dict[str, object]:
|
|
if not path.is_file() or path.stat().st_size <= 0:
|
|
raise BinarySizeError(f"binary is missing or empty: {path}")
|
|
size_bytes = path.stat().st_size
|
|
return {
|
|
"source_sha": source_sha,
|
|
"sha256": sha256_file(path),
|
|
"size_bytes": size_bytes,
|
|
"size_mib": size_bytes / MIB,
|
|
}
|
|
|
|
|
|
def parse_macho_sections(path: pathlib.Path) -> tuple[dict[str, int], dict[str, int]]:
|
|
if not path.is_file():
|
|
raise BinarySizeError(f"section report does not exist: {path}")
|
|
segments: dict[str, int] = {}
|
|
sections: dict[str, int] = {}
|
|
seen_segments: set[str] = set()
|
|
current_segment: str | None = None
|
|
for raw_line in path.read_text(encoding="utf-8").splitlines():
|
|
line = raw_line.strip()
|
|
segment_match = SEGMENT_PATTERN.match(line)
|
|
if segment_match:
|
|
current_segment = segment_match.group(1)
|
|
if current_segment in seen_segments:
|
|
raise BinarySizeError(
|
|
"section report contains duplicate segment "
|
|
f"{current_segment}: {path}"
|
|
)
|
|
seen_segments.add(current_segment)
|
|
if current_segment != "__PAGEZERO":
|
|
segments[current_segment] = int(segment_match.group(2))
|
|
continue
|
|
section_match = SECTION_PATTERN.match(line)
|
|
if section_match and current_segment and current_segment != "__PAGEZERO":
|
|
key = f"{current_segment}.{section_match.group(1)}"
|
|
if key in sections:
|
|
raise BinarySizeError(
|
|
f"section report contains duplicate section {key}: {path}"
|
|
)
|
|
sections[key] = int(section_match.group(2))
|
|
if not segments:
|
|
raise BinarySizeError(f"section report contains no Mach-O segments: {path}")
|
|
if "__TEXT" not in segments:
|
|
raise BinarySizeError(f"section report is missing __TEXT segment: {path}")
|
|
return segments, sections
|
|
|
|
|
|
def parse_elf_sections(path: pathlib.Path) -> tuple[dict[str, int], dict[str, int]]:
|
|
if not path.is_file():
|
|
raise BinarySizeError(f"section report does not exist: {path}")
|
|
sections: dict[str, int] = {}
|
|
for raw_line in path.read_text(encoding="utf-8").splitlines():
|
|
section_match = ELF_SECTION_PATTERN.match(raw_line.strip())
|
|
if not section_match:
|
|
continue
|
|
name = section_match.group(1)
|
|
if name in sections:
|
|
raise BinarySizeError(
|
|
f"section report contains duplicate section {name}: {path}"
|
|
)
|
|
sections[name] = int(section_match.group(2))
|
|
if not sections:
|
|
raise BinarySizeError(f"section report contains no ELF sections: {path}")
|
|
if ".text" not in sections:
|
|
raise BinarySizeError(f"section report is missing .text section: {path}")
|
|
return {}, sections
|
|
|
|
|
|
def parse_sections(
|
|
path: pathlib.Path,
|
|
target: str,
|
|
) -> tuple[dict[str, int], dict[str, int]]:
|
|
if target.endswith("-macos"):
|
|
return parse_macho_sections(path)
|
|
if target.endswith("-linux"):
|
|
return parse_elf_sections(path)
|
|
raise BinarySizeError(f"unsupported target: {target}")
|
|
|
|
|
|
def deltas(base: dict[str, int], head: dict[str, int]) -> dict[str, int]:
|
|
return {
|
|
key: head.get(key, 0) - base.get(key, 0)
|
|
for key in sorted(set(base) | set(head))
|
|
if head.get(key, 0) != base.get(key, 0)
|
|
}
|
|
|
|
|
|
def build_report(
|
|
*,
|
|
base_binary: pathlib.Path,
|
|
head_binary: pathlib.Path,
|
|
base_sections: pathlib.Path,
|
|
head_sections: pathlib.Path,
|
|
base_sha: str,
|
|
head_sha: str,
|
|
target: str,
|
|
warning_bytes: int,
|
|
) -> dict[str, object]:
|
|
if not SHA_PATTERN.fullmatch(base_sha) or not SHA_PATTERN.fullmatch(head_sha):
|
|
raise BinarySizeError("source SHAs must be lowercase 40-character hex values")
|
|
if not TARGET_PATTERN.fullmatch(target):
|
|
raise BinarySizeError(f"invalid target: {target!r}")
|
|
if warning_bytes <= 0:
|
|
raise BinarySizeError("warning threshold must be positive")
|
|
|
|
base = artifact_evidence(base_binary, base_sha)
|
|
head = artifact_evidence(head_binary, head_sha)
|
|
base_segments, base_section_values = parse_sections(base_sections, target)
|
|
head_segments, head_section_values = parse_sections(head_sections, target)
|
|
delta_bytes = int(head["size_bytes"]) - int(base["size_bytes"])
|
|
if delta_bytes > 0:
|
|
direction = "increase"
|
|
elif delta_bytes < 0:
|
|
direction = "decrease"
|
|
else:
|
|
direction = "unchanged"
|
|
return {
|
|
"schema_version": 1,
|
|
"status": "warning" if delta_bytes >= warning_bytes else "ok",
|
|
"target": target,
|
|
"warning_threshold_bytes": warning_bytes,
|
|
"base": base,
|
|
"head": head,
|
|
"delta": {
|
|
"direction": direction,
|
|
"size_bytes": delta_bytes,
|
|
"size_mib": delta_bytes / MIB,
|
|
"percent": (delta_bytes / int(base["size_bytes"])) * 100,
|
|
},
|
|
"segment_deltas_bytes": deltas(base_segments, head_segments),
|
|
"section_deltas_bytes": deltas(base_section_values, head_section_values),
|
|
}
|
|
|
|
|
|
def signed_bytes(value: int) -> str:
|
|
return f"{value:+,} bytes"
|
|
|
|
|
|
def append_delta_table(
|
|
lines: list[str],
|
|
heading: str,
|
|
values: dict[str, int],
|
|
*,
|
|
limit: int | None = None,
|
|
) -> None:
|
|
lines.extend(["", heading, ""])
|
|
ranked = sorted(values.items(), key=lambda item: (-abs(item[1]), item[0]))
|
|
if limit is not None:
|
|
ranked = ranked[:limit]
|
|
if not ranked:
|
|
lines.append("No changes detected.")
|
|
return
|
|
lines.extend(["| Name | Change in bytes |", "| --- | ---: |"])
|
|
lines.extend(f"| `{name}` | {value:+,} |" for name, value in ranked)
|
|
|
|
|
|
def markdown_report(report: dict[str, object]) -> str:
|
|
base = report["base"]
|
|
head = report["head"]
|
|
delta = report["delta"]
|
|
assert isinstance(base, dict)
|
|
assert isinstance(head, dict)
|
|
assert isinstance(delta, dict)
|
|
status = str(report["status"])
|
|
direction = str(delta["direction"])
|
|
if status == "warning":
|
|
warning_bytes = int(report["warning_threshold_bytes"])
|
|
signal = (
|
|
"Review recommended: the increase meets the informational threshold of "
|
|
f"{warning_bytes:,} bytes ({warning_bytes / MIB:.6f} MiB)."
|
|
)
|
|
elif direction == "decrease":
|
|
signal = "The PR binary is smaller than the base binary."
|
|
elif direction == "unchanged":
|
|
signal = "The PR and base binaries have the same file size."
|
|
else:
|
|
signal = "The increase is within the informational warning threshold."
|
|
lines: list[str] = [
|
|
"# Binary size",
|
|
"",
|
|
"This comparison is informational. "
|
|
"Release PGSO qualification remains authoritative.",
|
|
"",
|
|
f"Target: `{report['target']}`",
|
|
"",
|
|
"| Artifact | Bytes | MiB | Source | SHA-256 |",
|
|
"| --- | ---: | ---: | --- | --- |",
|
|
(
|
|
f"| Base | {int(base['size_bytes']):,} | "
|
|
f"{float(base['size_mib']):.6f} | `{base['source_sha']}` | "
|
|
f"`{base['sha256']}` |"
|
|
),
|
|
(
|
|
f"| PR | {int(head['size_bytes']):,} | "
|
|
f"{float(head['size_mib']):.6f} | `{head['source_sha']}` | "
|
|
f"`{head['sha256']}` |"
|
|
),
|
|
"",
|
|
(
|
|
f"Change: **{signed_bytes(int(delta['size_bytes']))}** "
|
|
f"({float(delta['size_mib']):+.6f} MiB, "
|
|
f"{float(delta['percent']):+.3f}%)."
|
|
),
|
|
"",
|
|
signal,
|
|
]
|
|
segment_deltas = report["segment_deltas_bytes"]
|
|
section_deltas = report["section_deltas_bytes"]
|
|
assert isinstance(segment_deltas, dict)
|
|
assert isinstance(section_deltas, dict)
|
|
append_delta_table(lines, "## Segment changes", segment_deltas)
|
|
append_delta_table(
|
|
lines,
|
|
"## Largest section changes",
|
|
section_deltas,
|
|
limit=10,
|
|
)
|
|
return "\n".join(lines) + "\n"
|
|
|
|
|
|
def parse_args(argv: Sequence[str] | None) -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(
|
|
description="Compare stripped ReleaseSafe fx binaries",
|
|
)
|
|
parser.add_argument("--base-binary", type=pathlib.Path, required=True)
|
|
parser.add_argument("--head-binary", type=pathlib.Path, required=True)
|
|
parser.add_argument("--base-sections", type=pathlib.Path, required=True)
|
|
parser.add_argument("--head-sections", type=pathlib.Path, required=True)
|
|
parser.add_argument("--base-sha", required=True)
|
|
parser.add_argument("--head-sha", required=True)
|
|
parser.add_argument("--target", required=True)
|
|
parser.add_argument(
|
|
"--warning-bytes",
|
|
type=int,
|
|
default=DEFAULT_WARNING_BYTES,
|
|
)
|
|
parser.add_argument("--output-json", type=pathlib.Path, required=True)
|
|
parser.add_argument("--output-markdown", type=pathlib.Path, required=True)
|
|
parser.add_argument("--github-output", type=pathlib.Path)
|
|
return parser.parse_args(argv)
|
|
|
|
|
|
def main(argv: Sequence[str] | None = None) -> int:
|
|
args = parse_args(argv)
|
|
try:
|
|
report = build_report(
|
|
base_binary=args.base_binary,
|
|
head_binary=args.head_binary,
|
|
base_sections=args.base_sections,
|
|
head_sections=args.head_sections,
|
|
base_sha=args.base_sha,
|
|
head_sha=args.head_sha,
|
|
target=args.target,
|
|
warning_bytes=args.warning_bytes,
|
|
)
|
|
except BinarySizeError as error:
|
|
raise SystemExit(str(error)) from error
|
|
|
|
args.output_json.parent.mkdir(parents=True, exist_ok=True)
|
|
args.output_markdown.parent.mkdir(parents=True, exist_ok=True)
|
|
args.output_json.write_text(
|
|
json.dumps(report, indent=2, sort_keys=True) + "\n",
|
|
encoding="utf-8",
|
|
)
|
|
args.output_markdown.write_text(markdown_report(report), encoding="utf-8")
|
|
if args.github_output:
|
|
warning = str(report["status"] == "warning").lower()
|
|
delta = report["delta"]
|
|
assert isinstance(delta, dict)
|
|
args.github_output.write_text(
|
|
f"warning={warning}\n"
|
|
f"delta_bytes={int(delta['size_bytes'])}\n"
|
|
f"status={report['status']}\n",
|
|
encoding="utf-8",
|
|
)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|