Coverage for apio / scons / plugin_util.py: 83%

272 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2025-12-24 01:53 +0000

1# -*- coding: utf-8 -*- 

2# -- This file is part of the Apio project 

3# -- (C) 2016-2018 FPGAwars 

4# -- Author Jesús Arroyo 

5# -- License GPLv2 

6# -- Derived from: 

7# ---- Platformio project 

8# ---- (C) 2014-2016 Ivan Kravets <me@ikravets.com> 

9# ---- License Apache v2 

10"""Helper functions for apio scons plugins.""" 

11 

12 

13from glob import glob 

14import sys 

15import os 

16import re 

17import json 

18import subprocess 

19from dataclasses import dataclass 

20from pathlib import Path 

21from typing import List, Dict, Optional, Union, Callable 

22from rich.table import Table 

23from rich import box 

24from SCons import Scanner 

25from SCons.Builder import Builder 

26from SCons.Action import FunctionAction, Action 

27from SCons.Node.FS import File 

28from SCons.Script.SConscript import SConsEnvironment 

29from SCons.Node import NodeList 

30from SCons.Node.Alias import Alias 

31from apio.scons.apio_env import ApioEnv 

32from apio.common.proto.apio_pb2 import SimParams 

33from apio.common.common_util import ( 

34 PROJECT_BUILD_PATH, 

35 has_testbench_name, 

36 is_source_file, 

37) 

38from apio.common.apio_console import cout, cerror, cwarning, ctable 

39from apio.common.apio_styles import INFO, BORDER, EMPH1, EMPH2, EMPH3 

40 

41 

42TESTBENCH_HINT = "Testbench file names must end with '_tb.v' or '_tb.sv'." 

43 

44 

45def map_params(params: Optional[List[Union[str, Path]]], fmt: str) -> str: 

46 """A common function construct a command string snippet from a list 

47 of arguments. The functon does the following: 

48 1. If params arg is None replace it with [] 

49 2. Drops empty or white space only items. 

50 3. Maps the items using the format string which contains exactly one 

51 placeholder {}. 

52 4. Joins the items with a white space char. 

53 

54 For examples, see the unit test at test_scons_util.py. 

55 """ 

56 # None designates empty list. Avoiding the pylint non safe default 

57 # warning. 

58 if params is None: 

59 params = [] 

60 

61 # Convert parmas to stripped strings. 

62 params = [str(x).strip() for x in params] 

63 

64 # Drop the empty params and map the rest. 

65 mapped_params = [fmt.format(x) for x in params if x] 

66 

67 # Join using a single space. 

68 return " ".join(mapped_params) 

69 

70 

71def get_constraint_file(apio_env: ApioEnv, file_ext: str) -> str: 

72 """Returns the name of the constraint file to use. 

73 

74 env is the sconstruction environment. 

75 

76 file_ext is a string with the constrained file extension. 

77 E.g. ".pcf" for ice40. 

78 

79 Returns the file name if found or exit with an error otherwise. 

80 """ 

81 

82 # -- If the user specified a 'constraint-file' in apio.ini then use it. 

83 user_specified = apio_env.params.apio_env_params.constraint_file 

84 

85 if user_specified: 

86 path = Path(user_specified) 

87 # -- Path should be relative. 

88 if path.is_absolute(): 88 ↛ 89line 88 didn't jump to line 89 because the condition on line 88 was never true

89 cerror(f"Constraint file path is not relative: {user_specified}") 

90 sys.exit(1) 

91 # -- Constrain file extension should match the architecture. 

92 if path.suffix != file_ext: 

93 cerror( 

94 f"Constraint file should have the extension '{file_ext}': " 

95 f"{user_specified}." 

96 ) 

97 sys.exit(1) 

98 # -- File should not be under _build 

99 if PROJECT_BUILD_PATH in path.parents: 

100 cerror( 

101 f"Constraint file should not be under {PROJECT_BUILD_PATH}: " 

102 f"{user_specified}." 

103 ) 

104 sys.exit(1) 

105 # -- Path should not contain '..' to avoid traveling outside of the 

106 # -- project and coming back. 

107 for part in path.parts: 

108 if part == "..": 

109 cerror( 

110 f"Constraint file path should not contain '..': " 

111 f"{user_specified}." 

112 ) 

