1use std::{env::current_dir, ffi::OsString, fs, process::Command};
10
11use camino::{Utf8Path, Utf8PathBuf};
12use marlin_verilator::{
13 AsVerilatedModel, VerilatedModelConfig, VerilatorRuntime,
14 VerilatorRuntimeOptions, eprintln_nocapture,
15};
16use owo_colors::OwoColorize;
17use snafu::{OptionExt, ResultExt, Whatever, whatever};
18
19#[doc(hidden)]
20pub mod __reexports {
21 pub use libloading;
22 pub use marlin_verilator as verilator;
23}
24
25pub mod prelude {
26 pub use crate as spade;
27 pub use crate::{SpadeRuntime, SpadeRuntimeOptions};
28 pub use marlin_spade_macro::spade;
29 pub use marlin_verilator::{AsDynamicVerilatedModel, AsVerilatedModel};
30}
31
32const SWIM_TOML: &str = "swim.toml";
33
34fn search_for_swim_toml(mut start: Utf8PathBuf) -> Option<Utf8PathBuf> {
35 while start.parent().is_some() {
36 if start.join(SWIM_TOML).is_file() {
37 return Some(start.join(SWIM_TOML));
38 }
39 start.pop();
40 }
41 None
42}
43
44pub struct SpadeRuntimeOptions {
47 pub swim_executable: OsString,
50
51 pub call_swim_build: bool,
55
56 pub verilator_options: VerilatorRuntimeOptions,
58}
59
60impl Default for SpadeRuntimeOptions {
61 fn default() -> Self {
62 Self {
63 swim_executable: "swim".into(),
64 call_swim_build: false,
65 verilator_options: VerilatorRuntimeOptions::default(),
66 }
67 }
68}
69
70impl SpadeRuntimeOptions {
71 pub fn default_logging() -> Self {
74 Self {
75 verilator_options: VerilatorRuntimeOptions::default_logging(),
76 ..Default::default()
77 }
78 }
79}
80
81#[derive(Default)]
84pub struct SpadeModelConfig {
85 pub verilator_config: VerilatedModelConfig,
87}
88
89pub struct SpadeRuntime {
91 verilator_runtime: VerilatorRuntime,
92}
93
94impl SpadeRuntime {
95 pub fn new(options: SpadeRuntimeOptions) -> Result<Self, Whatever> {
100 if options.verilator_options.log {
101 log::info!("Searching for swim project root");
102 }
103 let Some(swim_toml_path) = search_for_swim_toml(
104 current_dir()
105 .whatever_context("Failed to get current directory")?
106 .try_into()
107 .whatever_context(
108 "Failed to convert current directory to UTF-8",
109 )?,
110 ) else {
111 whatever!(
112 "Failed to find {SWIM_TOML} searching from current directory"
113 );
114 };
115 let mut swim_project_path = swim_toml_path.clone();
116 swim_project_path.pop();
117
118 let swim_toml_contents = fs::read_to_string(&swim_toml_path)
119 .whatever_context(format!(
120 "Failed to read contents of {SWIM_TOML} at {swim_toml_path}"
121 ))?;
122 let swim_toml: toml::Value = toml::from_str(&swim_toml_contents)
123 .whatever_context(format!(
124 "Failed to parse {SWIM_TOML} as a valid TOML file"
125 ))?;
126
127 if options.call_swim_build {
128 if options.verilator_options.log {
129 log::info!("Invoking `swim build` (this may take a while)");
130 }
131
132 let swim_project_name = swim_toml
133 .get("name")
134 .and_then(|name| name.as_str())
135 .whatever_context(format!(
136 "{SWIM_TOML} missing top-level `name` field"
137 ))?;
138
139 eprintln_nocapture!(
140 "{} {swim_project_name} ({swim_project_path})",
141 " Compiling".bold().green()
142 )?;
143
144 let swim_output = Command::new(options.swim_executable)
145 .arg("build")
146 .current_dir(&swim_project_path)
147 .output()
148 .whatever_context("Invocation of swim failed")?;
149
150 if !swim_output.status.success() {
151 whatever!(
152 "Invocation of swim failed with nonzero exit code {}\n\n--- STDOUT ---\n{}\n\n--- STDERR ---\n{}",
153 swim_output.status,
154 String::from_utf8(swim_output.stdout).unwrap_or_default(),
155 String::from_utf8(swim_output.stderr).unwrap_or_default()
156 );
157 }
158 }
159
160 let spade_sv_path = swim_project_path.join("build/spade.sv");
161
162 let extra_verilog = swim_toml.get("verilog").map(|verilog| {
163 (
164 verilog
165 .get("sources")
166 .and_then(|sources| sources.as_array())
167 .map(|sources| {
168 let mut result = vec![];
169 for source in
170 sources.iter().flat_map(|source| source.as_str())
171 {
172 if let Ok(paths) = glob::glob(
173 swim_project_path.join(source).as_str(),
174 ) {
175 for path in paths.flatten() {
176 if let Ok(path) =
177 Utf8PathBuf::try_from(path)
178 {
179 result.push(path);
180 }
181 }
182 }
183 }
184 result
185 }),
186 verilog
187 .get("include")
188 .and_then(|include| include.as_array())
189 .map(|sources| {
190 sources
191 .iter()
192 .flat_map(|source| source.as_str())
193 .flat_map(|source| {
194 Utf8Path::new(source).canonicalize_utf8()
195 })
196 .collect::<Vec<_>>()
197 }),
198 )
199 });
200
201 let mut source_files = vec![spade_sv_path.as_path()];
202 let mut include_files = vec![];
203
204 if let Some(extra_verilog) = &extra_verilog {
205 if let Some(sources) = &extra_verilog.0 {
206 source_files
207 .extend(sources.iter().map(|source| source.as_path()));
208 }
209 if let Some(include) = &extra_verilog.1 {
210 include_files.extend(
211 include.iter().map(|directory| directory.as_path()),
212 );
213 }
214 }
215
216 Ok(Self {
217 verilator_runtime: VerilatorRuntime::new(
218 &swim_project_path.join("build/thirdparty/marlin"),
220 &source_files,
221 &include_files,
222 [],
223 options.verilator_options,
224 )?,
225 })
226 }
227
228 pub fn create_model_simple<'ctx, M: AsVerilatedModel<'ctx>>(
231 &'ctx self,
232 ) -> Result<M, Whatever> {
233 self.verilator_runtime.create_model_simple()
234 }
235
236 pub fn create_model<'ctx, M: AsVerilatedModel<'ctx>>(
239 &'ctx self,
240 config: SpadeModelConfig,
241 ) -> Result<M, Whatever> {
242 self.verilator_runtime
243 .create_model(&config.verilator_config)
244 }
245}