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

244 statements  

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

13import sys 

14import os 

15import re 

16import json 

17from dataclasses import dataclass 

18from pathlib import Path 

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

20from rich.table import Table 

21from rich import box 

22from SCons import Scanner 

23from SCons.Builder import Builder 

24from SCons.Action import FunctionAction, Action 

25from SCons.Node.FS import File 

26from SCons.Script.SConscript import SConsEnvironment 

27from SCons.Node import NodeList 

28from SCons.Node.Alias import Alias 

29from apio.scons.apio_env import ApioEnv 

30from apio.common.common_util import ( 

31 has_testbench_name, 

32 is_source_file, 

33) 

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

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

36 

37 

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

39 

40 

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

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

43 of arguments. The functon does the following: 

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

45 2. Drops empty or white space only items. 

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

47 placeholder {}. 

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

49 

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

51 """ 

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

53 # warning. 

54 if params is None: 

55 params = [] 

56 

57 # Convert parmas to stripped strings. 

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

59 

60 # Drop the empty params and map the rest. 

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

62 

63 # Join using a single space. 

64 return " ".join(mapped_params) 

65 

66 

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

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

69 

70 env is the sconstruction environment. 

71 

72 file_ext is a string with the constrained file extension. 

73 E.g. ".pcf" for ice40. 

74 

75 Returns the file name if found or a default name otherwise otherwise. 

76 """ 

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

78 user_specified = apio_env.params.apio_env_params.constraint_file 

79 if user_specified: 

80 # -- Check for proper extension for this architecture. 

81 if not user_specified.endswith(file_ext): 

82 cerror( 

83 f"Constraint file '{user_specified}' should have " 

84 f"the extension '{file_ext}'." 

85 ) 

86 sys.exit(1) 

87 # -- Check for valid chars only, e.g. dir separators are not allowed. 

88 # -- This must be a simple file name that is expected to be found in 

89 # -- the root directory of the project. 

90 forbidden_chars = '<>:"/\\|?*\0' 

91 for c in user_specified: 

92 if c in forbidden_chars: 

93 cerror( 

94 f"Constrain filename '{user_specified}' contains an " 

95 f"illegal character: '{c}'" 

96 ) 

97 sys.exit(1) 

98 

99 return user_specified 

100 

101 # -- No user specified constraint file, try to look for it. 

102 # -- 

103 # -- Get existing files in alphabetical order. We search only in the root 

104 # -- directory of the project. 

105 files = apio_env.scons_env.Glob(f"*{file_ext}") 

106 n = len(files) 

107 

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

109 if n == 0: 

110 cerror( 

111 f"No constraint file '*{file_ext}' found, expected exactly one." 

112 ) 

113 sys.exit(1) 

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

115 if n == 1: 

116 result = str(files[0]) 

117 return result 

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

119 cerror( 

120 f"Found multiple '*{file_ext}' " 

121 "constraint files, expecting exactly one." 

122 ) 

123 sys.exit(1) 

124 

125 

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

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

128 files for dependencies. 

129 """ 

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

131 # Example: 

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

133 # Captures: 'v771499.list' 

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

135 

136 # A regex to match a verilog include directive. 

137 # Example 

138 # Text: `include "apio_testing.vh" 

139 # Capture: 'apio_testing.vh' 

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

141 

142 # A regex for inclusion via $readmemh() 

143 # Example 

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

145 # Capture: 'my_data.hex' 

146 readmemh_reference_re = re.compile( 

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

148 ) 

149 

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

151 # -- changed. 

152 core_dependencies = [ 

153 "apio.ini", 

154 "boards.jsonc", 

155 "fpgas.jsonc", 

156 "programmers.jsonc", 

157 ] 

158 

159 def verilog_src_scanner_func( 

160 file_node: File, env: SConsEnvironment, ignored_path 

161 ) -> List[str]: 

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

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

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

165 anyway all the source files in the project. 

166 

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

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

169 commented out code to break scons dependencies. 

170 """ 

171 _ = env # Unused 

172 

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

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