113 sys.exit(1) 

114 

115 # -- Constrain file looks good. 

116 return user_specified 

117 

118 # -- No user specified constraint file, we will try to look for it 

119 # -- in the project tree. 

120 glob_files: List[str] = glob(f"**/*{file_ext}", recursive=True) 

121 

122 # -- Exclude files that are under _build 

123 filtered_files: List[str] = [ 

124 f for f in glob_files if PROJECT_BUILD_PATH not in Path(f).parents 

125 ] 

126 

127 # -- Handle by file count. 

128 n = len(filtered_files) 

129 

130 # -- Case 1: No matching constrain files. 

131 if n == 0: 

132 cerror(f"No constraint file '*{file_ext}' found.") 

133 sys.exit(1) 

134 

135 # -- Case 2: Exactly one constrain file found. 

136 if n == 1: 

137 result = str(filtered_files[0]) 

138 return result 

139 

140 # -- Case 3: Multiple matching constrain files. 

141 cerror( 

142 f"Found {n} constraint files '*{file_ext}' " 

143 "in the project tree, which one to use?" 

144 ) 

145 cout( 

146 "Use the apio.ini constraint-file option to specify the desired file.", 

147 style=INFO, 

148 ) 

149 sys.exit(1) 

150 

151 

152def verilog_src_scanner(apio_env: ApioEnv) -> Scanner.Base: 

153 """Creates and returns a scons Scanner object for scanning verilog 

154 files for dependencies. 

155 """ 

156 # A Regex to icestudio propriaetry references for *.list files. 

157 # Example: 

158 # Text: ' parameter v771499 = "v771499.list"' 

159 # Captures: 'v771499.list' 

160 icestudio_list_re = re.compile(r"[\n|\s][^\/]?\"(.*\.list?)\"", re.M) 

161 

162 # A regex to match a verilog include directive. 

163 # Example 

164 # Text: `include "apio_testing.vh" 

165 # Capture: 'apio_testing.vh' 

166 verilog_include_re = re.compile(r'`\s*include\s+["]([^"]+)["]', re.M) 

167 

168 # A regex for inclusion via $readmemh() 

169 # Example 

170 # Test: '$readmemh("my_data.hex", State_buff);' 

171 # Capture: 'my_data.hex' 

172 readmemh_reference_re = re.compile( 

173 r"\$readmemh\([\'\"]([^\'\"]+)[\'\"]", re.M 

174 ) 

175 

176 # -- List of required and optional files that may require a rebuild if 

177 # -- changed. 

178 core_dependencies = [ 

179 "apio.ini", 

180 "boards.jsonc", 

181 "fpgas.jsonc", 

182 "programmers.jsonc", 

183 ] 

184 

185 def verilog_src_scanner_func( 

186 file_node: File, env: SConsEnvironment, ignored_path 

187 ) -> List[str]: 

188 """Given a [System]Verilog file, scan it and return a list of 

189 references to other files it depends on. It's not require to report 

190 dependency on another source file in the project since scons loads 

191 anyway all the source files in the project. 

192 

193 Returns a list of files. Dependencies that don't have an existing 

194 file are ignored and not returned. This is to avoid references in 

195 commented out code to break scons dependencies. 

196 """ 

197 _ = env # Unused 

198 

199 # Sanity check. Should be called only to scan verilog files. If 

200 # this fails, this is a programming error rather than a user error. 

201 assert is_source_file( 

202 file_node.name 

203 ), f"Not a src file: {file_node.name}" 

204 

205 # Get the directory of the file, relative to the project root which is 

206 # the current working directory. This value is equals to "." if the 

207 # file is in the project root. 

208 file_dir: str = file_node.get_dir().get_path() 

209 

210 # Prepare an empty set of dependencies. 

211 candidates_raw_set = set() 

212 

213 # Read the file. This returns [] if the file doesn't exist. 

214 file_content = file_node.get_text_contents() 

215 

216 # Get verilog includes references. 

217 candidates_raw_set.update(verilog_include_re.findall(file_content)) 

218 

219 # Get $readmemh() function references. 

220 candidates_raw_set.update(readmemh_reference_re.findall(file_content)) 

221 

222 # Get IceStudio references. 

223 candidates_raw_set.update(icestudio_list_re.findall(file_content)) 

