Coverage for apio/commands/apio_api.py: 90%
389 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 api' command"""
11# pylint: disable=too-many-lines
13import sys
14import os
15from typing import Self, cast, Any
16from dataclasses import dataclass
17import json
18from pathlib import Path
19import click
20from apio.commands import options
21from apio.managers.examples import Examples, ExampleInfo
22from apio.common.apio_console import cout, fatal_error
23from apio.common import proto_util, apio_platforms
24from apio.common.proto.stubs.apio_common_pb2 import ApioArch, EnvMutations
25from apio.common.debug_util import is_under_vscode_debugger
26from apio.common.common_util import get_project_source_files
27from apio.utils import (
28 cmd_util,
29 usb_util,
30 serial_util,
31 util,
32 env_options,
33)
34from apio.utils.usb_util import UsbDevice
35from apio.utils.serial_util import SerialDevice
36from apio.common.apio_styles import (
37 INFO,
38 ERROR,
39 SUCCESS,
40 WARNING,
41 EMPH1,
42 EMPH2,
43 EMPH3,
44 TITLE,
45)
46from apio.apio_context import (
47 ApioContext,
48 PackagesPolicy,
49 ProjectPolicy,
50 RemoteConfigPolicy,
51)
52from apio.utils.cmd_util import (
53 ApioGroup,
54 ApioSubgroup,
55 ApioCommand,
56 ApioCmdContext,
57)
58from apio.common import build_report
60timestamp_option = click.option(
61 "timestamp", # Var name.
62 "-t",
63 "--timestamp",
64 type=str,
65 metavar="text",
66 help="Set a user provided timestamp.",
67 cls=cmd_util.ApioOption,
68)
70output_option = click.option(
71 "output", # Var name.
72 "-o",
73 "--output",
74 type=str,
75 metavar="file-name",
76 help="Set output file.",
77 cls=cmd_util.ApioOption,
78)
81def write_as_json_doc(top_dict: dict, output_flag: str, force_flag: bool):
82 """A common function to write a dict as a JSON doc."""
83 # -- Format the top dict as json text.
84 text = json.dumps(top_dict, indent=2)
86 if output_flag:
87 # -- Output the json text to a user specified file.
88 output_path = Path(output_flag)
90 if output_path.is_dir(): 90 ↛ 91line 90 didn't jump to line 91 because the condition on line 90 was never true
91 fatal_error(f"The output path '{output_path}' is a directory.")
93 if output_path.exists() and not force_flag: 93 ↛ 94line 93 didn't jump to line 94 because the condition on line 93 was never true
94 fatal_error(
95 f"The file already exists '{output_path}'.",
96 info="Use the --force option to allow overwriting.",
97 )
99 # -- if there file path contains a parent dir, make
100 # -- sure it exists. If output_flag is just a file name such
101 # -- as 'foo.json', we don nothing.
102 dirname = os.path.dirname(output_flag)
103 if dirname:
104 os.makedirs(dirname, exist_ok=True)
106 # -- Write to file.
107 with open(output_flag, "w", encoding="utf-8") as f:
108 f.write(text)
109 else:
110 # -- Output the json text to stdout.
111 print(text, file=sys.stdout)
114# ------ apio api get-system
117# -- Text in the rich-text format of the python rich library.
118APIO_API_GET_SYSTEM_HELP = """
119The command 'apio api get-system' exports information about apio and \
120the underlying system as a JSON doc. It is similar to the command \
121'apio info system' which is intended for human consumption.
123The optional flag '--timestamp' allows the caller to embed in the JSON \
124document a known timestamp that allows to verify that the JSON document \
125was indeed was generated by the same invocation.
127Examples:[code]
128 apio api get-system # Write to stdout
129 apio api get-system -o apio.json # Write to a file[/code]
130"""
133@click.command(
134 name="get-system",
135 cls=ApioCommand,
136 short_help="Retrieve apio and system information.",
137 help=APIO_API_GET_SYSTEM_HELP,
138)
139# @click.pass_context
140@timestamp_option
141@output_option
142@options.force_option_gen(short_help="Overwrite output file.")
143def _get_system_cli(
144 *,
145 # Options
146 timestamp: str,
147 output: str,
148 force: bool,
149):
150 """Implements the 'apio apio get-system' command."""
152 apio_ctx = ApioContext(
153 project_policy=ProjectPolicy.NO_PROJECT,
154 remote_config_policy=RemoteConfigPolicy.CACHED_OK,
155 packages_policy=PackagesPolicy.ENSURE_PACKAGES,
156 )
158 platform = apio_ctx.platform
160 # -- The top dict that we will emit as json.
161 top_dict: dict[str, Any] = {}
163 # -- Append user timestamp if specified.
164 if timestamp: 164 ↛ 167line 164 didn't jump to line 167 because the condition on line 164 was always true
165 top_dict["timestamp"] = timestamp
167 section_dict: dict[str, Any] = {}
169 # -- Add fields.
170 section_dict["apio-cli-version"] = util.get_apio_version_str()
171 section_dict["release-info"] = util.get_apio_release_info()
172 section_dict["yosys-release-tag"] = (
173 apio_ctx.package_manager.get_yosys_release_tag()
174 )
175 section_dict["python-version"] = util.get_python_version()
176 section_dict["executable"] = sys.executable
177 section_dict["platform-info"] = apio_platforms.get_system_info()
178 section_dict["platform-id"] = platform.id
179 section_dict["is-darwin"] = platform.is_darwin
180 section_dict["is-linux"] = platform.is_linux
181 section_dict["is-windows"] = platform.is_windows
182 # section_dict["scons-shell-id"] = apio_ctx.scons_shell_id
183 section_dict["scons-shell-id"] = apio_ctx.scons_shell_id()
184 section_dict["vscode-debugger"] = is_under_vscode_debugger()
185 section_dict["pyinstaller"] = util.is_pyinstaller_app()
186 section_dict["apio-python_package"] = str(
187 util.get_path_in_apio_package("")
188 )
189 section_dict["apio-home-dir"] = str(apio_ctx.apio_home_dir)
190 section_dict["apio-packages-dir"] = str(apio_ctx.apio_packages_dir)
191 assert apio_ctx.definitions is not None
192 section_dict["apio-definitions-dir"] = str(
193 apio_ctx.definitions.package_definitions_dir
194 )
195 section_dict["remote-config-file-url"] = (
196 apio_ctx.remote_config.remote_config_file_url
197 )
198 section_dict["verible-formatter"] = str(
199 apio_ctx.apio_packages_dir / "verible/bin/verible-verilog-format"
200 )
201 section_dict["verible-language-server"] = str(
202 apio_ctx.apio_packages_dir / "verible/bin/verible-verilog-ls"
203 )
205 # -- Include all supported apio env vars, defined and undefined.
206 apio_vars_dict = {
207 var: env_options.get(var, None) for var in env_options.get_all()
208 }
209 section_dict["apio-env-vars"] = apio_vars_dict
211 # -- Add section
212 top_dict["system"] = section_dict
214 # -- Write out
215 write_as_json_doc(top_dict, output, force)
218# ------ apio api get-build-report
221# -- Text in the rich-text format of the python rich library.
222APIO_API_GET_BUILD_REPORT_HELP = """
223The command 'apio api get-build-report' provides utilization and max \
224clock information from a built project. The information is extracted \
225from the file 'hardware.pnr' that is generated by Apio when building \
226the project.
228The optional flag '--timestamp' allows the caller to embed in the JSON \
229document a known timestamp that allows to verify that the JSON document \
230was indeed was generated by the same invocation.
232Examples:[code]
233 apio api get-build-report # Report for default env
234 apio api get-build-report -e env1 # Report for specified env
235 apio api get-build-report -p foo/bar # Project in another dir
236 apio api get-build-report -o apio.json # Write to a file[/code]
237"""
240@click.command(
241 name="get-build-report",
242 cls=ApioCommand,
243 short_help="Get project build information.",
244 help=APIO_API_GET_BUILD_REPORT_HELP,
245)
246# @click.pass_context
247@options.env_option_gen()
248@options.project_dir_option
249@timestamp_option
250@output_option
251@options.force_option_gen(short_help="Overwrite output file.")
252def _get_build_report_cli(
253 *,
254 # Options
255 env: str,
256 project_dir: Path | None,
257 timestamp: str,
258 output: str,
259 force: bool,
260):
261 """Implements the 'apio apio get-build-report' command."""
263 apio_ctx = ApioContext(
264 project_policy=ProjectPolicy.PROJECT_REQUIRED,
265 remote_config_policy=RemoteConfigPolicy.CACHED_OK,
266 packages_policy=PackagesPolicy.ENSURE_PACKAGES,
267 project_dir_arg=project_dir,
268 env_arg=env,
269 report_env=False,
270 )
272 # -- Change to the project's folder.
273 os.chdir(apio_ctx.project_dir)
275 # -- The build process generates this report file.
276 pnr_json_file = apio_ctx.env_build_path / "hardware.pnr"
278 # -- Read the report
279 report = build_report.read_build_report(pnr_json_file)
281 # -- The top dict that we will emit as json.
282 top_dict: dict[str, Any] = {}
284 # -- Append user timestamp if specified.
285 if timestamp:
286 top_dict["timestamp"] = timestamp
288 section_dict: dict[str, Any] = {}
289 section_dict["arch"] = ApioArch.Name(report.arch)
290 section_dict["env"] = apio_ctx.project.env_name
292 resources_dict: dict[str, Any] = {}
293 for res in report.resources:
294 resources_dict[res.name] = {
295 "used": res.used,
296 "available": res.available,
297 "percentage": res.percentage,
298 }
300 section_dict["resources"] = resources_dict
302 clocks_dict: dict[str, Any] = {}
303 for clk in report.clocks:
304 clocks_dict[clk.name] = {"fmax_mhz": clk.fmax_mhz}
306 section_dict["clocks"] = clocks_dict
308 # -- Add section
309 top_dict["build-report"] = section_dict
311 # -- Write out
312 write_as_json_doc(top_dict, output, force)
315# ------ apio api get-project
318# -- Text in the rich-text format of the python rich library.
319APIO_API_GET_PROJECT_HELP = """
320The command 'apio api get-project' exports information about an Apio
321project as a JSON doc.
323The optional flag '--timestamp' allows the caller to embed in the JSON \
324document a known timestamp that allows to verify that the JSON document \
325was indeed was generated by the same invocation.
327Examples:[code]
328 apio api get-project # Report default env
329 apio api get-project -e env1 # Report specified env
330 apio api get-project -p foo/bar # Project in another dir
331 apio api get-project -o apio.json # Write to a file[/code]
332"""
335@click.command(
336 name="get-project",
337 cls=ApioCommand,
338 short_help="Get project information.",
339 help=APIO_API_GET_PROJECT_HELP,
340)
341# @click.pass_context
342@options.env_option_gen()
343@options.project_dir_option
344@timestamp_option
345@output_option
346@options.force_option_gen(short_help="Overwrite output file.")
347def _get_project_cli(
348 *,
349 # Options
350 env: str,
351 project_dir: Path | None,
352 timestamp: str,
353 output: str,
354 force: bool,
355):
356 """Implements the 'apio apio get-project' command."""
358 apio_ctx = ApioContext(
359 project_policy=ProjectPolicy.PROJECT_REQUIRED,
360 remote_config_policy=RemoteConfigPolicy.CACHED_OK,
361 packages_policy=PackagesPolicy.ENSURE_PACKAGES,
362 project_dir_arg=project_dir,
363 env_arg=env,
364 report_env=False,
365 )
366 assert apio_ctx.definitions is not None
368 # -- Change to the project's folder for function such as
369 # -- get_project_source_files() which expects to run at the project root.
370 os.chdir(apio_ctx.project_dir)
372 # -- The top dict that we will emit as json.
373 top_dict: dict[str, Any] = {}
375 # -- Append user timestamp if specified.
376 if timestamp: 376 ↛ 379line 376 didn't jump to line 379 because the condition on line 376 was always true
377 top_dict["timestamp"] = timestamp
379 section_dict: dict[str, Any] = {}
381 active_env_dict: dict[str, Any] = {}
382 active_env_dict["name"] = apio_ctx.project.env_name
383 active_env_dict["options"] = apio_ctx.project.env_options
384 section_dict["active-env"] = active_env_dict
386 section_dict["envs"] = apio_ctx.project.env_names
388 synth_srcs, test_srcs = get_project_source_files()
389 section_dict["synth-files"] = synth_srcs
390 section_dict["test-benches"] = test_srcs
392 pr = apio_ctx.project_resources
394 board_dict = {
395 "id": pr.board_id,
396 "is-custom": apio_ctx.definitions.is_custom_board(pr.board_id),
397 }
398 board_dict["definition"] = proto_util.proto_to_json_dict(
399 pr.board_definition
400 )
401 section_dict["board"] = board_dict
403 fpga_dict = {
404 "id": pr.fpga_id,
405 "is-custom": apio_ctx.definitions.is_custom_fpga(pr.fpga_id),
406 }
407 fpga_dict["definition"] = proto_util.proto_to_json_dict(pr.fpga_definition)
408 section_dict["fpga"] = fpga_dict
410 programmer_dict = {
411 "id": pr.programmer_id,
412 "is-custom": apio_ctx.definitions.is_custom_programmer(
413 pr.programmer_id
414 ),
415 }
416 programmer_dict["definition"] = proto_util.proto_to_json_dict(
417 pr.programmer_definition
418 )
419 section_dict["programmer"] = programmer_dict
421 # -- Add section
422 top_dict["project"] = section_dict
424 # -- Write out
425 write_as_json_doc(top_dict, output, force)
428# ------ apio api get-boards
431# -- Text in the rich-text format of the python rich library.
432APIO_API_GET_BOARDS_HELP = """
433The command 'apio api get-boards' exports apio standard boards information as \
434a JSON document. It does not show any custom boards in the user's project, \
435for that, use the 'apio api get-project' command.
437The optional flag '--timestamp' allows the caller to embed in the JSON \
438document a known timestamp that allows to verify that the JSON document \
439was indeed was generated by the same invocation.
441Examples:[code]
442 apio api get-boards # Write to stdout
443 apio api get-boards -o apio.json # Write to a file[/code]
444"""
447@click.command(
448 name="get-boards",
449 cls=ApioCommand,
450 short_help="Retrieve boards information.",
451 help=APIO_API_GET_BOARDS_HELP,
452)
453@timestamp_option
454@output_option
455@options.force_option_gen(short_help="Overwrite output file.")
456def _get_boards_cli(
457 *,
458 # Options
459 timestamp: str,
460 output: str,
461 force: bool,
462):
463 """Implements the 'apio apio get-boards' command."""
465 # -- For now, the information is not in a project context. That may
466 # -- change in the future.
467 apio_ctx = ApioContext(
468 project_policy=ProjectPolicy.NO_PROJECT,
469 remote_config_policy=RemoteConfigPolicy.CACHED_OK,
470 packages_policy=PackagesPolicy.ENSURE_PACKAGES,
471 )
472 assert apio_ctx.definitions is not None
474 # -- The top dict that we will emit as json.
475 top_dict: dict[str, Any] = {}
477 # -- Append user timestamp if specified.
478 if timestamp: 478 ↛ 482line 478 didn't jump to line 482 because the condition on line 478 was always true
479 top_dict["timestamp"] = timestamp
481 # -- Generate the boards section.
482 section: dict[str, Any] = {}
483 for board_id, board_definition in apio_ctx.definitions.boards.items():
484 # -- The board output dict.
485 board_dict: dict[str, Any] = {}
487 # -- We assume that these proto fields are requires and therefore
488 # -- must exist.
489 proto_util.check_is_required(
490 board_definition,
491 "description",
492 "fpga_id",
493 "programmer",
494 "programmer.id",
495 )
497 # -- Add board description
498 board_dict["description"] = board_definition.description
500 # -- Add board's fpga information.
501 fpga_id = board_definition.fpga_id
502 fpga_definition = apio_ctx.definitions.fpgas[fpga_id]
503 board_dict["fpga"] = {
504 "id": fpga_id,
505 "definition": proto_util.proto_to_json_dict(fpga_definition),
506 }
508 # -- Add board's programmer information.
509 programmer_id = board_definition.programmer.id
510 programmer_definition = apio_ctx.definitions.programmers[programmer_id]
511 board_dict["programmer"] = {
512 "id": programmer_id,
513 "definition": proto_util.proto_to_json_dict(programmer_definition),
514 }
516 # -- Add the board to the boards dict.
517 section[board_id] = board_dict
519 top_dict["boards"] = section
521 # -- Write out
522 write_as_json_doc(top_dict, output, force)
525# ------ apio api get-fpgas
528# -- Text in the rich-text format of the python rich library.
529APIO_API_GET_FPGAS_HELP = """
530The command 'apio api get-fpgas' exports apio standard FPGAs information as a \
531JSON document. It does not show any custom fpgas in the user's project, for \
532that, use the 'apio api get-project' command.
534The optional flag '--timestamp' allows the caller to embed in the JSON \
535document a known timestamp that allows to verify that the JSON document \
536was indeed was generated by the same invocation.
538Examples:[code]
539 apio api get-fpgas # Write to stdout
540 apio api get-fpgas -o apio.json # Write to a file[/code]
541"""
544@click.command(
545 name="get-fpgas",
546 cls=ApioCommand,
547 short_help="Retrieve FPGAs information.",
548 help=APIO_API_GET_FPGAS_HELP,
549)
550@timestamp_option
551@output_option
552@options.force_option_gen(short_help="Overwrite output file.")
553def _get_fpgas_cli(
554 *,
555 # Options
556 timestamp: str,
557 output: str,
558 force: bool,
559):
560 """Implements the 'apio apio get-fpgas' command."""
562 # -- For now, the information is not in a project context. That may
563 # -- change in the future.
564 apio_ctx = ApioContext(
565 project_policy=ProjectPolicy.NO_PROJECT,
566 remote_config_policy=RemoteConfigPolicy.CACHED_OK,
567 packages_policy=PackagesPolicy.ENSURE_PACKAGES,
568 )
569 assert apio_ctx.definitions is not None
571 # -- The top dict that we will emit as json.
572 top_dict: dict[str, Any] = {}
574 # -- Append user timestamp if specified.
575 if timestamp: 575 ↛ 579line 575 didn't jump to line 579 because the condition on line 575 was always true
576 top_dict["timestamp"] = timestamp
578 # -- Generate the fpgas section
579 section: dict[str, Any] = {}
580 for fpga_id, fpga_definition in apio_ctx.definitions.fpgas.items():
581 section[fpga_id] = proto_util.proto_to_json_dict(fpga_definition)
583 top_dict["fpgas"] = section
585 # -- Write out
586 write_as_json_doc(top_dict, output, force)
589# ------ apio api get-programmers
592# -- Text in the rich-text format of the python rich library.
593APIO_API_GET_PROGRAMMERS_HELP = """
594The command 'apio api get-programmers' exports apio standard programmers \
595information as a JSON document. It does not show any custom programmers in \
596the user's project, for that, use the 'apio api get-project' command.
598The optional flag '--timestamp' allows the caller to embed in the JSON \
599document a known timestamp that allows to verify that the JSON document \
600was indeed was generated by the same invocation.
602Examples:[code]
603 apio api get-programmers # Write to stdout
604 apio api get-programmers -o apio.json # Write to a file[/code]
605"""
608@click.command(
609 name="get-programmers",
610 cls=ApioCommand,
611 short_help="Retrieve programmers information.",
612 help=APIO_API_GET_PROGRAMMERS_HELP,
613)
614@timestamp_option
615@output_option
616@options.force_option_gen(short_help="Overwrite output file.")
617def _get_programmers_cli(
618 *,
619 # Options
620 timestamp: str,
621 output: str,
622 force: bool,
623):
624 """Implements the 'apio apio get-programmers' command."""
626 # -- For now, the information is not in a project context. That may
627 # -- change in the future.
628 apio_ctx = ApioContext(
629 project_policy=ProjectPolicy.NO_PROJECT,
630 remote_config_policy=RemoteConfigPolicy.CACHED_OK,
631 packages_policy=PackagesPolicy.ENSURE_PACKAGES,
632 )
633 assert apio_ctx.definitions is not None
635 # -- The top dict that we will emit as json.
636 top_dict: dict[str, Any] = {}
638 # -- Append user timestamp if specified.
639 if timestamp: 639 ↛ 643line 639 didn't jump to line 643 because the condition on line 639 was always true
640 top_dict["timestamp"] = timestamp
642 # -- Generate the 'programmers' section.
643 section: dict[str, Any] = {}
644 for (
645 programmer_id,
646 programmer_definition,
647 ) in apio_ctx.definitions.programmers.items():
648 section[programmer_id] = proto_util.proto_to_json_dict(
649 programmer_definition
650 )
652 top_dict["programmers"] = section
654 # -- Write out
655 write_as_json_doc(top_dict, output, force)
658# ------ apio api get-examples
661# -- Text in the rich-text format of the python rich library.
662APIO_API_GET_EXAMPLES_HELP = """
663The command 'apio api get-examples' exports apio examples information as a \
664JSON document.
666The optional flag '--timestamp' allows the caller to embed in the JSON \
667document a known timestamp that allows to verify that the JSON document \
668was indeed was generated by the same invocation.
670Examples:[code]
671 apio api get-examples # Write to stdout
672 apio api get-examples -o apio.json # Write to a file[/code]
673"""
676@click.command(
677 name="get-examples",
678 cls=ApioCommand,
679 short_help="Retrieve examples information.",
680 help=APIO_API_GET_EXAMPLES_HELP,
681)
682@timestamp_option
683@output_option
684@options.force_option_gen(short_help="Overwrite output file.")
685def _get_examples_cli(
686 *,
687 # Options
688 timestamp: str,
689 output: str,
690 force: bool,
691):
692 """Implements the 'apio apio get-examples' command."""
694 # -- For now, the information is not in a project context. That may
695 # -- change in the future.
696 apio_ctx = ApioContext(
697 project_policy=ProjectPolicy.NO_PROJECT,
698 remote_config_policy=RemoteConfigPolicy.CACHED_OK,
699 packages_policy=PackagesPolicy.ENSURE_PACKAGES,
700 )
702 # -- Get examples infos.
703 examples: list[ExampleInfo] = Examples(apio_ctx).get_examples_infos()
705 # -- Group examples by boards
706 boards_examples: dict[str, list[ExampleInfo]] = {}
707 for example in examples:
708 board_examples = boards_examples.get(example.board_id, [])
709 board_examples.append(example)
710 boards_examples[example.board_id] = board_examples
712 # -- The top dict that we will emit as json.
713 top_dict: dict[str, Any] = {}
715 # -- Append user timestamp if specified.
716 if timestamp: 716 ↛ 720line 716 didn't jump to line 720 because the condition on line 716 was always true
717 top_dict["timestamp"] = timestamp
719 # -- Generate the 'examples' section.
720 section: dict[str, Any] = {}
721 for board, board_examples in boards_examples.items():
722 board_dict = {}
723 # -- Generate board examples
724 for example_info in board_examples:
725 example_dict = {}
726 example_dict["description"] = example_info.description
727 board_dict[example_info.example_name] = example_dict
729 section[board] = board_dict
731 top_dict["examples"] = section
733 # -- Write out
734 write_as_json_doc(top_dict, output, force)
737# ------ apio api get-commands
740@dataclass(frozen=True)
741class CmdInfo:
742 """Represents the information of a single apio command."""
744 name: str
745 path: list[str]
746 cli: click.Command
747 children: list[Self]
750def scan_children(cmd_cli) -> dict:
751 """Return a dict describing this command subtree."""
752 result: dict[str, Any] = {}
754 # -- Sanity check
755 assert isinstance(result, dict), type(result)
757 # -- If this is a simple command, it has no sub commands.
758 if isinstance(cmd_cli, ApioCommand):
759 return result
761 # -- Here we have a group and it should have at least one sub command.
762 assert isinstance(cmd_cli, ApioGroup), type(cmd_cli)
763 subgroups: list[ApioSubgroup] = cmd_cli.subgroups
765 # -- Create the dict for the command subgroups.
766 subcommands_dict: dict[str, Any] = {}
767 result["commands"] = subcommands_dict
769 # -- Iterate the subgroups and populate them. We flaten the subcommands
770 # -- group into a single list of commands.
771 for subgroup in subgroups:
772 assert isinstance(subgroup, ApioSubgroup), type(subgroup)
773 assert isinstance(subgroup.title, str), type(subgroup.title)
774 for subcommand in subgroup.commands:
775 subcommand_dict = scan_children(subcommand)
776 assert subcommand.name is not None
777 subcommands_dict[subcommand.name] = subcommand_dict
779 # -- All done ok.
780 return result
783# -- Text in the rich-text format of the python rich library.
784APIO_API_GET_COMMANDS_HELP = """
785The command 'apio api get-commands' exports apio command structure \
786of Apio as a JSON doc. This is used by various tools such as
787documentation generators and tests.
789The optional flag '--timestamp' allows the caller to embed in the JSON \
790document a known timestamp that allows to verify that the JSON document \
791was indeed was generated by the same invocation.
793Examples:[code]
794 apio api get-commands # Write to stdout
795 apio api get-commands -o apio.json # Write to a file[/code]
796"""
799@click.command(
800 name="get-commands",
801 cls=ApioCommand,
802 short_help="Retrieve apio commands information.",
803 help=APIO_API_GET_COMMANDS_HELP,
804)
805@click.pass_context
806@timestamp_option
807@output_option
808@options.force_option_gen(short_help="Overwrite output file.")
809def _get_commands_cli(
810 # Click context
811 cmd_ctx: click.Context,
812 *,
813 # Options
814 timestamp: str,
815 output: str,
816 force: bool,
817):
818 """Implements the 'apio apio get-commands' command."""
820 # -- Find the top cli which is the "apio" command. Would access it
821 # -- directly but it would create a circular python import.
822 ctx = cast(ApioCmdContext, cmd_ctx)
823 while ctx.parent:
824 assert isinstance(ctx.parent, ApioCmdContext), type(ctx.parent)
825 ctx = ctx.parent
826 top_cli = ctx.command
827 assert top_cli.name == "apio", top_cli
829 # -- This initializes the console, print active env vars, etc.
830 ApioContext(
831 project_policy=ProjectPolicy.NO_PROJECT,
832 remote_config_policy=RemoteConfigPolicy.CACHED_OK,
833 packages_policy=PackagesPolicy.ENSURE_PACKAGES,
834 )
836 # -- The top dict that we will emit as json.
837 top_dict: dict[str, Any] = {}
839 # -- Append user timestamp if specified.
840 if timestamp: 840 ↛ 843line 840 didn't jump to line 843 because the condition on line 840 was always true
841 top_dict["timestamp"] = timestamp
843 section_dict: dict[str, Any] = {}
844 section_dict["apio"] = scan_children(top_cli)
845 top_dict["commands"] = section_dict
847 # -- Write out
848 write_as_json_doc(top_dict, output, force)
851# ------ apio api get-packages
853# -- Text in the rich-text format of the python rich library.
854APIO_API_GET_PACKAGES_HELP = """
855The command 'apio api get-packages' exports information about \
856the apio packages on the current platform as a JSON doc. Note that \
857the set of apio packages used depends on the platform used as some \
858packages may be platform specific.
860The optional flag '--timestamp' allows the caller to embed in the JSON \
861document a known timestamp that allows to verify that the JSON document \
862was indeed was generated by the same invocation.
864Examples:[code]
865 apio api get-packages # Write to stdout
866 apio api get-packages -o apio.json # Write to a file[/code]
867"""
870@click.command(
871 name="get-packages",
872 cls=ApioCommand,
873 short_help="Retrieve apio packages information.",
874 help=APIO_API_GET_PACKAGES_HELP,
875)
876# @click.pass_context
877@timestamp_option
878@output_option
879@options.force_option_gen(short_help="Overwrite output file.")
880def _get_packages_cli(
881 *,
882 # Options
883 timestamp: str,
884 output: str,
885 force: bool,
886):
887 """Implements the 'apio apio get-packages' command."""
889 apio_ctx = ApioContext(
890 project_policy=ProjectPolicy.NO_PROJECT,
891 remote_config_policy=RemoteConfigPolicy.CACHED_OK,
892 packages_policy=PackagesPolicy.ENSURE_PACKAGES,
893 )
895 package_manager = apio_ctx.package_manager
897 # -- The top dict that we will emit as json.
898 top_dict: dict[str, Any] = {}
900 # -- Append user timestamp if specified.
901 if timestamp: 901 ↛ 905line 901 didn't jump to line 905 because the condition on line 901 was always true
902 top_dict["timestamp"] = timestamp
904 # -- Packages section
905 section_dict: dict[str, Any] = {}
906 top_dict["packages"] = section_dict
908 for package_name in package_manager.platform_packages:
909 metadata = package_manager.installed_packages[package_name]
910 config = apio_ctx.all_packages[package_name]
911 build_info = package_manager.read_package_build_info(package_name)
912 section_dict[package_name] = {
913 "description": config.description,
914 "installation": metadata,
915 "build-info": build_info,
916 }
918 # -- Write out
919 write_as_json_doc(top_dict, output, force)
922# ------ apio api get-config
924# -- Text in the rich-text format of the python rich library.
925APIO_API_GET_CONFIG_HELP = """
926The command 'apio api get-config' exports information about \
927the resolution of the Apio configuration in the file 'config.jsonc' \
928for the current platform.
930The optional flag '--timestamp' allows the caller to embed in the JSON \
931document a known timestamp that allows to verify that the JSON document \
932was indeed was generated by the same invocation.
934Examples:[code]
935 apio api get-config # Write to stdout
936 apio api get-config -o apio.json # Write to a file[/code]
937"""
940@click.command(
941 name="get-config",
942 cls=ApioCommand,
943 short_help="Retrieve apio config setting.",
944 help=APIO_API_GET_CONFIG_HELP,
945)
946# @click.pass_context
947@timestamp_option
948@output_option
949@options.force_option_gen(short_help="Overwrite output file.")
950def _get_config_cli(
951 *,
952 # Options
953 timestamp: str,
954 output: str,
955 force: bool,
956):
957 """Implements the 'apio apio get-config' command."""
959 apio_ctx = ApioContext(
960 project_policy=ProjectPolicy.NO_PROJECT,
961 remote_config_policy=RemoteConfigPolicy.CACHED_OK,
962 packages_policy=PackagesPolicy.ENSURE_PACKAGES,
963 )
965 # -- The top dict that we will emit as json.
966 top_dict: dict[str, Any] = {}
968 # -- Append user timestamp if specified.
969 if timestamp: 969 ↛ 973line 969 didn't jump to line 973 because the condition on line 969 was always true
970 top_dict["timestamp"] = timestamp
972 # -- Packages section
973 section_dict: dict[str, Any] = {}
974 top_dict["config"] = section_dict
976 # -- Populate the config dict
977 apio_config = apio_ctx.config
978 section_dict["system"] = {}
979 section_dict["system"]["platform-id"] = apio_config.platform_id
980 section_dict["system"]["apio-home-dir"] = str(apio_ctx.apio_home_dir)
981 section_dict["system"]["apio-packages-dir"] = str(
982 apio_ctx.apio_packages_dir
983 )
985 section_dict["platform-packages"] = {}
986 for package_name, package_spec in apio_config.platform_packages.items():
987 section_dict["platform-packages"][package_name] = (
988 proto_util.proto_to_json_dict(package_spec)
989 )
991 section_dict["unused-packages"] = {}
992 for package_name, package_spec in apio_config.all_packages.items():
993 if package_name not in apio_config.platform_packages:
994 section_dict["unused-packages"][package_name] = (
995 proto_util.proto_to_json_dict(package_spec)
996 )
998 section_dict["platform-env"] = proto_util.proto_to_json_dict(
999 apio_config.platform_env
1000 )
1002 section_dict["remote-config"] = proto_util.proto_to_json_dict(
1003 apio_config.remote_config
1004 )
1006 # -- Write out
1007 write_as_json_doc(top_dict, output, force)
1010# ------ apio api scan-path
1013# -- Text in the rich-text format of the python rich library.
1014APIO_API_GET_SCAN_PATH_HELP = """
1015The command 'apio api scan-path' scans and reports anomalies related to \
1016accessing apio packages using the PATH additions.
1018The optional flag '--timestamp' allows the caller to embed in the JSON \
1019document a known timestamp that allows to verify that the JSON document \
1020was indeed was generated by the same invocation.
1022Examples:[code]
1023 apio api scan-path # Write to stdout
1024 apio api scan-path -o apio.json # Write to a file[/code]
1025"""
1028@click.command(
1029 name="scan-path",
1030 cls=ApioCommand,
1031 short_help="Scan and report available devices.",
1032 help=APIO_API_GET_SCAN_PATH_HELP,
1033)
1034@timestamp_option
1035@output_option
1036@options.force_option_gen(short_help="Overwrite output file.")
1037def _scan_path_cli(
1038 *,
1039 # Options
1040 timestamp: str,
1041 output: str,
1042 force: bool,
1043):
1044 """Implements the 'apio apio scan-path' command."""
1046 # pylint: disable=too-many-locals
1048 # -- Create the apio context. The path scan is not in a project context.
1049 apio_ctx = ApioContext(
1050 project_policy=ProjectPolicy.NO_PROJECT,
1051 remote_config_policy=RemoteConfigPolicy.CACHED_OK,
1052 packages_policy=PackagesPolicy.ENSURE_PACKAGES,
1053 )
1055 # -- The top dict that we will emit as json.
1056 top_dict: dict[str, Any] = {}
1058 # -- Append user timestamp if specified.
1059 if timestamp: 1059 ↛ 1063line 1059 didn't jump to line 1063 because the condition on line 1059 was always true
1060 top_dict["timestamp"] = timestamp
1062 # -- Get the env mutations.
1063 mutations: EnvMutations = apio_ctx.get_env_mutations_for_subprocess(
1064 include_apio_packages=True
1065 )
1067 # -- Collect file on paths, grouped by file name.
1068 files_index: dict[str, list[Path]] = {}
1069 for path_dir in mutations.add_to_path:
1070 dir_path = Path(path_dir)
1071 files_in_paths: list[Path] = [
1072 p for p in dir_path.glob("*") if p.is_file()
1073 ]
1074 for file_path in files_in_paths:
1075 files_index.setdefault(file_path.name, []).append(file_path)
1077 # -- Select conflicting and duplicate files with same name.
1078 conflicts: dict[str, list[str]] = {}
1079 duplicates: dict[str, list[str]] = {}
1080 for name, file_paths in files_index.items():
1081 # -- We report only names with more than one path.
1082 if len(file_paths) < 2: 1082 ↛ 1087line 1082 didn't jump to line 1087 because the condition on line 1082 was always true
1083 continue
1085 # -- Here we have at least two file paths, test if the files contents
1086 # -- are identical. We lazily read all into memory.
1087 first_file_data = file_paths[0].read_bytes()
1088 files_identical = all(
1089 p.read_bytes() == first_file_data for p in file_paths[1:]
1090 )
1092 # -- Append to either group.
1093 files_strs = [str(f) for f in file_paths]
1094 if files_identical:
1095 duplicates[name] = files_strs
1096 else:
1097 conflicts[name] = files_strs
1099 # -- Construct the top level dict.
1100 top_dict["apio-path-scan"] = {
1101 "path-dirs": list(mutations.add_to_path),
1102 "conflicts": conflicts,
1103 "duplicates": duplicates,
1104 }
1106 # -- Write out
1107 write_as_json_doc(top_dict, output, force)
1110# ------ apio api scan-devices
1113# -- Text in the rich-text format of the python rich library.
1114APIO_API_SCAN_DEVICES_HELP = """
1115The command 'apio api scan-devices' scans and report the available usb and \
1116serial devices.
1118The optional flag '--timestamp' allows the caller to embed in the JSON \
1119document a known timestamp that allows to verify that the JSON document \
1120was indeed was generated by the same invocation.
1122Examples:[code]
1123 apio api scan-devices # Write to stdout
1124 apio api scan-devices -o apio.json # Write to a file[/code]
1125"""
1128@click.command(
1129 name="scan-devices",
1130 cls=ApioCommand,
1131 short_help="Scan and report available devices.",
1132 help=APIO_API_SCAN_DEVICES_HELP,
1133)
1134@timestamp_option
1135@output_option
1136@options.force_option_gen(short_help="Overwrite output file.")
1137def _scan_devices_cli(
1138 *,
1139 # Options
1140 timestamp: str,
1141 output: str,
1142 force: bool,
1143):
1144 """Implements the 'apio apio scan-devices' command."""
1146 # -- For now, the information is not in a project context. That may
1147 # -- change in the future. We need the packages for the libusb operation.
1148 apio_ctx = ApioContext(
1149 project_policy=ProjectPolicy.NO_PROJECT,
1150 remote_config_policy=RemoteConfigPolicy.CACHED_OK,
1151 packages_policy=PackagesPolicy.ENSURE_PACKAGES,
1152 )
1154 # -- The top dict that we will emit as json.
1155 top_dict: dict[str, Any] = {}
1157 # -- Append user timestamp if specified.
1158 if timestamp: 1158 ↛ 1161line 1158 didn't jump to line 1161 because the condition on line 1158 was always true
1159 top_dict["timestamp"] = timestamp
1161 usb_devices: list[UsbDevice] = usb_util.scan_usb_devices(apio_ctx)
1163 # -- Scan and report usb devices.
1164 section = []
1165 for usb_device in usb_devices: 1165 ↛ 1166line 1165 didn't jump to line 1166 because the loop on line 1165 never started
1166 dev = {}
1167 dev["vid"] = usb_device.vid
1168 dev["pid"] = usb_device.pid
1169 dev["bus"] = str(usb_device.bus)
1170 dev["device"] = str(usb_device.device)
1171 dev["manufacturer"] = usb_device.manufacturer
1172 dev["product"] = usb_device.product
1173 dev["serial-number"] = usb_device.serial_number
1174 dev["device_type"] = usb_device.device_type
1176 section.append(dev)
1178 top_dict["usb-devices"] = section
1180 # -- Scan and report serial devices.
1181 serial_devices: list[SerialDevice] = serial_util.scan_serial_devices(
1182 apio_ctx
1183 )
1185 section = []
1186 for serial_device in serial_devices: 1186 ↛ 1187line 1186 didn't jump to line 1187 because the loop on line 1186 never started
1187 dev = {}
1188 dev["port"] = serial_device.port
1189 dev["port-name"] = serial_device.port_name
1190 dev["vendor-id"] = serial_device.vid
1191 dev["product-id"] = serial_device.pid
1192 dev["manufacturer"] = serial_device.manufacturer
1193 dev["product"] = serial_device.product
1194 dev["serial-number"] = serial_device.serial_number
1195 dev["device-type"] = serial_device.device_type
1197 section.append(dev)
1199 top_dict["serial-devices"] = section
1201 # -- Write out
1202 write_as_json_doc(top_dict, output, force)
1205# ------ apio api echo
1208# -- Text in the rich-text format of the python rich library.
1209APIO_API_ECHO_HELP = """
1210The command 'apio api echo' allows external programs such as the Apio VS Code \
1211extension to print a short message in a format that is consistent with \
1212that Apio theme that is currently selected in the user preferences.
1214The required option '--style' should have one of these values: OK, \
1215INFO, WARNING, ERROR, TITLE, EMPH1, EMPH2, or EMPH3. The style colors can \
1216be viewed with the command 'apio info themes'.
1218Examples:[code]
1219 apio api echo -t "Hello world", -s "INFO"
1220 apio api echo -t "Task completed successfully", -s "OK"
1221 apio api echo -t "Task failed", -s "ERROR"[/code]
1222"""
1224# -- Supported style names
1225STYLES = {
1226 "OK": SUCCESS,
1227 "INFO": INFO,
1228 "WARNING": WARNING,
1229 "ERROR": ERROR,
1230 "TITLE": TITLE,
1231 "EMPH1": EMPH1,
1232 "EMPH2": EMPH2,
1233 "EMPH3": EMPH3,
1234}
1236text_option = click.option(
1237 "text", # Var name.
1238 "-t",
1239 "--text",
1240 type=str,
1241 metavar="MESSAGE",
1242 required=True,
1243 help="Set message to echo.",
1244 cls=cmd_util.ApioOption,
1245)
1248style_option = click.option(
1249 "style", # Var name.
1250 "-s",
1251 "--style",
1252 type=click.Choice(STYLES.keys()),
1253 metavar="STYLE",
1254 required=True,
1255 help="Set style to use.",
1256 cls=cmd_util.ApioOption,
1257)
1260@click.command(
1261 name="echo",
1262 cls=ApioCommand,
1263 short_help="Print a message in given format.",
1264 help=APIO_API_ECHO_HELP,
1265)
1266@text_option
1267@style_option
1268def _echo_cli(
1269 *,
1270 # Options
1271 text: str,
1272 style: str,
1273):
1274 """Implements the 'apio apio echo command."""
1276 # -- Instanatiate Apio context, project and packages are not needed.
1277 _ = ApioContext(
1278 project_policy=ProjectPolicy.NO_PROJECT,
1279 remote_config_policy=RemoteConfigPolicy.CACHED_OK,
1280 packages_policy=PackagesPolicy.ENSURE_PACKAGES,
1281 )
1283 cout(text, style=STYLES[style])
1286# ------ apio apio
1288# -- Text in the rich-text format of the python rich library.
1289APIO_API_HELP = """
1290The command group 'apio api' contains subcommands that that are intended \
1291to be used by tools and programs such as icestudio, rather than being used \
1292directly by users.
1293"""
1295# -- We have only a single group with the title 'Subcommands'.
1296SUBGROUPS = [
1297 ApioSubgroup(
1298 "Subcommands",
1299 [
1300 _get_system_cli,
1301 _get_project_cli,
1302 _get_build_report_cli,
1303 _get_boards_cli,
1304 _get_fpgas_cli,
1305 _get_programmers_cli,
1306 _get_examples_cli,
1307 _get_commands_cli,
1308 _get_packages_cli,
1309 _get_config_cli,
1310 _scan_path_cli,
1311 _scan_devices_cli,
1312 _echo_cli,
1313 ],
1314 )
1315]
1318@click.command(
1319 name="api",
1320 cls=ApioGroup,
1321 subgroups=SUBGROUPS,
1322 short_help="Apio programmatic interface.",
1323 help=APIO_API_HELP,
1324)
1325def cli():
1326 """Implements the 'apio apio' command group."""
1328 # pass