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

107 statements  

« prev     ^ index     » next       coverage.py v7.17.2, created at 2026-11-07 05:08 +0000

1# This file is part of the Apio project 

2# (C) 2016-present FPGAwars 

3# Project authors: 

4# * Jesús Arroyo (Jesus89) (2016-2019) 

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

6# * The Apio Team (2024-present) 

7# License: GPLv2 

8 

9"""Implementation of 'apio fpgas' command""" 

10 

11from datetime import date 

12from pathlib import Path 

13from dataclasses import dataclass 

14import click 

15from rich.table import Table 

16from rich import box 

17from apio.common import apio_console, proto_util 

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

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

20from apio.common.proto.stubs.apio_common_pb2 import ApioArch 

21from apio.common.proto.stubs.apio_definitions_pb2 import FpgaDefinition 

22from apio.apio_context import ( 

23 ApioContext, 

24 PackagesPolicy, 

25 ProjectPolicy, 

26 RemoteConfigPolicy, 

27) 

28from apio.utils import util, cmd_util 

29from apio.commands import options 

30 

31 

32@dataclass(frozen=True) 

33class Entry: 

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

35 

36 fpga: str 

37 board_count: int 

38 fpga_arch: str 

39 fpga_vendor_part: str 

40 fpga_size: str 

41 fpga_params: str 

42 

43 def sort_key(self): 

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

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

46 

47 

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

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

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

51 fpga_dict = proto_util.proto_to_json_dict(fpga_definition) 

52 arch = fpga_dict["arch"] 

53 field_name = arch + "-params" 

54 field_value = fpga_dict[field_name] 

55 return (field_name, field_value) 

56 

57 

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

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

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

61 assert apio_ctx.definitions is not None 

62 

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

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

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

66 proto_util.check_is_required(board_definition, "fpga_id") 

67 fpga_id = board_definition.fpga_id 

68 old_count = boards_counts.get(fpga_id, 0) 

69 boards_counts[fpga_id] = old_count + 1 

70 

71 # -- Collect all entries. 

72 result: list[Entry] = [] 

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

74 proto_util.check_is_required( 

75 fpga_definition, "arch", "vendor_part", "size" 

76 ) 

77 board_count = boards_counts.get(fpga_id, 0) 

78 fpga_arch = fpga_definition.arch 

79 fpga_vendor_part = fpga_definition.vendor_part 

80 fpga_size = fpga_definition.size 

81 

82 # -- Arch specific params summary string. 

83 _, params = _get_fpga_arch_params(fpga_definition) 

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

85 fpga_params = " ".join(values) 

86 

87 # -- Append to the list 

88 result.append( 

89 Entry( 

90 fpga=fpga_id, 

91 board_count=board_count, 

92 fpga_arch=ApioArch.Name(fpga_arch), 

93 fpga_vendor_part=fpga_vendor_part, 

94 fpga_size=fpga_size, 

95 fpga_params=fpga_params, 

96 ) 

97 ) 

98 

99 # -- Sort boards by our preferred order. 

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

101 

102 # -- All done 

103 return result 

104 

105 

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

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

108 

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

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

111 

112 # -- Define the table. 

113 table = Table( 

114 show_header=True, 

115 show_lines=False, 

116 box=box.SQUARE, 

117 border_style=BORDER, 

118 title="Apio Supported FPGAs", 

119 title_justify="left", 

120 ) 

121 

122 # -- Add columns 

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

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

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

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

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

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

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

130 

131 # -- Add rows. 

132 last_arch = None 

133 for entry in entries: 

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

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

136 table.add_section() 

137 last_arch = entry.fpga_arch 

138 

139 # -- Collect row values. 

140 values = [] 

141 values.append(entry.fpga) 

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

143 values.append(entry.fpga_arch) 

144 values.append(entry.fpga_vendor_part) 

145 values.append(entry.fpga_size) 

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

147 values.append(entry.fpga_params) 

148 

149 # -- Add row. 

150 table.add_row(*values) 

151 

152 # -- Render the table. 

153 cout() 

154 ctable(table) 

155 

156 # -- Show summary. 

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

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

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

160 cout( 

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

162 style=INFO, 

163 ) 

164 

165 

166def _list_fpgas_docs_format(apio_ctx: ApioContext): 

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

168 

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

170 # -- expected to be installed. 

171 def_version = apio_ctx.package_manager.get_installed_package_version( 

172 "definitions" 

173 ) 

174 

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

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

177 

178 # -- Determine column sizes 

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

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

181 w3 = max( 

182 len("PART-NUM"), *(len(entry.fpga_vendor_part) for entry in entries) 

183 ) 

184 

185 # -- Print page header 

186 today = date.today() 

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

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

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

190 cwrite( 

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

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

193 ) 

194 cwrite( 

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

196 "and can latter be contributed in the " 

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

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

199 ) 

200 

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

202 last_arch = None 

203 for entry in entries: 

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

205 if last_arch != entry.fpga_arch: 

206 

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

208 

209 cwrite( 

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

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

212 "SIZE".ljust(w2), 

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

214 ) 

215 ) 

216 cwrite( 

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

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

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

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

221 ) 

222 ) 

223 

224 last_arch = entry.fpga_arch 

225 

226 cwrite( 

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

228 entry.fpga.ljust(w1), 

229 entry.fpga_size.ljust(w2), 

230 entry.fpga_vendor_part.ljust(w3), 

231 ) 

232 ) 

233 

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

235 

236 

237# -------- apio fpgas 

238 

239 

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

241APIO_FPGAS_HELP = """ 

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

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

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

245standard 'fpgas.jsonc' file. 

246 

247Examples:[code] 

248 apio fpgas # List all fpgas 

249 apio fpgas -v # List with extra columns 

250 apio fpgas | grep gowin # Filter FPGA results 

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

252""" 

253 

254 

255@click.command( 

256 name="fpgas", 

257 cls=cmd_util.ApioCommand, 

258 short_help="List available FPGA definitions.", 

259 help=APIO_FPGAS_HELP, 

260) 

261@options.verbose_option 

262@options.docs_format_option 

263@options.project_dir_option 

264def cli( 

265 *, 

266 # Options 

267 verbose: bool, 

268 docs: bool, 

269 project_dir: Path | None, 

270): 

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

272 definitions. 

273 """ 

274 

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

276 # -- want to ignore custom boards. 

277 project_policy = ( 

278 ProjectPolicy.NO_PROJECT if docs else ProjectPolicy.PROJECT_OPTIONAL 

279 ) 

280 

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

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

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

284 # -- not relevant for this command. 

285 apio_ctx = ApioContext( 

286 project_policy=project_policy, 

287 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

288 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

289 project_dir_arg=project_dir, 

290 report_env=False, 

291 ) 

292 

293 if docs: 

294 _list_fpgas_docs_format(apio_ctx) 

295 else: 

296 _list_fpgas(apio_ctx, verbose)