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