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

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 

8 

9"""Implementation of 'apio api' command""" 

10 

11# pylint: disable=too-many-lines 

12 

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 

59 

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) 

69 

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) 

79 

80 

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) 

85 

86 if output_flag: 

87 # -- Output the json text to a user specified file. 

88 output_path = Path(output_flag) 

89 

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.") 

92 

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 ) 

98 

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) 

105 

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) 

112 

113 

114# ------ apio api get-system 

115 

116 

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. 

122 

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. 

126 

127Examples:[code] 

128 apio api get-system # Write to stdout 

129 apio api get-system -o apio.json # Write to a file[/code] 

130""" 

131 

132 

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.""" 

151 

152 apio_ctx = ApioContext( 

153 project_policy=ProjectPolicy.NO_PROJECT, 

154 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

155 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

156 ) 

157 

158 platform = apio_ctx.platform 

159 

160 # -- The top dict that we will emit as json. 

161 top_dict: dict[str, Any] = {} 

162 

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 

166 

167 section_dict: dict[str, Any] = {} 

168 

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 ) 

204 

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 

210 

211 # -- Add section 

212 top_dict["system"] = section_dict 

213 

214 # -- Write out 

215 write_as_json_doc(top_dict, output, force) 

216 

217 

218# ------ apio api get-build-report 

219 

220 

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. 

227 

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. 

231 

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""" 

238 

239 

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.""" 

262 

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 ) 

271 

272 # -- Change to the project's folder. 

273 os.chdir(apio_ctx.project_dir) 

274 

275 # -- The build process generates this report file. 

276 pnr_json_file = apio_ctx.env_build_path / "hardware.pnr" 

277 

278 # -- Read the report 

279 report = build_report.read_build_report(pnr_json_file) 

280 

281 # -- The top dict that we will emit as json. 

282 top_dict: dict[str, Any] = {} 

283 

284 # -- Append user timestamp if specified. 

285 if timestamp: 

286 top_dict["timestamp"] = timestamp 

287 

288 section_dict: dict[str, Any] = {} 

289 section_dict["arch"] = ApioArch.Name(report.arch) 

290 section_dict["env"] = apio_ctx.project.env_name 

291 

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 } 

299 

300 section_dict["resources"] = resources_dict 

301 

302 clocks_dict: dict[str, Any] = {} 

303 for clk in report.clocks: 

304 clocks_dict[clk.name] = {"fmax_mhz": clk.fmax_mhz} 

305 

306 section_dict["clocks"] = clocks_dict 

307 

308 # -- Add section 

309 top_dict["build-report"] = section_dict 

310 

311 # -- Write out 

312 write_as_json_doc(top_dict, output, force) 

313 

314 

315# ------ apio api get-project 

316 

317 

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. 

322 

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. 

326 

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""" 

333 

334 

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.""" 

357 

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 

367 

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) 

371 

372 # -- The top dict that we will emit as json. 

373 top_dict: dict[str, Any] = {} 

374 

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 

378 

379 section_dict: dict[str, Any] = {} 

380 

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 

385 

386 section_dict["envs"] = apio_ctx.project.env_names 

387 

388 synth_srcs, test_srcs = get_project_source_files() 

389 section_dict["synth-files"] = synth_srcs 

390 section_dict["test-benches"] = test_srcs 

391 

392 pr = apio_ctx.project_resources 

393 

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 

402 

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 

409 

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 

420 

421 # -- Add section 

422 top_dict["project"] = section_dict 

423 

424 # -- Write out 

425 write_as_json_doc(top_dict, output, force) 

426 

427 

428# ------ apio api get-boards 

429 

430 

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. 

436 

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. 

440 

441Examples:[code] 

442 apio api get-boards # Write to stdout 

443 apio api get-boards -o apio.json # Write to a file[/code] 

444""" 

445 

446 

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.""" 

464 

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 

473 

474 # -- The top dict that we will emit as json. 

475 top_dict: dict[str, Any] = {} 

476 

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 

480 

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] = {} 

486 

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 ) 

496 

497 # -- Add board description 

498 board_dict["description"] = board_definition.description 

