Coverage for apio/common/build_report.py: 100%
49 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-09 01:55 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-09 01:55 +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 sys
13import json
14from dataclasses import dataclass
15from pathlib import Path
16from typing import List
17from apio.common.apio_console import cout, cerror
18from apio.common.apio_styles import INFO
21@dataclass(frozen=True)
22class ResourceReport:
23 """Represents the info of a single FPGA resource."""
25 name: str
26 available: int
27 used: int
28 percentage: float
31@dataclass(frozen=True)
32class ClockReport:
33 """Represents the info of a single clock signal."""
35 name: str
36 fmax_mhz: float
39@dataclass(frozen=True)
40class BuildReport:
41 """Represents FPGA resources utilization and clocks speeds."""
43 resources: List[ResourceReport]
44 clocks: List[ClockReport]
47def read_build_report(pnr_json_file_path: Path) -> BuildReport:
48 """Read the given hardware.pnr file, parse it, and return
49 a summary in the form of a BuildReport object. Fatal error on any
50 error. The resources and the clocks in the result are sorted
51 alphabetically by name, case insensitive"""
53 # pylint: disable=too-many-locals
54 # pylint: disable=broad-exception-caught
56 # -- Sanity checks
57 assert isinstance(pnr_json_file_path, Path), type(pnr_json_file_path)
58 assert pnr_json_file_path.name == "hardware.pnr", pnr_json_file_path
60 # -- Read the json text from the file
61 try:
62 json_text = pnr_json_file_path.read_text(encoding="utf-8")
63 except Exception as e:
64 cerror(f"Failed to read {str(pnr_json_file_path)}")
65 cerror(str(e))
66 cout(
67 "Did you build successfully this project env?",
68 style=INFO,
69 )
70 sys.exit(1)
72 # -- Parse the json text into a dict.
73 try:
74 json_dict = json.loads(json_text)
75 except Exception as e:
76 cerror(f"Failed parsing json file: {str(pnr_json_file_path)}")
77 cerror(str(e))
78 sys.exit(1)
80 # -- ECP5 (TRELLIS project) has a slightly different format of internal
81 # -- net name. We detect it by the existence of "TRELLIS" in at least
82 # -- one resource name.
83 is_ecp5 = any("TRELLIS" in key for key in json_dict["utilization"])
85 # -- Collect resources
86 resources: List[ResourceReport] = []
87 for resource_name, vals in json_dict["utilization"].items():
88 available: int = vals["available"]
89 used: int = vals["used"]
90 percentage: float = 100 * used / available
91 resources.append(
92 ResourceReport(resource_name, available, used, percentage)
93 )
95 # -- Sort resources alphabetically, case insensitive.
96 resources.sort(key=lambda r: r.name.lower())
98 # -- Collect clocks
99 clocks: List[ClockReport] = []
100 for clk_net, vals in json_dict["fmax"].items():
101 # -- Break the clk net name into parts
102 name_parts = clk_net.split("$")
104 # -- Extract the user net name part. The location depends on the
105 # -- architecture.
106 if is_ecp5:
107 name = name_parts[2]
108 else:
109 name = name_parts[0]
111 # -- Remove trailing '_'. Otherwise, on alhambra-ii/pll example, the
112 # -- internal clock 'sys_clk' is reported as 'sys_clk_'.
113 name = name.rstrip("_")
115 # -- Extract max speed
116 fmax_mhz = vals["achieved"]
118 # -- Append to clock list.
119 clocks.append(ClockReport(name, fmax_mhz))
121 # -- Sort clocks alphabetically, case insensitive.
122 clocks.sort(key=lambda r: r.name.lower())
124 result = BuildReport(resources, clocks)
125 return result