224 

225 # Since we don't know if the dependency's path is relative to the file 

226 # location or the project root, we try both. We prefer to have high 

227 # recall of dependencies of high precision, risking at most unnecessary 

228 # rebuilds. 

229 candidates_set = candidates_raw_set.copy() 

230 # If the file is not in the project dir, add a dependency also relative 

231 # to the project dir. 

232 if file_dir != ".": 

233 for raw_candidate in candidates_raw_set: 

234 candidate: str = os.path.join(file_dir, raw_candidate) 

235 candidates_set.add(candidate) 

236 

237 # Add the core dependencies. They are always relative to the project 

238 # root. 

239 candidates_set.update(core_dependencies) 

240 

241 # Filter out candidates that don't have a matching files to prevert 

242 # breaking the build. This handle for example the case where the 

243 # file references is in a comment or non reachable code. 

244 # See also https://stackoverflow.com/q/79302552/15038713 

245 dependencies = [] 

246 for dependency in candidates_set: 

247 if Path(dependency).exists(): 

248 dependencies.append(dependency) 

249 elif apio_env.is_debug(1): 249 ↛ 250line 249 didn't jump to line 250 because the condition on line 249 was never true

250 cout( 

251 f"Dependency candidate {dependency} does not exist, " 

252 "dropping." 

253 ) 

254 

255 # Sort the strings for determinism. 

256 dependencies = sorted(list(dependencies)) 

257 

258 # Debug info. 

259 if apio_env.is_debug(1): 259 ↛ 260line 259 didn't jump to line 260 because the condition on line 259 was never true

260 cout(f"Dependencies of {file_node}:", style=EMPH2) 

261 for dependency in dependencies: 

262 cout(f" {dependency}", style=EMPH2) 

263 

264 # All done 

265 return apio_env.scons_env.File(dependencies) 

266 

267 return apio_env.scons_env.Scanner(function=verilog_src_scanner_func) 

268 

269 

270def verilator_lint_action( 

271 apio_env: ApioEnv, 

272 *, 

273 extra_params: List[str] = None, 

274 lib_dirs: List[Path] = None, 

275 lib_files: List[Path] = None, 

276) -> List[ 

277 Callable[ 

278 [ 

279 List[File], 

280 List[Alias], 

281 SConsEnvironment, 

282 ], 

283 None, 

284 ] 

285 | str, 

286]: 

287 """Construct an verilator scons action. 

288 * extra_params: Optional additional arguments. 

289 * libs_dirs: Optional directories for include search. 

290 * lib_files: Optional additional files to include. 

291 Returns an action in a form of a list with two steps, a function to call 

292 and a string command. 

293 """ 

294 

295 # -- Sanity checks 

296 assert apio_env.targeting("lint") 

297 assert apio_env.params.target.HasField("lint") 

298 

299 # -- Keep short references. 

300 params = apio_env.params 

301 lint_params = params.target.lint 

302 

303 # -- Determine top module. 

304 top_module = ( 

305 lint_params.top_module 

306 if lint_params.top_module 

307 else params.apio_env_params.top_module 

308 ) 

309 

310 # -- Construct the action 

311 action = ( 

312 "verilator_bin --lint-only --quiet --bbox-unsup --timing " 

313 "-Wno-TIMESCALEMOD -Wno-MULTITOP -DAPIO_SIM=0 " 

314 "{0} {1} {2} {3} {4} {5} {6} {7} {8} {9} $SOURCES" 

315 ).format( 

316 "-Wall" if lint_params.verilator_all else "", 

317 "-Wno-style" if lint_params.verilator_no_style else "", 

318 map_params(lint_params.verilator_no_warns, "-Wno-{}"), 

319 map_params(lint_params.verilator_warns, "-Wwarn-{}"), 

320 f"--top-module {top_module}", 

321 get_define_flags(apio_env), 

322 map_params(extra_params, "{}"), 

323 map_params(lib_dirs, '-I"{}"'), 

324 apio_env.target + ".vlt", 

325 map_params(lib_files, '"{}"'), 

326 ) 

327 

328 return [source_files_issue_scanner_action(), action] 

329 

330 

331@dataclass(frozen=True) 

332class SimulationConfig: 

333 """Simulation parameters, for sim and test commands.""" 

334 

