Coverage for tsfpga/yosys/project.py: 95%
273 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-09 00:40 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-09 00:40 +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 os
12import sys
13import tempfile
14from contextlib import contextmanager
15from copy import deepcopy
16from pathlib import Path
17from threading import Lock
18from typing import TYPE_CHECKING, Any, NoReturn
20from vunit.ui import VUnit
22from tsfpga.generics import (
23 BitVectorGenericValue,
24 GenericValue,
25 GenericValues,
26 StringGenericValue,
27)
28from tsfpga.hdl_file import HdlFile
29from tsfpga.system_utils import (
30 copy_and_combine_dicts,
31 create_directory,
32 create_file,
33 read_file,
34)
36from .build_result import YosysBuildResult
37from .common import run_ghdl, run_yosys, to_yosys_path
38from .utilization_parser import (
39 YosysIntelUtilizationParser,
40 YosysMicrochipUtilizationParser,
41 YosysUtilizationParser,
42 YosysXilinxUtilizationParser,
43)
45if TYPE_CHECKING:
46 from collections.abc import Iterator
48 from tsfpga.module import BaseModule
49 from tsfpga.module_list import ModuleList
50 from tsfpga.vivado.build_result_checker import SizeChecker
53class YosysNetlistBuild:
54 """
55 Used for handling a synthesis-only (netlist) build of a design, using Yosys with the
56 ``ghdl-yosys-plugin`` as the VHDL front end.
58 Since this is a netlist build, there is no implementation (place & route) step, and hence
59 no bitstream is produced.
60 This is a great tool for getting quick feedback on the resource utilization of a design, or
61 a sub-component of a design, during development.
63 The ``top`` is typically a VHDL entity, in which case all of its VHDL dependencies are found
64 automatically via the compile order.
65 It can also be a Verilog/SystemVerilog module (or the design can have no VHDL at all), in
66 which case any VHDL entities that shall be instantiated from it (or from other VHDL
67 entities) must be explicitly listed using the ``vhdl_entities`` argument.
69 .. note::
70 Requires GHDL, Yosys, and the ``ghdl-yosys-plugin`` module to be installed and
71 available.
72 See the `ghdl-yosys-plugin documentation
73 <https://github.com/ghdl/ghdl-yosys-plugin>`__ for installation instructions.
74 """
76 #: Will always be ``True`` for this class, since it is a netlist build.
77 is_netlist_build = True
79 #: The parser used to interpret the Yosys utilization report.
80 #: The base class uses the one that reports only raw cell counts, since the plain ``synth``
81 #: command does not target any specific architecture.
82 #: Overridden by the architecture-specific subclasses (:class:`.YosysXilinxNetlistBuild`,
83 #: :class:`.YosysIntelNetlistBuild`, :class:`.YosysMicrochipNetlistBuild`).
84 _utilization_parser: type[YosysUtilizationParser] = YosysUtilizationParser
86 #: The Yosys ``synth*`` command used to synthesize the design.
87 #: The base class uses the generic ``synth`` command, which does not target any specific
88 #: architecture.
89 #: Overridden by the architecture-specific subclasses. Can also be set when subclassing, e.g.
90 #: to ``"synth_intel_alm"`` to target an architecture that has no subclass of its own.
91 _synth_command: str = "synth"
93 #: Further arguments to the ``synth*`` command, placed before its ``-top`` argument.
94 #: Can be set when subclassing, e.g. to pass an option that this class has no argument for.
95 #: Note that this is a tuple, so that it is never modified in place and shared between
96 #: objects. A subclass that adds arguments in its constructor assigns a new tuple instead.
97 _synth_arguments: tuple[str, ...] = ()
99 #: Whether the ``_synth_command`` needs an explicit ``-flatten`` flag appended to it in order
100 #: to flatten the design before synthesis (see :meth:`._get_synth_command`).
101 #: This is the case for e.g. the ``synth`` and ``synth_xilinx`` commands.
102 #: Some ``synth_*`` commands (e.g. ``synth_intel`` and ``synth_microchip``) instead flatten
103 #: the design by default, and do not accept a ``-flatten`` flag. Such subclasses shall set
104 #: this attribute to ``False``.
105 _needs_explicit_flatten_flag: bool = True
107 def __init__( # noqa: PLR0913, PLR0917
108 self,
109 name: str,
110 modules: ModuleList,
111 top: str | None = None,
112 vhdl_entities: list[str] | None = None,
113 generics: GenericValues | None = None,
114 build_result_checkers: list[SizeChecker] | None = None,
115 vhdl_standard: str = "08",
116 ghdl_path: Path | None = None,
117 yosys_path: Path | None = None,
118 ghdl_plugin_path: Path | None = None,
119 ghdl_prefix: Path | None = None,
120 defined_at: Path | None = None,
121 **other_arguments: Any, # noqa: ANN401
122 ) -> None:
123 """
124 Class constructor. Performs a shallow copy of the mutable arguments, so that the user
125 can e.g. append items to their list after creating an object.
127 Arguments:
128 name: Project name.
129 modules: Modules that shall be included in the build.
130 Only VHDL source files are considered, since the build uses the GHDL front end.
131 top: Name of top level entity.
132 If left out, the top level name will be inferred from the ``name``.
133 Is typically a VHDL entity, but can also be a Verilog/SystemVerilog module -- see
134 ``vhdl_entities`` below.
135 vhdl_entities: Only used if ``top`` is not a VHDL entity (i.e. if it is a
136 Verilog/SystemVerilog module, or if the design has no VHDL at all).
137 A list of the names of the VHDL entities that shall be made available for
138 instantiation from the non-VHDL top level (or from other VHDL entities), since
139 there is in that case no single VHDL top level to automatically find these
140 dependencies from.
141 Not used, and shall be left as ``None``, if ``top`` is a VHDL entity, since in
142 that case all of its VHDL dependencies are found automatically via the compile
143 order.
144 generics: A dict with generics values (name: value). Use this parameter
145 for "static" generics that do not change between multiple builds of this
146 project.
148 Compare to the build-time generic argument in :meth:`build`.
150 The generic value shall be of type :data:`.GenericValue`.
151 build_result_checkers:
152 Checkers that will be executed after a successful build. Is used to automatically
153 check that e.g. resource utilization is not greater than expected.
154 vhdl_standard: The VHDL standard that shall be used by GHDL when analyzing the
155 source files (e.g. ``"93"`` or ``"08"``).
156 ghdl_path: Path to the GHDL executable.
157 If omitted, the default location from the system PATH will be used.
158 yosys_path: Path to the Yosys executable.
159 If omitted, the default location from the system PATH will be used.
160 ghdl_plugin_path: Path to the ``ghdl-yosys-plugin`` module (typically named
161 ``ghdl.so``).
162 Can be left out if the plugin is already available to Yosys without explicitly
163 loading it (e.g. if it has been installed in the Yosys plugin directory).
164 ghdl_prefix: Value to set the ``GHDL_PREFIX`` environment variable to when running
165 Yosys with the ``ghdl-yosys-plugin``. The plugin is loaded as part of the Yosys
166 process, and can not find the GHDL standard libraries (``std``, ``ieee``, ...)
167 on its own the way the standalone ``ghdl`` executable can.
168 Corresponds to the "library prefix" printed by ``ghdl --disp-config``.
169 If left out, this is auto-detected by calling ``ghdl --disp-config`` against
170 the executable given by ``ghdl_path`` above.
171 Set explicitly to override the auto-detected value, or if auto-detection fails.
172 defined_at: Optional path to the file where you defined this project.
173 To get a useful ``build_fpga.py --list`` message. Is useful when you have many
174 projects set up.
175 other_arguments: Optional further arguments. Will not be used by tsfpga, but will
176 instead be passed on to
178 * :func:`BaseModule.get_synthesis_files()
179 <tsfpga.module.BaseModule.get_synthesis_files>`
180 * :func:`BaseModule.pre_build() <tsfpga.module.BaseModule.pre_build>`
181 * :func:`YosysNetlistBuild.pre_create`
182 * :func:`YosysNetlistBuild.pre_build`
183 * :func:`YosysNetlistBuild.post_build`
185 along with further arguments supplied at build-time to :meth:`.create` and
186 :meth:`.build`.
188 .. note::
189 This is a "kwargs" style argument. You can pass any number of named arguments.
190 """
191 self.name = name
192 self.modules = modules.copy()
193 self.top = name + "_top" if top is None else top
194 self.vhdl_entities = [] if vhdl_entities is None else list(vhdl_entities)
195 self.static_generics = {} if generics is None else generics.copy()
196 self.build_result_checkers = (
197 [] if build_result_checkers is None else build_result_checkers.copy()
198 )
199 self.defined_at = defined_at
200 self.other_arguments = None if other_arguments is None else other_arguments.copy()
202 self._vhdl_standard = vhdl_standard
203 self._ghdl_path = ghdl_path
204 self._yosys_path = yosys_path
205 self._ghdl_plugin_path = ghdl_plugin_path
206 self._ghdl_prefix = ghdl_prefix
208 # Lazily created/cached. See '_get_vunit_project'.
209 self._vunit_proj: VUnit | None = None
210 # Owns the VUnit project's throwaway output directory: holding it here ties that
211 # directory's lifetime to this object's.
212 self._vunit_output_dir: tempfile.TemporaryDirectory[str] | None = None
214 def project_file(self, project_path: Path) -> Path:
215 """
216 Arguments:
217 project_path: A path containing a Yosys netlist build.
219 Return:
220 The Yosys command script of this build, in the given folder.
221 """
222 return project_path / f"{self.name}.ys"
224 def _get_ghdl_workdir(self, project_path: Path) -> Path:
225 return project_path / "ghdl"
227 def _get_vunit_project(self) -> VUnit:
228 if self._vunit_proj is None:
229 # VUnit is used only to calculate the compile order of the VHDL source files.
230 # Cleanup errors are ignored because this is scratch space: failing to delete it
231 # must not fail a build.
232 self._vunit_output_dir = tempfile.TemporaryDirectory(
233 prefix="tsfpga_yosys_vunit_", ignore_cleanup_errors=True
234 )
235 argv = [
236 "--output-path",
237 self._vunit_output_dir.name,
238 "--log-level",
239 "error",
240 "--no-color",
241 ]
243 with _suppress_stdout():
244 # Note that VUnit's builtins are not compiled, since that is only done when
245 # 'add_vhdl_builtins' is called. They would add dozens of VUnit-internal VHDL
246 # files to the compile order.
247 self._vunit_proj = VUnit.from_argv(argv=argv)
249 for module in self.modules:
250 vunit_library = self._vunit_proj.add_library(
251 library_name=module.library_name, allow_duplicate=True
252 )
253 for hdl_file in module.get_synthesis_files(
254 include_verilog_files=False, include_systemverilog_files=False
255 ):
256 vunit_library.add_source_file(hdl_file.path)
258 return self._vunit_proj
260 def _find_vhdl_source_file(self, entity_name: str) -> tuple[BaseModule, HdlFile] | None:
261 """
262 Arguments:
263 entity_name: Name of a VHDL entity.
265 Return: A tuple ``(module, hdl_file)`` for the VHDL source file that defines the given
266 entity name, or ``None`` if no such VHDL source file is found in the modules of
267 this build.
268 """
269 matches = [
270 (module, hdl_file)
271 for module in self.modules
272 for hdl_file in module.get_synthesis_files(
273 include_verilog_files=False, include_systemverilog_files=False
274 )
275 if hdl_file.path.stem == entity_name
276 ]
278 if len(matches) > 1:
279 raise ValueError(f'Found multiple VHDL source files for entity "{entity_name}".')
281 return matches[0] if matches else None
283 def _find_top_vhdl_source_file(self) -> tuple[BaseModule, HdlFile] | None:
284 """
285 Return: The module and source file of the ``top``, if it is a VHDL entity.
286 ``None`` otherwise, e.g. if it is a Verilog/SystemVerilog module.
287 """
288 return self._find_vhdl_source_file(entity_name=self.top)
290 @property
291 def _top_is_vhdl(self) -> bool:
292 """
293 Whether the ``top`` is a VHDL entity, as opposed to e.g. a Verilog/SystemVerilog module.
294 """
295 return self._find_top_vhdl_source_file() is not None
297 def _get_vhdl_files_in_compile_order(self) -> list[tuple[str, str]]:
298 """
299 Return: A list of tuples ``(file_path, library_name)`` in the order they need to be
300 analyzed by GHDL.
301 """
302 vunit_proj = self._get_vunit_project()
303 top_level_match = self._find_top_vhdl_source_file()
305 if top_level_match is None:
306 # The 'top' is not a VHDL entity (e.g. it is a Verilog/SystemVerilog module, or the
307 # design has no VHDL at all). There is no single VHDL top level to compute an
308 # implementation subset relative to, so analyze all the VHDL source files in the
309 # modules of this build instead.
310 compile_order = vunit_proj.get_compile_order()
311 else:
312 # Look up the source file directly by its (unique, resolved) path, rather than
313 # reconstructing a file name pattern, since the VHDL file ending can be either
314 # '.vhd', '.vhdl' or '.vho' (see 'HdlFile.file_endings_mapping').
315 _, top_hdl_file = top_level_match
316 top_source_file = vunit_proj.get_source_file(file_name=top_hdl_file.path.resolve())
317 compile_order = vunit_proj.get_implementation_subset(source_files=[top_source_file])
319 return [
320 (Path(source_file.name).resolve().as_posix(), source_file.library.name)
321 for source_file in compile_order
322 ]
324 def _get_verilog_source_files(self) -> list[Path]:
325 """
326 Return: A list of paths to all the Verilog and SystemVerilog source files (not headers)
327 found in the modules of this build.
328 These are read directly by Yosys, bypassing GHDL entirely, and may be instantiated
329 from the VHDL top level as unbound components with a matching name.
330 """
331 source_types = (HdlFile.Type.VERILOG_SOURCE, HdlFile.Type.SYSTEMVERILOG_SOURCE)
333 return [
334 hdl_file.path
335 for module in self.modules
336 for hdl_file in module.get_synthesis_files(include_vhdl_files=False)
337 if hdl_file.type in source_types
338 ]
340 def _get_verilog_include_directories(self) -> list[Path]:
341 """
342 Return: A sorted list of the unique directories that contain Verilog and SystemVerilog
343 header files in the modules of this build.
344 Used so that Yosys can resolve ```` `include ```` directives in the source files.
345 """
346 header_types = (HdlFile.Type.VERILOG_HEADER, HdlFile.Type.SYSTEMVERILOG_HEADER)
348 return sorted(
349 {
350 hdl_file.path.parent
351 for module in self.modules
352 for hdl_file in module.get_synthesis_files(include_vhdl_files=False)
353 if hdl_file.type in header_types
354 }
355 )
357 def _get_read_verilog_command(self) -> str | None:
358 """
359 Return: A Yosys ``read_verilog`` command that reads all the Verilog and SystemVerilog
360 source files found in the modules of this build, or ``None`` if there are no such
361 files.
362 """
363 verilog_files = self._get_verilog_source_files()
364 if not verilog_files:
365 return None
367 # Note the asymmetry: file paths below are quoted, since Yosys splits unquoted command
368 # arguments on whitespace, which would otherwise break for paths containing spaces
369 # (e.g. common on Windows).
370 # Include directories can NOT be quoted, however. Yosys' 'read_verilog' takes the
371 # directory as everything after the "-I" in the token, so a quoted path ends up
372 # containing the quote characters and never matches a real directory.
373 # This means that include directories containing spaces are not supported by Yosys.
374 include_flags = " ".join(
375 f"-I{to_yosys_path(directory)}" for directory in self._get_verilog_include_directories()
376 )
377 file_arguments = " ".join(f'"{to_yosys_path(file_path)}"' for file_path in verilog_files)
379 # The '-sv' flag enables the SystemVerilog parser, which is a superset of Verilog and
380 # hence works fine for plain Verilog files as well.
381 command = f"read_verilog -sv {include_flags} {file_arguments}"
382 return " ".join(command.split())
384 def create(
385 self,
386 project_path: Path,
387 **other_arguments: Any, # noqa: ANN401
388 ) -> bool:
389 """
390 Analyze all the VHDL source files with GHDL, so that the design is ready to be
391 elaborated and synthesized by :meth:`.build`.
393 Arguments:
394 project_path: Path where the GHDL analysis result shall be placed.
395 other_arguments: Optional further arguments. Will not be used by tsfpga, but will
396 instead be sent to
398 * :func:`BaseModule.get_synthesis_files()
399 <tsfpga.module.BaseModule.get_synthesis_files>`
400 * :func:`YosysNetlistBuild.pre_create`
402 along with further ``other_arguments`` supplied to :meth:`.__init__`.
404 Return:
405 True if everything went well.
406 """
407 print(f"Creating Yosys netlist build {self.name} in {project_path}")
409 # The pre-create hook might have side effects. E.g. change some register constants.
410 # So we make a deep copy of the module list before the hook is called.
411 self.modules = deepcopy(self.modules)
413 all_arguments = copy_and_combine_dicts(self.other_arguments, other_arguments)
414 if not self.pre_create(project_path=project_path, **all_arguments):
415 print("ERROR: Project pre-create hook returned False. Failing the build.")
416 return False
418 return self._analyze_vhdl(workdir=self._get_ghdl_workdir(project_path=project_path))
420 def _analyze_vhdl(self, workdir: Path) -> bool:
421 """
422 Analyze all the VHDL source files into a GHDL work library, so that the design can be
423 elaborated by the ``ghdl-yosys-plugin``.
425 Arguments:
426 workdir: The GHDL work library location. Will be emptied first, so that the result
427 reflects the source files as they are right now.
429 Return:
430 True if everything went well.
431 """
432 create_directory(workdir, empty=True)
434 for file_path, library_name in self._get_vhdl_files_in_compile_order():
435 arguments = [
436 "-a",
437 f"--std={self._vhdl_standard}",
438 f"--workdir={workdir}",
439 f"-P={workdir}",
440 f"--work={library_name}",
441 file_path,
442 ]
444 if not run_ghdl(ghdl_path=self._ghdl_path, arguments=arguments, cwd=workdir):
445 print(f'ERROR: GHDL analysis failed for "{self.name}".')
446 return False
448 return True
450 def pre_create(
451 self,
452 **kwargs: Any, # noqa: ANN401, ARG002
453 ) -> bool:
454 """
455 Override this function in a subclass if you wish to do something useful with it.
456 Will be called from :meth:`.create` right before the GHDL analysis is started.
458 .. Note::
459 This default method does nothing. Shall be overridden by project that utilize
460 this mechanism.
462 Arguments:
463 kwargs: Will have all the :meth:`.create` parameters in it, as well as everything in
464 the ``other_arguments`` argument to :func:`YosysNetlistBuild.__init__`.
466 Return:
467 True if everything went well.
468 """
469 return True
471 def _get_synth_command(self) -> str:
472 # The design is flattened so that the produced utilization report contains the
473 # primitive counts for the whole design, and not just the top level.
474 flatten_flag = " -flatten" if self._needs_explicit_flatten_flag else ""
475 arguments = "".join(f" {argument}" for argument in self._synth_arguments)
476 return f"{self._synth_command}{arguments} -top {self.top}{flatten_flag}"
478 def _get_ghdl_elaborate_command(
479 self, workdir: Path, entity_name: str, library_name: str, generic_arguments: str
480 ) -> str:
481 # Note: Unlike the paths used in '_get_read_verilog_command' and '_get_yosys_script',
482 # the 'workdir' path below is deliberately *not* quoted. The 'ghdl' command, provided by
483 # the 'ghdl-yosys-plugin', tokenizes its own argument line by naive whitespace splitting
484 # and does not strip surrounding quotes, so quoting here would actually break paths that
485 # contain spaces even worse than leaving them unquoted (verified empirically). This is a
486 # limitation of the plugin, not something that can be worked around from this side.
487 parts = [
488 "ghdl",
489 f"--std={self._vhdl_standard}",
490 f"--workdir={workdir}",
491 f"-P={workdir}",
492 f"--work={library_name}",
493 ]
494 if generic_arguments:
495 parts.append(generic_arguments)
496 parts.append(entity_name)
498 return " ".join(parts)
500 def _get_ghdl_commands(
501 self,
502 workdir: Path,
503 all_generics: GenericValues,
504 ) -> list[str]:
505 """
506 Return: A list of Yosys ``ghdl`` commands that elaborate the VHDL entities of this
507 build, making them available to Yosys.
508 """
509 top_level_match = self._find_top_vhdl_source_file()
510 if top_level_match is not None:
511 # The 'top' is a VHDL entity: elaborate it directly. GHDL will pull in everything it
512 # depends on, including any Verilog/SystemVerilog modules read by
513 # '_get_read_verilog_command', which are bound by name to unbound VHDL component
514 # instantiations.
515 top_level_module, _ = top_level_match
516 entities = [(self.top, top_level_module)]
518 # Project generics target the project top level, which is this VHDL entity.
519 generic_arguments = " ".join(
520 f"-g{name}={_get_ghdl_generic_value(value)}" for name, value in all_generics.items()
521 )
522 else:
523 # The 'top' is a Verilog/SystemVerilog module (or the design has no VHDL at all).
524 # Elaborate each of the explicitly listed 'vhdl_entities' individually, so that they
525 # become available (under their own entity name) for Yosys's 'hierarchy' pass to
526 # bind to instantiations from the Verilog/SystemVerilog top level (or from other
527 # VHDL entities). Any entity that ends up unused is pruned by Yosys.
528 entities = []
529 for entity_name in self.vhdl_entities:
530 match = self._find_vhdl_source_file(entity_name=entity_name)
531 if match is None:
532 raise ValueError(
533 f'Could not find a VHDL source file for entity "{entity_name}" '
534 '(listed in "vhdl_entities").'
535 )
536 module, _ = match
537 entities.append((entity_name, module))
539 # The project generics target the Verilog/SystemVerilog top level, where they are
540 # applied as parameters by '_get_hierarchy_command'. They must not be passed to the
541 # entities listed in 'vhdl_entities', which are submodules that generally do not
542 # declare them. Per-entity generics are not supported.
543 generic_arguments = ""
545 return [
546 self._get_ghdl_elaborate_command(
547 workdir=workdir,
548 entity_name=entity_name,
549 library_name=module.library_name,
550 generic_arguments=generic_arguments,
551 )
552 for entity_name, module in entities
553 ]
555 def _get_hierarchy_command(self, all_generics: GenericValues) -> str | None:
556 """
557 Return: A Yosys ``hierarchy`` command that sets the top level parameters, when ``top`` is
558 a Verilog/SystemVerilog module. ``None`` when there is nothing to set, or when
559 ``top`` is a VHDL entity (where generics are instead passed to GHDL).
560 """
561 if not all_generics or self._top_is_vhdl:
562 return None
564 # Note that the 'synth' command runs 'hierarchy' itself, but the parameter values set
565 # here are preserved by that later run.
566 parameters = " ".join(
567 f"-chparam {name} {_get_verilog_parameter_value(value)}"
568 for name, value in all_generics.items()
569 )
570 return f"hierarchy -top {self.top} {parameters}"
572 def _get_yosys_script(
573 self,
574 workdir: Path,
575 all_generics: GenericValues,
576 utilization_report_file: Path,
577 ) -> str:
578 commands = []
580 # Read any Verilog/SystemVerilog source files before elaborating the VHDL, so that Yosys
581 # can bind the unbound components in the VHDL design to the modules read here.
582 read_verilog_command = self._get_read_verilog_command()
583 if read_verilog_command is not None:
584 commands.append(read_verilog_command)
586 commands += self._get_ghdl_commands(workdir=workdir, all_generics=all_generics)
588 hierarchy_command = self._get_hierarchy_command(all_generics=all_generics)
589 if hierarchy_command is not None:
590 commands.append(hierarchy_command)
592 commands += [
593 self._get_synth_command(),
594 f'tee -o "{to_yosys_path(utilization_report_file)}" stat',
595 ]
597 return "\n".join(commands) + "\n"
599 def build(
600 self,
601 project_path: Path,
602 output_path: Path | None = None,
603 generics: GenericValues | None = None,
604 **pre_and_post_build_parameters: Any, # noqa: ANN401
605 ) -> YosysBuildResult:
606 """
607 Synthesize the design with Yosys.
609 Arguments:
610 project_path: A path containing the result of a call to :meth:`.create`.
611 output_path: The utilization report, and any other artifacts, will be placed here.
612 Will default to ``project_path`` if not set.
613 generics: A dict with generics values (`dict(name: value)`). Use for run-time
614 generics, i.e. values that can change between each build of this project.
615 Compare to the create-time generics argument in :meth:`.__init__`.
616 The generic value types follow the same rules as for :meth:`.__init__`.
617 pre_and_post_build_parameters: Optional further arguments. Will not be used by
618 tsfpga, but will instead be sent to
620 * :func:`BaseModule.pre_build() <tsfpga.module.BaseModule.pre_build>`
621 * :func:`YosysNetlistBuild.pre_build`
622 * :func:`YosysNetlistBuild.post_build`
624 along with further ``other_arguments`` supplied to :meth:`.__init__`.
626 .. note::
627 This is a "kwargs" style argument. You can pass any number of named arguments.
629 Return:
630 Result object with build information.
631 """
632 workdir = self._get_ghdl_workdir(project_path=project_path)
633 if not workdir.exists():
634 raise ValueError(
635 f'Project "{self.name}" does not exist in the specified location: {project_path}. '
636 "Call 'create' before 'build'."
637 )
639 output_path = project_path if output_path is None else output_path
640 create_directory(output_path, empty=False)
642 print(f"Synthesizing Yosys netlist build {self.name} in {project_path}")
644 all_generics = copy_and_combine_dicts(self.static_generics, generics)
645 all_parameters = copy_and_combine_dicts(self.other_arguments, pre_and_post_build_parameters)
646 all_parameters.update(
647 project_path=project_path, output_path=output_path, generics=all_generics
648 )
650 # See 'create' for the rationale of doing this copy here as well.
651 self.modules = deepcopy(self.modules)
653 result = YosysBuildResult(name=self.name)
655 for module in self.modules:
656 if not module.pre_build(project=self, **all_parameters):
657 print(
658 f"ERROR: Module {module.name} pre-build hook returned False. Failing the build."
659 )
660 result.success = False
661 return result
663 # Make sure register packages are up to date.
664 module.create_register_synthesis_files()
666 if not self.pre_build(**all_parameters):
667 print("ERROR: Project pre-build hook returned False. Failing the build.")
668 result.success = False
669 return result
671 # The hooks above, and the register generation, may have changed the VHDL source files.
672 # A typical example is a 'pre_build' hook that updates register constants based on the
673 # build-time generics.
674 # Hence the design must be analyzed again here, rather than relying on what 'create'
675 # analyzed, which would elaborate stale sources.
676 # The cached VUnit project is dropped as well, since the set of source files, and their
677 # compile order, may have changed along with them.
678 self._vunit_proj = None
679 self._vunit_output_dir = None
681 if not self._analyze_vhdl(workdir=workdir):
682 result.success = False
683 return result
685 utilization_report_file = output_path / f"{self.name}_utilization.txt"
686 script = self._get_yosys_script(
687 workdir=workdir,
688 all_generics=all_generics,
689 utilization_report_file=utilization_report_file,
690 )
692 script_file = self.project_file(project_path=output_path)
693 create_file(script_file, script)
695 if not run_yosys(
696 yosys_path=self._yosys_path,
697 ghdl_plugin_path=self._ghdl_plugin_path,
698 script_file=script_file,
699 cwd=output_path,
700 ghdl_path=self._ghdl_path,
701 ghdl_prefix=self._ghdl_prefix,
702 ):
703 print(f'ERROR: Yosys synthesis failed for "{self.name}".')
704 result.success = False
705 return result
707 result.synthesis_size = self._get_size(utilization_report_file=utilization_report_file)
708 result.success = self._check_size(build_result=result)
710 # Send the result object, along with everything else, to the post-build function.
711 all_parameters.update(build_result=result)
713 if not self.post_build(**all_parameters):
714 print("ERROR: Project post-build hook returned False. Failing the build.")
715 result.success = False
717 return result
719 def pre_build(
720 self,
721 **kwargs: Any, # noqa: ANN401, ARG002
722 ) -> bool:
723 """
724 Override this function in a subclass if you wish to do something useful with it.
725 Will be called from :meth:`.build` right before the call to Yosys.
727 Arguments:
728 kwargs: Will have all the :meth:`.build` parameters in it. Including additional
729 parameters from the user.
731 Return:
732 True if everything went well.
733 """
734 return True
736 def post_build(
737 self,
738 **kwargs: Any, # noqa: ANN401, ARG002
739 ) -> bool:
740 """
741 Override this function in a subclass if you wish to do something useful with it.
742 Will be called from :meth:`.build` right after the call to Yosys.
744 .. Note::
745 This default method does nothing. Shall be overridden by project that utilize
746 this mechanism.
748 Arguments:
749 kwargs: Will have all the :meth:`.build` parameters in it. Including additional
750 parameters from the user. Will also include ``build_result``.
752 Return:
753 True if everything went well.
754 """
755 return True
757 def _get_size(self, utilization_report_file: Path) -> dict[str, int]:
758 return self._utilization_parser.get_size(report=read_file(utilization_report_file))
760 def _check_size(self, build_result: YosysBuildResult) -> bool:
761 success = True
762 for build_result_checker in self.build_result_checkers:
763 checker_result = build_result_checker.check(build_result)
764 success = success and checker_result
766 return success
768 def open(
769 self,
770 project_path: Path,
771 ) -> NoReturn:
772 """
773 Not implemented. A Yosys netlist build has no GUI to open.
774 """
775 raise NotImplementedError("Yosys netlist build can not be opened")
777 def __str__(self) -> str:
778 result = f"{self.name}\n"
780 if self.defined_at is not None:
781 result += f"Defined at: {self.defined_at.resolve()}\n"
783 result += f"Type: {self.__class__.__name__}\n"
784 result += f"Top level: {self.top}\n"
786 generics = self._dict_to_string(self.static_generics) if self.static_generics else "-"
787 result += f"Generics: {generics}\n"
789 if self.other_arguments:
790 result += f"Arguments: {self._dict_to_string(self.other_arguments)}\n"
792 return result
794 @staticmethod
795 def _dict_to_string(data: dict[str, Any]) -> str:
796 return ", ".join([f"{name}={value}" for name, value in data.items()])
799class YosysXilinxNetlistBuild(YosysNetlistBuild):
800 """
801 Used for handling a Yosys netlist build that targets Xilinx primitives (LUTs, FDs,
802 RAMBs, DSP48s, ...), using the Yosys ``synth_xilinx`` command.
804 Since the produced utilization report uses the same resource naming convention as the
805 Vivado utilization report, the checkers in :mod:`.vivado.build_result_checker` can be used
806 directly to check e.g. the LUT or RAMB count of the design.
807 """
809 _utilization_parser = YosysXilinxUtilizationParser
810 _synth_command = "synth_xilinx"
812 def __init__(
813 self,
814 family: str | None = None,
815 **kwargs: Any, # noqa: ANN401
816 ) -> None:
817 """
818 Arguments:
819 family: Optionally target a specific Xilinx device family (e.g. ``"xc7"``).
820 See the Yosys ``synth_xilinx`` command documentation for valid values.
821 kwargs: Further arguments accepted by :meth:`.YosysNetlistBuild.__init__`.
822 """
823 super().__init__(**kwargs)
825 if family is not None:
826 self._synth_arguments = (*self._synth_arguments, "-family", family)
829class YosysIntelNetlistBuild(YosysNetlistBuild):
830 """
831 Used for handling a Yosys netlist build that targets Intel (Altera) primitives
832 (``*_lcell_comb``, ``dffeas``, ``altsyncram``, ...), using the Yosys ``synth_intel`` command.
834 .. note::
835 Targets the MAX10, Cyclone IV, Cyclone IV E and Cyclone 10 LP families.
836 For ALM-based Intel devices (Cyclone V, Cyclone 10 GX) the ``synth_intel_alm`` command
837 shall be used instead, which is not covered by this class. Subclass
838 :class:`.YosysNetlistBuild` and set its ``_synth_command`` to ``"synth_intel_alm"`` for
839 that, though note that no aggregated resource counts will be available in that case.
841 Since the produced utilization report uses the resource names ``"Total LUTs"``, ``"FFs"``,
842 ``"Block RAMs"`` and ``"DSP Blocks"``, the corresponding checkers in
843 :mod:`.vivado.build_result_checker` can be used directly.
844 """
846 _utilization_parser = YosysIntelUtilizationParser
847 _synth_command = "synth_intel"
848 _needs_explicit_flatten_flag = False
850 def __init__(
851 self,
852 family: str | None = None,
853 **kwargs: Any, # noqa: ANN401
854 ) -> None:
855 """
856 Arguments:
857 family: Optionally target a specific Intel device family (e.g. ``"cycloneiv"``).
858 See the Yosys ``synth_intel`` command documentation for valid values.
859 kwargs: Further arguments accepted by :meth:`.YosysNetlistBuild.__init__`.
860 """
861 super().__init__(**kwargs)
863 if family is not None:
864 self._synth_arguments = (*self._synth_arguments, "-family", family)
867class YosysMicrochipNetlistBuild(YosysNetlistBuild):
868 """
869 Used for handling a Yosys netlist build that targets Microchip primitives
870 (``CFG*``, ``SLE``, ``RAM1K20``, ``MACC_PA``, ...), using the Yosys ``synth_microchip``
871 command.
873 .. note::
874 Targets the PolarFire family, which is the only one currently supported by the Yosys
875 ``synth_microchip`` command.
877 Since the produced utilization report uses the resource names ``"Total LUTs"``, ``"FFs"``,
878 ``"Block RAMs"`` and ``"DSP Blocks"``, the corresponding checkers in
879 :mod:`.vivado.build_result_checker` can be used directly.
880 """
882 _utilization_parser = YosysMicrochipUtilizationParser
883 _synth_command = "synth_microchip"
884 _needs_explicit_flatten_flag = False
886 def __init__(
887 self,
888 family: str | None = None,
889 discard_ffinit: bool = False,
890 **kwargs: Any, # noqa: ANN401
891 ) -> None:
892 """
893 Arguments:
894 family: Optionally target a specific Microchip device family.
895 See the Yosys ``synth_microchip`` command documentation for valid values.
896 discard_ffinit: The Yosys ``synth_microchip`` command will raise an error if the
897 design contains a flip-flop with an initial value that can not be legalized to
898 a supported flip-flop type (which is a common occurrence, since e.g. VHDL signals
899 initialized to a default value result in flip-flops with an initial value).
900 Set this to ``True`` to instead discard the initial value and let synthesis
901 proceed. Corresponds to the Yosys ``-discard-ffinit`` flag.
902 kwargs: Further arguments accepted by :meth:`.YosysNetlistBuild.__init__`.
903 """
904 super().__init__(**kwargs)
906 if family is not None:
907 self._synth_arguments = (*self._synth_arguments, "-family", family)
909 if discard_ffinit:
910 self._synth_arguments = (*self._synth_arguments, "-discard-ffinit")
913def _get_ghdl_generic_value(
914 value: GenericValue,
915) -> str:
916 """
917 Convert a generic value of a native Python type (or one of the tsfpga generic value
918 wrapper classes) to a string suitable for the ``-g<name>=<value>`` argument of the
919 ``ghdl-yosys-plugin`` ``ghdl`` command.
920 """
921 # Note that bool is a sub-class of int in Python, so check for bool must be first.
922 if isinstance(value, bool):
923 # The plugin does not recognize "1"/"0" as boolean literals, only "true"/"false".
924 return "true" if value else "false"
926 if isinstance(value, int):
927 return str(value)
929 if isinstance(value, float):
930 return str(value)
932 if isinstance(value, BitVectorGenericValue):
933 return value.value
935 if isinstance(value, StringGenericValue):
936 return value.value
938 message = f'Unsupported type for generic. Got type="{type(value)}", value="{value}".'
939 if isinstance(value, str):
940 message += (
941 " Please use either of the explicit types StringGenericValue or BitVectorGenericValue."
942 )
944 raise TypeError(message)
947#: Guards the global ``sys.stdout`` swap in :func:`._suppress_stdout`.
948#: Netlist builds are created from VUnit test-runner worker threads (see
949#: :class:`.BuildProjectList`), which installs its own object as the global ``sys.stdout``.
950#: Without this lock, two threads entering the swap concurrently interleave their
951#: save/restore, and the last one out installs an already-closed file as ``sys.stdout``,
952#: breaking every subsequent print in the process.
953_SUPPRESS_STDOUT_LOCK = Lock()
956@contextmanager
957def _suppress_stdout() -> Iterator[None]:
958 """
959 Suppress the (very chatty) printouts made by VUnit when creating a project.
961 Note that this swaps the process-global ``sys.stdout``, so it is serialized with a lock and
962 the critical section is kept as small as possible.
963 """
964 with _SUPPRESS_STDOUT_LOCK, Path(os.devnull).open("w") as devnull:
965 old_stdout = sys.stdout
966 sys.stdout = devnull
967 try:
968 yield
969 finally:
970 sys.stdout = old_stdout
973def _get_verilog_parameter_value(value: GenericValue) -> str:
974 """
975 Convert a generic value of a native Python type (or one of the tsfpga generic value
976 wrapper classes) to a string suitable for the ``-chparam`` argument of the Yosys
977 ``hierarchy`` command.
978 """
979 if isinstance(value, bool):
980 # Note that this must be before the 'int' check below, since 'bool' is a subclass of it.
981 return "1" if value else "0"
983 if isinstance(value, BitVectorGenericValue):
984 return f"{value.length}'b{value.value}"
986 if isinstance(value, (int, float)):
987 return str(value)
989 message = (
990 "Yosys can not set string parameters of a Verilog/SystemVerilog top level. "
991 f'Got type="{type(value)}", value="{value}".'
992 )
993 raise TypeError(message)