Coverage for apio/commands/apio_fpgas.py: 91%

104 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-11-06 10:20 +0000

1# -*- coding: utf-8 -*- 

2# -- This file is part of the Apio project 

3# -- (C) 2016-2024 FPGAwars 

4# -- Authors 

5# -- * Jesús Arroyo (2016-2019) 

6# -- * Juan Gonzalez (obijuan) (2019-2024) 

7# -- License GPLv2 

8"""Implementation of 'apio fpgas' command""" 

9 

10import sys 

11from datetime import date 

12from pathlib import Path 

13from dataclasses import dataclass 

14from typing import List, Dict, Optional 

15import click 

16from rich.table import Table 

17from rich import box 

18from apio.common import apio_console 

19from apio.common.apio_console import cout, ctable, cwrite 

20from apio.common.apio_styles import INFO, BORDER, EMPH1 

21from apio.apio_context import ( 

22 ApioContext, 

23 PackagesPolicy, 

24 ProjectPolicy, 

25 RemoteConfigPolicy, 

26) 

27from apio.utils import util, cmd_util 

28from apio.commands import options 

29 

30 

31@dataclass(frozen=True) 

32class Entry: 

33 """A class to hold the field of a single line of the report.""" 

34 

35 # pylint: disable=too-many-instance-attributes 

36 

37 fpga: str 

38 board_count: int 

39 fpga_arch: str 

40 fpga_part_num: str 

41 fpga_size: str 

42 fpga_type: str 

43 fpga_pack: str 

44 fpga_speed: str 

45 

46 def sort_key(self): 

47 """A key for sorting the fpga entries in our preferred order.""" 

48 return (util.fpga_arch_sort_key(self.fpga_arch), self.fpga.lower()) 

49 

50 

51def _collect_fpgas_entries(apio_ctx: ApioContext) -> List[Entry]: 

52 """Returns a sorted list of supported fpgas entries.""" 

53 

54 # -- Collect a sparse dict with fpga ids to board count. 

55 boards_counts: Dict[str, int] = {} 

56 for board_info in apio_ctx.boards.values(): 

57 fpga_id = board_info.get("fpga-id", None) 

58 if fpga_id: 58 ↛ 56line 58 didn't jump to line 56 because the condition on line 58 was always true

59 old_count = boards_counts.get(fpga_id, 0) 

60 boards_counts[fpga_id] = old_count + 1 

61 

62 # -- Collect all entries. 

63 result: List[Entry] = [] 

64 for fpga_id, fpga_info in apio_ctx.fpgas.items(): 

65 # -- Construct the Entry for this fpga. 

66 board_count = boards_counts.get(fpga_id, 0) 

67 fpga_arch = fpga_info.get("arch", "") 

68 fpga_part_num = fpga_info.get("part-num", "") 

69 fpga_size = fpga_info.get("size", "") 

70 fpga_type = fpga_info.get("type", "") 

71 fpga_pack = fpga_info.get("pack", "") 

72 fpga_speed = fpga_info.get("speed", "") 

73 # -- Append to the list 

74 result.append( 

75 Entry( 

76 fpga=fpga_id, 

77 board_count=board_count, 

78 fpga_arch=fpga_arch, 

79 fpga_part_num=fpga_part_num, 

80 fpga_size=fpga_size, 

81 fpga_type=fpga_type, 

82 fpga_pack=fpga_pack, 

83 fpga_speed=fpga_speed, 

84 ) 

85 ) 

86 

87 # -- Sort boards by our preferred order. 

88 result.sort(key=lambda x: x.sort_key()) 

89 

90 # -- All done 

91 return result 

92 

93 

94def _list_fpgas(apio_ctx: ApioContext, verbose: bool): 

95 """Prints all the available FPGA definitions.""" 

96 

97 # -- Collect a sorted list of supported fpgas. 

98 entries: List[Entry] = _collect_fpgas_entries(apio_ctx) 

99 

100 # -- Define the table. 

101 table = Table( 

102 show_header=True, 

103 show_lines=False, 

104 box=box.SQUARE, 

105 border_style=BORDER, 

106 title="Apio Supported FPGAs", 

107 title_justify="left", 

108 ) 

109 

110 # -- Add columns 

111 table.add_column("FPGA-ID", no_wrap=True, style=EMPH1) 

112 table.add_column("BOARDS", no_wrap=True, justify="center") 

113 table.add_column("ARCH", no_wrap=True) 

114 table.add_column("PART-NUMBER", no_wrap=True) 

115 table.add_column("SIZE", no_wrap=True, justify="right") 

116 if verbose: 116 ↛ 117line 116 didn't jump to line 117 because the condition on line 116 was never true

117 table.add_column("TYPE", no_wrap=True) 

118 table.add_column("PACK", no_wrap=True) 

119 table.add_column("SPEED", no_wrap=True, justify="center") 

120 

121 # -- Add rows. 

122 last_arch = None 

123 for entry in entries: 

124 # -- If switching architecture, add an horizontal separation line. 

125 if last_arch != entry.fpga_arch and apio_console.is_terminal(): 

126 table.add_section() 

127 last_arch = entry.fpga_arch 

128 

129 # -- Collect row values. 

130 values = [] 

131 values.append(entry.fpga) 

132 values.append(f"{entry.board_count:>2}" if entry.board_count else "") 

133 values.append(entry.fpga_arch) 

134 values.append(entry.fpga_part_num) 

135 values.append(entry.fpga_size) 

136 if verbose: 136 ↛ 137line 136 didn't jump to line 137 because the condition on line 136 was never true

137 values.append(entry.fpga_type) 

138 values.append(entry.fpga_pack) 

139 values.append(entry.fpga_speed) 

140 