175 assert is_source_file( 

176 file_node.name 

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

178 

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

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

181 # file is in the project root. 

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

183 

184 # Prepare an empty set of dependencies. 

185 candidates_raw_set = set() 

186 

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

188 file_content = file_node.get_text_contents() 

189 

190 # Get verilog includes references. 

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

192 

193 # Get $readmemh() function references. 

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

195 

196 # Get IceStudio references. 

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

198 

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

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

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

202 # rebuilds. 

203 candidates_set = candidates_raw_set.copy() 

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

205 # to the project dir. 

206 if file_dir != ".": 

207 for raw_candidate in candidates_raw_set: 

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

209 candidates_set.add(candidate) 

210 

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

212 # root. 

213 candidates_set.update(core_dependencies) 

214 

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

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

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

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

219 dependencies = [] 

220 for dependency in candidates_set: 

221 if Path(dependency).exists(): 

222 dependencies.append(dependency) 

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

224 cout( 

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

226 "dropping." 

227 ) 

228 

229 # Sort the strings for determinism. 

230 dependencies = sorted(list(dependencies)) 

231 

232 # Debug info. 

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

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

235 for dependency in dependencies: 

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

237 

238 # All done 

239 return apio_env.scons_env.File(dependencies) 

240 

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

242 

243 

244def verilator_lint_action( 

245 apio_env: ApioEnv, 

246 *, 

247 extra_params: List[str] = None, 

248 lib_dirs: List[Path] = None, 

249 lib_files: List[Path] = None, 

250) -> List[ 

251 Callable[ 

252 [ 

253 List[File], 

254 List[Alias], 

255 SConsEnvironment, 

256 ], 

257 None, 

258 ] 

259 | str, 

260]: 

261 """Construct an verilator scons action. 

262 * extra_params: Optional additional arguments. 

263 * libs_dirs: Optional directories for include search. 

264 * lib_files: Optional additional files to include. 

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

266 and a string command. 

267 """ 

268 

269 # -- Sanity checks 

270 assert apio_env.targeting("lint") 

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

272 

273 # -- Keep short references. 

274 params = apio_env.params 

275 lint_params = params.target.lint 

276 

277 # -- Determine top module. 

278 top_module = ( 

279 lint_params.top_module 

280 if lint_params.top_module 

281 else params.apio_env_params.top_module 

282 ) 

283 

284 # -- Construct the action 

285 action = ( 

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

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

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

289 ).format( 

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

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

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

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

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

295 get_define_flags(apio_env), 

296 map_params(extra_params, "{}"), 

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

298 apio_env.target + ".vlt", 

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

300 ) 

301 

302 return [source_files_issue_scanner_action(), action] 

303 

304 

305@dataclass(frozen=True) 

306class SimulationConfig: 

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

308 

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

310 build_testbench_name: str # testbench_name prefixed by build dir. 

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

312 

313 

314def waves_target( 

315 api_env: ApioEnv, 

316 name: str, 

317 vcd_file_target: NodeList, 

318 sim_config: SimulationConfig, 

319 no_gtkwave: bool, 

320) -> List[Alias]: 

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

322 vcd_file_target is the simulator target that generated the vcd file 

323 with the signals. Returns the new targets. 

324 """ 

325 

326 # -- Construct the commands list. 

327 commands = [] 

328 

329 if no_gtkwave: 329 ↛ 345line 329 didn't jump to line 345 because the condition on line 329 was always true

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

331 # -- of the echo command itself. 

332 commands.append( 

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

334 ) 

335 

336 else: 

337 # -- Normal pase, invoking gtkwave. 

338 

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

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

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

342 # -- The time penalty is negligible. 

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

344 # -- environment.bat script. 

345 if api_env.is_windows: 

346 commands.append("gdk-pixbuf-query-loaders --update-cache") 

347 

348 # -- The actual wave viewer command. 

349 commands.append( 

350 "gtkwave {0} {1} {2}.gtkw".format( 

351 '--rcvar "splash_disable on" --rcvar "do_initial_zoom_fit 1"', 

352 vcd_file_target[0], 

353 sim_config.testbench_name, 

354 ) 

355 ) 

356 

357 target = api_env.alias( 

358 name, 

359 source=vcd_file_target, 

360 action=commands, 

361 always_build=True, 

362 ) 

363 

364 return target 

365 

366 

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

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

369 and exit.""" 

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

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

372 cout(TESTBENCH_HINT, style=INFO) 

373 sys.exit(1) 

374 

375 

376def get_sim_config( 

377 apio_env: ApioEnv, 

378 testbench: str, 

379 synth_srcs: List[str], 

380 test_srcs: List[str], 

381) -> SimulationConfig: 

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

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

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

385 returned by get_project_source_files().""" 

386 

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

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

389 if testbench: 

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

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

392 check_valid_testbench_name(testbench) 

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

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

395 # -- were found in the project. 

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

397 cout(TESTBENCH_HINT, style=INFO) 

398 sys.exit(1) 

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

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

401 # -- one in the project. 

402 testbench = test_srcs[0] 

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

404 else: 

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

406 # -- testbench files in the project. 

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

408 cout( 

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

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

411 style=INFO, 

412 ) 

413 sys.exit(1) 

414 

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

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

417 

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

419 # -- testbench file. 

420 testbench_name = basename(testbench) 

421 build_testbench_name = str(apio_env.env_build_path / testbench_name) 

422 srcs = synth_srcs + [testbench] 

423 return SimulationConfig(testbench_name, build_testbench_name, srcs) 

424 

425 

426def get_tests_configs( 

427 apio_env: ApioEnv, 

428 testbench: str, 

429 synth_srcs: List[str], 

430 test_srcs: list[str], 

431) -> List[SimulationConfig]: 

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

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

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

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

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

437 # List of testbenches to be tested. 

438 

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

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

441 if testbench: 

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

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

444 check_valid_testbench_name(testbench) 

445 testbenches = [testbench] 

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

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

448 # -- testbench files in the project. 

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

450 cout(TESTBENCH_HINT, style=INFO) 

451 sys.exit(1) 

452 else: 

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

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

455 testbenches = test_srcs 

456 

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

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

459 

460 # Construct a config for each testbench. 

461 configs = [] 

462 for tb in testbenches: 

463 testbench_name = basename(tb) 

464 build_testbench_name = str(apio_env.env_build_path / testbench_name) 

465 srcs = synth_srcs + [tb] 

466 configs.append( 

467 SimulationConfig(testbench_name, build_testbench_name, srcs) 

468 ) 

469 

470 return configs 

471 

472 

473def announce_testbench_action() -> FunctionAction: 

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

475 

476 def announce_testbench( 

477 source: List[File], 

478 target: List[Alias], 

479 env: SConsEnvironment, 

480 ): 

481 """The action function.""" 

482 _ = (target, env) # Unused 

483 

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

485 testbenches = [ 

486 file 

487 for file in source 

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

489 ] 

490 assert len(testbenches) == 1, testbenches 

491 

492 # -- Announce it. 

493 cout() 

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

495 

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

497 return Action(announce_testbench, strfunction=None) 

498 

499 

500def source_files_issue_scanner_action() -> FunctionAction: 

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

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

503 

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

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

506 

507 # A regex to identify "INTERACTIVE_SIM" in testbenches 

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

509 

510 def report_source_files_issues( 

511 source: List[File], 

512 target: List[Alias], 

513 env: SConsEnvironment, 

514 ): 

515 """The scanner function.""" 

516 

517 _ = (target, env) # Unused 

518 

519 for file in source: 

520 

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

522 # -- otherwise. 

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

524 file.name 

525 ): 

526 continue 

527 

528 # -- Read the testbench file text. 

529 file_text = file.get_text_contents() 

530 

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

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

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

534 

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

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

537 cwarning( 

538 "The Apio macro `INTERACTIVE_SIM is deprecated. " 

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

540 ) 

541 

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

543 # -- ourselves in report_source_files_issues. 

544 return Action(report_source_files_issues, strfunction=None) 

545 

546 

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

548 table = Table( 

549 show_header=True, 

550 show_lines=False, 

551 box=box.SQUARE, 

552 border_style=BORDER, 

553 title="FPGA Resource Utilization", 

554 title_justify="left", 

555 padding=(0, 2), 

556 ) 

557 

558 # -- Add columns. 

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

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

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

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

563 

564 # -- Add rows 

565 utilization = report["utilization"] 

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

567 used = vals["used"] 

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

569 available = vals["available"] 

570 available_str = f"{available} " 

571 percents = int(100 * used / available) 

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

573 style = EMPH3 if used > 0 else None 

574 table.add_row( 

575 resource, used_str, available_str, percents_str, style=style 

576 ) 

577 

578 # -- Render the table 

579 cout() 

580 ctable(table) 

581 

582 

583def _maybe_print_pnr_clocks_report( 

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

585) -> bool: 

586 clocks = report["fmax"] 

587 if len(clocks) == 0: 

588 return False 

589 

590 table = Table( 

591 show_header=True, 

592 show_lines=True, 

593 box=box.SQUARE, 

594 border_style=BORDER, 

595 title="Clock Information", 

596 title_justify="left", 

597 padding=(0, 2), 

598 ) 

599 

600 # -- Add columns 

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

602 table.add_column( 

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

604 ) 

605 

606 # -- Add rows. 

607 clocks = report["fmax"] 

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

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

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

611 # -- Extract speed 

612 max_mhz = vals["achieved"] 

613 # -- Add row. 

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

615 

616 # -- Render the table 

617 cout() 

618 ctable(table) 

619 return True 

620 

621 

622def _print_pnr_report( 

623 json_txt: str, 

624 clk_name_index: int, 

625 verbose: bool, 

626) -> None: 

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

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

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

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

631 

632 # -- Print the utilization table. 

633 _print_pnr_utilization_report(report) 

634 

635 # -- Print the optional clocks table. 

636 clock_report_printed = _maybe_print_pnr_clocks_report( 

637 report, clk_name_index 

638 ) 

639 

640 # -- Print summary. 

641 cout("") 

642 if not clock_report_printed: 

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

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

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

646 

647 

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

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

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

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

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

653 

654 def print_pnr_report( 

655 source: List[File], 

656 target: List[Alias], 

657 env: SConsEnvironment, 

658 ): 

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

660 friendly way.""" 

661 _ = (target, env) # Unused 

662 json_file: File = source[0] 

663 json_txt: str = json_file.get_text_contents() 

664 _print_pnr_report(json_txt, clk_name_index, verbose) 

665 

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

667 

668 

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

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

671 arg.""" 

672 

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

674 params = apio_env.params 

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

676 

677 # Get the programer command template arg. 

678 programmer_cmd = params.target.upload.programmer_cmd 

679 assert programmer_cmd, params 

680 

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

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

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

684 

685 return programmer_cmd 

686 

687 

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

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

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

691 flags: List[str] = [] 

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

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

694 

695 return " ".join(flags) 

696 

697 

698def iverilog_action( 

699 apio_env: ApioEnv, 

700 *, 

701 verbose: bool, 

702 vcd_output_name: str, 

703 is_interactive: bool, 

704 extra_params: List[str] = None, 

705 lib_dirs: List[Path] = None, 

706 lib_files: List[Path] = None, 

707) -> str: 

708 """Construct an iverilog scons action string. 

709 * env: Rhe scons environment. 

710 * verbose: IVerilog will show extra info. 

711 * vcd_output_name: Value for the macro VCD_OUTPUT. 

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

713 * extra_params: Optional list of additional IVerilog params. 

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

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

716 * 

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

718 """ 

719 

720 # pylint: disable=too-many-arguments 

721 

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

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

724 

725 # -- Construct the action string. 

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

727 action = ( 

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

729 ).format( 

730 "-v" if verbose else "", 

731 f"-DVCD_OUTPUT={escaped_vcd_output_name}", 

732 get_define_flags(apio_env), 

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

734 # 'INTERACTIVE_SIM is deprecated and will go away. 

735 "-DINTERACTIVE_SIM" if is_interactive else "", 

736 map_params(extra_params, "{}"), 

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

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

739 ) 

740 

741 return action 

742 

743 

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

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

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

747 return result 

748 

749 

750def make_verilator_config_builder(lib_path: Path): 

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

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

753 assert isinstance(lib_path, Path), lib_path 

754 

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

756 glob_path = str(lib_path / "*") 

757 

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

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

760 

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

762 # -- encountered in all the architectures. 

763 lines = ["`verilator_config"] 

764 for rule in [ 

765 "COMBDLY", 

766 "WIDTHEXPAND", 

767 "SPECIFYIGN", 

768 "PINMISSING", 

769 "ASSIGNIN", 

770 "WIDTHTRUNC", 

771 "INITIALDLY", 

772 ]: 

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

774 

775 # -- Join the lines into text. 

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

777 

778 def verilator_config_func(target, source, env): 

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

780 _ = (source, env) # Unused 

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

782 target_file.write(text) 

783 return 0 

784 

785 return Builder( 

786 action=Action( 

787 verilator_config_func, "Creating verilator config file." 

788 ), 

789 suffix=".vlt", 

790 )