499 

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 } 

507 

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 } 

515 

516 # -- Add the board to the boards dict. 

517 section[board_id] = board_dict 

518 

519 top_dict["boards"] = section 

520 

521 # -- Write out 

522 write_as_json_doc(top_dict, output, force) 

523 

524 

525# ------ apio api get-fpgas 

526 

527 

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. 

533 

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. 

537 

538Examples:[code] 

539 apio api get-fpgas # Write to stdout 

540 apio api get-fpgas -o apio.json # Write to a file[/code] 

541""" 

542 

543 

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.""" 

561 

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 

570 

571 # -- The top dict that we will emit as json. 

572 top_dict: dict[str, Any] = {} 

573 

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 

577 

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) 

582 

583 top_dict["fpgas"] = section 

584 

585 # -- Write out 

586 write_as_json_doc(top_dict, output, force) 

587 

588 

589# ------ apio api get-programmers 

590 

591 

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. 

597 

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. 

601 

602Examples:[code] 

603 apio api get-programmers # Write to stdout 

604 apio api get-programmers -o apio.json # Write to a file[/code] 

605""" 

606 

607 

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.""" 

625 

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 

634 

635 # -- The top dict that we will emit as json. 

636 top_dict: dict[str, Any] = {} 

637 

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 

641 

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 ) 

651 

652 top_dict["programmers"] = section 

653 

654 # -- Write out 

655 write_as_json_doc(top_dict, output, force) 

656 

657 

658# ------ apio api get-examples 

659 

660 

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. 

665 

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. 

669 

670Examples:[code] 

671 apio api get-examples # Write to stdout 

672 apio api get-examples -o apio.json # Write to a file[/code] 

673""" 

674 

675 

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.""" 

693 

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 ) 

701 

702 # -- Get examples infos. 

703 examples: list[ExampleInfo] = Examples(apio_ctx).get_examples_infos() 

704 

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 

711 

712 # -- The top dict that we will emit as json. 

713 top_dict: dict[str, Any] = {} 

714 

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 

718 

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 

728 

729 section[board] = board_dict 

730 

731 top_dict["examples"] = section 

732 

733 # -- Write out 

734 write_as_json_doc(top_dict, output, force) 

735 

736 

737# ------ apio api get-commands 

738 

739 

740@dataclass(frozen=True) 

741class CmdInfo: 

742 """Represents the information of a single apio command.""" 

743 

744 name: str 

745 path: list[str] 

746 cli: click.Command 

747 children: list[Self] 

748 

749 

750def scan_children(cmd_cli) -> dict: 

751 """Return a dict describing this command subtree.""" 

752 result: dict[str, Any] = {} 

753 

754 # -- Sanity check 

755 assert isinstance(result, dict), type(result) 

756 

757 # -- If this is a simple command, it has no sub commands. 

758 if isinstance(cmd_cli, ApioCommand): 

759 return result 

760 

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 

764 

765 # -- Create the dict for the command subgroups. 

766 subcommands_dict: dict[str, Any] = {} 

767 result["commands"] = subcommands_dict 

768 

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 

778 

779 # -- All done ok. 

780 return result 

781 

782 

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. 

788 

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. 

792 

793Examples:[code] 

794 apio api get-commands # Write to stdout 

795 apio api get-commands -o apio.json # Write to a file[/code] 

796""" 

797 

798 

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.""" 

819 

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 

828 

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 ) 

835 

836 # -- The top dict that we will emit as json. 

837 top_dict: dict[str, Any] = {} 

838 

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 

842 

843 section_dict: dict[str, Any] = {} 

844 section_dict["apio"] = scan_children(top_cli) 

845 top_dict["commands"] = section_dict 

846 

847 # -- Write out 

848 write_as_json_doc(top_dict, output, force) 

849 

850 

851# ------ apio api get-packages 

852 

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. 

859 

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. 

863 

864Examples:[code] 

865 apio api get-packages # Write to stdout 

866 apio api get-packages -o apio.json # Write to a file[/code] 

867""" 

868 

869 

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.""" 

888 

889 apio_ctx = ApioContext( 

890 project_policy=ProjectPolicy.NO_PROJECT, 

891 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

892 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

893 ) 

