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

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

9 

10from pathlib import Path 

11from datetime import date 

12from dataclasses import dataclass 

13import click 

14from rich.table import Table 

15from rich import box 

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

17from apio.common.apio_styles import INFO 

18from apio.common import apio_console, proto_util 

19from apio.common.apio_styles import BORDER, EMPH1 

20from apio.utils import util, cmd_util 

21from apio.commands import options 

22from apio.managers.examples import Examples 

23from apio.common.proto.apio_common_pb2 import ApioArch 

24from apio.apio_context import ( 

25 ApioContext, 

26 PackagesPolicy, 

27 ProjectPolicy, 

28 RemoteConfigPolicy, 

29) 

30 

31 

32@dataclass(frozen=True) 

33class Entry: 

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

35 

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

37 

38 board_id: str 

39 examples_count: str 

40 board_description: str 

41 fpga_arch: str 

42 fpga_size: str 

43 fpga_id: str 

44 fpga_part_num: str 

45 programmer: str 

46 

47 def sort_key(self): 

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

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

50 

51 

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

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

54 # -- definitions. 

55 assert apio_ctx.definitions is not None 

56 

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

58 examples = Examples(apio_ctx) 

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

60 

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

62 result: list[Entry] = [] 

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

64 proto_util.check_is_required( 

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

66 ) 

67 fpga_id = board_definition.fpga_id 

68 fpga_definition = apio_ctx.definitions.fpgas[fpga_id] 

69 proto_util.check_is_required( 

70 fpga_definition, "arch", "size", "part_num" 

71 ) 

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

73 board_description = board_definition.description 

74 fpga_arch = ApioArch.Name(fpga_definition.arch) 

75 fpga_size = fpga_definition.size 

76 fpga_part_num = fpga_definition.part_num 

77 programmer_id = board_definition.programmer.id 

78 

79 result.append( 

80 Entry( 

81 board_id=board_id, 

82 examples_count=examples_count, 

83 board_description=board_description, 

84 fpga_arch=fpga_arch, 

85 fpga_size=fpga_size, 

86 fpga_id=fpga_id, 

87 fpga_part_num=fpga_part_num, 

88 programmer=programmer_id, 

89 ) 

90 ) 

91 

92 # -- Sort boards by our preferred order. 

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

94 

95 # -- All done. 

96 return result 

97 

98 

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

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

101 

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

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

104 

105 # -- Define the table. 

106 table = Table( 

107 show_header=True, 

108 show_lines=False, 

109 box=box.SQUARE, 

110 border_style=BORDER, 

111 title_justify="left", 

112 title="Apio Supported Boards", 

113 ) 

114 

115 # -- Add columns. 

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

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

118 if verbose: 

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

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

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

122 if verbose: 

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

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

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

126 

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

128 last_arch = None 

129 for entry in entries: 

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

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

132 table.add_section() 

133 last_arch = entry.fpga_arch 

134 

135 # -- Collect row values. 

136 values = [] 

137 values.append(entry.board_id) 

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

139 if verbose: 

140 values.append(entry.board_description) 

141 values.append(entry.fpga_arch) 

142 values.append(entry.fpga_size) 

143 if verbose: 

144 values.append(entry.fpga_id) 

145 values.append(entry.fpga_part_num) 

146 values.append(entry.programmer) 

147 

148 # -- Add row. 

149 table.add_row(*values) 

150 

151 # -- Render the table. 

152 cout() 

153 ctable(table) 

154 

155 # -- Show the summary. 

156 

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

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

159 if not verbose: 

160 cout( 

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

162 style=INFO, 

163 ) 

164 

165 

166def _list_boards_docs_format(apio_ctx: ApioContext): 

167 """Output boards 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 definitions_package_version = ( 

172 apio_ctx.package_manager.get_installed_package_version("definitions") 

173 ) 

174 

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

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

177 

178 # -- Determine column sizes 

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

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

181 w3 = max( 

182 len("DESCRIPTION"), 

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

184 ) 

185 w4 = max(len("FPGA"), *(len(entry.fpga_part_num) for entry in entries)) 

186 

187 # -- Print page header 

188 today = date.today() 

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

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

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

192 cwrite( 

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

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

195 ) 

196 cwrite( 

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

198 "can latter be contributed to Apio in the " 

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

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

201 ) 

202 

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

204 last_arch = None 

205 for entry in entries: 

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

207 if last_arch != entry.fpga_arch: 

208 

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

210 

211 cwrite( 

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

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

214 "SIZE".ljust(w2), 

215 "DESCRIPTION".ljust(w3), 

216 "FPGA".ljust(w4), 

217 ) 

218 ) 

219 cwrite( 

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

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

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

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

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

225 ) 

226 ) 

227 

228 last_arch = entry.fpga_arch 

229 

230 cwrite( 

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

232 entry.board_id.ljust(w1), 

233 entry.fpga_size.ljust(w2), 

234 entry.board_description.ljust(w3), 

235 entry.fpga_part_num.ljust(w4), 

236 ) 

237 ) 

238 

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

240 

241 

242# ------------- apio boards 

243 

244 

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

246APIO_BOARDS_HELP = """ 

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

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

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

250 

251Examples:[code] 

252 apio boards # List all boards 

253 apio boards -v # List with extra columns 

254 apio boards | grep ecp5 # Filter boards results 

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

256 

257""" 

258 

259 

260@click.command( 

261 name="boards", 

262 cls=cmd_util.ApioCommand, 

263 short_help="List available board definitions.", 

264 help=APIO_BOARDS_HELP, 

265) 

266@options.verbose_option 

267@options.docs_format_option 

268@options.project_dir_option 

269def cli( 

270 *, 

271 # Options 

272 verbose: bool, 

273 docs: bool, 

274 project_dir: Path | None, 

275): 

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

277 definitions.""" 

278 

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

280 # -- want to ignore custom boards. 

281 project_policy = ( 

282 ProjectPolicy.NO_PROJECT if docs else ProjectPolicy.PROJECT_OPTIONAL 

283 ) 

284 

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

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

287 # -- the example package. 

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

289 # -- not relevant for this command. 

290 apio_ctx = ApioContext( 

291 project_policy=project_policy, 

292 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

293 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

294 project_dir_arg=project_dir, 

295 report_env=False, 

296 ) 

297 

298 if docs: 

299 _list_boards_docs_format(apio_ctx) 

300 else: 

301 _list_boards(apio_ctx, verbose)