335 testbench_name: str # The testbench name, without the 'v' suffix. 

336 build_testbench_name: str # testbench_name prefixed by build dir. 

337 srcs: List[str] # List of source files to compile. 

338 

339 

340def detached_action(api_env: ApioEnv, cmd: List[str]) -> Action: 

341 """ 

342 Launch the given command, given as a list of tokens, in a detached 

343 (non blocking) mode. 

344 """ 

345 

346 def action_func( 

347 target: List[Alias], source: List[File], env: SConsEnvironment 

348 ): 

349 """A call back function to perform the detached command invocation.""" 

350 

351 # -- Make the linter happy 

352 # pylint: disable=consider-using-with 

353 _ = (target, source, env) 

354 

355 # -- NOTE: To debug these Popen operations, comment out the stdout= 

356 # -- and stderr= lines to see the output and error messages from the 

357 # -- commands. 

358 

359 # -- Handle the case of Window. 

360 if api_env.is_windows: 

361 creationflags = ( 

362 subprocess.DETACHED_PROCESS 

363 | subprocess.CREATE_NEW_PROCESS_GROUP 

364 ) 

365 subprocess.Popen( 

366 cmd, 

367 creationflags=creationflags, 

368 stdout=subprocess.DEVNULL, 

369 stderr=subprocess.DEVNULL, 

370 close_fds=True, 

371 shell=False, 

372 ) 

373 return 0 

374 

375 # -- Handle the rest (macOS and Linux) 

376 subprocess.Popen( 

377 cmd, 

378 stdout=subprocess.DEVNULL, 

379 stderr=subprocess.DEVNULL, 

380 close_fds=True, 

381 start_new_session=True, 

382 shell=False, 

383 ) 

384 return 0 

385 

386 # -- Create the command display string that will be shown to the user. 

387 cmd_str: str = subprocess.list2cmdline(cmd) 

388 display_str: str = "[detached] " + cmd_str 

389 

390 # -- Create the action and return. 

391 action = Action(action_func, display_str) 

392 return action 

393 

394 

395def gtkwave_target( 

396 api_env: ApioEnv, 

397 name: str, 

398 vcd_file_target: NodeList, 

399 sim_config: SimulationConfig, 

400 sim_params: SimParams, 

401) -> List[Alias]: 

402 """Construct a target to launch the QTWave signal viewer. 

403 vcd_file_target is the simulator target that generated the vcd file 

404 with the signals. Returns the new targets. 

405 """ 

406 

407 # -- Construct the list of actions. 

408 actions = [] 

409 

410 if sim_params.no_gtkwave: 410 ↛ 426line 410 didn't jump to line 426 because the condition on line 410 was always true

411 # -- User asked to skip gtkwave. The '@' suppresses the printing 

412 # -- of the echo command itself. 

413 actions.append( 

414 "@echo 'Flag --no-gtkwave was found, skipping GTKWave.'" 

415 ) 

416 

417 else: 

418 # -- Normal pase, invoking gtkwave. 

419 

420 # -- On windows we need to setup the cache. This could be done once 

421 # -- when the oss-cad-suite is installed but since we currently don't 

422 # -- have a package setup mechanism, we do it here on each invocation. 

423 # -- The time penalty is negligible. 

424 # -- With the stock oss-cad-suite windows package, this is done in the 

425 # -- environment.bat script. 

426 if api_env.is_windows: 

427 actions.append("gdk-pixbuf-query-loaders --update-cache") 

428 

429 # -- The actual wave viewer command. 

430 gtkwave_cmd = [ 

431 "gtkwave", 

432 "--rcvar", 

433 "splash_disable on", 

434 "--rcvar", 

435 "do_initial_zoom_fit 1", 

436 str(vcd_file_target[0]), 

437 sim_config.testbench_name + ".gtkw", 

438 ] 

439 

440 # -- Handle the case where gtkwave is run as a detached app, not 

441 # -- waiting for it to close and not showing its output. 

442 if sim_params.detach_gtkwave: 

443 gtkwave_action = detached_action(api_env, gtkwave_cmd) 

444 else: 

445 gtkwave_action = subprocess.list2cmdline(gtkwave_cmd) 

446 

447 actions.append(gtkwave_action) 

448 

449 # -- Define a target with the action(s) we created. 