894 

895 package_manager = apio_ctx.package_manager 

896 

897 # -- The top dict that we will emit as json. 

898 top_dict: dict[str, Any] = {} 

899 

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 

903 

904 # -- Packages section 

905 section_dict: dict[str, Any] = {} 

906 top_dict["packages"] = section_dict 

907 

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 } 

917 

918 # -- Write out 

919 write_as_json_doc(top_dict, output, force) 

920 

921 

922# ------ apio api get-config 

923 

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. 

929 

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. 

933 

934Examples:[code] 

935 apio api get-config # Write to stdout 

936 apio api get-config -o apio.json # Write to a file[/code] 

937""" 

938 

939 

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.""" 

958 

959 apio_ctx = ApioContext( 

960 project_policy=ProjectPolicy.NO_PROJECT, 

961 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

962 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

963 ) 

964 

965 # -- The top dict that we will emit as json. 

966 top_dict: dict[str, Any] = {} 

967 

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 

971 

972 # -- Packages section 

973 section_dict: dict[str, Any] = {} 

974 top_dict["config"] = section_dict 

975 

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 ) 

984 

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 ) 

990 

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 ) 

997 

998 section_dict["platform-env"] = proto_util.proto_to_json_dict( 

999 apio_config.platform_env 

1000 ) 

1001 

1002 section_dict["remote-config"] = proto_util.proto_to_json_dict( 

1003 apio_config.remote_config 

1004 ) 

1005 

1006 # -- Write out 

1007 write_as_json_doc(top_dict, output, force) 

1008 

1009 

1010# ------ apio api scan-path 

1011 

1012 

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. 

1017 

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. 

1021 

1022Examples:[code] 

1023 apio api scan-path # Write to stdout 

1024 apio api scan-path -o apio.json # Write to a file[/code] 

1025""" 

1026 

1027 

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.""" 

1045 

1046 # pylint: disable=too-many-locals 

1047 

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 ) 

1054 

1055 # -- The top dict that we will emit as json. 

1056 top_dict: dict[str, Any] = {} 

1057 

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 

1061 

1062 # -- Get the env mutations. 

1063 mutations: EnvMutations = apio_ctx.get_env_mutations_for_subprocess( 

1064 include_apio_packages=True 

1065 ) 

1066 

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) 

1076 

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 

1084 

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 ) 

1091 

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 

1098 

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 } 

1105 

1106 # -- Write out 

1107 write_as_json_doc(top_dict, output, force) 

1108 

1109 

1110# ------ apio api scan-devices 

1111 

1112 

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. 

1117 

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. 

1121 

1122Examples:[code] 

1123 apio api scan-devices # Write to stdout 

1124 apio api scan-devices -o apio.json # Write to a file[/code] 

1125""" 

1126 

1127 

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.""" 

1145 

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 ) 

1153 

1154 # -- The top dict that we will emit as json. 

1155 top_dict: dict[str, Any] = {} 

1156 

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 

1160 

1161 usb_devices: list[UsbDevice] = usb_util.scan_usb_devices(apio_ctx) 

1162 

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 

1175 

1176 section.append(dev) 

1177 

1178 top_dict["usb-devices"] = section 

1179 

1180 # -- Scan and report serial devices. 

1181 serial_devices: list[SerialDevice] = serial_util.scan_serial_devices( 

1182 apio_ctx 

1183 ) 

1184 

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 

1196 

1197 section.append(dev) 

1198 

1199 top_dict["serial-devices"] = section 

1200 

1201 # -- Write out 

1202 write_as_json_doc(top_dict, output, force) 

1203 

1204 

1205# ------ apio api echo 

1206 

1207 

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. 

1213 

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'. 

1217 

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""" 

1223 

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} 

1235 

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) 

1246 

1247 

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) 

1258 

1259 

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.""" 

1275 

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 ) 

1282 

1283 cout(text, style=STYLES[style]) 

1284 

1285 

1286# ------ apio apio 

1287 

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""" 

1294 

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] 

1316 

1317 

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.""" 

1327 

1328 # pass