Coverage for apio/commands/apio_info.py: 88%
230 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 info' command"""
11import sys
12from pathlib import Path
13from datetime import date
14import click
15from rich.table import Table
16from rich.text import Text
17from rich import box
18from rich import markup
19from rich.color import ANSI_COLOR_NAMES
20from apio.common.apio_styles import BORDER, EMPH1, EMPH2, EMPH3, INFO
21from apio.common import proto_util, apio_platforms
22from apio.common.debug_util import is_under_vscode_debugger
23from apio.utils import util, env_options
24from apio.commands import options
25from apio.apio_context import (
26 ApioContext,
27 PackagesPolicy,
28 ProjectPolicy,
29 RemoteConfigPolicy,
30)
31from apio.common.proto.stubs.apio_common_pb2 import ApioArch
32from apio.utils.cmd_util import ApioGroup, ApioSubgroup, ApioCommand
33from apio.common.apio_themes import THEMES_TABLE, THEME_LIGHT
34from apio.managers.remote_config import (
35 get_datetime_stamp,
36 days_between_datetime_stamps,
37)
38from apio.common.apio_console import (
39 PADDING,
40 cout,
41 cwrite,
42 cstyle,
43 ctable,
44 get_theme,
45 configure,
46)
48# ------ apio info system
51def construct_remote_config_status_str(apio_ctx: ApioContext) -> str:
52 """Query the apio profile and construct a short string indicating the
53 status of the cached remote config."""
54 metadata = apio_ctx.remote_config.metadata
55 timestamp_now = get_datetime_stamp()
56 config_status = []
57 # -- Handle the case of a having a cached config.
58 config_days = days_between_datetime_stamps(
59 metadata.loaded_at, timestamp_now, default=None
60 )
61 # -- Determine cache age in days, if possible.
62 if config_days is not None: 62 ↛ 67line 62 didn't jump to line 67 because the condition on line 62 was always true
63 config_status.append(
64 f"Cached {util.plurality(config_days, 'day')} ago"
65 )
66 else:
67 config_status.append("cache timestamp error")
69 # -- Indicate if there is a sign of a failed refresh attempt.
70 if metadata.HasField("refresh_failure_on"): 70 ↛ 71line 70 didn't jump to line 71 because the condition on line 70 was never true
71 config_status.append("refresh failed.")
73 # -- Concatenate and return.
74 return ", ".join(config_status)
77# -- Text in the rich-text format of the python rich library.
78APIO_INFO_SYSTEM_HELP = """
79The command 'apio info system' provides general information about your \
80system and Apio CLI installation, which is useful for diagnosing Apio \
81CLI installation issues.
83Examples:[code]
84 apio info system # System info.[/code]
86[NOTE] For programmatic access to this information use 'apio api get-system'.
88[ADVANCED] The default location of the Apio CLI home directory, \
89where apio saves preferences and packages, is in the '.apio' directory \
90under the user home directory but can be changed using the system \
91environment variable 'APIO_HOME'.
92"""
95@click.command(
96 name="system",
97 cls=ApioCommand,
98 short_help="Show system information.",
99 help=APIO_INFO_SYSTEM_HELP,
100)
101def _system_cli():
102 """Implements the 'apio info system' command."""
104 # -- Create the apio context. We use 'cached_ok' to cause the config
105 # -- to be loaded so we can report it.
106 apio_ctx = ApioContext(
107 project_policy=ProjectPolicy.NO_PROJECT,
108 remote_config_policy=RemoteConfigPolicy.CACHED_OK,
109 packages_policy=PackagesPolicy.ENSURE_PACKAGES,
110 )
112 platform = apio_ctx.platform
114 # -- Define the table.
115 table = Table(
116 show_header=True,
117 show_lines=True,
118 padding=PADDING,
119 box=box.SQUARE,
120 border_style=BORDER,
121 title="Apio System Information",
122 title_justify="left",
123 )
125 table.add_column("ITEM", no_wrap=True, min_width=20)
126 table.add_column("VALUE", no_wrap=True, style=EMPH1)
128 # -- Add rows
129 table.add_row("Apio CLI version", util.get_apio_version_str())
130 table.add_row("Release info", util.get_apio_release_info() or "(none)")
131 table.add_row(
132 "Yosys release tag",
133 apio_ctx.package_manager.get_yosys_release_tag(),
134 )
135 table.add_row("Python version", util.get_python_version())
136 table.add_row("Executable", sys.executable)
137 table.add_row("System info", apio_platforms.get_system_info())
138 table.add_row("Platform id", platform.id)
139 table.add_row("Is Darwin", str(platform.is_darwin))
140 table.add_row("Is Linux", str(platform.is_linux))
141 table.add_row("Is Windows", str(platform.is_windows))
142 table.add_row("Scons shell id", apio_ctx.scons_shell_id())
143 table.add_row("VSCode debugger", str(is_under_vscode_debugger()))
144 table.add_row("Pyinstaller", str(util.is_pyinstaller_app()))
145 table.add_row(
146 "Apio Python package", str(util.get_path_in_apio_package(""))
147 )
148 table.add_row("Apio home dir", str(apio_ctx.apio_home_dir))
149 table.add_row("Apio packages dir", str(apio_ctx.apio_packages_dir))
150 assert apio_ctx.definitions is not None
151 table.add_row(
152 "Apio definitions dir",
153 str(apio_ctx.definitions.package_definitions_dir),
154 )
155 table.add_row(
156 "Remote config file URL", apio_ctx.remote_config.remote_config_file_url
157 )
158 table.add_row(
159 "Remote config status", construct_remote_config_status_str(apio_ctx)
160 )
161 table.add_row(
162 "Verible formatter",
163 str(apio_ctx.apio_packages_dir / "verible/bin/verible-verilog-format"),
164 )
165 table.add_row(
166 "Verible language server",
167 str(apio_ctx.apio_packages_dir / "verible/bin/verible-verilog-ls"),
168 )
170 # -- Add a row for each define apio env var (e.g.APIO_DEBUG)
171 for var in env_options.get_defined():
172 val = env_options.get(var)
173 table.add_row(var, markup.escape(val))
175 # -- Render the table.
176 cout()
177 ctable(table)
178 cout(
179 "For programmatic system info use 'apio api get-system'.",
180 style=INFO,
181 )
184# ------ apio info project
186# -- Text in the rich-text format of the python rich library.
187APIO_INFO_PROJECT_HELP = """
188The command 'apio info project' collects general information an apio \
189project and displays it in a table format.
191Examples:[code]
192 apio info project # Project info.
193 apio info project --env my-env # Select a specific project env.[/code]
195[NOTE] For programmatic access to this information use 'apio api get-project'.
196"""
199@click.command(
200 name="project",
201 cls=ApioCommand,
202 short_help="Show project information.",
203 help=APIO_INFO_PROJECT_HELP,
204)
205@options.env_option_gen()
206@options.project_dir_option
207def _project_cli(
208 *,
209 # Options
210 env: str,
211 project_dir: Path | None,
212):
213 """Implements the 'apio info project' command."""
215 # -- Create the apio context. We use 'cached_ok' to cause the config
216 # -- to be loaded so we can report it.
217 apio_ctx = ApioContext(
218 project_policy=ProjectPolicy.PROJECT_REQUIRED,
219 remote_config_policy=RemoteConfigPolicy.CACHED_OK,
220 packages_policy=PackagesPolicy.ENSURE_PACKAGES,
221 project_dir_arg=project_dir,
222 env_arg=env,
223 report_env=False,
224 )
226 # -- Shortcuts
227 project = apio_ctx.project
228 res = apio_ctx.project_resources
230 # -- Define the table.
231 table = Table(
232 show_header=True,
233 show_lines=True,
234 padding=PADDING,
235 box=box.SQUARE,
236 border_style=BORDER,
237 title="Apio project Information",
238 title_justify="left",
239 )
241 table.add_column("ITEM", no_wrap=True, min_width=20)
242 table.add_column("VALUE", no_wrap=True, style=EMPH1)
244 # -- Add rows
245 board_id = project.get_str_option("board")
246 assert isinstance(board_id, str)
248 fpga_id = res.fpga_id
249 programmer_id = res.programmer_id
251 defs = apio_ctx.definitions
252 assert defs is not None
254 board_definition_src = (
255 "User custom" if defs.is_custom_board(board_id) else "Apio standard"
256 )
257 fpga_definition_src = (
258 "User custom" if defs.is_custom_fpga(fpga_id) else "Apio standard"
259 )
260 programmer_definition_src = (
261 "User custom"
262 if defs.is_custom_programmer(programmer_id)
263 else "Apio standard"
264 )
266 fpga_definition = res.fpga_definition
267 proto_util.check_is_required(
268 fpga_definition, "arch", "vendor_part", "size"
269 )
271 table.add_row("Total project envs", str(len(project.env_names)))
272 table.add_row("Active project env", project.env_name)
273 table.add_row("Top module name", project.get_str_option("top-module", ""))
274 table.add_row("Board id", board_id)
275 table.add_row("Board definition", board_definition_src)
276 table.add_row("FPGA id", fpga_id)
277 table.add_row("FPGA definition", fpga_definition_src)
278 table.add_row("FPGA part", fpga_definition.vendor_part)
279 table.add_row("FPGA Architecture", ApioArch.Name(fpga_definition.arch))
280 table.add_row("FPGA size", fpga_definition.size)
281 table.add_row("Programmer id", programmer_id)
282 table.add_row("Programmer definition", programmer_definition_src)
284 # -- Render the table.
285 cout()
286 ctable(table)
287 cout(
288 "For programmatic system info use 'apio api get-project'.",
289 style=INFO,
290 )
293# ------ apio info platforms
295# -- Text in the rich-text format of the python rich library.
296APIO_INFO_PLATFORMS_HELP = """
297The command 'apio info platforms' lists the platform IDs supported by Apio, \
298with the effective platform ID of your system highlighted.
300Examples:[code]
301 apio info platforms # List supported platform ids.[/code]
303The automatic platform ID detection of Apio can be overridden by \
304defining a different platform ID using the APIO_PLATFORM environment variable.
305"""
308@click.command(
309 name="platforms",
310 cls=ApioCommand,
311 short_help="Supported platforms.",
312 help=APIO_INFO_PLATFORMS_HELP,
313)
314def _platforms_cli():
315 """Implements the 'apio info platforms' command."""
317 # Create the apio context.
318 apio_ctx = ApioContext(
319 project_policy=ProjectPolicy.NO_PROJECT,
320 remote_config_policy=RemoteConfigPolicy.CACHED_OK,
321 packages_policy=PackagesPolicy.ENSURE_PACKAGES,
322 )
324 # -- Define the table.
325 table = Table(
326 show_header=True,
327 show_lines=True,
328 padding=PADDING,
329 box=box.SQUARE,
330 border_style=BORDER,
331 title="Apio Supported Platforms",
332 title_justify="left",
333 )
335 table.add_column(" PLATFORM ID", no_wrap=True)
336 table.add_column("TYPE", no_wrap=True)
337 table.add_column("VARIANT", no_wrap=True)
339 # -- Add rows.
340 for (
341 platform_id,
342 apio_platform,
343 ) in apio_platforms.get_apio_platforms().items():
345 # -- Mark the current platform.
346 if platform_id == apio_ctx.platform.id:
347 style = EMPH3
348 marker = "* "
349 else:
350 style = None
351 marker = " "
353 table.add_row(
354 f"{marker}{platform_id}",
355 apio_platform.type,
356 apio_platform.variant,
357 style=style,
358 )
360 # -- Render the table.
361 cout()
362 ctable(table)
365# ------ apio info colors
367# -- Text in the rich-text format of the python rich library.
368APIO_INFO_COLORS_HELP = """
369The command 'apio info colors' shows how ansi colors are rendered on \
370the platform, and is typically used to diagnose color related issues.
372The command shows the themes colors even if the current theme is 'no-colors'.
374Examples:[code]
375 apio info colors # Rich library output (default)
376 apio inf col -p # Using shortcuts.[/code]
377"""
380@click.command(
381 name="colors",
382 cls=ApioCommand,
383 short_help="Colors table.",
384 help=APIO_INFO_COLORS_HELP,
385)
386def _colors_cli():
387 """Implements the 'apio info colors' command."""
389 # -- This initializes the output console.
390 ApioContext(
391 project_policy=ProjectPolicy.NO_PROJECT,
392 remote_config_policy=RemoteConfigPolicy.CACHED_OK,
393 packages_policy=PackagesPolicy.ENSURE_PACKAGES,
394 )
396 # -- Print title.
397 cout("", "ANSI Colors", "")
399 # -- Create a reversed num->name map
400 lookup = {}
401 for name, num in ANSI_COLOR_NAMES.items():
402 assert 0 <= num <= 255
403 lookup[num] = name
405 # -- Make sure the current theme supports colors, otherwise they will
406 # -- suppressed
407 if get_theme().colors_enabled: 407 ↛ 410line 407 didn't jump to line 410 because the condition on line 407 was always true
408 saved_theme_name = None
409 else:
410 saved_theme_name = get_theme().name
411 configure(theme_name=THEME_LIGHT.name)
413 # -- Print the table.
414 num_rows = 64
415 num_cols = 4
416 for row in range(num_rows):
417 values = []
418 for col in range(num_cols):
419 num = row + (col * num_rows)
420 name = lookup.get(num, None)
421 if name is None:
422 # -- No color name.
423 values.append(" " * 24)
424 else:
425 # -- Color name is available.
426 # -- Note that the color names and styling is always done by
427 # -- the rich library regardless of the choice of output.
428 s = f"{num:3} {name:20}"
429 values.append(cstyle(s, style=name))
431 # -- Construct the line.
432 line = " ".join(values)
434 # -- Output the line.
435 cout(line)
437 cout()
439 # -- Restore the original theme.
440 if saved_theme_name: 440 ↛ 441line 440 didn't jump to line 441 because the condition on line 440 was never true
441 configure(theme_name=saved_theme_name)
444# ------ apio info themes
446# -- Text in the rich-text format of the python rich library.
447APIO_INFO_THEMES_HELP = """
448The command 'apio info themes' shows the colors of the Apio themes. It can \
449be used to select the theme that works the best for you. Type \
450'apio preferences -h' for information on our to select a theme.
452The command shows colors even if the current theme is 'no-colors'.
454[code]
455Examples:
456 apio info themes # Show themes colors
457 apio inf col -p # Using shortcuts.[/code]
458"""
461@click.command(
462 name="themes",
463 cls=ApioCommand,
464 short_help="Show apio themes.",
465 help=APIO_INFO_THEMES_HELP,
466)
467def _themes_cli() -> None:
468 """Implements the 'apio info themes' command."""
470 # -- This initializes the output console.
471 ApioContext(
472 project_policy=ProjectPolicy.NO_PROJECT,
473 remote_config_policy=RemoteConfigPolicy.CACHED_OK,
474 packages_policy=PackagesPolicy.ENSURE_PACKAGES,
475 )
477 # -- Collect the list of apio list names.
478 style_names_set = set()
479 for theme_info in THEMES_TABLE.values():
480 style_names_set.update(list(theme_info.styles.keys()))
481 style_names_list = sorted(list(style_names_set), key=str.lower)
483 # -- Define the table.
484 table = Table(
485 show_header=True,
486 show_lines=True,
487 padding=PADDING,
488 box=box.SQUARE,
489 border_style=BORDER,
490 title="Apio Themes Style Colors",
491 title_justify="left",
492 )
494 # -- Get selected theme
495 selected_theme = get_theme()
496 selected_theme_name = selected_theme.name
498 # -- Add the table columns, one per theme.
499 for theme_name, theme in THEMES_TABLE.items():
500 assert theme_name == theme.name
501 column_name = theme_name.upper()
502 if theme_name == selected_theme_name:
503 column_name = f"*{column_name}*"
504 table.add_column(column_name, no_wrap=True, justify="center")
506 # -- Append the table rows
507 for style_name in style_names_list:
508 row_values: list[Text] = []
509 for theme_name, theme_info in THEMES_TABLE.items():
510 # Get style
511 colors_enabled = theme_info.colors_enabled
512 if colors_enabled:
513 if style_name not in theme_info.styles: 513 ↛ 514line 513 didn't jump to line 514 because the condition on line 513 was never true
514 styled_text = Text("---")
515 else:
516 styled_text = Text(
517 style_name, style=theme_info.styles[style_name]
518 )
519 else:
520 styled_text = Text(style_name)
522 # -- Apply the style
523 row_values.append(styled_text)
525 table.add_row(*row_values)
527 # -- Make sure the current theme supports colors, otherwise they will
528 # -- suppressed
529 if get_theme().colors_enabled: 529 ↛ 532line 529 didn't jump to line 532 because the condition on line 529 was always true
530 saved_theme_name = None
531 else:
532 saved_theme_name = get_theme().name
533 configure(theme_name=THEME_LIGHT.name)
535 # -- Render the table.
536 cout()
537 ctable(table)
539 if saved_theme_name: 539 ↛ 540line 539 didn't jump to line 540 because the condition on line 539 was never true
540 configure(theme_name=saved_theme_name)
542 cout("To change your theme use 'apio preferences -t ...'", style=INFO)
543 cout()
546# ------ apio info commands
549def _list_boards_table_format(commands):
550 """Format and output the commands table. 'commands' is a sorted
551 list of [command_name, command_description]
552 """
553 # -- Generate the table.
554 table = Table(
555 show_header=True,
556 show_lines=True,
557 padding=PADDING,
558 box=box.SQUARE,
559 border_style=BORDER,
560 title="Apio commands",
561 title_justify="left",
562 )
564 table.add_column("APIO COMMAND", no_wrap=True, min_width=20, style=EMPH2)
565 table.add_column("DESCRIPTION", no_wrap=True)
567 for cmd in commands:
568 table.add_row(cmd[0], cmd[1])
570 # -- Render the table.
571 cout()
572 ctable(table)
575def _list_boards_docs_format(commands):
576 """Format and output the commands markdown doc. 'commands' is a sorted
577 list of [command_name, command_description]
578 """
580 header1 = "COMMAND"
581 header2 = "DESCRIPTION"
583 # -- Replace the command names with markdown link to the command doc page.
584 tagged_commands = [
585 [f"[{c[0]}](cmd-apio-{c[0]}.md)", c[1]] for c in commands
586 ]
588 # -- Determine column sizes
589 w1 = max(len(header1), *(len(cmd[0]) for cmd in tagged_commands))
590 w2 = max(len(header2), *(len(cmd[1]) for cmd in tagged_commands))
592 # -- Print page header
593 today = date.today()
594 today_str = f"{today.strftime('%B')} {today.day}, {today.year}"
595 cwrite("\n<!-- BEGIN generation by 'apio commands --docs' -->\n")
596 cwrite("\n# Apio CLI commands\n")
597 cwrite(
598 f"\nThis markdown page was generated automatically on {today_str}.\n\n"
599 )
601 # -- Table header
602 cwrite(
603 "| {0} | {1} |\n".format(
604 header1.ljust(w1),
605 header2.ljust(w2),
606 )
607 )
609 cwrite(
610 "| {0} | {1} |\n".format(
611 "-" * w1,
612 "-" * w2,
613 )
614 )
616 # -- Add the rows
617 for tagged_cmd in tagged_commands:
618 cwrite(
619 "| {0} | {1} |\n".format(
620 tagged_cmd[0].ljust(w1),
621 tagged_cmd[1].ljust(w2),
622 )
623 )
625 # -- All done.
626 cwrite("\n<!-- END generation by 'apio commands --docs' -->\n\n")
629# -- Text in the rich-text format of the python rich library.
630APIO_INFO_COMMANDS_HELP = """
631The command 'apio info commands' lists the the available apio commands \
632in a table format. If the option '--docs' is specified, the command outputs \
633the list as a markdown document that is used to automatically update the \
634Apio documentation.
636Examples:[code]
637 apio info commands
638 apio info commands --docs > docs/commands-list.md[/code]
639"""
642@click.command(
643 name="commands",
644 cls=ApioCommand,
645 short_help="Show apio commands.",
646 help=APIO_INFO_COMMANDS_HELP,
647)
648@options.docs_format_option
649def _commands_cli(
650 *,
651 # Options
652 docs,
653):
654 """Implements the 'apio info commands' command."""
656 # -- We perform this lazy cyclic import here to allow the two modules
657 # -- to initialize properly without a cyclic import.
658 #
659 # pylint: disable=import-outside-toplevel
660 # pylint: disable=cyclic-import
661 from apio.commands import apio as apio_main
663 # -- This initializes the output console.
664 ApioContext(
665 project_policy=ProjectPolicy.NO_PROJECT,
666 remote_config_policy=RemoteConfigPolicy.CACHED_OK,
667 packages_policy=PackagesPolicy.IGNORE_PACKAGES,
668 )
670 # -- Collect commands as a list of <name, description>
671 commands = []
672 for subgroup in apio_main.SUBGROUPS:
673 for cmd in subgroup.commands:
674 commands.append([cmd.name, cmd.get_short_help_str()])
676 # -- Sort the commands list alphabetically.
677 commands.sort()
679 # -- Generate the output
680 if docs: 680 ↛ 681line 680 didn't jump to line 681 because the condition on line 680 was never true
681 _list_boards_docs_format(commands)
682 else:
683 _list_boards_table_format(commands)
686# ------ apio info
688# -- Text in the rich-text format of the python rich library.
689APIO_INFO_HELP = """
690The command group 'apio info' contains subcommands that provide \
691additional information about Apio and your system.
692"""
694# -- We have only a single group with the title 'Subcommands'.
695SUBGROUPS = [
696 ApioSubgroup(
697 "Subcommands",
698 [
699 _system_cli,
700 _project_cli,
701 _platforms_cli,
702 _colors_cli,
703 _themes_cli,
704 _commands_cli,
705 ],
706 ),
707]
710@click.command(
711 name="info",
712 cls=ApioGroup,
713 subgroups=SUBGROUPS,
714 short_help="Apio's info and info.",
715 help=APIO_INFO_HELP,
716)
717def cli():
718 """Implements the 'apio info' command group."""
720 # pass