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

100 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 fpgas' command""" 

9 

10import sys 

11from datetime import date 

12from pathlib import Path 

13from dataclasses import dataclass 

14from typing import List, Dict, Optional 

15import click 

16from rich.table import Table 

17from rich import box 

18from apio.common import apio_console 

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

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

21from apio.apio_context import ( 

22 ApioContext, 

23 PackagesPolicy, 

24 ProjectPolicy, 

25 RemoteConfigPolicy, 

26) 

27from apio.utils import util, cmd_util, resource_util 

28from apio.commands import options 

29 

30 

31@dataclass(frozen=True) 

32class Entry: 

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

34 

35 fpga: str 

36 board_count: int 

37 fpga_arch: str 

38 fpga_part_num: str 

39 fpga_size: str 

40 fpga_params: str 

41 

42 def sort_key(self): 

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

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

45 

46 

47def _collect_fpgas_entries(apio_ctx: ApioContext) -> List[Entry]: 

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

49 

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

51 boards_counts: Dict[str, int] = {} 

52 for board_info in apio_ctx.boards.values(): 

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

54 if fpga_id: 54 ↛ 52line 54 didn't jump to line 52 because the condition on line 54 was always true

55 old_count = boards_counts.get(fpga_id, 0) 

56 boards_counts[fpga_id] = old_count + 1 

57 

58 # -- Collect all entries. 

59 result: List[Entry] = [] 

60 for fpga_id, fpga_info in apio_ctx.fpgas.items(): 

61 # -- Construct the Entry for this fpga. 

62 board_count = boards_counts.get(fpga_id, 0) 

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

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

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

66 

67 # -- Arch specific params summary string. 

68 _, params = resource_util.get_fpga_arch_params(fpga_info) 

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

70 fpga_params = " ".join(values) 

71 

72 # -- Append to the list 

73 result.append( 

74 Entry( 

75 fpga=fpga_id, 

76 board_count=board_count, 

77 fpga_arch=fpga_arch, 

78 fpga_part_num=fpga_part_num, 

79 fpga_size=fpga_size, 

80 fpga_params=fpga_params, 

81 ) 

82 ) 

83 

84 # -- Sort boards by our preferred order. 

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

86 

87 # -- All done 

88 return result 

89 

90 

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

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

93 

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

95 entries: List[Entry] = _collect_fpgas_entries(apio_ctx) 

96 

97 # -- Define the table. 

98 table = Table( 

99 show_header=True, 

100 show_lines=False, 

101 box=box.SQUARE, 

102 border_style=BORDER, 

103 title="Apio Supported FPGAs", 

104 title_justify="left", 

105 ) 

106 

107 # -- Add columns 

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

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

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

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

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

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

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

115 

116 # -- Add rows. 

117 last_arch = None 

118 for entry in entries: 

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

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

121 table.add_section() 

122 last_arch = entry.fpga_arch 

123 

124 # -- Collect row values. 

125 values = [] 

126 values.append(entry.fpga) 

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

128 values.append(entry.fpga_arch) 

129 values.append(entry.fpga_part_num) 

130 values.append(entry.fpga_size) 

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

132 values.append(entry.fpga_params) 

133 

134 # -- Add row. 

135 table.add_row(*values) 

136 

137 # -- Render the table. 

138 cout() 

139 ctable(table) 

140 

141 # -- Show summary. 

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

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

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

145 cout( 

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

147 style=INFO, 

148 ) 

149 

150 

151def _list_fpgas_docs_format(apio_ctx: ApioContext): 

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

153 

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

155 # -- expected to be installed. 

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

157 

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

159 entries: List[Entry] = _collect_fpgas_entries(apio_ctx) 

160 

161 # -- Determine column sizes 

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

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

164 w3 = max(len("PART-NUM"), *(len(entry.fpga_part_num) for entry in entries)) 

165 

166 # -- Print page header 

167 today = date.today() 

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

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

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

171 cwrite( 

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

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

174 ) 

175 cwrite( 

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

177 "and can latter be contributed in the " 

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

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

180 ) 

181 

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

183 last_arch = None 

184 for entry in entries: 

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

186 if last_arch != entry.fpga_arch: 

187 

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

189 

190 cwrite( 

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

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

193 "SIZE".ljust(w2), 

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

195 ) 

196 ) 

197 cwrite( 

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

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

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

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

202 ) 

203 ) 

204 

205 last_arch = entry.fpga_arch 

206 

207 cwrite( 

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

209 entry.fpga.ljust(w1), 

210 entry.fpga_size.ljust(w2), 

211 entry.fpga_part_num.ljust(w3), 

212 ) 

213 ) 

214 

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

216 

217 

218# -------- apio fpgas 

219 

220 

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

222APIO_FPGAS_HELP = """ 

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

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

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

226standard 'fpgas.jsonc' file. 

227 

228Examples:[code] 

229 apio fpgas # List all fpgas 

230 apio fpgas -v # List with extra columns 

231 apio fpgas | grep gowin # Filter FPGA results 

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

233""" 

234 

235 

236@click.command( 

237 name="fpgas", 

238 cls=cmd_util.ApioCommand, 

239 short_help="List available FPGA definitions.", 

240 help=APIO_FPGAS_HELP, 

241) 

242@options.verbose_option 

243@options.docs_format_option 

244@options.project_dir_option 

245def cli( 

246 *, 

247 # Options 

248 verbose: bool, 

249 docs: bool, 

250 project_dir: Optional[Path], 

251): 

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

253 definitions. 

254 """ 

255 

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

257 # -- want to ignore custom boards. 

258 project_policy = ( 

259 ProjectPolicy.NO_PROJECT if docs else ProjectPolicy.PROJECT_OPTIONAL 

260 ) 

261 

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

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

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

265 # -- not relevant for this command. 

266 apio_ctx = ApioContext( 

267 project_policy=project_policy, 

268 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

269 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

270 project_dir_arg=project_dir, 

271 report_env=False, 

272 ) 

273 

274 if docs: 

275 _list_fpgas_docs_format(apio_ctx) 

276 else: 

277 _list_fpgas(apio_ctx, verbose) 

278 

279 sys.exit(0)