Coverage for apio/commands/apio_boards.py: 99%

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 boards' command""" 

10 

11from pathlib import Path 

12from datetime import date 

13from dataclasses import dataclass 

14import click 

15from rich.table import Table 

16from rich import box 

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

18from apio.common.apio_styles import INFO 

19from apio.common import apio_console, proto_util 

20from apio.common.apio_styles import BORDER, EMPH1 

21from apio.utils import util, cmd_util 

22from apio.commands import options 

23from apio.managers.examples import Examples 

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

25from apio.apio_context import ( 

26 ApioContext, 

27 PackagesPolicy, 

28 ProjectPolicy, 

29 RemoteConfigPolicy, 

30) 

31 

32 

33@dataclass(frozen=True) 

34class Entry: 

35 """Holds the values of a single board report line.""" 

36 

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

38 

39 board_id: str 

40 examples_count: str 

41 board_description: str 

42 fpga_arch: str 

43 fpga_size: str 

44 fpga_id: str 

45 fpga_vendor_part: str 

46 programmer: str 

47 

48 def sort_key(self): 

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

50 return (util.fpga_arch_sort_key(self.fpga_arch), self.board_id.lower()) 

51 

52 

53def _collect_board_entries(apio_ctx: ApioContext) -> list[Entry]: 

54 # -- The context is expected to have the board, fpga, and programmers 

55 # -- definitions. 

56 assert apio_ctx.definitions is not None 

57 

58 # -- Get examples counts by board. This is a sparse dictionary. 

59 examples = Examples(apio_ctx) 

60 examples_counts: dict[str, int] = examples.count_examples_by_board() 

61 

62 # -- Collect the boards info into a list of entires, one per board. 

63 result: list[Entry] = [] 

64 for board_id, board_definition in apio_ctx.definitions.boards.items(): 

65 proto_util.check_is_required( 

66 board_definition, "fpga_id", "description", "programmer.id" 

67 ) 

68 fpga_id = board_definition.fpga_id 

69 fpga_definition = apio_ctx.definitions.fpgas[fpga_id] 

70 proto_util.check_is_required( 

71 fpga_definition, "arch", "size", "vendor_part" 

72 ) 

73 examples_count = " " + str(examples_counts.get(board_id, "")) 

74 board_description = board_definition.description 

75 fpga_arch = ApioArch.Name(fpga_definition.arch) 

76 fpga_size = fpga_definition.size 

77 fpga_vendor_part = fpga_definition.vendor_part 

78 programmer_id = board_definition.programmer.id 

79 

80 result.append( 

81 Entry( 

82 board_id=board_id, 

83 examples_count=examples_count, 

84 board_description=board_description, 

85 fpga_arch=fpga_arch, 

86 fpga_size=fpga_size, 

87 fpga_id=fpga_id, 

88 fpga_vendor_part=fpga_vendor_part, 

89 programmer=programmer_id, 

90 ) 

91 ) 

92 

93 # -- Sort boards by our preferred order. 

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

95 

96 # -- All done. 

97 return result 

98 

99 

100def _list_boards(apio_ctx: ApioContext, verbose: bool): 

101 """Prints all the available board definitions.""" 

102 

103 # -- Collect the boards info into a list of entires, one per board. 

104 entries: list[Entry] = _collect_board_entries(apio_ctx) 

105 

106 # -- Define the table. 

107 table = Table( 

108 show_header=True, 

109 show_lines=False, 

110 box=box.SQUARE, 

111 border_style=BORDER, 

112 title_justify="left", 

113 title="Apio Supported Boards", 

114 ) 

115 

116 # -- Add columns. 

117 table.add_column("BOARD-ID", no_wrap=True, style=EMPH1) 

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

119 if verbose: 

120 table.add_column("PRODUCT", no_wrap=True, max_width=25) 

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

122 table.add_column("SIZE", no_wrap=True) 

123 if verbose: 

124 table.add_column("FPGA-ID", no_wrap=True) 

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

126 table.add_column("PROGRAMMER", no_wrap=True) 

127 

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

129 last_arch = None 

130 for entry in entries: 

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

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

133 table.add_section() 

134 last_arch = entry.fpga_arch 

135 

136 # -- Collect row values. 

137 values = [] 

138 values.append(entry.board_id) 

139 values.append(str(entry.examples_count)) 

140 if verbose: 

141 values.append(entry.board_description) 

142 values.append(entry.fpga_arch) 

143 values.append(entry.fpga_size) 

144 if verbose: 

145 values.append(entry.fpga_id) 

