33# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
44
55import os
6+ import shutil
67from pathlib import Path
78from typing import List , Optional , Callable , Dict
89
910from .simulator import CosimCollateralDir , Simulator , SourceFiles
1011
1112
1213class Verilator (Simulator ):
13- """Run and compile funcs for Verilator."""
14+ """Run and compile funcs for Verilator.
15+
16+ Calls ``verilator_bin`` directly (bypassing the Perl wrapper) to generate
17+ C++ from RTL, then builds the simulation executable with CMake + Ninja.
18+ Falls back to ``make`` when cmake/ninja are not available."""
1419
1520 DefaultDriver = CosimCollateralDir / "driver.cpp"
1621
@@ -39,13 +44,64 @@ def __init__(
3944 make_default_logs = make_default_logs ,
4045 macro_definitions = macro_definitions ,
4146 )
42- self .verilator = "verilator "
47+ self .verilator_bin = "verilator_bin "
4348 if "VERILATOR_PATH" in os .environ :
44- self .verilator = os .environ ["VERILATOR_PATH" ]
49+ vpath = os .environ ["VERILATOR_PATH" ]
50+ # Backwards compatibility: if the env var points to the Perl wrapper,
51+ # redirect to verilator_bin.
52+ basename = Path (vpath ).stem
53+ if basename == "verilator" :
54+ self .verilator_bin = str (Path (vpath ).parent / "verilator_bin" )
55+ else :
56+ self .verilator_bin = vpath
57+
58+ def _find_verilator_root (self ) -> Path :
59+ """Locate VERILATOR_ROOT for runtime includes and sources.
60+
61+ Checks the ``VERILATOR_ROOT`` environment variable first, then attempts
62+ to derive the root from the location of ``verilator_bin``. Supports both
63+ source-tree layouts (``$ROOT/include/verilated.h``) and system package
64+ layouts (``$PREFIX/share/verilator/include/verilated.h``)."""
65+ if "VERILATOR_ROOT" in os .environ :
66+ root = Path (os .environ ["VERILATOR_ROOT" ])
67+ if root .is_dir ():
68+ return root
69+ # verilator_bin is typically in $PREFIX/bin/
70+ verilator_bin_path = shutil .which (self .verilator_bin )
71+ if verilator_bin_path :
72+ prefix = Path (verilator_bin_path ).resolve ().parent .parent
73+ # Source-tree layout: $VERILATOR_ROOT/bin/verilator_bin
74+ if (prefix / "include" / "verilated.h" ).exists ():
75+ return prefix
76+ # System package layout: $PREFIX/share/verilator/include/verilated.h
77+ pkg_root = prefix / "share" / "verilator"
78+ if (pkg_root / "include" / "verilated.h" ).exists ():
79+ return pkg_root
80+ raise RuntimeError (
81+ "Cannot find VERILATOR_ROOT. Set the VERILATOR_ROOT environment "
82+ "variable or ensure verilator_bin is in PATH." )
83+
84+ @property
85+ def _use_cmake (self ) -> bool :
86+ """True when both cmake and ninja are available on PATH."""
87+ return shutil .which ("cmake" ) is not None and \
88+ shutil .which ("ninja" ) is not None
4589
4690 def compile_commands (self ) -> List [List [str ]]:
91+ """Return the commands for the full compile flow.
92+
93+ When cmake and ninja are available the returned list contains three
94+ commands run sequentially:
95+ 1. ``verilator_bin`` – generates C++ from RTL.
96+ 2. ``cmake`` – configures the C++ build (Ninja generator).
97+ 3. ``ninja`` – builds the simulation executable.
98+
99+ Otherwise falls back to two commands:
100+ 1. ``verilator_bin --exe`` – generates C++ and a Makefile.
101+ 2. ``make`` – builds via the generated Makefile.
102+ """
47103 cmd : List [str ] = [
48- self .verilator ,
104+ self .verilator_bin ,
49105 "--cc" ,
50106 ]
51107
@@ -62,30 +118,115 @@ def compile_commands(self) -> List[List[str]]:
62118 "-Wno-TIMESCALEMOD" ,
63119 "-Wno-fatal" ,
64120 "-sv" ,
65- "--exe" ,
66- "--build" ,
67121 "-j" ,
68122 "0" ,
69123 "--output-split" ,
70124 "--autoflush" ,
71125 "--assert" ,
72- str (Verilator .DefaultDriver ),
73- ]
74- cflags = [
75- "-DTOP_MODULE=" + self .sources .top ,
76126 ]
77127 if self .debug :
78128 cmd += [
79129 "--trace-fst" , "--trace-params" , "--trace-structs" ,
80130 "--trace-underscore"
81131 ]
132+
133+ if self ._use_cmake :
134+ cmd += [str (p ) for p in self .sources .rtl_sources ]
135+ build_dir = str (Path .cwd () / "obj_dir" / "cmake_build" )
136+ cmake_cmd = ["cmake" , "-G" , "Ninja" , "-S" , build_dir , "-B" , build_dir ]
137+ ninja_cmd = ["ninja" , "-C" , build_dir ]
138+ return [cmd , cmake_cmd , ninja_cmd ]
139+
140+ # -- make fallback --
141+ # Let verilator generate a Makefile with --exe so it includes the
142+ # driver, CFLAGS, and LDFLAGS directly.
143+ cmd += ["--exe" , str (Verilator .DefaultDriver )]
144+ cflags = ["-DTOP_MODULE=" + self .sources .top ]
145+ if self .debug :
82146 cflags .append ("-DTRACE" )
83- if len (cflags ) > 0 :
84- cmd += ["-CFLAGS" , " " .join (cflags )]
85- if len (self .sources .dpi_so ) > 0 :
147+ cmd += ["-CFLAGS" , " " .join (cflags )]
148+ if self .sources .dpi_so :
86149 cmd += ["-LDFLAGS" , " " .join (["-l" + so for so in self .sources .dpi_so ])]
87150 cmd += [str (p ) for p in self .sources .rtl_sources ]
88- return [cmd ]
151+ top = self .sources .top
152+ make_cmd = ["make" , "-C" , "obj_dir" , "-f" , f"V{ top } .mk" , "-j" ]
153+ return [cmd , make_cmd ]
154+
155+ def _write_cmake (self , obj_dir : Path ) -> Path :
156+ """Write a CMakeLists.txt for building the verilated simulation.
157+
158+ Returns the path to the CMake build directory."""
159+ verilator_root = self ._find_verilator_root ()
160+ include_dir = verilator_root / "include"
161+ exe_name = "V" + self .sources .top
162+
163+ runtime_sources = [
164+ include_dir / "verilated.cpp" ,
165+ include_dir / "verilated_threads.cpp" ,
166+ ]
167+ if self .sources .dpi_so :
168+ runtime_sources .append (include_dir / "verilated_dpi.cpp" )
169+ if self .debug :
170+ runtime_sources .append (include_dir / "verilated_fst_c.cpp" )
171+
172+ rt_src = "\n " .join (str (s ) for s in runtime_sources )
173+ driver = str (Verilator .DefaultDriver )
174+ inc = str (include_dir )
175+ vltstd = str (include_dir / "vltstd" )
176+
177+ defs = [f"TOP_MODULE={ self .sources .top } " ]
178+ if self .debug :
179+ defs .append ("TRACE" )
180+ defs_str = "\n " .join (defs )
181+
182+ # Link DPI shared objects by full path.
183+ dpi_link = ""
184+ if self .sources .dpi_so :
185+ dpi_paths = self .sources .dpi_so_paths ()
186+ dpi_link = "\n " .join (str (p ) for p in dpi_paths )
187+
188+ content = f"""\
189+ cmake_minimum_required(VERSION 3.20)
190+ project({ exe_name } CXX)
191+
192+ file(GLOB GENERATED_SOURCES "${{CMAKE_CURRENT_SOURCE_DIR}}/../*.cpp")
193+
194+ add_executable({ exe_name }
195+ ${{GENERATED_SOURCES}}
196+ { rt_src }
197+ { driver }
198+ )
199+
200+ target_include_directories({ exe_name } PRIVATE
201+ { inc }
202+ { vltstd }
203+ ${{CMAKE_CURRENT_SOURCE_DIR}}/..
204+ )
205+
206+ target_compile_definitions({ exe_name } PRIVATE
207+ { defs_str }
208+ )
209+
210+ find_package(Threads REQUIRED)
211+ target_link_libraries({ exe_name } PRIVATE
212+ Threads::Threads
213+ { dpi_link }
214+ )
215+ """
216+ build_dir = obj_dir / "cmake_build"
217+ build_dir .mkdir (parents = True , exist_ok = True )
218+ (build_dir / "CMakeLists.txt" ).write_text (content )
219+ return build_dir
220+
221+ def compile (self ) -> int :
222+ """Set VERILATOR_ROOT, write the CMakeLists.txt (if using cmake), then
223+ delegate to the base class which runs all commands from
224+ :meth:`compile_commands`."""
225+ verilator_root = self ._find_verilator_root ()
226+ os .environ ["VERILATOR_ROOT" ] = str (verilator_root )
227+ if self ._use_cmake :
228+ self ._write_cmake (Path .cwd () / "obj_dir" )
229+ return super ().compile ()
89230
90231 @property
91232 def waveform_extension (self ) -> str :
@@ -95,5 +236,9 @@ def waveform_extension(self) -> str:
95236 def run_command (self , gui : bool ):
96237 if gui :
97238 raise RuntimeError ("Verilator does not support GUI mode." )
98- exe = Path .cwd () / "obj_dir" / ("V" + self .sources .top )
239+ exe_name = "V" + self .sources .top
240+ if self ._use_cmake :
241+ exe = Path .cwd () / "obj_dir" / "cmake_build" / exe_name
242+ else :
243+ exe = Path .cwd () / "obj_dir" / exe_name
99244 return [str (exe )]
0 commit comments