450 target = api_env.alias( 

451 name, 

452 source=vcd_file_target, 

453 action=actions, 

454 always_build=True, 

455 ) 

456 

457 return target 

458 

459 

460def check_valid_testbench_name(testbench: str) -> None: 

461 """Check if a testbench name is valid. If not, print an error message 

462 and exit.""" 

463 if not is_source_file(testbench) or not has_testbench_name(testbench): 463 ↛ 464line 463 didn't jump to line 464 because the condition on line 463 was never true

464 cerror(f"'{testbench}' is not a valid testbench file name.") 

465 cout(TESTBENCH_HINT, style=INFO) 

466 sys.exit(1) 

467 

468 

469def get_sim_config( 

470 apio_env: ApioEnv, 

471 testbench: str, 

472 synth_srcs: List[str], 

473 test_srcs: List[str], 

474) -> SimulationConfig: 

475 """Returns a SimulationConfig for a sim command. 'testbench' is 

476 an optional testbench file name. 'synth_srcs' and 'test_srcs' are the 

477 all the project's synth and testbench files found in the project as 

478 returned by get_project_source_files().""" 

479 

480 # -- Handle the testbench file selection. The end result is a single 

481 # -- testbench file name in testbench that we simulate, or a fatal error. 

482 if testbench: 

483 # -- Case 1 - Testbench file name is specified in the command or 

484 # -- apio.ini. Fatal error if invalid. 

485 check_valid_testbench_name(testbench) 

486 elif len(test_srcs) == 0: 486 ↛ 489line 486 didn't jump to line 489 because the condition on line 486 was never true

487 # -- Case 2 Testbench name was not specified and no testbench files 

488 # -- were found in the project. 

489 cerror("No testbench files found in the project.") 

490 cout(TESTBENCH_HINT, style=INFO) 

491 sys.exit(1) 

492 elif len(test_srcs) == 1: 492 ↛ 500line 492 didn't jump to line 500 because the condition on line 492 was always true

493 # -- Case 3 Testbench name was not specified but there is exactly 

494 # -- one in the project. 

495 testbench = test_srcs[0] 

496 cout(f"Found testbench file {testbench}", style=EMPH1) 

497 else: 

498 # -- Case 4 Testbench name was not specified and there are multiple 

499 # -- testbench files in the project. 

500 cerror("Multiple testbench files found in the project.") 

501 cout( 

502 "Please specify the testbench file name in the command " 

503 "or in apio.ini 'default-testbench' option.", 

504 style=INFO, 

505 ) 

506 sys.exit(1) 

507 

508 # -- This should not happen. If it does, it's a programming error. 

509 assert testbench, "get_sim_config(): Missing testbench file name" 

510 

511 # -- Construct a SimulationParams with all the synth files + the 

512 # -- testbench file. 

513 testbench_name = basename(testbench) 

514 build_testbench_name = str(apio_env.env_build_path / testbench_name) 

515 srcs = synth_srcs + [testbench] 

516 return SimulationConfig(testbench_name, build_testbench_name, srcs) 

517 

518 

519def get_tests_configs( 

520 apio_env: ApioEnv, 

521 testbench: str, 

522 synth_srcs: List[str], 

523 test_srcs: list[str], 

524) -> List[SimulationConfig]: 

525 """Return a list of SimulationConfigs for each of the testbenches that 

526 need to be run for a 'apio test' command. If testbench is empty, 

527 all the testbenches in test_srcs will be tested. Otherwise, only the 

528 testbench in testbench will be tested. synth_srcs and test_srcs are 

529 source and test file lists as returned by get_project_source_files().""" 

530 # List of testbenches to be tested. 

531 

532 # -- Handle the testbench files selection. The end result is a list of one 

533 # -- or more testbench file names in testbenches that we test. 

534 if testbench: 

535 # -- Case 1 - a testbench file name is specified in the command or 

536 # -- apio.ini. Fatal error if invalid. 

537 check_valid_testbench_name(testbench) 

538 testbenches = [testbench] 

539 elif len(test_srcs) == 0: 539 ↛ 542line 539 didn't jump to line 542 because the condition on line 539 was never true

540 # -- Case 2 - Testbench file name was not specified and there are no 

541 # -- testbench files in the project. 

542 cerror("No testbench files found in the project.") 