146 values.append(entry.fpga_vendor_part) 

147 values.append(entry.programmer) 

148 

149 # -- Add row. 

150 table.add_row(*values) 

151 

152 # -- Render the table. 

153 cout() 

154 ctable(table) 

155 

156 # -- Show the summary. 

157 

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

159 cout(f"Total of {util.plurality(entries, 'board')}") 

160 if not verbose: 

161 cout( 

162 "Run 'apio boards -v' for additional columns.", 

163 style=INFO, 

164 ) 

165 

166 

167def _list_boards_docs_format(apio_ctx: ApioContext): 

168 """Output boards information in a format for Apio Docs.""" 

169 

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

171 # -- expected to be installed. 

172 definitions_package_version = ( 

173 apio_ctx.package_manager.get_installed_package_version("definitions") 

174 ) 

175 

176 # -- Collect the boards info into a list of entires, one per board. 

177 entries: list[Entry] = _collect_board_entries(apio_ctx) 

178 

179 # -- Determine column sizes 

180 w1 = max(len("BOARD"), *(len(entry.board_id) for entry in entries)) 

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

182 w3 = max( 

183 len("DESCRIPTION"), 

184 *(len(entry.board_description) for entry in entries), 

185 ) 

186 w4 = max(len("FPGA"), *(len(entry.fpga_vendor_part) for entry in entries)) 

187 

188 # -- Print page header 

189 today = date.today() 

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

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

192 cwrite("\n# Supported FPGA Boards\n") 

193 cwrite( 

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

195 f"from version `{definitions_package_version}` of the Apio definitions package.\n" 

196 ) 

197 cwrite( 

198 "\n> Custom board definitions can be added in the project directory " 

199 "can latter be contributed to Apio in the " 

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

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

202 ) 

203 

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

205 last_arch = None 

206 for entry in entries: 

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

208 if last_arch != entry.fpga_arch: 

209 

210 cout(f"\n## {entry.fpga_arch.upper()} boards") 

211 

212 cwrite( 

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

214 "BOARD-ID".ljust(w1), 

215 "SIZE".ljust(w2), 

216 "DESCRIPTION".ljust(w3), 

217 "FPGA".ljust(w4), 

218 ) 

219 ) 

220 cwrite( 

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

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

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

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

225 ":-".ljust(w4, "-"), 

226 ) 

227 ) 

228 

229 last_arch = entry.fpga_arch 

230 

231 cwrite( 

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

233 entry.board_id.ljust(w1), 

234 entry.fpga_size.ljust(w2), 

235 entry.board_description.ljust(w3), 

236 entry.fpga_vendor_part.ljust(w4), 

237 ) 

238 ) 

239 

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

241 

242 

243# ------------- apio boards 

244 

245 

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

247APIO_BOARDS_HELP = """ 

248The command 'apio boards' lists the FPGA boards recognized by Apio. \ 

249Custom boards can be defined by placing a custom 'boards.jsonc' file in the \ 

250project directory, which will override Apio’s default 'boards.jsonc' file. 

251 

252Examples:[code] 

253 apio boards # List all boards 

254 apio boards -v # List with extra columns 

255 apio boards | grep ecp5 # Filter boards results 

256 apio boards --docs # Generate a report for Apio docs[/code] 

257 

258""" 

259 

260 

261@click.command( 

262 name="boards", 

263 cls=cmd_util.ApioCommand, 

264 short_help="List available board definitions.", 

265 help=APIO_BOARDS_HELP, 

266) 

267@options.verbose_option 

268@options.docs_format_option 

269@options.project_dir_option 

270def cli( 

271 *, 

272 # Options 

273 verbose: bool, 

274 docs: bool, 

275 project_dir: Path | None, 

276): 

277 """Implements the 'boards' command which lists available board 

278 definitions.""" 

279 

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

281 # -- want to ignore custom boards. 

282 project_policy = ( 

283 ProjectPolicy.NO_PROJECT if docs else ProjectPolicy.PROJECT_OPTIONAL 

284 ) 

285 

286 # -- Create the apio context. If the project exists, it's custom 

287 # -- boards.jsonc is also loaded. Config is required since we query 

288 # -- the example package. 

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

290 # -- not relevant for this command. 

291 apio_ctx = ApioContext( 

292 project_policy=project_policy, 

293 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

294 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

295 project_dir_arg=project_dir, 

296 report_env=False, 

297 ) 

298 

299 if docs: 

300 _list_boards_docs_format(apio_ctx) 

301 else: 

302 _list_boards(apio_ctx, verbose)