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