543 cout(TESTBENCH_HINT, style=INFO) 

544 sys.exit(1) 

545 else: 

546 # -- Case 3 - Testbench file name was not specified but there are one 

547 # -- or more testbench files in the project. 

548 testbenches = test_srcs 

549 

550 # -- If this fails, it's a programming error. 

551 assert testbenches, "get_tests_configs(): no testbenches" 

552 

553 # Construct a config for each testbench. 

554 configs = [] 

555 for tb in testbenches: 

556 testbench_name = basename(tb) 

557 build_testbench_name = str(apio_env.env_build_path / testbench_name) 

558 srcs = synth_srcs + [tb] 

559 configs.append( 

560 SimulationConfig(testbench_name, build_testbench_name, srcs) 

561 ) 

562 

563 return configs 

564 

565 

566def announce_testbench_action() -> FunctionAction: 

567 """Returns an action that prints a title with the testbench name.""" 

568 

569 def announce_testbench( 

570 target: List[Alias], 

571 source: List[File], 

572 env: SConsEnvironment, 

573 ): 

574 """The action function.""" 

575 _ = (target, env) # Unused 

576 

577 # -- We expect to find exactly one testbench. 

578 testbenches = [ 

579 file 

580 for file in source 

581 if (is_source_file(file.name) and has_testbench_name(file.name)) 

582 ] 

583 assert len(testbenches) == 1, testbenches 

584 

585 # -- Announce it. 

586 cout() 

587 cout(f"Testbench {testbenches[0]}", style=EMPH3) 

588 

589 # -- Run the action but don't announce the action. 

590 return Action(announce_testbench, strfunction=None) 

591 

592 

593def source_files_issue_scanner_action() -> FunctionAction: 

594 """Returns a SCons action that scans the source files and print 

595 error or warning messages about issues it finds.""" 

596 

597 # A regex to identify "$dumpfile(" in testbenches. 

598 testbench_dumpfile_re = re.compile(r"[$]dumpfile\s*[(]") 

599 

600 # A regex to identify "INTERACTIVE_SIM" in testbenches 

601 interactive_sim_re = re.compile(r"INTERACTIVE_SIM") 

602 

603 def report_source_files_issues( 

604 target: List[Alias], 

605 source: List[File], 

606 env: SConsEnvironment, 

607 ): 

608 """The scanner function.""" 

609 

610 _ = (target, env) # Unused 

611 

612 for file in source: 

613 

614 # -- For now we report issues only in testbenches so skip 

615 # -- otherwise. 

616 if not is_source_file(file.name) or not has_testbench_name( 

617 file.name 

618 ): 

619 continue 

620 

621 # -- Read the testbench file text. 

622 file_text = file.get_text_contents() 

623 

624 # -- if contains $dumpfile, print a warning. 

625 if testbench_dumpfile_re.findall(file_text): 625 ↛ 626line 625 didn't jump to line 626 because the condition on line 625 was never true

626 cwarning("Avoid using $dumpfile() in Apio testbenches.") 

627 

628 # -- if contains $dumpfile, print a warning. 

629 if interactive_sim_re.findall(file_text): 629 ↛ 630line 629 didn't jump to line 630 because the condition on line 629 was never true

630 cwarning( 

631 "The Apio macro `INTERACTIVE_SIM is deprecated. " 

632 "Use `APIO_SIM (0 or 1) instead." 

633 ) 

634 

635 # -- Run the action but don't announce the action. We will print 

636 # -- ourselves in report_source_files_issues. 

637 return Action(report_source_files_issues, strfunction=None) 

638 

639 

640def _print_pnr_utilization_report(report: Dict[str, any]): 

641 table = Table( 

642 show_header=True, 

643 show_lines=False, 

644 box=box.SQUARE, 

645 border_style=BORDER, 

646 title="FPGA Resource Utilization", 

647 title_justify="left", 

648 padding=(0, 2), 

649 ) 

650 

651 # -- Add columns. 

652 table.add_column("RESOURCE", no_wrap=True) 

653 table.add_column("USED", no_wrap=True, justify="right") 

654 table.add_column("TOTAL", no_wrap=True, justify="right") 

655 table.add_column("UTIL.", no_wrap=True, justify="right") 

656 

657 # -- Add rows 

