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

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

9 

10import sys 

11from pathlib import Path 

12from datetime import date 

13from dataclasses import dataclass 

14from typing import List, Dict, Optional 

15import click 

16from rich.table import Table 

17from rich import box 

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

19from apio.common.apio_styles import INFO 

20from apio.common import apio_console 

21from apio.common.apio_styles import BORDER, EMPH1 

22from apio.utils import util, cmd_util 

23from apio.commands import options 

24from apio.managers.examples import Examples 

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

40 examples_count: str 

41 board_description: str 

42 fpga_arch: str 

43 fpga_size: str 

44 fpga_id: str 

45 fpga_part_num: str 

46 fpga_type: str 

47 fpga_pack: str 

48 fpga_speed: str 

49 programmer: str 

50 

51 def sort_key(self): 

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

53 return (util.fpga_arch_sort_key(self.fpga_arch), self.board.lower()) 

54 

55 

56def _collect_board_entries(apio_ctx) -> List[Entry]: 

57 

58 # pylint: disable=too-many-locals 

59 

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

61 examples = Examples(apio_ctx) 

62 examples_counts: Dict[str, int] = examples.count_examples_by_board() 

63 

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

65 result: List[Entry] = [] 

66 for board, board_info in apio_ctx.boards.items(): 

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

68 fpga_info = apio_ctx.fpgas.get(fpga_id, {}) 

69 

70 examples_count = " " + str(examples_counts.get(board, "")) 

71 board_description = board_info.get("description", "") 

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

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

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

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

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

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

78 programmer_id = board_info.get("programmer", {}).get("id", "") 

79 

80 result.append( 

81 Entry( 

82 board=board, 

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_part_num=fpga_part_num, 

89 fpga_type=fpga_type, 

90 fpga_pack=fpga_pack, 

91 fpga_speed=fpga_speed, 

92 programmer=programmer_id, 

93 ) 

94 ) 

95 

96 # -- Sort boards by our preferred order. 

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

98 

99 # -- All done. 

100 return result 

101 

102 

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

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

105 

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

107 entries: List[Entry] = _collect_board_entries(apio_ctx) 

108 

109 # -- Define the table. 

110 table = Table( 

111 show_header=True, 

112 show_lines=False, 

113 box=box.SQUARE, 

114 border_style=BORDER, 

115 title_justify="left", 

116 title="Apio Supported Boards", 

117 ) 

118 

119 # -- Add columns. 

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

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

122 if verbose: 

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

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

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

126 if verbose: 

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

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

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

130 

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

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.board) 

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

143 if verbose: 

144 values.append(entry.board_description) 

145 values.append(entry.fpga_arch) 

146 values.append(str(entry.fpga_size)) 

147 if verbose: 

148 values.append(entry.fpga_id) 

149 values.append(entry.fpga_part_num) 

150 values.append(entry.programmer) 

151 

152 # -- Add row. 

153 table.add_row(*values) 

154 

155 # -- Render the table. 

156 cout() 

157 ctable(table) 

158 

159 # -- Show the summary. 

160 

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

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

163 if not verbose: 

164 cout( 

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

166 style=INFO, 

167 ) 

168 

169 

170def _list_boards_docs_format(apio_ctx: ApioContext): 

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

172 

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

174 # -- expected to be installed. 

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

176 

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

178 entries: List[Entry] = _collect_board_entries(apio_ctx) 

179 

180 # -- Determine column sizes 

181 w1 = max(len("BOARD"), *(len(entry.board) for entry in entries)) 

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

183 w3 = max( 

184 len("DESCRIPTION"), 

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

186 ) 

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

188 

189 # -- Print page header 

190 today = date.today() 

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

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

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

194 cwrite( 

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

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

197 ) 

198 cwrite( 

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

200 "and new board definitions can be contributed in the " 

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

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

203 ) 

204 

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

206 last_arch = None 

207 for entry in entries: 

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

209 if last_arch != entry.fpga_arch: 

210 

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

212 

213 cwrite( 

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

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

216 "SIZE".ljust(w2), 

217 "DESCRIPTION".ljust(w3), 

218 "FPGA".ljust(w4), 

219 ) 

220 ) 

221 cwrite( 

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

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

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

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

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

227 ) 

228 ) 

229 

230 last_arch = entry.fpga_arch 

231 

232 cwrite( 

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

234 entry.board.ljust(w1), 

235 entry.fpga_size.ljust(w2), 

236 entry.board_description.ljust(w3), 

237 entry.fpga_part_num.ljust(w4), 

238 ) 

239 ) 

240 

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

242 

243 

244# ------------- apio boards 

245 

246 

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

248APIO_BOARDS_HELP = """ 

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

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

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

252 

253Examples:[code] 

254 apio boards # List all boards 

255 apio boards -v # List with extra columns 

256 apio boards | grep ecp5 # Filter boards results 

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

258 

259""" 

260 

261 

262@click.command( 

263 name="boards", 

264 cls=cmd_util.ApioCommand, 

265 short_help="List available board definitions.", 

266 help=APIO_BOARDS_HELP, 

267) 

268@options.verbose_option 

269@options.docs_format_option 

270@options.project_dir_option 

271def cli( 

272 # Options 

273 verbose: bool, 

274 docs: bool, 

275 project_dir: Optional[Path], 

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) 

303 

304 sys.exit(0)