use std::{
io,
path::{Path, PathBuf},
process::Command,
str,
};
use fj::{abi, version::Version};
use fj_operations::shape_processor;
use tracing::debug;
use crate::{platform::HostPlatform, Parameters};
pub struct Model {
src_path: PathBuf,
lib_path: PathBuf,
manifest_path: PathBuf,
parameters: Parameters,
}
impl Model {
pub fn new(
path: impl AsRef<Path>,
parameters: Parameters,
) -> Result<Self, Error> {
let path = path.as_ref();
let crate_dir = path.canonicalize()?;
let metadata = cargo_metadata::MetadataCommand::new()
.current_dir(&crate_dir)
.exec()?;
let pkg = package_associated_with_directory(&metadata, &crate_dir)?;
let src_path = crate_dir.join("src");
let lib_path = {
let name = pkg.name.replace('-', "_");
let file = HostPlatform::lib_file_name(&name);
let target_dir =
metadata.target_directory.clone().into_std_path_buf();
target_dir.join("debug").join(file)
};
Ok(Self {
src_path,
lib_path,
manifest_path: pkg.manifest_path.as_std_path().to_path_buf(),
parameters,
})
}
pub fn watch_path(&self) -> PathBuf {
self.src_path.clone()
}
pub fn evaluate(&self) -> Result<Evaluation, Error> {
let manifest_path = self.manifest_path.display().to_string();
let cargo_output = Command::new("cargo")
.arg("rustc")
.args(["--manifest-path", &manifest_path])
.args(["--crate-type", "cdylib"])
.output()?;
if !cargo_output.status.success() {
let output =
String::from_utf8(cargo_output.stderr).unwrap_or_else(|_| {
String::from("Failed to fetch command output")
});
return Err(Error::Compile { output });
}
let seconds_taken = str::from_utf8(&cargo_output.stderr)
.unwrap()
.rsplit_once(' ')
.unwrap()
.1
.trim();
let mut warnings = None;
let shape = unsafe {
let lib = libloading::Library::new(&self.lib_path)
.map_err(Error::LoadingLibrary)?;
let version_pkg_host = fj::version::VERSION_PKG.to_string();
let version_pkg_model: libloading::Symbol<*const Version> =
lib.get(b"VERSION_PKG").map_err(Error::LoadingVersion)?;
let version_pkg_model = (**version_pkg_model).to_string();
debug!(
"Comparing package versions (host: {}, model: {})",
version_pkg_host, version_pkg_model
);
if version_pkg_host != version_pkg_model {
let host = String::from_utf8_lossy(version_pkg_host.as_bytes())
.into_owned();
let model = version_pkg_model;
return Err(Error::VersionMismatch { host, model });
}
let version_full_host = fj::version::VERSION_FULL.to_string();
let version_full_model: libloading::Symbol<*const Version> =
lib.get(b"VERSION_FULL").map_err(Error::LoadingVersion)?;
let version_full_model = (**version_full_model).to_string();
debug!(
"Comparing full versions (host: {}, model: {})",
version_full_host, version_full_model
);
if version_full_host != version_full_model {
let host =
String::from_utf8_lossy(version_full_host.as_bytes())
.into_owned();
let model = version_full_model;
warnings =
Some(format!("{}", Error::VersionMismatch { host, model }));
}
let init: libloading::Symbol<abi::InitFunction> = lib
.get(abi::INIT_FUNCTION_NAME.as_bytes())
.map_err(Error::LoadingInit)?;
let mut host = Host::new(&self.parameters);
match init(&mut abi::Host::from(&mut host)) {
abi::ffi_safe::Result::Ok(_metadata) => {}
abi::ffi_safe::Result::Err(e) => {
return Err(Error::InitializeModel(e.into()));
}
}
let model = host.take_model().ok_or(Error::NoModelRegistered)?;
model.shape(&host).map_err(Error::Shape)?
};
Ok(Evaluation {
shape,
compile_time: seconds_taken.into(),
warning: warnings,
})
}
}
#[derive(Debug)]
pub struct Evaluation {
pub shape: fj::Shape,
pub compile_time: String,
pub warning: Option<String>,
}
pub struct Host<'a> {
args: &'a Parameters,
model: Option<Box<dyn fj::models::Model>>,
}
impl<'a> Host<'a> {
pub fn new(parameters: &'a Parameters) -> Self {
Self {
args: parameters,
model: None,
}
}
pub fn take_model(&mut self) -> Option<Box<dyn fj::models::Model>> {
self.model.take()
}
}
impl<'a> fj::models::Host for Host<'a> {
fn register_boxed_model(&mut self, model: Box<dyn fj::models::Model>) {
self.model = Some(model);
}
}
impl<'a> fj::models::Context for Host<'a> {
fn get_argument(&self, name: &str) -> Option<&str> {
self.args.get(name).map(String::as_str)
}
}
fn package_associated_with_directory<'m>(
metadata: &'m cargo_metadata::Metadata,
dir: &Path,
) -> Result<&'m cargo_metadata::Package, Error> {
for pkg in metadata.workspace_packages() {
let crate_dir = pkg
.manifest_path
.parent()
.and_then(|p| p.canonicalize().ok());
if crate_dir.as_deref() == Some(dir) {
return Ok(pkg);
}
}
Err(ambiguous_path_error(metadata, dir))
}
fn ambiguous_path_error(
metadata: &cargo_metadata::Metadata,
dir: &Path,
) -> Error {
let mut possible_paths = Vec::new();
for id in &metadata.workspace_members {
let cargo_toml = &metadata[id].manifest_path;
let crate_dir = cargo_toml
.parent()
.expect("A Cargo.toml always has a parent");
let simplified_path = crate_dir
.strip_prefix(&metadata.workspace_root)
.unwrap_or(crate_dir);
possible_paths.push(simplified_path.into());
}
Error::AmbiguousPath {
dir: dir.to_path_buf(),
possible_paths,
}
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error(
"Failed to load model library\n\
This might be a bug in Fornjot, or, at the very least, this error \
message should be improved. Please report this!"
)]
LoadingLibrary(#[source] libloading::Error),
#[error(
"Failed to load the Fornjot version that the model uses\n\
- Is your model using the `fj` library? All models must!\n\
- Was your model created with a really old version of Fornjot?"
)]
LoadingVersion(#[source] libloading::Error),
#[error(
"Failed to load the model's `init` function\n\
- Did you define a model function using `#[fj::model]`?"
)]
LoadingInit(#[source] libloading::Error),
#[error("Host version ({host}) and model version ({model}) do not match")]
VersionMismatch {
host: String,
model: String,
},
#[error("Error compiling model\n{output}")]
Compile {
output: String,
},
#[error("I/O error while loading model")]
Io(#[from] io::Error),
#[error("Unable to initialize the model")]
InitializeModel(#[source] fj::models::Error),
#[error("No model was registered")]
NoModelRegistered,
#[error("Unable to determine the model's geometry")]
Shape(#[source] fj::models::Error),
#[error("Shape processing error")]
ShapeProcessor(#[from] shape_processor::Error),
#[error("Error watching model for changes")]
Notify(#[from] notify::Error),
#[error("Unable to determine the crate's metadata")]
CargoMetadata(#[from] cargo_metadata::Error),
#[error(
"It doesn't look like \"{}\" is a crate directory. Did you mean one of {}?",
dir.display(),
possible_paths.iter().map(|p| p.display().to_string())
.collect::<Vec<_>>()
.join(", ")
)]
AmbiguousPath {
dir: PathBuf,
possible_paths: Vec<PathBuf>,
},
}