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

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

10from datetime import date 

11from pathlib import Path 

12from dataclasses import dataclass 

13import click 

14from rich.table import Table 

15from rich import box 

16from apio.common import apio_console, proto_util 

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

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

19from apio.common.proto.apio_common_pb2 import ApioArch 

20from apio.common.proto.apio_definitions_pb2 import FpgaDefinition 

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 fpga: str 

36 board_count: int 

37 fpga_arch: str 

38 fpga_part_num: str 

39 fpga_size: str 

40 fpga_params: str 

41 

42 def sort_key(self): 

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

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

45 

46 

47def _get_fpga_arch_params(fpga_definition: FpgaDefinition) -> tuple[str, dict]: 

48 """Extracts the arch specific params of an fpga, Returns a tuple 

49 with the field name and the field value.""" 

50 fpga_dict = proto_util.proto_to_json_dict(fpga_definition) 

51 arch = fpga_dict["arch"] 

52 field_name = arch + "-params" 

53 field_value = fpga_dict[field_name] 

54 return (field_name, field_value) 

55 

56 

57def _collect_fpgas_entries(apio_ctx: ApioContext) -> list[Entry]: 

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

59 # -- Context should have the board, fpgas, and programmer definitions. 

60 assert apio_ctx.definitions is not None 

61 

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

63 boards_counts: dict[str, int] = {} 

64 for board_definition in apio_ctx.definitions.boards.values(): 

65 proto_util.check_is_required(board_definition, "fpga_id") 

66 fpga_id = board_definition.fpga_id 

67 old_count = boards_counts.get(fpga_id, 0) 

68 boards_counts[fpga_id] = old_count + 1 

69 

70 # -- Collect all entries. 

71 result: list[Entry] = [] 

72 for fpga_id, fpga_definition in apio_ctx.definitions.fpgas.items(): 

73 proto_util.check_is_required( 

74 fpga_definition, "arch", "part_num", "size" 

75 ) 

76 board_count = boards_counts.get(fpga_id, 0) 

77 fpga_arch = fpga_definition.arch 

78 fpga_part_num = fpga_definition.part_num 

79 fpga_size = fpga_definition.size 

80 

81 # -- Arch specific params summary string. 

82 _, params = _get_fpga_arch_params(fpga_definition) 

83 values = [f"\\[{v}]" for v in params.values()] 

84 fpga_params = " ".join(values) 

85 

86 # -- Append to the list 

87 result.append( 

88 Entry( 

89 fpga=fpga_id, 

90 board_count=board_count, 

91 fpga_arch=ApioArch.Name(fpga_arch), 

92 fpga_part_num=fpga_part_num, 

93 fpga_size=fpga_size, 

94 fpga_params=fpga_params, 

95 ) 

96 ) 

97 

98 # -- Sort boards by our preferred order. 

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

100 

101 # -- All done 

102 return result 

103 

104 

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

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

107 

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

109 entries: list[Entry] = _collect_fpgas_entries(apio_ctx) 

110 

111 # -- Define the table. 

112 table = Table( 

113 show_header=True, 

114 show_lines=False, 

115 box=box.SQUARE, 

116 border_style=BORDER, 

117 title="Apio Supported FPGAs", 

118 title_justify="left", 

119 ) 

120 

121 # -- Add columns 

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

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

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

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

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

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

128 table.add_column("PARAMETERS", no_wrap=True) 

129 

130 # -- Add rows. 

131 last_arch = None 

132 for entry in entries: 

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

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

135 table.add_section() 

136 last_arch = entry.fpga_arch 

137 

138 # -- Collect row values. 

139 values = [] 

140 values.append(entry.fpga) 

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

142 values.append(entry.fpga_arch) 

143 values.append(entry.fpga_part_num) 

144 values.append(entry.fpga_size) 

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

146 values.append(entry.fpga_params) 

147 

148 # -- Add row. 

149 table.add_row(*values) 

150 

151 # -- Render the table. 

152 cout() 

153 ctable(table) 

154 

155 # -- Show summary. 

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

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

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

159 cout( 

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

161 style=INFO, 

162 ) 

163 

164 

165def _list_fpgas_docs_format(apio_ctx: ApioContext): 

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

167 

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

169 # -- expected to be installed. 

170 def_version = apio_ctx.package_manager.get_installed_package_version( 

171 "definitions" 

172 ) 

173 

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

175 entries: list[Entry] = _collect_fpgas_entries(apio_ctx) 

176 

177 # -- Determine column sizes 

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

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

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

181 

182 # -- Print page header 

183 today = date.today() 

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

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

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

187 cwrite( 

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

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

190 ) 

191 cwrite( 

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

193 "and can latter be contributed in the " 

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

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

196 ) 

197 

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

199 last_arch = None 

200 for entry in entries: 

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

202 if last_arch != entry.fpga_arch: 

203 

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

205 

206 cwrite( 

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

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

209 "SIZE".ljust(w2), 

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

211 ) 

212 ) 

213 cwrite( 

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

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

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

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

218 ) 

219 ) 

220 

221 last_arch = entry.fpga_arch 

222 

223 cwrite( 

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

225 entry.fpga.ljust(w1), 

226 entry.fpga_size.ljust(w2), 

227 entry.fpga_part_num.ljust(w3), 

228 ) 

229 ) 

230 

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

232 

233 

234# -------- apio fpgas 

235 

236 

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

238APIO_FPGAS_HELP = """ 

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

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

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

242standard 'fpgas.jsonc' file. 

243 

244Examples:[code] 

245 apio fpgas # List all fpgas 

246 apio fpgas -v # List with extra columns 

247 apio fpgas | grep gowin # Filter FPGA results 

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

249""" 

250 

251 

252@click.command( 

253 name="fpgas", 

254 cls=cmd_util.ApioCommand, 

255 short_help="List available FPGA definitions.", 

256 help=APIO_FPGAS_HELP, 

257) 

258@options.verbose_option 

259@options.docs_format_option 

260@options.project_dir_option 

261def cli( 

262 *, 

263 # Options 

264 verbose: bool, 

265 docs: bool, 

266 project_dir: Path | None, 

267): 

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

269 definitions. 

270 """ 

271 

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

273 # -- want to ignore custom boards. 

274 project_policy = ( 

275 ProjectPolicy.NO_PROJECT if docs else ProjectPolicy.PROJECT_OPTIONAL 

276 ) 

277 

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

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

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

281 # -- not relevant for this command. 

282 apio_ctx = ApioContext( 

283 project_policy=project_policy, 

284 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

285 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

286 project_dir_arg=project_dir, 

287 report_env=False, 

288 ) 

289 

290 if docs: 

291 _list_fpgas_docs_format(apio_ctx) 

292 else: 

293 _list_fpgas(apio_ctx, verbose)