658 utilization = report["utilization"] 

659 for resource, vals in utilization.items(): 

660 used = vals["used"] 

661 used_str = f"{used} " if used else "" 

662 available = vals["available"] 

663 available_str = f"{available} " 

664 percents = int(100 * used / available) 

665 percents_str = f"{percents}% " if used else "" 

666 style = EMPH3 if used > 0 else None 

667 table.add_row( 

668 resource, used_str, available_str, percents_str, style=style 

669 ) 

670 

671 # -- Render the table 

672 cout() 

673 ctable(table) 

674 

675 

676def _maybe_print_pnr_clocks_report( 

677 report: Dict[str, any], clk_name_index: int 

678) -> bool: 

679 clocks = report["fmax"] 

680 if len(clocks) == 0: 

681 return False 

682 

683 table = Table( 

684 show_header=True, 

685 show_lines=True, 

686 box=box.SQUARE, 

687 border_style=BORDER, 

688 title="Clock Information", 

689 title_justify="left", 

690 padding=(0, 2), 

691 ) 

692 

693 # -- Add columns 

694 table.add_column("CLOCK", no_wrap=True) 

695 table.add_column( 

696 "MAX SPEED [Mhz]", no_wrap=True, justify="right", style=EMPH3 

697 ) 

698 

699 # -- Add rows. 

700 clocks = report["fmax"] 

701 for clk_net, vals in clocks.items(): 

702 # -- Extract clock name from the net name. 

703 clk_signal = clk_net.split("$")[clk_name_index] 

704 # -- Extract speed 

705 max_mhz = vals["achieved"] 

706 # -- Add row. 

707 table.add_row(clk_signal, f"{max_mhz:.2f}") 

708 

709 # -- Render the table 

710 cout() 

711 ctable(table) 

712 return True 

713 

714 

715def _print_pnr_report( 

716 json_txt: str, 

717 clk_name_index: int, 

718 verbose: bool, 

719) -> None: 

720 """Accepts the text of the pnr json report and prints it in 

721 a user friendly way. Used by the 'apio report' command.""" 

722 # -- Parse the json text into a tree of dicts. 

723 report: Dict[str, any] = json.loads(json_txt) 

724 

725 # -- Print the utilization table. 

726 _print_pnr_utilization_report(report) 

727 

728 # -- Print the optional clocks table. 

729 clock_report_printed = _maybe_print_pnr_clocks_report( 

730 report, clk_name_index 

731 ) 

732 

733 # -- Print summary. 

734 cout("") 

735 if not clock_report_printed: 

736 cout("No clocks were found in the design.", style=INFO) 

737 if not verbose: 737 ↛ exitline 737 didn't return from function '_print_pnr_report' because the condition on line 737 was always true

738 cout("Run 'apio report --verbose' for more details.", style=INFO) 

739 

740 

741def report_action(clk_name_index: int, verbose: bool) -> FunctionAction: 

742 """Returns a SCons action to format and print the PNR reort from the 

743 PNR json report file. Used by the 'apio report' command. 

744 'script_id' identifies the calling SConstruct script and 'verbose' 

745 indicates if the --verbose flag was invoked.""" 

746 

747 def print_pnr_report( 

748 target: List[Alias], 

749 source: List[File], 

750 env: SConsEnvironment, 

751 ): 

752 """Action function. Loads the pnr json report and print in a user 

753 friendly way.""" 

754 _ = (target, env) # Unused 

755 json_file: File = source[0] 

756 json_txt: str = json_file.get_text_contents() 

757 _print_pnr_report(json_txt, clk_name_index, verbose) 

758 

759 return Action(print_pnr_report, "Formatting pnr report.") 

760 

761 

762def get_programmer_cmd(apio_env: ApioEnv) -> str: 

763 """Return the programmer command as derived from the scons "prog" 

764 arg.""" 

765 

766 # Should be called only if scons paramsm has 'upload' target parmas. 

767 params = apio_env.params 

768 assert params.target.HasField("upload"), params 

769 

770 # Get the programer command template arg. 

771 programmer_cmd = params.target.upload.programmer_cmd 

772 assert programmer_cmd, params 

773 

774 # -- [NOTE] Generally speaking we would expect the command to include 

775 # -- $SOURCE for the binary file path but since we allow custom commands 

