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

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

19 

20 

21@dataclass(frozen=True) 

22class ResourceReport: 

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

24 

25 name: str 

26 available: int 

27 used: int 

28 percentage: float 

29 

30 

31@dataclass(frozen=True) 

32class ClockReport: 

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

34 

35 name: str 

36 fmax_mhz: float 

37 

38 

39@dataclass(frozen=True) 

40class BuildReport: 

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

42 

43 resources: List[ResourceReport] 

44 clocks: List[ClockReport] 

45 

46 

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

52 

53 # pylint: disable=too-many-locals 

54 # pylint: disable=broad-exception-caught 

55 

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 

59 

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) 

71 

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) 

79 

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

84 

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 ) 

94 

95 # -- Sort resources alphabetically, case insensitive. 

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

97 

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

103 

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] 

110 

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

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

113 name = name.rstrip("_") 

114 

115 # -- Extract max speed 

116 fmax_mhz = vals["achieved"] 

117 

118 # -- Append to clock list. 

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

120 

121 # -- Sort clocks alphabetically, case insensitive. 

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

123 

124 result = BuildReport(resources, clocks) 

125 return result