mirror of
https://github.com/LostArtefacts/TRX.git
synced 2025-05-02 14:47:58 +03:00
build: simplify build pipeline a bit
This commit is contained in:
parent
f871f298a7
commit
77e904e56d
6 changed files with 27 additions and 25 deletions
478
scripts/render_progress
Executable file
478
scripts/render_progress
Executable file
|
@ -0,0 +1,478 @@
|
|||
#!/usr/bin/python3
|
||||
from enum import Enum
|
||||
import re
|
||||
import typing as T
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
GRID_MAX_SQUARES = 50
|
||||
GRID_SQUARE_SIZE = 12
|
||||
GRID_SQUARE_MARGIN = 2
|
||||
LEGEND_SQUARE_SIZE = 12
|
||||
LEGEND_SQUARE_MARGIN = 2
|
||||
LEGEND_ROW_PADDING = 3
|
||||
LEGEND_MARGIN = 15
|
||||
TEXT_SIZE = 15
|
||||
TEXT_MARGIN = 5
|
||||
DOCS_DIR = Path(__file__).parent.parent / "docs"
|
||||
PROGRESS_TXT_FILE = DOCS_DIR / "progress.txt"
|
||||
PROGRESS_SVG_FILE = DOCS_DIR / "progress.svg"
|
||||
|
||||
|
||||
class FunctionStatus(Enum):
|
||||
todo = "todo"
|
||||
named = "named"
|
||||
decompiled = "decompiled"
|
||||
|
||||
|
||||
COLOR_MAP = {
|
||||
FunctionStatus.decompiled: "forestgreen",
|
||||
FunctionStatus.named: "lightpink",
|
||||
FunctionStatus.todo: "mistyrose",
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class Box:
|
||||
x1: float
|
||||
y1: float
|
||||
x2: float
|
||||
y2: float
|
||||
|
||||
@property
|
||||
def dx(self) -> float:
|
||||
return self.x2 - self.x1
|
||||
|
||||
@property
|
||||
def dy(self) -> float:
|
||||
return self.y2 - self.y1
|
||||
|
||||
|
||||
@dataclass
|
||||
class SquarifyResult(Box):
|
||||
item: T.Any
|
||||
|
||||
|
||||
class Squarify:
|
||||
def __init__(
|
||||
self,
|
||||
items: T.Iterable[T.Any],
|
||||
key: T.Callable[[T.Any], float],
|
||||
box: Box,
|
||||
normalize: bool = True,
|
||||
) -> None:
|
||||
if normalize:
|
||||
total_size = sum(map(key, items))
|
||||
total_area = box.dx * box.dy
|
||||
|
||||
def normalized_key(item: T.Any) -> float:
|
||||
return key(item) * total_area / total_size
|
||||
|
||||
self.key = normalized_key
|
||||
else:
|
||||
self.key = key
|
||||
|
||||
self.items = list(sorted(items, key=self.key, reverse=True))
|
||||
self.box = box
|
||||
|
||||
def layoutrow(
|
||||
self, items: T.Iterable[T.Any]
|
||||
) -> T.Iterable[SquarifyResult]:
|
||||
covered_area = sum(self.key(item) for item in items)
|
||||
dx = covered_area / self.box.dy
|
||||
y = self.box.y1
|
||||
for item in items:
|
||||
yield SquarifyResult(
|
||||
item=item,
|
||||
x1=self.box.x1,
|
||||
y1=y,
|
||||
x2=self.box.x1 + dx,
|
||||
y2=y + self.key(item) / dx,
|
||||
)
|
||||
y += self.key(item) / dx
|
||||
|
||||
def layoutcol(
|
||||
self, items: T.Iterable[T.Any]
|
||||
) -> T.Iterable[SquarifyResult]:
|
||||
covered_area = sum(self.key(item) for item in items)
|
||||
dy = covered_area / self.box.dx
|
||||
x = self.box.x1
|
||||
for item in items:
|
||||
yield SquarifyResult(
|
||||
item=item,
|
||||
x1=x,
|
||||
y1=self.box.y1,
|
||||
x2=x + self.key(item) / dy,
|
||||
y2=self.box.y1 + dy,
|
||||
)
|
||||
x += self.key(item) / dy
|
||||
|
||||
def layout(self, items: T.Iterable[T.Any]) -> T.Iterable[SquarifyResult]:
|
||||
yield from (
|
||||
self.layoutrow(items)
|
||||
if self.box.dx >= self.box.dy
|
||||
else self.layoutcol(items)
|
||||
)
|
||||
|
||||
def leftoverrow(self, items: T.Iterable[T.Any]) -> Box:
|
||||
covered_area = sum(self.key(item) for item in items)
|
||||
dx = covered_area / self.box.dy
|
||||
return Box(
|
||||
x1=self.box.x1 + dx,
|
||||
y1=self.box.y1,
|
||||
x2=self.box.x1 + self.box.dx,
|
||||
y2=self.box.y1 + self.box.dy,
|
||||
)
|
||||
|
||||
def leftovercol(self, items: T.Iterable[T.Any]) -> Box:
|
||||
covered_area = sum(self.key(item) for item in items)
|
||||
dy = covered_area / self.box.dx
|
||||
return Box(
|
||||
x1=self.box.x1,
|
||||
y1=self.box.y1 + dy,
|
||||
x2=self.box.x1 + self.box.dx,
|
||||
y2=self.box.y1 + self.box.dy,
|
||||
)
|
||||
|
||||
def leftover(self, items: T.Iterable[T.Any]) -> Box:
|
||||
return (
|
||||
self.leftoverrow(items)
|
||||
if self.box.dx >= self.box.dy
|
||||
else self.leftovercol(items)
|
||||
)
|
||||
|
||||
def worst_ratio(self, items: T.Iterable[T.Any]) -> float:
|
||||
return max(
|
||||
max(result.dx / result.dy, result.dy / result.dx)
|
||||
for result in self.layout(items)
|
||||
)
|
||||
|
||||
def run(
|
||||
self, items: T.Optional[T.List[T.Any]] = None
|
||||
) -> T.Iterable[SquarifyResult]:
|
||||
if not items:
|
||||
items = self.items
|
||||
|
||||
if len(items) == 0:
|
||||
return
|
||||
|
||||
if len(items) == 1:
|
||||
yield from self.layout(items)
|
||||
return
|
||||
|
||||
i = 1
|
||||
while i < len(items) and self.worst_ratio(
|
||||
items[:i]
|
||||
) >= self.worst_ratio(items[: i + 1]):
|
||||
i += 1
|
||||
current = items[:i]
|
||||
remaining = items[i:]
|
||||
|
||||
leftover_box = self.leftover(current)
|
||||
yield from self.layout(current)
|
||||
yield from Squarify(
|
||||
remaining, key=self.key, box=leftover_box, normalize=False
|
||||
).run()
|
||||
|
||||
|
||||
def squarify(
|
||||
items: T.List[T.Any], key: T.Callable[[T.Any], float], box: Box
|
||||
) -> T.Iterable[SquarifyResult]:
|
||||
yield from Squarify(items, key, box).run()
|
||||
|
||||
|
||||
@dataclass
|
||||
class Function:
|
||||
name: str
|
||||
offset: int
|
||||
size: int
|
||||
flags: str
|
||||
|
||||
@property
|
||||
def is_decompiled(self) -> bool:
|
||||
return "+" in self.flags or "x" in self.flags
|
||||
|
||||
@property
|
||||
def is_called(self) -> bool:
|
||||
return "*" in self.flags
|
||||
|
||||
@property
|
||||
def is_named(self) -> bool:
|
||||
return not self.name.startswith("sub_")
|
||||
|
||||
@property
|
||||
def status(self) -> FunctionStatus:
|
||||
if self.is_decompiled:
|
||||
return FunctionStatus.decompiled
|
||||
elif self.is_named:
|
||||
return FunctionStatus.named
|
||||
return FunctionStatus.todo
|
||||
|
||||
|
||||
def collect_functions() -> T.Iterable[Function]:
|
||||
for line in PROGRESS_TXT_FILE.open():
|
||||
line = line.strip()
|
||||
if line.startswith("#") or not line:
|
||||
continue
|
||||
func_name, offset, size, flags = re.split(r"\s+", line)
|
||||
if not offset.replace("-", ""):
|
||||
continue
|
||||
yield Function(
|
||||
name=func_name,
|
||||
offset=int(offset, 16),
|
||||
size=int(size, 16),
|
||||
flags=flags,
|
||||
)
|
||||
|
||||
|
||||
class Shape:
|
||||
@property
|
||||
def box(self) -> Box:
|
||||
raise NotImplementedError("not implemented")
|
||||
|
||||
def render(self) -> str:
|
||||
raise NotImplementedError("not implemented")
|
||||
|
||||
|
||||
def get_common_bbox(shapes: T.List[Shape]) -> Box:
|
||||
return Box(
|
||||
x1=min(shape.box.x1 for shape in shapes),
|
||||
y1=min(shape.box.y1 for shape in shapes),
|
||||
x2=max(shape.box.x2 for shape in shapes),
|
||||
y2=max(shape.box.y2 for shape in shapes),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Rectangle(Shape):
|
||||
x: float
|
||||
y: float
|
||||
class_name: str
|
||||
dx: float
|
||||
dy: float
|
||||
title: T.Optional[str] = None
|
||||
|
||||
@property
|
||||
def box(self) -> Box:
|
||||
return Box(
|
||||
x1=self.x,
|
||||
y1=self.y,
|
||||
x2=self.x + self.dx,
|
||||
y2=self.y + self.dy,
|
||||
)
|
||||
|
||||
def render(self) -> str:
|
||||
return (
|
||||
f"<rect "
|
||||
f'width="{self.dx:.02f}" '
|
||||
f'height="{self.dy:.02f}" '
|
||||
f'x="{self.x:.02f}" '
|
||||
f'y="{self.y:.02f}" '
|
||||
f'class="{self.class_name}"'
|
||||
+ (f"><title>{self.title}</title></rect>" if self.title else "/>")
|
||||
)
|
||||
|
||||
|
||||
class Square(Rectangle):
|
||||
def __init__(
|
||||
self,
|
||||
x: float,
|
||||
y: float,
|
||||
class_name: str,
|
||||
size: float = GRID_SQUARE_SIZE,
|
||||
title: T.Optional[str] = None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
x=x, y=y, class_name=class_name, dx=size, dy=size, title=title
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Text(Shape):
|
||||
x: float
|
||||
y: float
|
||||
text: str
|
||||
|
||||
@property
|
||||
def box(self) -> Box:
|
||||
return Box(x1=self.x, y1=self.y, x2=self.x, y2=self.y + TEXT_SIZE)
|
||||
|
||||
def render(self) -> str:
|
||||
return (
|
||||
f"<text "
|
||||
f'x="{self.x:.02f}" y="{self.y + TEXT_SIZE/2:.02f}">'
|
||||
f"{self.text}"
|
||||
f"</text>"
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class LegendText(Shape):
|
||||
x: float
|
||||
y: float
|
||||
class_name: str
|
||||
text: str
|
||||
|
||||
@property
|
||||
def _square(self) -> Square:
|
||||
return Square(
|
||||
x=self.x,
|
||||
y=self.y + (TEXT_SIZE - LEGEND_SQUARE_SIZE) / 2,
|
||||
class_name=self.class_name,
|
||||
size=LEGEND_SQUARE_SIZE,
|
||||
)
|
||||
|
||||
@property
|
||||
def _text(self) -> Text:
|
||||
return Text(
|
||||
x=LEGEND_SQUARE_SIZE + TEXT_MARGIN,
|
||||
y=self.y,
|
||||
text=self.text,
|
||||
)
|
||||
|
||||
@property
|
||||
def box(self) -> Box:
|
||||
return get_common_bbox([self._square, self._text])
|
||||
|
||||
def render(self) -> str:
|
||||
return self._square.render() + self._text.render()
|
||||
|
||||
|
||||
def render_grid(
|
||||
all_functions: T.List[Function], by: float
|
||||
) -> T.Iterable[Shape]:
|
||||
for i, function in enumerate(all_functions):
|
||||
x = (i % GRID_MAX_SQUARES) * (GRID_SQUARE_SIZE + GRID_SQUARE_MARGIN)
|
||||
y = (i // GRID_MAX_SQUARES) * (GRID_SQUARE_SIZE + GRID_SQUARE_MARGIN)
|
||||
yield Square(
|
||||
x=x,
|
||||
y=by + y,
|
||||
class_name=function.status.value,
|
||||
title=function.name,
|
||||
)
|
||||
|
||||
|
||||
def render_tree_grid(
|
||||
all_functions: T.List[Function], box: Box
|
||||
) -> T.Iterable[Shape]:
|
||||
for result in squarify(
|
||||
all_functions, key=lambda function: float(function.size), box=box
|
||||
):
|
||||
result.x2 = max(0, result.x2 - GRID_SQUARE_MARGIN)
|
||||
result.y2 = max(0, result.y2 - GRID_SQUARE_MARGIN)
|
||||
|
||||
yield Rectangle(
|
||||
x=result.x1,
|
||||
y=result.y1,
|
||||
dx=result.dx,
|
||||
dy=result.dy,
|
||||
class_name=result.item.status.value,
|
||||
title=result.item.name,
|
||||
)
|
||||
|
||||
|
||||
def render_svg(all_functions: T.List[Function]) -> T.Iterable[Shape]:
|
||||
y = 0.0
|
||||
yield Text(
|
||||
0,
|
||||
y,
|
||||
"TombATI functions, arranged according to the physical file layout:",
|
||||
)
|
||||
y = TEXT_SIZE + GRID_SQUARE_MARGIN
|
||||
|
||||
shapes = list(render_grid(all_functions, y))
|
||||
yield from shapes
|
||||
|
||||
y = max(shape.box.y2 for shape in shapes)
|
||||
y += GRID_SQUARE_SIZE + LEGEND_MARGIN
|
||||
yield Text(
|
||||
0, y, "TombATI functions, arranged according to the function sizes:"
|
||||
)
|
||||
y += TEXT_SIZE + GRID_SQUARE_MARGIN
|
||||
dx = max(shape.box.x2 for shape in shapes)
|
||||
|
||||
shapes = list(
|
||||
render_tree_grid(all_functions, Box(x1=0, y1=y, x2=dx, y2=y + y))
|
||||
)
|
||||
yield from shapes
|
||||
|
||||
y = max(shape.box.y2 for shape in shapes)
|
||||
y += GRID_SQUARE_SIZE + LEGEND_MARGIN
|
||||
|
||||
ready_functions = [func for func in all_functions if func.is_decompiled]
|
||||
named_functions = [
|
||||
func
|
||||
for func in all_functions
|
||||
if func.is_named and func not in ready_functions
|
||||
]
|
||||
todo_functions = [
|
||||
func
|
||||
for func in all_functions
|
||||
if func not in ready_functions and func not in named_functions
|
||||
]
|
||||
|
||||
def sum_size(functions: T.Iterable[Function]) -> int:
|
||||
return sum(func.size for func in functions)
|
||||
|
||||
for (status, text, functions) in [
|
||||
(FunctionStatus.decompiled, "Functions decompiled", ready_functions),
|
||||
(
|
||||
FunctionStatus.named,
|
||||
"Functions not decompiled, but with known names",
|
||||
named_functions,
|
||||
),
|
||||
(
|
||||
FunctionStatus.todo,
|
||||
"Functions not decompiled, with unknown names",
|
||||
todo_functions,
|
||||
),
|
||||
]:
|
||||
yield LegendText(
|
||||
x=0,
|
||||
y=y,
|
||||
class_name=status.value,
|
||||
text=f"{text} (count): {len(functions)/len(all_functions):.02%}",
|
||||
)
|
||||
y += TEXT_SIZE + LEGEND_ROW_PADDING
|
||||
|
||||
yield LegendText(
|
||||
x=0,
|
||||
y=y,
|
||||
class_name=status.value,
|
||||
text=f"{text} (bytesize): {sum_size(functions)/sum_size(all_functions):.02%}",
|
||||
)
|
||||
y += TEXT_SIZE + LEGEND_ROW_PADDING
|
||||
|
||||
|
||||
def main() -> None:
|
||||
functions = list(collect_functions())
|
||||
|
||||
with PROGRESS_SVG_FILE.open("w") as handle:
|
||||
shapes = list(render_svg(functions))
|
||||
bbox = get_common_bbox(shapes)
|
||||
|
||||
svg = (
|
||||
f'<svg version="1.1" '
|
||||
f'width="{bbox.dx:.02f}" '
|
||||
f'height="{bbox.dy:.02f}" '
|
||||
f'xmlns="http://www.w3.org/2000/svg">'
|
||||
f"<style>"
|
||||
)
|
||||
for status in FunctionStatus:
|
||||
svg += f".{status.value}{{fill:{COLOR_MAP[status]}}}"
|
||||
svg += f"text{{alignment-baseline:central;font-family:sans-serif;font-size:{TEXT_SIZE}px}}"
|
||||
svg += "</style>"
|
||||
|
||||
print(
|
||||
svg,
|
||||
file=handle,
|
||||
)
|
||||
|
||||
for shape in shapes:
|
||||
print(shape.render(), file=handle)
|
||||
|
||||
print("</svg>", file=handle)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
Loading…
Add table
Add a link
Reference in a new issue