Coverage for apio/commands/apio_fpgas.py: 95%
107 statements
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-23 03:53 +0000
« 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 fpgas' command"""
10from datetime import date
11from pathlib import Path
12from dataclasses import dataclass
13import click
14from rich.table import Table
15from rich import box
16from apio.common import apio_console, proto_util
17from apio.common.apio_console import cout, ctable, cwrite
18from apio.common.apio_styles import INFO, BORDER, EMPH1
19from apio.common.proto.apio_common_pb2 import ApioArch
20from apio.common.proto.apio_definitions_pb2 import FpgaDefinition
21from apio.apio_context import (
22 ApioContext,
23 PackagesPolicy,
24 ProjectPolicy,
25 RemoteConfigPolicy,
26)
27from apio.utils import util, cmd_util
28from apio.commands import options
31@dataclass(frozen=True)
32class Entry:
33 """A class to hold the field of a single line of the report."""
35 fpga: str
36 board_count: int
37 fpga_arch: str
38 fpga_part_num: str
39 fpga_size: str
40 fpga_params: str
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())
47def _get_fpga_arch_params(fpga_definition: FpgaDefinition) -> tuple[str, dict]:
48 """Extracts the arch specific params of an fpga, Returns a tuple
49 with the field name and the field value."""
50 fpga_dict = proto_util.proto_to_json_dict(fpga_definition)
51 arch = fpga_dict["arch"]
52 field_name = arch + "-params"
53 field_value = fpga_dict[field_name]
54 return (field_name, field_value)
57def _collect_fpgas_entries(apio_ctx: ApioContext) -> list[Entry]:
58 """Returns a sorted list of supported fpgas entries."""
59 # -- Context should have the board, fpgas, and programmer definitions.
60 assert apio_ctx.definitions is not None
62 # -- Collect a sparse dict with fpga ids to board count.
63 boards_counts: dict[str, int] = {}
64 for board_definition in apio_ctx.definitions.boards.values():
65 proto_util.check_is_required(board_definition, "fpga_id")
66 fpga_id = board_definition.fpga_id
67 old_count = boards_counts.get(fpga_id, 0)
68 boards_counts[fpga_id] = old_count + 1
70 # -- Collect all entries.
71 result: list[Entry] = []
72 for fpga_id, fpga_definition in apio_ctx.definitions.fpgas.items():
73 proto_util.check_is_required(
74 fpga_definition, "arch", "part_num", "size"
75 )
76 board_count = boards_counts.get(fpga_id, 0)
77 fpga_arch = fpga_definition.arch
78 fpga_part_num = fpga_definition.part_num
79 fpga_size = fpga_definition.size
81 # -- Arch specific params summary string.
82 _, params = _get_fpga_arch_params(fpga_definition)
83 values = [f"\\[{v}]" for v in params.values()]
84 fpga_params = " ".join(values)
86 # -- Append to the list
87 result.append(
88 Entry(
89 fpga=fpga_id,
90 board_count=board_count,
91 fpga_arch=ApioArch.Name(fpga_arch),
92 fpga_part_num=fpga_part_num,
93 fpga_size=fpga_size,
94 fpga_params=fpga_params,
95 )
96 )
98 # -- Sort boards by our preferred order.
99 result.sort(key=lambda x: x.sort_key())
101 # -- All done
102 return result
105def _list_fpgas(apio_ctx: ApioContext, verbose: bool):
106 """Prints all the available FPGA definitions."""
108 # -- Collect a sorted list of supported fpgas.
109 entries: list[Entry] = _collect_fpgas_entries(apio_ctx)
111 # -- Define the table.
112 table = Table(
113 show_header=True,
114 show_lines=False,
115 box=box.SQUARE,
116 border_style=BORDER,
117 title="Apio Supported FPGAs",
118 title_justify="left",
119 )
121 # -- Add columns
122 table.add_column("FPGA-ID", no_wrap=True, style=EMPH1)
123 table.add_column("BOARDS", no_wrap=True, justify="center")
124 table.add_column("ARCH", no_wrap=True)
125 table.add_column("PART-NUMBER", no_wrap=True)
126 table.add_column("SIZE", no_wrap=True, justify="right")
127 if verbose: 127 ↛ 128line 127 didn't jump to line 128 because the condition on line 127 was never true
128 table.add_column("PARAMETERS", no_wrap=True)
130 # -- Add rows.
131 last_arch = None
132 for entry in entries:
133 # -- If switching architecture, add an horizontal separation line.
134 if last_arch != entry.fpga_arch and apio_console.is_terminal():
135 table.add_section()
136 last_arch = entry.fpga_arch
138 # -- Collect row values.
139 values = []
140 values.append(entry.fpga)
141 values.append(f"{entry.board_count:>2}" if entry.board_count else "")
142 values.append(entry.fpga_arch)
143 values.append(entry.fpga_part_num)
144 values.append(entry.fpga_size)
145 if verbose: 145 ↛ 146line 145 didn't jump to line 146 because the condition on line 145 was never true
146 values.append(entry.fpga_params)
148 # -- Add row.
149 table.add_row(*values)
151 # -- Render the table.
152 cout()
153 ctable(table)
155 # -- Show summary.
156 if apio_console.is_terminal(): 156 ↛ exitline 156 didn't return from function '_list_fpgas' because the condition on line 156 was always true
157 cout(f"Total of {util.plurality(entries, 'fpga')}")
158 if not verbose: 158 ↛ exitline 158 didn't return from function '_list_fpgas' because the condition on line 158 was always true
159 cout(
160 "Run 'apio fpgas -v' for additional columns.",
161 style=INFO,
162 )
165def _list_fpgas_docs_format(apio_ctx: ApioContext):
166 """Output fpgas information in a format for Apio Docs."""
168 # -- Get the version of the 'definitions' package use. At this point it's
169 # -- expected to be installed.
170 def_version = apio_ctx.package_manager.get_installed_package_version(
171 "definitions"
172 )
174 # -- Collect the fpagas info into a list of entires, one per fpga.
175 entries: list[Entry] = _collect_fpgas_entries(apio_ctx)
177 # -- Determine column sizes
178 w1 = max(len("FPGA-ID"), *(len(entry.fpga) for entry in entries))
179 w2 = max(len("SIZE"), *(len(entry.fpga_size) for entry in entries))
180 w3 = max(len("PART-NUM"), *(len(entry.fpga_part_num) for entry in entries))
182 # -- Print page header
183 today = date.today()
184 today_str = f"{today.strftime('%B')} {today.day}, {today.year}"
185 cwrite("\n<!-- BEGIN generation by 'apio fpgas --docs' -->\n")
186 cwrite("\n# Supported FPGAs\n")
187 cwrite(
188 f"\nThis markdown page was generated automatically on {today_str} "
189 f"from version `{def_version}` of the Apio definitions package.\n"
190 )
191 cwrite(
192 "\n> Custom FPGAs definitions can be added in the project directory "
193 "and can latter be contributed in the "
194 "[apio-definitions](https://github.com/FPGAwars/apio-definitions/"
195 "tree/main/definitions) repository.\n"
196 )
198 # -- Add the rows, with separation line between architecture groups.
199 last_arch = None
200 for entry in entries:
201 # -- If switching architecture, add an horizontal separation line.
202 if last_arch != entry.fpga_arch:
204 cwrite(f"\n## {entry.fpga_arch.upper()} FPGAs\n")
206 cwrite(
207 "\n| {0} | {1} | {2} |\n".format(
208 "FPGA-ID".ljust(w1),
209 "SIZE".ljust(w2),
210 "PART-NUM".ljust(w3),
211 )
212 )
213 cwrite(
214 "| {0} | {1} | {2} |\n".format(
215 ":-".ljust(w1, "-"),
216 ":-".ljust(w2, "-"),
217 ":-".ljust(w3, "-"),
218 )
219 )
221 last_arch = entry.fpga_arch
223 cwrite(
224 "| {0} | {1} | {2} |\n".format(
225 entry.fpga.ljust(w1),
226 entry.fpga_size.ljust(w2),
227 entry.fpga_part_num.ljust(w3),
228 )
229 )
231 cwrite("\n<!-- END generation by 'apio fpgas --docs' -->\n\n")
234# -------- apio fpgas
237# -- Text in the rich-text format of the python rich library.
238APIO_FPGAS_HELP = """
239The command 'apio fpgas' lists the FPGAs recognized by Apio. Custom FPGAs \
240supported by the underlying Yosys toolchain can be defined by placing a \
241custom 'fpgas.jsonc' file in the project directory, overriding Apio’s \
242standard 'fpgas.jsonc' file.
244Examples:[code]
245 apio fpgas # List all fpgas
246 apio fpgas -v # List with extra columns
247 apio fpgas | grep gowin # Filter FPGA results
248 apio fpgas --docs # Generate a report for Apio docs[/code]
249"""
252@click.command(
253 name="fpgas",
254 cls=cmd_util.ApioCommand,
255 short_help="List available FPGA definitions.",
256 help=APIO_FPGAS_HELP,
257)
258@options.verbose_option
259@options.docs_format_option
260@options.project_dir_option
261def cli(
262 *,
263 # Options
264 verbose: bool,
265 docs: bool,
266 project_dir: Path | None,
267):
268 """Implements the 'fpgas' command which lists available fpga
269 definitions.
270 """
272 # -- Determine context policy for the apio context. For docs output we
273 # -- want to ignore custom boards.
274 project_policy = (
275 ProjectPolicy.NO_PROJECT if docs else ProjectPolicy.PROJECT_OPTIONAL
276 )
278 # -- Create the apio context. If project dir has a fpgas.jsonc file,
279 # -- it will be loaded instead of the apio's standard file.
280 # -- We suppress the message with the env and board ids since it's
281 # -- not relevant for this command.
282 apio_ctx = ApioContext(
283 project_policy=project_policy,
284 remote_config_policy=RemoteConfigPolicy.CACHED_OK,
285 packages_policy=PackagesPolicy.ENSURE_PACKAGES,
286 project_dir_arg=project_dir,
287 report_env=False,
288 )
290 if docs:
291 _list_fpgas_docs_format(apio_ctx)
292 else:
293 _list_fpgas(apio_ctx, verbose)