Coverage for tsfpga/vivado/project.py: 90%
205 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-08-29 20:51 +0000
« prev ^ index » next coverage.py v7.10.6, created at 2025-08-29 20:51 +0000
1# --------------------------------------------------------------------------------------------------
2# Copyright (c) Lukas Vik. All rights reserved.
3#
4# This file is part of the tsfpga project, a project platform for modern FPGA development.
5# https://tsfpga.com
6# https://github.com/tsfpga/tsfpga
7# --------------------------------------------------------------------------------------------------
9from __future__ import annotations
11import shutil
12from copy import deepcopy
13from pathlib import Path
14from typing import TYPE_CHECKING, Any, NoReturn
16from tsfpga import TSFPGA_TCL
17from tsfpga.build_step_tcl_hook import BuildStepTclHook
18from tsfpga.constraint import Constraint
19from tsfpga.system_utils import create_file, read_file
21from .build_result import BuildResult
22from .common import run_vivado_gui, run_vivado_tcl
23from .hierarchical_utilization_parser import HierarchicalUtilizationParser
24from .logic_level_distribution_parser import LogicLevelDistributionParser
25from .tcl import VivadoTcl
27if TYPE_CHECKING:
28 from tsfpga.module_list import ModuleList
30 from .build_result_checker import MaximumLogicLevel, SizeChecker
33class VivadoProject:
34 """
35 Used for handling a Xilinx Vivado HDL project
36 """
38 def __init__( # noqa: PLR0913
39 self,
40 name: str,
41 modules: ModuleList,
42 part: str,
43 top: str | None = None,
44 generics: dict[str, Any] | None = None,
45 constraints: list[Constraint] | None = None,
46 tcl_sources: list[Path] | None = None,
47 build_step_hooks: list[BuildStepTclHook] | None = None,
48 vivado_path: Path | None = None,
49 default_run_index: int = 1,
50 impl_explore: bool = False,
51 defined_at: Path | None = None,
52 **other_arguments: Any, # noqa: ANN401
53 ) -> None:
54 """
55 Class constructor. Performs a shallow copy of the mutable arguments, so that the user
56 can e.g. append items to their list after creating an object.
58 Arguments:
59 name: Project name.
60 modules: Modules that shall be included in the project.
61 part: Part identification.
62 top: Name of top level entity.
63 If left out, the top level name will be inferred from the ``name``.
64 generics: A dict with generics values (name: value). Use this parameter
65 for "static" generics that do not change between multiple builds of this
66 project. These will be set in the project when it is created.
68 Compare to the build-time generic argument in :meth:`build`.
70 The generic value shall be of type
72 * :class:`bool` (suitable for VHDL type ``boolean`` and ``std_logic``),
73 * :class:`int` (suitable for VHDL type ``integer``, ``natural``, etc.),
74 * :class:`float` (suitable for VHDL type ``real``),
75 * :class:`.BitVectorGenericValue` (suitable for VHDL type ``std_logic_vector``,
76 ``unsigned``, etc.), or
77 * :class:`.StringGenericValue` (suitable for VHDL type ``string``).
78 constraints: Constraints that will be applied to the project.
79 tcl_sources: A list of TCL files. Use for e.g. block design, pinning, settings, etc.
80 build_step_hooks: Build step hooks that will be applied to the project.
81 vivado_path: A path to the Vivado executable.
82 If omitted, the default location from the system PATH will be used.
83 default_run_index: Default run index (synth_X and impl_X) that is set in the
84 project.
85 Can also use the argument to :meth:`build() <VivadoProject.build>` to
86 specify at build-time.
87 impl_explore: Run multiple implementation strategies in parallel.
88 defined_at: Optional path to the file where you defined this project.
89 To get a useful ``build_fpga.py --list`` message. Is useful when you have many
90 projects set up.
91 other_arguments: Optional further arguments. Will not be used by tsfpga, but will
92 instead be passed on to
94 * :func:`BaseModule.get_synthesis_files()
95 <tsfpga.module.BaseModule.get_synthesis_files>`
96 * :func:`BaseModule.get_ip_core_files()
97 <tsfpga.module.BaseModule.get_ip_core_files>`
98 * :func:`BaseModule.get_scoped_constraints()
99 <tsfpga.module.BaseModule.get_scoped_constraints>`
100 * :func:`VivadoProject.pre_create`
101 * :func:`BaseModule.pre_build() <tsfpga.module.BaseModule.pre_build>`
102 * :func:`VivadoProject.pre_build`
103 * :func:`VivadoProject.post_build`
105 along with further arguments supplied at build-time to :meth:`.create` and
106 :meth:`.build`.
108 .. note::
109 This is a "kwargs" style argument. You can pass any number of named arguments.
110 """
111 self.name = name
112 self.modules = modules.copy()
113 self.part = part
114 self.static_generics = {} if generics is None else generics.copy()
115 self.constraints = [] if constraints is None else constraints.copy()
116 self.tcl_sources = [] if tcl_sources is None else tcl_sources.copy()
117 self.build_step_hooks = [] if build_step_hooks is None else build_step_hooks.copy()
118 self._vivado_path = vivado_path
119 self.default_run_index = default_run_index
120 self.impl_explore = impl_explore
121 self.defined_at = defined_at
122 self.other_arguments = None if other_arguments is None else other_arguments.copy()
124 # Will be set by subclass when applicable
125 self.is_netlist_build = False
126 self.analyze_synthesis_timing = True
127 self.report_logic_level_distribution = False
128 self.ip_cores_only = False
130 self.top = name + "_top" if top is None else top
132 self.tcl = VivadoTcl(name=self.name)
134 for constraint in self.constraints:
135 if not isinstance(constraint, Constraint):
136 raise TypeError(f'Got bad type for "constraints" element: {constraint}')
138 for tcl_source in self.tcl_sources:
139 if not isinstance(tcl_source, Path):
140 raise TypeError(f'Got bad type for "tcl_sources" element: {tcl_source}')
142 for build_step_hook in self.build_step_hooks:
143 if not isinstance(build_step_hook, BuildStepTclHook):
144 raise TypeError(f'Got bad type for "build_step_hooks" element: {build_step_hook}')
146 def project_file(self, project_path: Path) -> Path:
147 """
148 Arguments:
149 project_path: A path containing a Vivado project.
151 Return:
152 The project file of this project, in the given folder
153 """
154 return project_path / (self.name + ".xpr")
156 def _setup_tcl_sources(self) -> None:
157 tsfpga_tcl_sources = [
158 TSFPGA_TCL / "vivado_default_run.tcl",
159 TSFPGA_TCL / "vivado_fast_run.tcl",
160 TSFPGA_TCL / "vivado_messages.tcl",
161 ]
163 if self.impl_explore:
164 tsfpga_tcl_sources.append(TSFPGA_TCL / "vivado_strategies.tcl")
166 # Add tsfpga TCL sources first. The user might want to change something in the tsfpga
167 # settings. Conversely, tsfpga should not modify something that the user has set up.
168 self.tcl_sources = tsfpga_tcl_sources + self.tcl_sources
170 def _setup_build_step_hooks(self) -> None:
171 # Check that no ERROR messages have been sent by Vivado. After synthesis as well as
172 # after implementation.
173 self.build_step_hooks.append(
174 BuildStepTclHook(
175 TSFPGA_TCL / "check_no_error_messages.tcl", "STEPS.SYNTH_DESIGN.TCL.POST"
176 )
177 )
178 self.build_step_hooks.append(
179 BuildStepTclHook(
180 TSFPGA_TCL / "check_no_error_messages.tcl", "STEPS.WRITE_BITSTREAM.TCL.PRE"
181 )
182 )
184 # Check the implemented timing and resource utilization via TCL build hooks.
185 # This is different than for synthesis, where it is embedded in the build script.
186 # This is due to Vivado limitations related to post-synthesis hooks.
187 # Specifically, the report_utilization figures do not include IP cores when it is run in
188 # a post-synthesis hook.
189 self.build_step_hooks.append(
190 BuildStepTclHook(TSFPGA_TCL / "report_utilization.tcl", "STEPS.WRITE_BITSTREAM.TCL.PRE")
191 )
192 self.build_step_hooks.append(
193 BuildStepTclHook(TSFPGA_TCL / "check_timing.tcl", "STEPS.WRITE_BITSTREAM.TCL.PRE")
194 )
195 self.build_step_hooks.append(
196 BuildStepTclHook(TSFPGA_TCL / "check_cdc.tcl", "STEPS.WRITE_BITSTREAM.TCL.PRE")
197 )
199 if not self.analyze_synthesis_timing:
200 # In this special case however, the synthesized design is never opened (to save
201 # execution time), meaning 'report_utilization' is not run by the
202 # 'build_vivado_project.tcl' script.
203 # So in order to get a utilization report anyway we add it as a hook.
204 # This mode is exclusively used by netlist builds, which very rarely include IP cores,
205 # so it is acceptable that the utilization report might be erroneous with regards to
206 # IP cores.
207 self.build_step_hooks.append(
208 BuildStepTclHook(
209 TSFPGA_TCL / "report_utilization.tcl", "STEPS.SYNTH_DESIGN.TCL.POST"
210 )
211 )
213 if self.report_logic_level_distribution:
214 # Used by netlist builds
215 self.build_step_hooks.append(
216 BuildStepTclHook(
217 TSFPGA_TCL / "report_logic_level_distribution.tcl",
218 "STEPS.SYNTH_DESIGN.TCL.POST",
219 )
220 )
222 def _create_tcl(
223 self, project_path: Path, ip_cache_path: Path | None, all_arguments: dict[str, Any]
224 ) -> Path:
225 """
226 Make a TCL file that creates a Vivado project
227 """
228 if project_path.exists():
229 raise ValueError(f"Folder already exists: {project_path}")
230 project_path.mkdir(parents=True)
232 create_vivado_project_tcl = project_path / "create_vivado_project.tcl"
233 tcl = self.tcl.create(
234 project_folder=project_path,
235 modules=self.modules,
236 part=self.part,
237 top=self.top,
238 run_index=self.default_run_index,
239 generics=self.static_generics,
240 constraints=self.constraints,
241 tcl_sources=self.tcl_sources,
242 build_step_hooks=self.build_step_hooks,
243 ip_cache_path=ip_cache_path,
244 disable_io_buffers=self.is_netlist_build,
245 ip_cores_only=self.ip_cores_only,
246 other_arguments=all_arguments,
247 )
248 create_file(create_vivado_project_tcl, tcl)
250 return create_vivado_project_tcl
252 def create(
253 self,
254 project_path: Path,
255 ip_cache_path: Path | None = None,
256 **other_arguments: Any, # noqa: ANN401
257 ) -> bool:
258 """
259 Create a Vivado project
261 Arguments:
262 project_path: Path where the project shall be placed.
263 ip_cache_path: Path to a folder where the Vivado IP cache can be
264 placed. If omitted, the Vivado IP cache mechanism will not be enabled.
265 other_arguments: Optional further arguments. Will not be used by tsfpga, but will
266 instead be sent to
268 * :func:`BaseModule.get_synthesis_files()
269 <tsfpga.module.BaseModule.get_synthesis_files>`
270 * :func:`BaseModule.get_ip_core_files()
271 <tsfpga.module.BaseModule.get_ip_core_files>`
272 * :func:`BaseModule.get_scoped_constraints()
273 <tsfpga.module.BaseModule.get_scoped_constraints>`
274 * :func:`VivadoProject.pre_create`
276 along with further ``other_arguments`` supplied to :meth:`.__init__`.
278 .. note::
279 This is a "kwargs" style argument. You can pass any number of named arguments.
281 Return:
282 True if everything went well.
283 """
284 print(f"Creating Vivado project in {project_path}")
285 self._setup_tcl_sources()
286 self._setup_build_step_hooks()
288 # The pre-create hook might have side effects. E.g. change some register constants.
289 # So we make a deep copy of the module list before the hook is called.
290 # Note that the modules are copied before the pre-build hooks as well,
291 # since we do not know if we might be performing a create-only or
292 # build-only operation. The copy does not take any significant time, so this is not
293 # an issue.
294 self.modules = deepcopy(self.modules)
296 # Send all available arguments that are reasonable to use in pre-create and module getter
297 # functions. Prefer run-time values over the static.
298 all_arguments = copy_and_combine_dicts(self.other_arguments, other_arguments)
299 all_arguments.update(generics=self.static_generics, part=self.part)
301 if not self.pre_create(
302 project_path=project_path, ip_cache_path=ip_cache_path, **all_arguments
303 ):
304 print("ERROR: Project pre-create hook returned False. Failing the build.")
305 return False
307 create_vivado_project_tcl = self._create_tcl(
308 project_path=project_path, ip_cache_path=ip_cache_path, all_arguments=all_arguments
309 )
310 return run_vivado_tcl(self._vivado_path, create_vivado_project_tcl)
312 def pre_create(
313 self,
314 **kwargs: Any, # noqa: ANN401, ARG002
315 ) -> bool:
316 """
317 Override this function in a subclass if you wish to do something useful with it.
318 Will be called from :meth:`.create` right before the call to Vivado.
320 An example use case for this function is when TCL source scripts for the Vivado project
321 have to be auto generated. This could e.g. be scripts that set IP repo paths based on the
322 Vivado system PATH.
324 .. Note::
325 This default method does nothing. Shall be overridden by project that utilize
326 this mechanism.
328 Arguments:
329 kwargs: Will have all the :meth:`.create` parameters in it, as well as everything in
330 the ``other_arguments`` argument to :func:`VivadoProject.__init__`.
332 Return:
333 True if everything went well.
334 """
335 return True
337 def _build_tcl( # noqa: PLR0913
338 self,
339 project_path: Path,
340 output_path: Path,
341 num_threads: int,
342 run_index: int,
343 all_generics: dict[str, Any],
344 synth_only: bool,
345 from_impl: bool,
346 impl_explore: bool,
347 ) -> Path:
348 """
349 Make a TCL file that builds a Vivado project
350 """
351 project_file = self.project_file(project_path)
352 if not project_file.exists():
353 raise ValueError(
354 f"Project file does not exist in the specified location: {project_file}"
355 )
357 build_vivado_project_tcl = project_path / "build_vivado_project.tcl"
358 tcl = self.tcl.build(
359 project_file=project_file,
360 output_path=output_path,
361 num_threads=num_threads,
362 run_index=run_index,
363 generics=all_generics,
364 synth_only=synth_only,
365 from_impl=from_impl,
366 analyze_synthesis_timing=self.analyze_synthesis_timing,
367 impl_explore=impl_explore,
368 )
369 create_file(build_vivado_project_tcl, tcl)
371 return build_vivado_project_tcl
373 def pre_build(
374 self,
375 **kwargs: Any, # noqa: ANN401, ARG002
376 ) -> bool:
377 """
378 Override this function in a subclass if you wish to do something useful with it.
379 Will be called from :meth:`.build` right before the call to Vivado.
381 Arguments:
382 kwargs: Will have all the :meth:`.build` parameters in it. Including additional
383 parameters from the user.
385 Return:
386 True if everything went well.
387 """
388 return True
390 def post_build(
391 self,
392 **kwargs: Any, # noqa: ANN401, ARG002
393 ) -> bool:
394 """
395 Override this function in a subclass if you wish to do something useful with it.
396 Will be called from :meth:`.build` right after the call to Vivado.
398 An example use case for this function is to encrypt the bit file, or generate any other
399 material that shall be included in FPGA release artifacts.
401 .. Note::
402 This default method does nothing. Shall be overridden by project that utilize
403 this mechanism.
405 Arguments:
406 kwargs: Will have all the :meth:`.build` parameters in it. Including additional
407 parameters from the user. Will also include ``build_result`` with
408 implemented/synthesized size, which can be used for asserting the expected resource
409 utilization.
411 Return:
412 True if everything went well.
413 """
414 return True
416 def build( # noqa: C901, PLR0912, PLR0913
417 self,
418 project_path: Path,
419 output_path: Path | None = None,
420 run_index: int | None = None,
421 generics: dict[str, Any] | None = None,
422 synth_only: bool = False,
423 from_impl: bool = False,
424 num_threads: int = 12,
425 **pre_and_post_build_parameters: Any, # noqa: ANN401
426 ) -> BuildResult:
427 """
428 Build a Vivado project
430 Arguments:
431 project_path: A path containing a Vivado project.
432 output_path: Results (bit file, ...) will be placed here.
433 run_index: Select Vivado run (synth_X and impl_X) to build with.
434 generics: A dict with generics values (`dict(name: value)`). Use for run-time
435 generics, i.e. values that can change between each build of this project.
437 Compare to the create-time generics argument in :meth:`.__init__`.
439 The generic value types follow the same rules as for :meth:`.__init__`.
440 synth_only: Run synthesis and then stop.
441 from_impl: Run the ``impl`` steps and onward on an existing synthesized design.
442 num_threads: Number of parallel threads to use during run.
443 pre_and_post_build_parameters: Optional further arguments. Will not be used by tsfpga,
444 but will instead be sent to
446 * :func:`BaseModule.pre_build() <tsfpga.module.BaseModule.pre_build>`
447 * :func:`VivadoProject.pre_build`
448 * :func:`VivadoProject.post_build`
450 along with further ``other_arguments`` supplied to :meth:`.__init__`.
452 .. note::
453 This is a "kwargs" style argument. You can pass any number of named arguments.
455 Return:
456 Result object with build information.
457 """
458 synth_only = synth_only or self.is_netlist_build
460 if output_path is None and not synth_only:
461 raise ValueError("Must specify output_path when doing an implementation run")
463 if synth_only:
464 print(f"Synthesizing Vivado project in {project_path}")
465 else:
466 print(f"Building Vivado project in {project_path}, placing artifacts in {output_path}")
468 # Combine to all available generics. Prefer run-time values over static.
469 all_generics = copy_and_combine_dicts(self.static_generics, generics)
471 # Run index is optional to specify at build-time
472 run_index = self.default_run_index if run_index is None else run_index
474 # Send all available information to pre- and post build functions. Prefer build-time values
475 # over the static arguments.
476 all_parameters = copy_and_combine_dicts(self.other_arguments, pre_and_post_build_parameters)
477 all_parameters.update(
478 project_path=project_path,
479 output_path=output_path,
480 run_index=run_index,
481 generics=all_generics,
482 synth_only=synth_only,
483 from_impl=from_impl,
484 num_threads=num_threads,
485 )
487 # The pre-build hooks (either project pre-build hook or any of the module's pre-build hooks)
488 # might have side effects. E.g. change some register constants. So we make a deep copy of
489 # the module list before any of these hooks are called. Note that the modules are copied
490 # before the pre-create hook as well, since we do not know if we might be performing a
491 # create-only or build-only operation. The copy does not take any significant time, so this
492 # is not an issue.
493 self.modules = deepcopy(self.modules)
495 result = BuildResult(self.name)
497 for module in self.modules:
498 if not module.pre_build(project=self, **all_parameters):
499 print(
500 f"ERROR: Module {module.name} pre-build hook returned False. Failing the build."
501 )
502 result.success = False
503 return result
505 # Make sure register packages are up to date
506 module.create_register_synthesis_files()
508 if not self.pre_build(**all_parameters):
509 print("ERROR: Project pre-build hook returned False. Failing the build.")
510 result.success = False
511 return result
513 # We ignore the type of 'output_path' going from 'Path | None' to 'Path'.
514 # It is only used if 'synth_only' is False, and we have an assertion that 'output_path' is
515 # not None in that case above.
517 build_vivado_project_tcl = self._build_tcl(
518 project_path=project_path,
519 output_path=output_path,
520 num_threads=num_threads,
521 run_index=run_index,
522 all_generics=all_generics,
523 synth_only=synth_only,
524 from_impl=from_impl,
525 impl_explore=self.impl_explore,
526 )
528 if not run_vivado_tcl(self._vivado_path, build_vivado_project_tcl):
529 result.success = False
530 return result
532 result.synthesis_size = self._get_size(project_path, f"synth_{run_index}")
533 if self.report_logic_level_distribution:
534 result.logic_level_distribution = self._get_logic_level_distribution(
535 project_path, f"synth_{run_index}"
536 )
538 if not synth_only:
539 if self.impl_explore:
540 runs_path = project_path / f"{self.name}.runs"
541 for run in runs_path.iterdir():
542 if "impl_explore_" in run.resolve().name:
543 # Check files for existence, since not all runs may have completed
544 bit_file = run / f"{self.top}.bit"
545 bin_file = run / f"{self.top}.bin"
546 if bit_file.exists() or bin_file.exists():
547 impl_folder = run
548 run_name = run.resolve().name
549 break
550 else:
551 run_name = f"impl_{run_index}"
552 impl_folder = project_path / f"{self.name}.runs" / run_name
553 bit_file = impl_folder / f"{self.top}.bit"
554 bin_file = impl_folder / f"{self.top}.bin"
556 shutil.copy2(bit_file, output_path / f"{self.name}.bit")
557 shutil.copy2(bin_file, output_path / f"{self.name}.bin")
558 result.implementation_size = self._get_size(project_path, run_name)
560 # Send the result object, along with everything else, to the post-build function
561 all_parameters.update(build_result=result)
563 if not self.post_build(**all_parameters):
564 print("ERROR: Project post-build hook returned False. Failing the build.")
565 result.success = False
567 return result
569 def open(self, project_path: Path) -> bool:
570 """
571 Open the project in Vivado GUI.
573 Arguments:
574 project_path: A path containing a Vivado project.
576 Return:
577 True if everything went well.
578 """
579 return run_vivado_gui(self._vivado_path, self.project_file(project_path))
581 def _get_size(self, project_path: Path, run: str) -> dict[str, int]:
582 """
583 Reads the hierarchical utilization report and returns the top level size
584 for the specified run.
585 """
586 report_as_string = read_file(
587 project_path / f"{self.name}.runs" / run / "hierarchical_utilization.rpt"
588 )
589 return HierarchicalUtilizationParser.get_size(report_as_string)
591 def _get_logic_level_distribution(self, project_path: Path, run: str) -> str:
592 """
593 Reads the hierarchical utilization report and returns the top level size
594 for the specified run.
595 """
596 report_as_string = read_file(
597 project_path / f"{self.name}.runs" / run / "logical_level_distribution.rpt"
598 )
599 return LogicLevelDistributionParser.get_table(report_as_string)
601 def __str__(self) -> str:
602 result = f"{self.name}\n"
604 if self.defined_at is not None:
605 result += f"Defined at: {self.defined_at.resolve()}\n"
607 result += f"Type: {self.__class__.__name__}\n"
608 result += f"Top level: {self.top}\n"
610 generics = self._dict_to_string(self.static_generics) if self.static_generics else "-"
611 result += f"Generics: {generics}\n"
613 if self.other_arguments:
614 result += f"Arguments: {self._dict_to_string(self.other_arguments)}\n"
616 return result
618 @staticmethod
619 def _dict_to_string(data: dict[str, Any]) -> str:
620 return ", ".join([f"{name}={value}" for name, value in data.items()])
623class VivadoNetlistProject(VivadoProject):
624 """
625 Used for handling Vivado build of a module without top level pinning.
626 """
628 def __init__(
629 self,
630 analyze_synthesis_timing: bool = False,
631 build_result_checkers: list[SizeChecker | MaximumLogicLevel] | None = None,
632 **kwargs: Any, # noqa: ANN401
633 ) -> None:
634 """
635 Arguments:
636 analyze_synthesis_timing: Enable analysis of the synthesized design's timing.
637 This will make the build flow open the design, and check for unhandled clock
638 crossings and pulse width violations.
639 Enabling it will add significant build time (can be as much as +40%).
640 Also, in order for clock crossing check to work, the clocks have to be created
641 using a constraint file.
642 build_result_checkers:
643 Checkers that will be executed after a successful build. Is used to automatically
644 check that e.g. resource utilization is not greater than expected.
645 kwargs: Further arguments accepted by :meth:`.VivadoProject.__init__`.
646 """
647 super().__init__(**kwargs)
649 self.is_netlist_build = True
650 self.analyze_synthesis_timing = analyze_synthesis_timing
651 self.report_logic_level_distribution = True
652 self.build_result_checkers = [] if build_result_checkers is None else build_result_checkers
654 def build(
655 self,
656 **kwargs: Any, # noqa: ANN401
657 ) -> BuildResult:
658 """
659 Build the project.
661 Arguments:
662 kwargs: All arguments as accepted by :meth:`.VivadoProject.build`.
663 """
664 result = super().build(**kwargs)
665 result.success = result.success and self._check_size(result)
667 return result
669 def _check_size(self, build_result: BuildResult) -> bool:
670 if not build_result.success:
671 print(f"Can not do post_build check for {self.name} since it did not succeed.")
672 return False
674 success = True
675 for build_result_checker in self.build_result_checkers:
676 checker_result = build_result_checker.check(build_result)
677 success = success and checker_result
679 return success
682class VivadoIpCoreProject(VivadoProject):
683 """
684 A Vivado project that is only used to generate simulation models of IP cores.
685 """
687 ip_cores_only = True
689 def __init__(
690 self,
691 **kwargs: Any, # noqa: ANN401
692 ) -> None:
693 """
694 Arguments:
695 kwargs: Arguments as accepted by :meth:`.VivadoProject.__init__`.
696 """
697 super().__init__(**kwargs)
699 def build(
700 self,
701 **kwargs: Any, # noqa: ANN401
702 ) -> NoReturn:
703 """
704 Not implemented.
705 """
706 raise NotImplementedError("IP core project can not be built")
709def copy_and_combine_dicts(
710 dict_first: dict[str, Any] | None, dict_second: dict[str, Any] | None
711) -> dict[str, Any]:
712 """
713 Will prefer values in the second dict, in case the same key occurs in both.
714 Will return an empty dictionary if both are ``None``.
715 """
716 if dict_first is None:
717 if dict_second is None:
718 return {}
720 return dict_second.copy()
722 if dict_second is None:
723 return dict_first.copy()
725 result = dict_first.copy()
726 result.update(dict_second)
728 return result