Coverage for apio/commands/apio_api.py: 88%
250 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-06 10:20 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-06 10:20 +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 api' command"""
10import sys
11import os
12from typing import Dict, List, Self, Optional
13from dataclasses import dataclass
14import json
15from pathlib import Path
16import click
17from apio.commands import options
19# from apio.managers import packages
20from apio.managers.examples import Examples, ExampleInfo
21from apio.common.apio_console import cout, cerror
22from apio.common.apio_styles import INFO
23from apio.common.common_util import get_project_source_files
24from apio.utils import cmd_util, usb_util, serial_util, util
25from apio.utils.usb_util import UsbDevice
26from apio.utils.serial_util import SerialDevice
27from apio.apio_context import (
28 ApioContext,
29 PackagesPolicy,
30 ProjectPolicy,
31 RemoteConfigPolicy,
32)
33from apio.utils.cmd_util import (
34 ApioGroup,
35 ApioSubgroup,
36 ApioCommand,
37 ApioCmdContext,
38)
41timestamp_option = click.option(
42 "timestamp", # Var name.
43 "-t",
44 "--timestamp",
45 type=str,
46 metavar="text",
47 help="Set a user provided timestamp.",
48 cls=cmd_util.ApioOption,
49)
51output_option = click.option(
52 "output", # Var name.
53 "-o",
54 "--output",
55 type=str,
56 metavar="file-name",
57 help="Set output file.",
58 cls=cmd_util.ApioOption,
59)
62def write_as_json_doc(top_dict: Dict, output_flag: str, force_flag: bool):
63 """A common function to write a dict as a JSON doc."""
64 # -- Format the top dict as json text.
65 text = json.dumps(top_dict, indent=2)
67 if output_flag:
68 # -- Output the json text to a user specified file.
69 output_path = Path(output_flag)
71 if output_path.is_dir(): 71 ↛ 72line 71 didn't jump to line 72 because the condition on line 71 was never true
72 cerror(f"The output path {output_path} is a directory.")
73 sys.exit(1)
75 if output_path.exists() and not force_flag: 75 ↛ 76line 75 didn't jump to line 76 because the condition on line 75 was never true
76 cerror(f"The file already exists {output_path}.")
77 cout("Use the --force option to allow overwriting.", style=INFO)
78 sys.exit(1)
80 with open(output_flag, "w", encoding="utf-8") as f:
81 f.write(text)
82 else:
83 # -- Output the json text to stdout.
84 print(text, file=sys.stdout)
87# ------ apio api get-system
90# -- Text in the rich-text format of the python rich library.
91APIO_API_GET_SYSTEM_HELP = """
92The command 'apio api get-system' exports information about apio and \
93the underlying system as a JSON foc. It is similar to the command \
94'apio info system' which is intended for human consumption.
97The optional flag '--timestamp' allows the caller to embed in the JSON \
98document a known timestamp that allows to verify that the JSON document \
99was indeed was generated by the same invocation.
101Examples:[code]
102 apio api get-system # Write to stdout
103 apio api get-system -o apio.json # Write to a file[/code]
104"""
107@click.command(
108 name="get-system",
109 cls=ApioCommand,
110 short_help="Retrieve apio and system information.",
111 help=APIO_API_GET_SYSTEM_HELP,
112)
113# @click.pass_context
114@timestamp_option
115@output_option
116@options.force_option_gen(short_help="Overwrite output file.")
117def _get_system_cli(
118 # Options
119 timestamp: str,
120 output: str,
121 force: bool,
122):
123 """Implements the 'apio apio get-system' command."""
125 apio_ctx = ApioContext(
126 project_policy=ProjectPolicy.NO_PROJECT,
127 remote_config_policy=RemoteConfigPolicy.CACHED_OK,
128 packages_policy=PackagesPolicy.ENSURE_PACKAGES,
129 )
131 # -- The top dict that we will emit as json.
132 top_dict = {}
134 # -- Append user timestamp if specified.
135 if timestamp: 135 ↛ 138line 135 didn't jump to line 138 because the condition on line 135 was always true
136 top_dict["timestamp"] = timestamp
138 section_dict = {}
140 # -- Add fields.
141 section_dict["apio-version"] = util.get_apio_version()
142 section_dict["python-version"] = util.get_python_version()
143 section_dict["platform-id"] = apio_ctx.platform_id
144 section_dict["apio-python_package"] = str(
145 util.get_path_in_apio_package("")
146 )
147 section_dict["apio-home-dir"] = str(apio_ctx.apio_home_dir)
148 section_dict["apio-packages-dir"] = str(apio_ctx.apio_packages_dir)
149 section_dict["remote-config-url"] = apio_ctx.profile.remote_config_url
150 section_dict["verible-formatter"] = str(
151 apio_ctx.apio_packages_dir / "verible/bin/verible-verilog-format"
152 )
153 section_dict["verible-language-server"] = str(
154 apio_ctx.apio_packages_dir / "verible/bin/verible-verilog-ls"
155 )
157 # -- Add section
158 top_dict["system"] = section_dict
160 # -- Write out
161 write_as_json_doc(top_dict, output, force)
164# ------ apio api get-project
167# -- Text in the rich-text format of the python rich library.
168APIO_API_GET_PROJECT_HELP = """
169The command 'apio api get-project' exports information about an Apio
170project as a JSON foc.
172The optional flag '--timestamp' allows the caller to embed in the JSON \
173document a known timestamp that allows to verify that the JSON document \
174was indeed was generated by the same invocation.
176Examples:[code]
177 apio api get-project # Report default env
178 apio api get-project -e env1 # Report specified env
179 apio api get-project -p foo/bar # Project in another dir
180 apio api get-project -o apio.json # Write to a file[/code]
181"""
184@click.command(
185 name="get-project",
186 cls=ApioCommand,
187 short_help="Get project information.",
188 help=APIO_API_GET_PROJECT_HELP,
189)
190# @click.pass_context
191@options.env_option_gen()
192@options.project_dir_option
193@timestamp_option
194@output_option
195@options.force_option_gen(short_help="Overwrite output file.")
196def _get_project_cli(
197 # Options
198 env: str,
199 project_dir: Optional[Path],
200 timestamp: str,
201 output: str,
202 force: bool,
203):
204 """Implements the 'apio apio get-project' command."""
206 apio_ctx = ApioContext(
207 project_policy=ProjectPolicy.PROJECT_REQUIRED,
208 remote_config_policy=RemoteConfigPolicy.CACHED_OK,
209 packages_policy=PackagesPolicy.ENSURE_PACKAGES,
210 project_dir_arg=project_dir,
211 env_arg=env,
212 )
214 # -- Change to the project's folder.
215 os.chdir(apio_ctx.project_dir)
217 # -- The top dict that we will emit as json.
218 top_dict = {}
220 # -- Append user timestamp if specified.
221 if timestamp: 221 ↛ 224line 221 didn't jump to line 224 because the condition on line 221 was always true
222 top_dict["timestamp"] = timestamp
224 section_dict = {}
226 active_env_dict = {}
227 active_env_dict["name"] = apio_ctx.project.env_name
228 active_env_dict["options"] = apio_ctx.project.env_options
229 section_dict["active-env"] = active_env_dict
231 section_dict["envs"] = apio_ctx.project.env_names
233 synth_srcs, test_srcs = get_project_source_files()
234 section_dict["synth-files"] = synth_srcs
235 section_dict["test-benches"] = test_srcs
237 # -- Add section
238 top_dict["project"] = section_dict
240 # -- Write out
241 write_as_json_doc(top_dict, output, force)
244# ------ apio api get-boards
247# -- Text in the rich-text format of the python rich library.
248APIO_API_GET_BOARDS_HELP = """
249The command 'apio api get-boards' exports apio boards information as a \
250JSON document.
252The optional flag '--timestamp' allows the caller to embed in the JSON \
253document a known timestamp that allows to verify that the JSON document \
254was indeed was generated by the same invocation.
256Examples:[code]
257 apio api get-boards # Write to stdout
258 apio api get-boards -o apio.json # Write to a file[/code]
259"""
262@click.command(
263 name="get-boards",
264 cls=ApioCommand,
265 short_help="Retrieve boards information.",
266 help=APIO_API_GET_BOARDS_HELP,
267)
268@timestamp_option
269@output_option
270@options.force_option_gen(short_help="Overwrite output file.")
271def _get_boards_cli(
272 # Options
273 timestamp: str,
274 output: str,
275 force: bool,
276):
277 """Implements the 'apio apio get-boards' command."""
279 # -- For now, the information is not in a project context. That may
280 # -- change in the future.
281 apio_ctx = ApioContext(
282 project_policy=ProjectPolicy.NO_PROJECT,
283 remote_config_policy=RemoteConfigPolicy.CACHED_OK,
284 packages_policy=PackagesPolicy.ENSURE_PACKAGES,
285 )
287 # -- The top dict that we will emit as json.
288 top_dict = {}
290 # -- Append user timestamp if specified.
291 if timestamp: 291 ↛ 295line 291 didn't jump to line 295 because the condition on line 291 was always true
292 top_dict["timestamp"] = timestamp
294 # -- Generate the boards section.
295 section = {}
296 for board_id, board_info in apio_ctx.boards.items():
297 # -- The board output dict.
298 board_dict = {}
300 # -- Add board description
301 board_dict["description"] = board_info.get("description", None)
303 # -- Add board's fpga information.
304 fpga_dict = {}
305 fpga_id = board_info.get("fpga-id", None)
306 fpga_info = apio_ctx.fpgas.get(fpga_id, {})
307 fpga_dict["id"] = fpga_id
308 fpga_dict["part-num"] = fpga_info.get("part-num", None)
309 fpga_dict["arch"] = fpga_info.get("arch", None)
310 fpga_dict["size"] = fpga_info.get("size", None)
311 board_dict["fpga"] = fpga_dict
313 # -- Add board's programmer information.
314 programmer_dict = {}
315 programmer_id = board_info.get("programmer", {}).get("id", None)
316 programmer_dict["id"] = programmer_id
317 board_dict["programmer"] = programmer_dict
319 # -- Add the board to the boards dict.
320 section[board_id] = board_dict
322 top_dict["boards"] = section
324 # -- Write out
325 write_as_json_doc(top_dict, output, force)
328# ------ apio api get-fpgas
331# -- Text in the rich-text format of the python rich library.
332APIO_API_GET_FPGAS_HELP = """
333The command 'apio api get-fpgas' exports apio FPGAss information as a \
334JSON document.
336The optional flag '--timestamp' allows the caller to embed in the JSON \
337document a known timestamp that allows to verify that the JSON document \
338was indeed was generated by the same invocation.
340Examples:[code]
341 apio api get-fpgas # Write to stdout
342 apio api get-fpgas -o apio.json # Write to a file[/code]
343"""
346@click.command(
347 name="get-fpgas",
348 cls=ApioCommand,
349 short_help="Retrieve FPGAs information.",
350 help=APIO_API_GET_FPGAS_HELP,
351)
352@timestamp_option
353@output_option
354@options.force_option_gen(short_help="Overwrite output file.")
355def _get_fpgas_cli(
356 # Options
357 timestamp: str,
358 output: str,
359 force: bool,
360):
361 """Implements the 'apio apio get-fpgas' command."""
363 # -- For now, the information is not in a project context. That may
364 # -- change in the future.
365 apio_ctx = ApioContext(
366 project_policy=ProjectPolicy.NO_PROJECT,
367 remote_config_policy=RemoteConfigPolicy.CACHED_OK,
368 packages_policy=PackagesPolicy.ENSURE_PACKAGES,
369 )
371 # -- The top dict that we will emit as json.
372 top_dict = {}
374 # -- Append user timestamp if specified.
375 if timestamp: 375 ↛ 379line 375 didn't jump to line 379 because the condition on line 375 was always true
376 top_dict["timestamp"] = timestamp
378 # -- Generate the fpgas section
379 section = {}
380 for fpga_id, fpga_info in apio_ctx.fpgas.items():
381 # -- The fpga output dict.
382 fpga_dict = {}
384 fpga_dict["part-num"] = fpga_info.get("part-num", None)
385 fpga_dict["arch"] = fpga_info.get("arch", None)
386 fpga_dict["size"] = fpga_info.get("size", None)
388 # -- Add the fpga to the fpgas dict.
389 section[fpga_id] = fpga_dict
391 top_dict["fpgas"] = section
393 # -- Write out
394 write_as_json_doc(top_dict, output, force)
397# ------ apio api get-programmers
400# -- Text in the rich-text format of the python rich library.
401APIO_API_GET_PROGRAMMERS_HELP = """
402The command 'apio api get-programmers' exports apio programmers information \
403as a JSON document.
405The optional flag '--timestamp' allows the caller to embed in the JSON \
406document a known timestamp that allows to verify that the JSON document \
407was indeed was generated by the same invocation.
409Examples:[code]
410 apio api get-programmers # Write to stdout
411 apio api get-programmers -o apio.json # Write to a file[/code]
412"""
415@click.command(
416 name="get-programmers",
417 cls=ApioCommand,
418 short_help="Retrieve programmers information.",
419 help=APIO_API_GET_PROGRAMMERS_HELP,
420)
421@timestamp_option
422@output_option
423@options.force_option_gen(short_help="Overwrite output file.")
424def _get_programmers_cli(
425 # Options
426 timestamp: str,
427 output: str,
428 force: bool,
429):
430 """Implements the 'apio apio get-programmers' command."""
432 # -- For now, the information is not in a project context. That may
433 # -- change in the future.
434 apio_ctx = ApioContext(
435 project_policy=ProjectPolicy.NO_PROJECT,
436 remote_config_policy=RemoteConfigPolicy.CACHED_OK,
437 packages_policy=PackagesPolicy.ENSURE_PACKAGES,
438 )
440 # -- The top dict that we will emit as json.
441 top_dict = {}
443 # -- Append user timestamp if specified.
444 if timestamp: 444 ↛ 448line 444 didn't jump to line 448 because the condition on line 444 was always true
445 top_dict["timestamp"] = timestamp
447 # -- Generate the 'programmers' section.
448 section = {}
449 for programmer_id, programmer_info in apio_ctx.programmers.items():
450 section[programmer_id] = programmer_info
452 top_dict["programmers"] = section
454 # -- Write out
455 write_as_json_doc(top_dict, output, force)
458# ------ apio api get-examples
461# -- Text in the rich-text format of the python rich library.
462APIO_API_GET_EXAMPLES_HELP = """
463The command 'apio api get-examples' exports apio examples information as a \
464JSON document.
466The optional flag '--timestamp' allows the caller to embed in the JSON \
467document a known timestamp that allows to verify that the JSON document \
468was indeed was generated by the same invocation.
470Examples:[code]
471 apio api get-examples # Write to stdout
472 apio api get-examples -o apio.json # Write to a file[/code]
473"""
476@click.command(
477 name="get-examples",
478 cls=ApioCommand,
479 short_help="Retrieve examples information.",
480 help=APIO_API_GET_EXAMPLES_HELP,
481)
482@timestamp_option
483@output_option
484@options.force_option_gen(short_help="Overwrite output file.")
485def _get_examples_cli(
486 # Options
487 timestamp: str,
488 output: str,
489 force: bool,
490):
491 """Implements the 'apio apio get-examples' command."""
493 # -- For now, the information is not in a project context. That may
494 # -- change in the future.
495 apio_ctx = ApioContext(
496 project_policy=ProjectPolicy.NO_PROJECT,
497 remote_config_policy=RemoteConfigPolicy.CACHED_OK,
498 packages_policy=PackagesPolicy.ENSURE_PACKAGES,
499 )
501 # -- Get examples infos.
502 examples: List[ExampleInfo] = Examples(apio_ctx).get_examples_infos()
504 # -- Group examples by boards
505 boards_examples: Dict[str, List[ExampleInfo]] = {}
506 for example in examples:
507 board_examples = boards_examples.get(example.board_id, [])
508 board_examples.append(example)
509 boards_examples[example.board_id] = board_examples
511 # -- The top dict that we will emit as json.
512 top_dict = {}
514 # -- Append user timestamp if specified.
515 if timestamp: 515 ↛ 519line 515 didn't jump to line 519 because the condition on line 515 was always true
516 top_dict["timestamp"] = timestamp
518 # -- Generate the 'examples' section.
519 section = {}
520 for board, board_examples in boards_examples.items():
521 board_dict = {}
522 # -- Generate board examples
523 for example_info in board_examples:
524 example_dict = {}
525 example_dict["description"] = example_info.description
526 board_dict[example_info.example_name] = example_dict
528 section[board] = board_dict
530 top_dict["examples"] = section
532 # -- Write out
533 write_as_json_doc(top_dict, output, force)
536# ------ apio api get-commands
539@dataclass(frozen=True)
540class CmdInfo:
541 """Represents the information of a single apio command."""
543 name: str
544 path: List[str]
545 cli: click.Command
546 children: List[Self]
549def scan_children(cmd_cli) -> Dict:
550 """Return a dict describing this command subtree."""
551 result = {}
553 # -- Sanity check
554 assert isinstance(result, dict), type(result)
556 # -- If this is a simple command, it has no sub commands.
557 if isinstance(cmd_cli, ApioCommand):
558 return result
560 # -- Here we have a group and it should have at least one sub command.
561 assert isinstance(cmd_cli, ApioGroup), type(cmd_cli)
562 subgroups: List[ApioSubgroup] = cmd_cli.subgroups
564 # -- Create the dict for the command subgroups.
565 subcommands_dict = {}
566 result["commands"] = subcommands_dict
568 # -- Iterate the subgroups and populate them. We flaten the subcommands
569 # -- group into a single list of commands.
570 for subgroup in subgroups:
571 assert isinstance(subgroup, ApioSubgroup), type(subgroup)
572 assert isinstance(subgroup.title, str), type(subgroup.title)
573 for subcommand in subgroup.commands:
574 subcommand_dict = scan_children(subcommand)
575 subcommands_dict[subcommand.name] = subcommand_dict
577 # -- All done ok.
578 return result
581# -- Text in the rich-text format of the python rich library.
582APIO_API_GET_COMMANDS_HELP = """
583The command 'apio api get-commands' exports apio command structure \
584of Apio as a JSON doc. This is used by various tools such as
585documentation generators and tests.
587The optional flag '--timestamp' allows the caller to embed in the JSON \
588document a known timestamp that allows to verify that the JSON document \
589was indeed was generated by the same invocation.
591Examples:[code]
592 apio api get-commands # Write to stdout
593 apio api get-commands -o apio.json # Write to a file[/code]
594"""
597@click.command(
598 name="get-commands",
599 cls=ApioCommand,
600 short_help="Retrieve apio commands information.",
601 help=APIO_API_GET_COMMANDS_HELP,
602)
603@click.pass_context
604@timestamp_option
605@output_option
606@options.force_option_gen(short_help="Overwrite output file.")
607def _get_commands_cli(
608 # Click context
609 cmd_ctx: ApioCmdContext,
610 # Options
611 timestamp: str,
612 output: str,
613 force: bool,
614):
615 """Implements the 'apio apio get-commands' command."""
617 # -- Find the top cli which is the "apio" command. Would access it
618 # -- directly but it would create a circular python import.
619 ctx = cmd_ctx
620 while ctx.parent:
621 ctx = ctx.parent
622 assert isinstance(ctx, ApioCmdContext), type(ctx)
623 top_cli = ctx.command
624 assert top_cli.name == "apio", top_cli
626 # -- This initializes the console, print active env vars, etc.
627 ApioContext(
628 project_policy=ProjectPolicy.NO_PROJECT,
629 remote_config_policy=RemoteConfigPolicy.CACHED_OK,
630 packages_policy=PackagesPolicy.ENSURE_PACKAGES,
631 )
633 # -- The top dict that we will emit as json.
634 top_dict = {}
636 # -- Append user timestamp if specified.
637 if timestamp: 637 ↛ 640line 637 didn't jump to line 640 because the condition on line 637 was always true
638 top_dict["timestamp"] = timestamp
640 section_dict = {}
641 section_dict["apio"] = scan_children(top_cli)
642 top_dict["commands"] = section_dict
644 # -- Write out
645 write_as_json_doc(top_dict, output, force)
648# ------ apio api scan-devices
651# -- Text in the rich-text format of the python rich library.
652APIO_API_SCAN_DEVICES_HELP = """
653The command 'apio api scan-devices' scans and report the available usb and \
654serial devices.
656The optional flag '--timestamp' allows the caller to embed in the JSON \
657document a known timestamp that allows to verify that the JSON document \
658was indeed was generated by the same invocation.
660Examples:[code]
661 apio api scan-devices # Write to stdout
662 apio api scan-devices -o apio.json # Write to a file[/code]
663"""
666@click.command(
667 name="scan-devices",
668 cls=ApioCommand,
669 short_help="Scan and report available devices.",
670 help=APIO_API_SCAN_DEVICES_HELP,
671)
672@timestamp_option
673@output_option
674@options.force_option_gen(short_help="Overwrite output file.")
675def _scan_devices_cli(
676 # Options
677 timestamp: str,
678 output: str,
679 force: bool,
680):
681 """Implements the 'apio apio scan-devices' command."""
683 # -- For now, the information is not in a project context. That may
684 # -- change in the future. We need the config since we use libusb from
685 # -- the packages.
686 apio_ctx = ApioContext(
687 project_policy=ProjectPolicy.NO_PROJECT,
688 remote_config_policy=RemoteConfigPolicy.CACHED_OK,
689 packages_policy=PackagesPolicy.ENSURE_PACKAGES,
690 )
692 # -- The top dict that we will emit as json.
693 top_dict = {}
695 # -- Append user timestamp if specified.
696 if timestamp: 696 ↛ 702line 696 didn't jump to line 702 because the condition on line 696 was always true
697 top_dict["timestamp"] = timestamp
699 # -- We need the packages for the 'libusb' backend.
700 # packages.install_missing_packages_on_the_fly(apio_ctx.packages_context)
702 usb_devices: List[UsbDevice] = usb_util.scan_usb_devices(apio_ctx)
704 # -- Scan and report usb devices.
705 section = []
706 for device in usb_devices: 706 ↛ 707line 706 didn't jump to line 707 because the loop on line 706 never started
707 dev = {}
708 dev["vid"] = device.vendor_id
709 dev["pid"] = device.product_id
710 dev["bus"] = device.bus
711 dev["device"] = device.device
712 dev["manufacturer"] = device.manufacturer
713 dev["product"] = device.product
714 dev["serial-number"] = device.serial_number
715 dev["device_type"] = device.device_type
717 section.append(dev)
719 top_dict["usb-devices"] = section
721 # -- Scan and report serial devices.
722 serial_devices: List[SerialDevice] = serial_util.scan_serial_devices()
724 section = []
725 for device in serial_devices: 725 ↛ 726line 725 didn't jump to line 726 because the loop on line 725 never started
726 dev = {}
727 dev["port"] = device.port
728 dev["port-name"] = device.port_name
729 dev["vendor-id"] = device.vendor_id
730 dev["product-id"] = device.product_id
731 dev["manufacturer"] = device.manufacturer
732 dev["product"] = device.product
733 dev["serial-number"] = device.serial_number
734 dev["device-type"] = device.device_type
736 section.append(dev)
738 top_dict["serial-devices"] = section
740 # -- Write out
741 write_as_json_doc(top_dict, output, force)
744# ------ apio apio
746# -- Text in the rich-text format of the python rich library.
747APIO_API_HELP = """
748The command group 'apio api' contains subcommands that that are intended \
749to be used by tools and programs such as icestudio, rather than being used \
750directly by users.
751"""
753# -- We have only a single group with the title 'Subcommands'.
754SUBGROUPS = [
755 ApioSubgroup(
756 "Subcommands",
757 [
758 _get_system_cli,
759 _get_project_cli,
760 _get_boards_cli,
761 _get_fpgas_cli,
762 _get_programmers_cli,
763 _get_examples_cli,
764 _get_commands_cli,
765 _scan_devices_cli,
766 ],
767 )
768]
771@click.command(
772 name="api",
773 cls=ApioGroup,
774 subgroups=SUBGROUPS,
775 short_help="Apio programmatic interface.",
776 help=APIO_API_HELP,
777)
778def cli():
779 """Implements the 'apio apio' command group."""
781 # pass