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

106 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-25 02:31 +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 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.lower()) 

51 

52 

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

54 

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

56 examples = Examples(apio_ctx) 

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

58 

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

60 result: List[Entry] = [] 

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

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

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

64 

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

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

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

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

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

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

71 

72 result.append( 

73 Entry( 

74 board=board, 

75 examples_count=examples_count, 

76 board_description=board_description, 

77 fpga_arch=fpga_arch, 

78 fpga_size=fpga_size, 

79 fpga_id=fpga_id, 

80 fpga_part_num=fpga_part_num, 

81 programmer=programmer_id, 

82 ) 

83 ) 

84 

85 # -- Sort boards by our preferred order. 

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

87 

88 # -- All done. 

89 return result 

90 

91 

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

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

94 

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

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

97 

98 # -- Define the table. 

99 table = Table( 

100 show_header=True, 

101 show_lines=False, 

102 box=box.SQUARE, 

103 border_style=BORDER, 

104 title_justify="left", 

105 title="Apio Supported Boards", 

106 ) 

107 

108 # -- Add columns. 

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

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

111 if verbose: 

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

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

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

115 if verbose: 

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

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

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

119 

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

121 last_arch = None 

122 for entry in entries: 

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

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

125 table.add_section() 

126 last_arch = entry.fpga_arch 

127 

128 # -- Collect row values. 

129 values = [] 

130 values.append(entry.board) 

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

132 if verbose: 

133 values.append(entry.board_description) 

134 values.append(entry.fpga_arch) 

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

136 if verbose: 

137 values.append(entry.fpga_id) 

138 values.append(entry.fpga_part_num) 

139 values.append(entry.programmer) 

140 

141 # -- Add row. 

142 table.add_row(*values) 

143 

144 # -- Render the table. 

145 cout() 

146 ctable(table) 

147 

148 # -- Show the summary. 

149 

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

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

152 if not verbose: 

153 cout( 

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

155 style=INFO, 

156 ) 

157 

158 

159def _list_boards_docs_format(apio_ctx: ApioContext): 

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

161 

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

163 # -- expected to be installed. 

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

165 

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

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

168 

169 # -- Determine column sizes 

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

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

172 w3 = max( 

173 len("DESCRIPTION"), 

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

175 ) 

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

177 

178 # -- Print page header 

179 today = date.today() 

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

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

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

183 cwrite( 

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

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

186 ) 

187 cwrite( 

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

189 "can latter be contributed to Apio in the " 

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

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

192 ) 

193 

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

195 last_arch = None 

196 for entry in entries: 

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

198 if last_arch != entry.fpga_arch: 

199 

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

201 

202 cwrite( 

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

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

205 "SIZE".ljust(w2), 

206 "DESCRIPTION".ljust(w3), 

207 "FPGA".ljust(w4), 

208 ) 

209 ) 

210 cwrite( 

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

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

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

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

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

216 ) 

217 ) 

218 

219 last_arch = entry.fpga_arch 

220 

221 cwrite( 

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

223 entry.board.ljust(w1), 

224 entry.fpga_size.ljust(w2), 

225 entry.board_description.ljust(w3), 

226 entry.fpga_part_num.ljust(w4), 

227 ) 

228 ) 

229 

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

231 

232 

233# ------------- apio boards 

234 

235 

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

237APIO_BOARDS_HELP = """ 

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

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

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

241 

242Examples:[code] 

243 apio boards # List all boards 

244 apio boards -v # List with extra columns 

245 apio boards | grep ecp5 # Filter boards results 

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

247 

248""" 

249 

250 

251@click.command( 

252 name="boards", 

253 cls=cmd_util.ApioCommand, 

254 short_help="List available board definitions.", 

255 help=APIO_BOARDS_HELP, 

256) 

257@options.verbose_option 

258@options.docs_format_option 

259@options.project_dir_option 

260def cli( 

261 *, 

262 # Options 

263 verbose: bool, 

264 docs: bool, 

265 project_dir: Optional[Path], 

266): 

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

268 definitions.""" 

269 

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

271 # -- want to ignore custom boards. 

272 project_policy = ( 

273 ProjectPolicy.NO_PROJECT if docs else ProjectPolicy.PROJECT_OPTIONAL 

274 ) 

275 

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

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

278 # -- the example package. 

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

280 # -- not relevant for this command. 

281 apio_ctx = ApioContext( 

282 project_policy=project_policy, 

283 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

284 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

285 project_dir_arg=project_dir, 

286 report_env=False, 

287 ) 

288 

289 if docs: 

290 _list_boards_docs_format(apio_ctx) 

291 else: 

292 _list_boards(apio_ctx, verbose) 

293 

294 sys.exit(0)