Coverage for apio/common/build_report.py: 100%
41 statements
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-23 03:53 +0000
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-23 03:53 +0000
1# -*- coding: utf-8 -*-
2# -- This file is part of the Apio project
3# -- (C) 2016-2018 FPGAwars
4# -- Author Jesús Arroyo
5# -- License GPLv2
6# -- Derived from:
7# ---- Platformio project
8# ---- (C) 2014-2016 Ivan Kravets <me@ikravets.com>
9# ---- License Apache v2
10"""Utilities related to the build report file hardware.pnr."""
12import json
13from dataclasses import dataclass
14from pathlib import Path
15from apio.common.apio_console import fatal_error
18@dataclass(frozen=True)
19class ResourceReport:
20 """Represents the info of a single FPGA resource."""
22 name: str
23 available: int
24 used: int
25 percentage: float
28@dataclass(frozen=True)
29class ClockReport:
30 """Represents the info of a single clock signal."""
32 name: str
33 fmax_mhz: float
36@dataclass(frozen=True)
37class BuildReport:
38 """Represents FPGA resources utilization and clocks speeds."""
40 resources: list[ResourceReport]
41 clocks: list[ClockReport]
44def read_build_report(pnr_json_file_path: Path) -> BuildReport:
45 """Read the given hardware.pnr file, parse it, and return
46 a summary in the form of a BuildReport object. Fatal error on any
47 error. The resources and the clocks in the result are sorted
48 alphabetically by name, case insensitive"""
50 # pylint: disable=too-many-locals
51 # pylint: disable=broad-exception-caught
53 # -- Sanity checks
54 assert isinstance(pnr_json_file_path, Path), type(pnr_json_file_path)
55 assert pnr_json_file_path.name == "hardware.pnr", pnr_json_file_path
57 # -- Read the json text from the file
58 try:
59 json_text = pnr_json_file_path.read_text(encoding="utf-8")
60 except Exception as e:
61 fatal_error(
62 f"Failed to read {str(pnr_json_file_path)}",
63 cause=e,
64 info="Did you build successfully this project env?",
65 )
67 # -- Parse the json text into a dict.
68 try:
69 json_dict = json.loads(json_text)
70 except Exception as e:
71 fatal_error(
72 f"Failed parsing json file: {str(pnr_json_file_path)}", cause=e
73 )
75 # -- ECP5 (TRELLIS project) has a slightly different format of internal
76 # -- net name. We detect it by the existence of "TRELLIS" in at least
77 # -- one resource name.
78 is_ecp5 = any("TRELLIS" in key for key in json_dict["utilization"])
80 # -- Collect resources
81 resources: list[ResourceReport] = []
82 for resource_name, vals in json_dict["utilization"].items():
83 available: int = vals["available"]
84 used: int = vals["used"]
85 percentage: float = 100 * used / available
86 resources.append(
87 ResourceReport(resource_name, available, used, percentage)
88 )
90 # -- Sort resources alphabetically, case insensitive.
91 resources.sort(key=lambda r: r.name.lower())
93 # -- Collect clocks
94 clocks: list[ClockReport] = []
95 for clk_net, vals in json_dict["fmax"].items():
96 # -- Break the clk net name into parts
97 name_parts = clk_net.split("$")
99 # -- Extract the user net name part. The location depends on the
100 # -- architecture.
101 if is_ecp5:
102 name = name_parts[2]
103 else:
104 name = name_parts[0]
106 # -- Remove trailing '_'. Otherwise, on alhambra-ii/pll example, the
107 # -- internal clock 'sys_clk' is reported as 'sys_clk_'.
108 name = name.rstrip("_")
110 # -- Extract max speed
111 fmax_mhz = vals["achieved"]
113 # -- Append to clock list.
114 clocks.append(ClockReport(name, fmax_mhz))
116 # -- Sort clocks alphabetically, case insensitive.
117 clocks.sort(key=lambda r: r.name.lower())
119 result = BuildReport(resources, clocks)
120 return result