use std::{
env, fs,
io::ErrorKind,
path::{Path, PathBuf},
};
use proc_macro2::Span;
use syn::Error;
use toml::Value;
use crate::util::{bundled_wit_folder, strip_line_comment};
const SDK_WIT_FILE_NAME: &str = "miden.wit";
pub(crate) const SDK_WIT_SOURCE: &str = include_str!("../wit/miden.wit");
pub(crate) struct ResolvedWit {
pub paths: Vec<String>,
pub world: Option<String>,
}
#[derive(Default)]
pub(crate) struct ResolveOptions {
pub allow_missing_local_wit: bool,
}
pub(crate) fn resolve_wit_paths(options: ResolveOptions) -> Result<ResolvedWit, Error> {
let manifest_dir = env::var("CARGO_MANIFEST_DIR").map_err(|err| {
Error::new(Span::call_site(), format!("failed to read CARGO_MANIFEST_DIR: {err}"))
})?;
let manifest_path = Path::new(&manifest_dir).join("Cargo.toml");
let manifest_content = fs::read_to_string(&manifest_path).map_err(|err| {
Error::new(
Span::call_site(),
format!("failed to read manifest '{}': {err}", manifest_path.display()),
)
})?;
let manifest: Value = manifest_content.parse().map_err(|err| {
Error::new(
Span::call_site(),
format!("failed to parse manifest '{}': {err}", manifest_path.display()),
)
})?;
let canonical_prelude_dir = ensure_sdk_wit()?;
let mut resolved = Vec::new();
let prelude_dir = canonical_prelude_dir
.to_str()
.ok_or_else(|| {
Error::new(
Span::call_site(),
format!("path '{}' contains invalid UTF-8", canonical_prelude_dir.display()),
)
})?
.to_owned();
resolved.push(prelude_dir);
if let Some(dependencies) = manifest
.get("package")
.and_then(Value::as_table)
.and_then(|package| package.get("metadata"))
.and_then(Value::as_table)
.and_then(|metadata| metadata.get("component"))
.and_then(Value::as_table)
.and_then(|component| component.get("target"))
.and_then(Value::as_table)
.and_then(|target| target.get("dependencies"))
.and_then(Value::as_table)
{
for (name, dependency) in dependencies.iter() {
let table = dependency.as_table().ok_or_else(|| {
Error::new(
Span::call_site(),
format!(
"dependency '{name}' under \
[package.metadata.component.target.dependencies] must be a table"
),
)
})?;
let path_value = table.get("path").and_then(Value::as_str).ok_or_else(|| {
Error::new(
Span::call_site(),
format!("dependency '{name}' is missing a 'path' entry"),
)
})?;
let raw_path = PathBuf::from(path_value);
let absolute = if raw_path.is_absolute() {
raw_path
} else {
Path::new(&manifest_dir).join(&raw_path)
};
let canonical = fs::canonicalize(&absolute).unwrap_or_else(|_| absolute.clone());
let metadata = fs::metadata(&canonical).map_err(|err| {
Error::new(
Span::call_site(),
format!(
"failed to read metadata for dependency '{name}' path '{}': {err}",
canonical.display()
),
)
})?;
let search_path = if metadata.is_dir() {
canonical
} else if let Some(parent) = canonical.parent() {
parent.to_path_buf()
} else {
return Err(Error::new(
Span::call_site(),
format!(
"dependency '{name}' path '{}' does not have a parent directory",
canonical.display()
),
));
};
let path_str = search_path.to_str().ok_or_else(|| {
Error::new(
Span::call_site(),
format!("dependency '{name}' path contains invalid UTF-8"),
)
})?;
if !resolved.iter().any(|existing| existing == path_str) {
resolved.push(path_str.to_owned());
}
}
}
let local_wit_root = Path::new(&manifest_dir).join("wit");
let mut world = None;
if local_wit_root.exists() && !options.allow_missing_local_wit {
let local_root = fs::canonicalize(&local_wit_root).unwrap_or(local_wit_root);
let local_root_str = local_root.to_str().ok_or_else(|| {
Error::new(
Span::call_site(),
format!("path '{}' contains invalid UTF-8", local_root.display()),
)
})?;
if !resolved.iter().any(|existing| existing == local_root_str) {
resolved.push(local_root_str.to_owned());
}
world = detect_world_name(&local_root)?;
}
Ok(ResolvedWit {
paths: resolved,
world,
})
}
fn ensure_sdk_wit() -> Result<PathBuf, Error> {
let autogenerated_wit_folder = bundled_wit_folder()?;
let sdk_wit_path = autogenerated_wit_folder.join(SDK_WIT_FILE_NAME);
let sdk_version: &str = env!("CARGO_PKG_VERSION");
let expected_source = format!(
"/// NOTE: This file is auto-generated from the Miden SDK.\n/// Version: \
v{sdk_version}\n/// Any manual edits will be overwritten.\n\n{SDK_WIT_SOURCE}"
);
let should_write_wit = match fs::read_to_string(&sdk_wit_path) {
Ok(existing) => existing != expected_source,
Err(err) if err.kind() == ErrorKind::NotFound => true,
Err(err) => {
return Err(Error::new(
Span::call_site(),
format!("failed to read '{}': {err}", sdk_wit_path.display()),
));
}
};
if should_write_wit {
fs::write(&sdk_wit_path, expected_source).map_err(|err| {
Error::new(
Span::call_site(),
format!("failed to write '{}': {err}", sdk_wit_path.display()),
)
})?;
}
Ok(fs::canonicalize(&autogenerated_wit_folder).unwrap_or(autogenerated_wit_folder))
}
fn detect_world_name(wit_root: &Path) -> Result<Option<String>, Error> {
let mut entries = fs::read_dir(wit_root)
.map_err(|err| {
Error::new(Span::call_site(), format!("failed to read '{}': {err}", wit_root.display()))
})?
.collect::<Result<Vec<_>, _>>()
.map_err(|err| {
Error::new(
Span::call_site(),
format!("failed to iterate '{}': {err}", wit_root.display()),
)
})?;
entries.sort_by_key(|entry| entry.file_name());
for entry in entries {
let path = entry.path();
if path.file_name().is_some_and(|name| name == "deps") {
continue;
}
if path.is_dir() {
continue;
}
if path.extension().and_then(|ext| ext.to_str()) != Some("wit") {
continue;
}
if let Some((package, world)) = parse_package_and_world(&path)? {
return Ok(Some(format!("{package}/{world}")));
}
}
Ok(None)
}
fn parse_package_and_world(path: &Path) -> Result<Option<(String, String)>, Error> {
let contents = fs::read_to_string(path).map_err(|err| {
Error::new(
Span::call_site(),
format!("failed to read WIT file '{}': {err}", path.display()),
)
})?;
let package = extract_package_name(&contents);
let world = extract_world_name(&contents);
match (package, world) {
(Some(package), Some(world)) => Ok(Some((package, world))),
_ => Ok(None),
}
}
fn extract_package_name(contents: &str) -> Option<String> {
for line in contents.lines() {
let trimmed = strip_line_comment(line).trim_start();
if let Some(rest) = trimmed.strip_prefix("package ") {
let mut token = rest.trim();
if let Some(idx) = token.find(';') {
token = &token[..idx];
}
let mut name = token.trim();
if let Some(idx) = name.find('@') {
name = &name[..idx];
}
return Some(name.trim().to_string());
}
}
None
}
pub(crate) fn extract_world_name(contents: &str) -> Option<String> {
for line in contents.lines() {
let trimmed = strip_line_comment(line).trim_start();
if let Some(rest) = trimmed.strip_prefix("world ") {
let mut name = String::new();
for ch in rest.trim().chars() {
if ch.is_alphanumeric() || ch == '-' || ch == '_' {
name.push(ch);
} else {
break;
}
}
if !name.is_empty() {
return Some(name);
}
}
}
None
}