141 # -- Add row. 

142 table.add_row(*values) 

143 

144 # -- Render the table. 

145 cout() 

146 ctable(table) 

147 

148 # -- Show summary. 

149 if apio_console.is_terminal(): 149 ↛ exitline 149 didn't return from function '_list_fpgas' because the condition on line 149 was always true

150 cout(f"Total of {util.plurality(entries, 'fpga')}") 

151 if not verbose: 151 ↛ exitline 151 didn't return from function '_list_fpgas' because the condition on line 151 was always true

152 cout( 

153 "Run 'apio fpgas -v' for additional columns.", 

154 style=INFO, 

155 ) 

156 

157 

158def _list_fpgas_docs_format(apio_ctx: ApioContext): 

159 """Output fpgas information in a format for Apio Docs.""" 

160 

161 # -- Get the version of the 'definitions' package use. At this point it's 

162 # -- expected to be installed. 

163 def_version, _ = apio_ctx.profile.get_installed_package_info("definitions") 

164 

165 # -- Collect the fpagas info into a list of entires, one per fpga. 

166 entries: List[Entry] = _collect_fpgas_entries(apio_ctx) 

167 

168 # -- Determine column sizes 

169 w1 = max(len("FPGA-ID"), *(len(entry.fpga) for entry in entries)) 

170 w2 = max(len("SIZE"), *(len(entry.fpga_size) for entry in entries)) 

171 w3 = max(len("PART-NUM"), *(len(entry.fpga_part_num) for entry in entries)) 

172 

173 # -- Print page header 

174 today = date.today() 

175 today_str = f"{today.strftime('%B')} {today.day}, {today.year}" 

176 cwrite("\n<!-- BEGIN generation by 'apio fpgas --docs' -->\n") 

177 cwrite("\n# Supported FPGAs\n") 

178 cwrite( 

179 f"\nThis markdown page was generated automatically on {today_str} " 

180 f"from version `{def_version}` of the Apio definitions package.\n" 

181 ) 

182 cwrite( 

183 "\n> Custom FPGAs definitions can be added in the project directory " 

184 "and new FPGAs definitions can be contributed in the " 

185 "[apio-definitions](https://github.com/FPGAwars/apio-definitions/" 

186 "tree/main/definitions) repository.\n" 

187 ) 

188 

189 # -- Add the rows, with separation line between architecture groups. 

190 last_arch = None 

191 for entry in entries: 

192 # -- If switching architecture, add an horizontal separation line. 

193 if last_arch != entry.fpga_arch: 

194 

195 cwrite(f"\n## {entry.fpga_arch.upper()} FPGAs\n") 

196 

197 cwrite( 

198 "\n| {0} | {1} | {2} |\n".format( 

199 "FPGA-ID".ljust(w1), 

200 "SIZE".ljust(w2), 

201 "PART-NUM".ljust(w3), 

202 ) 

203 ) 

204 cwrite( 

205 "| {0} | {1} | {2} |\n".format( 

206 ":-".ljust(w1, "-"), 

207 ":-".ljust(w2, "-"), 

208 ":-".ljust(w3, "-"), 

209 ) 

210 ) 

211 

212 last_arch = entry.fpga_arch 

213 

214 cwrite( 

215 "| {0} | {1} | {2} |\n".format( 

216 entry.fpga.ljust(w1), 

217 entry.fpga_size.ljust(w2), 

218 entry.fpga_part_num.ljust(w3), 

219 ) 

220 ) 

221 

222 cwrite("\n<!-- END generation by 'apio fpgas --docs' -->\n\n") 

223 

224 

225# -------- apio fpgas 

226 

227 

228# -- Text in the rich-text format of the python rich library. 

229APIO_FPGAS_HELP = """ 

230The command 'apio fpgas' lists the FPGAs recognized by Apio. Custom FPGAs \ 

231supported by the underlying Yosys toolchain can be defined by placing a \ 

232custom 'fpgas.jsonc' file in the project directory, overriding Apio’s \ 

233standard 'fpgas.jsonc' file. 

234 

235Examples:[code] 

236 apio fpgas # List all fpgas 

237 apio fpgas -v # List with extra columns 

238 apio fpgas | grep gowin # Filter FPGA results 

239 apio fpgas --docs # Generate a report for Apio docs[/code] 

240""" 

241 

242 

243@click.command( 

244 name="fpgas", 

245 cls=cmd_util.ApioCommand, 

246 short_help="List available FPGA definitions.", 

247 help=APIO_FPGAS_HELP, 

248) 

249@options.verbose_option 

250@options.docs_format_option 

251@options.project_dir_option 

252def cli( 

253 # Options 

254 verbose: bool, 

255 docs: bool, 

256 project_dir: Optional[Path], 

257): 

258 """Implements the 'fpgas' command which lists available fpga 

259 definitions. 

260 """ 

261 

262 # -- Determine context policy for the apio context. For docs output we 

263 # -- want to ignore custom boards. 

264 project_policy = ( 

265 ProjectPolicy.NO_PROJECT if docs else ProjectPolicy.PROJECT_OPTIONAL 

266 ) 

267 

268 # -- Create the apio context. If project dir has a fpgas.jsonc file, 

269 # -- it will be loaded instead of the apio's standard file. 

270 # -- We suppress the message with the env and board ids since it's 

271 # -- not relevant for this command. 

272 apio_ctx = ApioContext( 

273 project_policy=project_policy, 

274 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

275 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

276 project_dir_arg=project_dir, 

277 report_env=False, 

278 ) 

279 

280 if docs: 

281 _list_fpgas_docs_format(apio_ctx) 

282 else: 

283 _list_fpgas(apio_ctx, verbose) 

284 

285 sys.exit(0)