1use std::{
2 io,
3 path::{Path, PathBuf},
4 process::Command,
5 str,
6};
7
8use fj::{abi, version::Version};
9use fj_operations::shape_processor;
10use tracing::debug;
11
12use crate::{platform::HostPlatform, Parameters};
13
14pub struct Model {
16 src_path: PathBuf,
17 lib_path: PathBuf,
18 manifest_path: PathBuf,
19 parameters: Parameters,
20}
21
22impl Model {
23 pub fn new(
28 path: impl AsRef<Path>,
29 parameters: Parameters,
30 ) -> Result<Self, Error> {
31 let path = path.as_ref();
32
33 let crate_dir = path.canonicalize()?;
34
35 let metadata = cargo_metadata::MetadataCommand::new()
36 .current_dir(&crate_dir)
37 .exec()?;
38
39 let pkg = package_associated_with_directory(&metadata, &crate_dir)?;
40 let src_path = crate_dir.join("src");
41
42 let lib_path = {
43 let name = pkg.name.replace('-', "_");
44 let file = HostPlatform::lib_file_name(&name);
45 let target_dir =
46 metadata.target_directory.clone().into_std_path_buf();
47 target_dir.join("debug").join(file)
48 };
49
50 Ok(Self {
51 src_path,
52 lib_path,
53 manifest_path: pkg.manifest_path.as_std_path().to_path_buf(),
54 parameters,
55 })
56 }
57
58 pub fn watch_path(&self) -> PathBuf {
60 self.src_path.clone()
61 }
62
63 pub fn evaluate(&self) -> Result<Evaluation, Error> {
65 let manifest_path = self.manifest_path.display().to_string();
66
67 let cargo_output = Command::new("cargo")
68 .arg("rustc")
69 .args(["--manifest-path", &manifest_path])
70 .args(["--crate-type", "cdylib"])
71 .output()?;
72
73 if !cargo_output.status.success() {
74 let output =
75 String::from_utf8(cargo_output.stderr).unwrap_or_else(|_| {
76 String::from("Failed to fetch command output")
77 });
78
79 return Err(Error::Compile { output });
80 }
81
82 let seconds_taken = str::from_utf8(&cargo_output.stderr)
83 .unwrap()
84 .rsplit_once(' ')
85 .unwrap()
86 .1
87 .trim();
88
89 let mut warnings = None;
90
91 let shape = unsafe {
108 let lib = libloading::Library::new(&self.lib_path)
109 .map_err(Error::LoadingLibrary)?;
110
111 let version_pkg_host = fj::version::VERSION_PKG.to_string();
112
113 let version_pkg_model: libloading::Symbol<*const Version> =
114 lib.get(b"VERSION_PKG").map_err(Error::LoadingVersion)?;
115 let version_pkg_model = (**version_pkg_model).to_string();
116
117 debug!(
118 "Comparing package versions (host: {}, model: {})",
119 version_pkg_host, version_pkg_model
120 );
121 if version_pkg_host != version_pkg_model {
122 let host = String::from_utf8_lossy(version_pkg_host.as_bytes())
123 .into_owned();
124 let model = version_pkg_model;
125
126 return Err(Error::VersionMismatch { host, model });
127 }
128
129 let version_full_host = fj::version::VERSION_FULL.to_string();
130
131 let version_full_model: libloading::Symbol<*const Version> =
132 lib.get(b"VERSION_FULL").map_err(Error::LoadingVersion)?;
133 let version_full_model = (**version_full_model).to_string();
134
135 debug!(
136 "Comparing full versions (host: {}, model: {})",
137 version_full_host, version_full_model
138 );
139 if version_full_host != version_full_model {
140 let host =
141 String::from_utf8_lossy(version_full_host.as_bytes())
142 .into_owned();
143 let model = version_full_model;
144
145 warnings =
146 Some(format!("{}", Error::VersionMismatch { host, model }));
147 }
148
149 let init: libloading::Symbol<abi::InitFunction> = lib
150 .get(abi::INIT_FUNCTION_NAME.as_bytes())
151 .map_err(Error::LoadingInit)?;
152
153 let mut host = Host::new(&self.parameters);
154
155 match init(&mut abi::Host::from(&mut host)) {
156 abi::ffi_safe::Result::Ok(_metadata) => {}
157 abi::ffi_safe::Result::Err(e) => {
158 return Err(Error::InitializeModel(e.into()));
159 }
160 }
161
162 let model = host.take_model().ok_or(Error::NoModelRegistered)?;
163
164 model.shape(&host).map_err(Error::Shape)?
165 };
166
167 Ok(Evaluation {
168 shape,
169 compile_time: seconds_taken.into(),
170 warning: warnings,
171 })
172 }
173}
174
175#[derive(Debug)]
179pub struct Evaluation {
180 pub shape: fj::Shape,
182
183 pub compile_time: String,
185
186 pub warning: Option<String>,
188}
189
190pub struct Host<'a> {
191 args: &'a Parameters,
192 model: Option<Box<dyn fj::models::Model>>,
193}
194
195impl<'a> Host<'a> {
196 pub fn new(parameters: &'a Parameters) -> Self {
197 Self {
198 args: parameters,
199 model: None,
200 }
201 }
202
203 pub fn take_model(&mut self) -> Option<Box<dyn fj::models::Model>> {
204 self.model.take()
205 }
206}
207
208impl<'a> fj::models::Host for Host<'a> {
209 fn register_boxed_model(&mut self, model: Box<dyn fj::models::Model>) {
210 self.model = Some(model);
211 }
212}
213
214impl<'a> fj::models::Context for Host<'a> {
215 fn get_argument(&self, name: &str) -> Option<&str> {
216 self.args.get(name).map(String::as_str)
217 }
218}
219
220fn package_associated_with_directory<'m>(
221 metadata: &'m cargo_metadata::Metadata,
222 dir: &Path,
223) -> Result<&'m cargo_metadata::Package, Error> {
224 for pkg in metadata.workspace_packages() {
225 let crate_dir = pkg
226 .manifest_path
227 .parent()
228 .and_then(|p| p.canonicalize().ok());
229
230 if crate_dir.as_deref() == Some(dir) {
231 return Ok(pkg);
232 }
233 }
234
235 Err(ambiguous_path_error(metadata, dir))
236}
237
238fn ambiguous_path_error(
239 metadata: &cargo_metadata::Metadata,
240 dir: &Path,
241) -> Error {
242 let mut possible_paths = Vec::new();
243
244 for id in &metadata.workspace_members {
245 let cargo_toml = &metadata[id].manifest_path;
246 let crate_dir = cargo_toml
247 .parent()
248 .expect("A Cargo.toml always has a parent");
249 let simplified_path = crate_dir
252 .strip_prefix(&metadata.workspace_root)
253 .unwrap_or(crate_dir);
254
255 possible_paths.push(simplified_path.into());
256 }
257
258 Error::AmbiguousPath {
259 dir: dir.to_path_buf(),
260 possible_paths,
261 }
262}
263
264#[derive(Debug, thiserror::Error)]
266pub enum Error {
267 #[error(
269 "Failed to load model library\n\
270 This might be a bug in Fornjot, or, at the very least, this error \
271 message should be improved. Please report this!"
272 )]
273 LoadingLibrary(#[source] libloading::Error),
274
275 #[error(
277 "Failed to load the Fornjot version that the model uses\n\
278 - Is your model using the `fj` library? All models must!\n\
279 - Was your model created with a really old version of Fornjot?"
280 )]
281 LoadingVersion(#[source] libloading::Error),
282
283 #[error(
285 "Failed to load the model's `init` function\n\
286 - Did you define a model function using `#[fj::model]`?"
287 )]
288 LoadingInit(#[source] libloading::Error),
289
290 #[error("Host version ({host}) and model version ({model}) do not match")]
292 VersionMismatch {
293 host: String,
295
296 model: String,
298 },
299
300 #[error("Error compiling model\n{output}")]
302 Compile {
303 output: String,
305 },
306
307 #[error("I/O error while loading model")]
309 Io(#[from] io::Error),
310
311 #[error("Unable to initialize the model")]
313 InitializeModel(#[source] fj::models::Error),
314
315 #[error("No model was registered")]
318 NoModelRegistered,
319
320 #[error("Unable to determine the model's geometry")]
322 Shape(#[source] fj::models::Error),
323
324 #[error("Shape processing error")]
327 ShapeProcessor(#[from] shape_processor::Error),
328
329 #[error("Error watching model for changes")]
331 Notify(#[from] notify::Error),
332
333 #[error("Unable to determine the crate's metadata")]
336 CargoMetadata(#[from] cargo_metadata::Error),
337
338 #[error(
341 "It doesn't look like \"{}\" is a crate directory. Did you mean one of {}?",
342 dir.display(),
343 possible_paths.iter().map(|p| p.display().to_string())
344 .collect::<Vec<_>>()
345 .join(", ")
346 )]
347 AmbiguousPath {
348 dir: PathBuf,
350 possible_paths: Vec<PathBuf>,
353 },
354}