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

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.""" 

11 

12import json 

13from dataclasses import dataclass 

14from pathlib import Path 

15from apio.common.apio_console import fatal_error 

16 

17 

18@dataclass(frozen=True) 

19class ResourceReport: 

20 """Represents the info of a single FPGA resource.""" 

21 

22 name: str 

23 available: int 

24 used: int 

25 percentage: float 

26 

27 

28@dataclass(frozen=True) 

29class ClockReport: 

30 """Represents the info of a single clock signal.""" 

31 

32 name: str 

33 fmax_mhz: float 

34 

35 

36@dataclass(frozen=True) 

37class BuildReport: 

38 """Represents FPGA resources utilization and clocks speeds.""" 

39 

40 resources: list[ResourceReport] 

41 clocks: list[ClockReport] 

42 

43 

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""" 

49 

50 # pylint: disable=too-many-locals 

51 # pylint: disable=broad-exception-caught 

52 

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 

56 

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 ) 

66 

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 ) 

74 

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"]) 

79 

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 ) 

89 

90 # -- Sort resources alphabetically, case insensitive. 

91 resources.sort(key=lambda r: r.name.lower()) 

92 

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("$") 

98 

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] 

105 

106 # -- Remove trailing '_'. Otherwise, on alhambra-ii/pll example, the 

107 # -- internal clock 'sys_clk' is reported as 'sys_clk_'. 

108 name = name.rstrip("_") 

109 

110 # -- Extract max speed 

111 fmax_mhz = vals["achieved"] 

112 

113 # -- Append to clock list. 

114 clocks.append(ClockReport(name, fmax_mhz)) 

115 

116 # -- Sort clocks alphabetically, case insensitive. 

117 clocks.sort(key=lambda r: r.name.lower()) 

118 

119 result = BuildReport(resources, clocks) 

120 return result