776 # -- using apio.ini's 'programmer-cmd' option, we don't check for it here. 

777 

778 return programmer_cmd 

779 

780 

781def get_define_flags(apio_env: ApioEnv) -> str: 

782 """Return a string with the -D flags for the verilog defines. Returns 

783 an empty string if there are no defines.""" 

784 flags: List[str] = [] 

785 for define in apio_env.params.apio_env_params.defines: 785 ↛ 786line 785 didn't jump to line 786 because the loop on line 785 never started

786 flags.append("-D" + define) 

787 

788 return " ".join(flags) 

789 

790 

791def iverilog_action( 

792 apio_env: ApioEnv, 

793 *, 

794 verbose: bool, 

795 vcd_output_name: str, 

796 is_interactive: bool, 

797 extra_params: List[str] = None, 

798 lib_dirs: List[Path] = None, 

799 lib_files: List[Path] = None, 

800) -> str: 

801 """Construct an iverilog scons action string. 

802 * env: Rhe scons environment. 

803 * verbose: IVerilog will show extra info. 

804 * vcd_output_name: Value for the macro VCD_OUTPUT. 

805 * is_interactive: True for apio sim, False otherwise. 

806 * extra_params: Optional list of additional IVerilog params. 

807 * lib_dirs: Optional list of dir pathes to include. 

808 * lib_files: Optional list of library files to compile. 

809 * 

810 * Returns the scons action string for the IVerilog command. 

811 """ 

812 

813 # pylint: disable=too-many-arguments 

814 

815 # Escaping for windows. '\' -> '\\' 

816 escaped_vcd_output_name = vcd_output_name.replace("\\", "\\\\") 

817 

818 # -- Construct the action string. 

819 # -- The -g2012 is for system-verilog support. 

820 action = ( 

821 "iverilog -g2012 {0} -o $TARGET {1} {2} {3} {4} {5} {6} {7} $SOURCES" 

822 ).format( 

823 "-v" if verbose else "", 

824 f"-DVCD_OUTPUT={escaped_vcd_output_name}", 

825 get_define_flags(apio_env), 

826 f"-DAPIO_SIM={int(is_interactive)}", 

827 # 'INTERACTIVE_SIM is deprecated and will go away. 

828 "-DINTERACTIVE_SIM" if is_interactive else "", 

829 map_params(extra_params, "{}"), 

830 map_params(lib_dirs, '-I"{}"'), 

831 map_params(lib_files, '"{}"'), 

832 ) 

833 

834 return action 

835 

836 

837def basename(file_name: str) -> str: 

838 """Given a file name, returns it with the extension removed.""" 

839 result, _ = os.path.splitext(file_name) 

840 return result 

841 

842 

843def make_verilator_config_builder(lib_path: Path): 

844 """Create a scons Builder that writes a verilator config file 

845 (hardware.vlt) that suppresses warnings in the lib directory.""" 

846 assert isinstance(lib_path, Path), lib_path 

847 

848 # -- Construct a glob of all library files. 

849 glob_path = str(lib_path / "*") 

850 

851 # -- Escape for windows. A single backslash is converted into two. 

852 glob_str = str(glob_path).replace("\\", "\\\\") 

853 

854 # -- Generate the files lines. We suppress a union of all the errors we 

855 # -- encountered in all the architectures. 

856 lines = ["`verilator_config"] 

857 for rule in [ 

858 "COMBDLY", 

859 "WIDTHEXPAND", 

860 "SPECIFYIGN", 

861 "PINMISSING", 

862 "ASSIGNIN", 

863 "WIDTHTRUNC", 

864 "INITIALDLY", 

865 ]: 

866 lines.append(f'lint_off -rule {rule} -file "{glob_str}"') 

867 

868 # -- Join the lines into text. 

869 text = "\n".join(lines) + "\n" 

870 

871 def verilator_config_func(target, source, env): 

872 """Creates a verilator .vlt config files.""" 

873 _ = (source, env) # Unused 

874 with open(target[0].get_path(), "w", encoding="utf-8") as target_file: 

875 target_file.write(text) 

876 return 0 

877 

878 return Builder( 

879 action=Action( 

880 verilator_config_func, "Creating verilator config file." 

881 ), 

882 suffix=".vlt", 

883 )