1use std::{
2 path::{Path, PathBuf},
3 sync::Arc,
4};
5
6use miden_assembly::{DefaultSourceManager, Linkage, ProjectTargetSelector};
7use miden_assembly_syntax::diagnostics::{IntoDiagnostic, Report};
8use miden_mast_package::{Package, PackageId};
9use miden_package_registry::PackageCache;
10
11#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct LinkLibrary {
14 pub name: PackageId,
21 pub path: Option<PathBuf>,
23 pub linkage: Linkage,
25}
26
27impl LinkLibrary {
28 pub fn name(&self) -> &str {
30 self.name.as_ref()
31 }
32
33 pub fn is_core(&self) -> bool {
34 matches!(self.name.as_ref(), "miden-core" | "core" | "std")
35 }
36
37 pub fn is_protocol(&self) -> bool {
38 matches!(self.name.as_ref(), "miden-protocol" | "protocol" | "base")
39 }
40
41 pub fn load(
42 &self,
43 search_paths: &[PathBuf],
44 registry: &mut dyn PackageCache<Error = Report>,
45 ) -> Result<Arc<Package>, Report> {
46 if let Some(path) = self.path.as_deref() {
47 return self.load_from_path(path, registry);
48 }
49
50 let path = self.find(search_paths)?;
52
53 self.load_from_path(&path, registry)
54 }
55
56 fn load_from_path(
57 &self,
58 path: &Path,
59 registry: &mut dyn PackageCache<Error = Report>,
60 ) -> Result<Arc<Package>, Report> {
61 if path.extension().is_some_and(|ext| ext.eq_ignore_ascii_case("masm")) {
62 let source_manager = Arc::new(DefaultSourceManager::default());
63 return miden_assembly::Assembler::new(source_manager)
64 .assemble_library_from_root(path, None)
65 .map(Arc::from);
66 }
67
68 if path.extension().is_some_and(|ext| ext.eq_ignore_ascii_case("masp")) {
69 let bytes = std::fs::read(path).into_diagnostic()?;
70 return miden_mast_package::Package::read_from_bytes_trusted(&bytes)
71 .map_err(|e| {
72 Report::msg(format!(
73 "failed to load Miden package from {}: {e}",
74 path.display()
75 ))
76 })
77 .map(Arc::new);
78 }
79
80 let source_manager = Arc::new(DefaultSourceManager::default());
81 let assembler = miden_assembly::Assembler::new(source_manager);
82 let mut project_assembler = assembler.for_project_at_path(path, registry)?;
83 project_assembler.assemble(ProjectTargetSelector::Library, "release")
84 }
85
86 fn find(&self, search_paths: &[PathBuf]) -> Result<PathBuf, Report> {
87 use std::fs;
88
89 for search_path in search_paths {
90 let reader = fs::read_dir(search_path).map_err(|err| {
91 Report::msg(format!(
92 "invalid library search path '{}': {err}",
93 search_path.display()
94 ))
95 })?;
96 for entry in reader {
97 let Ok(entry) = entry else {
98 continue;
99 };
100 let path = entry.path();
101 if path.extension().is_none_or(|ext| !ext.eq_ignore_ascii_case("masp")) {
102 continue;
103 }
104 let Some(stem) = path.file_stem().and_then(|stem| stem.to_str()) else {
105 continue;
106 };
107 if stem != self.name() {
108 continue;
109 }
110
111 if !path.is_file() {
112 return Err(Report::msg(format!(
113 "unable to load Miden Assembly package from '{}': not a file",
114 path.display()
115 )));
116 }
117 return Ok(path);
118 }
119 }
120
121 Err(Report::msg(format!(
122 "unable to locate library '{}' using any of the provided search paths",
123 self.name
124 )))
125 }
126}
127
128pub(crate) fn load_package_from_path(path: &Path) -> Result<Arc<Package>, Report> {
129 let bytes = std::fs::read(path).into_diagnostic()?;
130 miden_mast_package::Package::read_from_bytes_trusted(&bytes)
131 .map_err(|e| {
132 Report::msg(format!("failed to load Miden package from {}: {e}", path.display()))
133 })
134 .map(Arc::new)
135}
136
137#[cfg(feature = "tui")]
138impl clap::builder::ValueParserFactory for LinkLibrary {
139 type Parser = LinkLibraryParser;
140
141 fn value_parser() -> Self::Parser {
142 LinkLibraryParser
143 }
144}
145
146#[cfg(feature = "tui")]
147#[doc(hidden)]
148#[derive(Clone)]
149pub struct LinkLibraryParser;
150
151#[cfg(feature = "tui")]
152impl clap::builder::TypedValueParser for LinkLibraryParser {
153 type Value = LinkLibrary;
154
155 fn possible_values(
156 &self,
157 ) -> Option<Box<dyn Iterator<Item = clap::builder::PossibleValue> + '_>> {
158 use clap::builder::PossibleValue;
159
160 Some(Box::new(
161 [
162 PossibleValue::new("masm").help("A Miden Assembly project directory"),
163 PossibleValue::new("masp").help("A compiled Miden package file"),
164 ]
165 .into_iter(),
166 ))
167 }
168
169 fn parse_ref(
177 &self,
178 _cmd: &clap::Command,
179 _arg: Option<&clap::Arg>,
180 value: &std::ffi::OsStr,
181 ) -> Result<Self::Value, clap::error::Error> {
182 use clap::error::{Error, ErrorKind};
183
184 let value = value.to_str().ok_or_else(|| Error::new(ErrorKind::InvalidUtf8))?;
185 let (kind, name) = value
186 .split_once('=')
187 .map(|(kind, name)| (Some(kind), name))
188 .unwrap_or((None, value));
189
190 let linkage = match kind {
191 Some(kind) => match kind.split_once(':') {
192 Some(("masp" | "masm", "static")) => Linkage::Static,
193 Some(("masp" | "masm", "dynamic")) => Linkage::Dynamic,
194 Some(("masp" | "masm", other)) => {
195 return Err(Error::raw(
196 ErrorKind::ValueValidation,
197 format!("unrecognized linkage modifier '{other}'"),
198 ));
199 }
200 None if matches!(kind, "masp" | "masm") => Linkage::Dynamic,
201 Some(_) | None => {
202 return Err(Error::raw(
203 ErrorKind::ValueValidation,
204 "invalid link library kind: supported values are 'masp'",
205 ));
206 }
207 },
208 None => Linkage::Dynamic,
209 };
210
211 if name.is_empty() {
212 return Err(Error::raw(
213 ErrorKind::ValueValidation,
214 "invalid link library: must specify a name or path",
215 ));
216 }
217
218 let maybe_path = Path::new(name);
219 let extension = maybe_path.extension().map(|ext| ext.to_str().unwrap());
220 let is_package = match kind {
221 Some("masp") => true,
222 Some("masm") => false,
223 Some(kind) => {
224 return Err(Error::raw(
225 ErrorKind::InvalidValue,
226 format!("'{kind}' is not a valid library kind"),
227 ));
228 }
229 None => match extension {
230 Some("masp") => true,
231 Some("masm") | Some("toml") | None => false,
232 Some(kind) => {
233 return Err(Error::raw(
234 ErrorKind::InvalidValue,
235 format!("'{kind}' is not a valid library kind"),
236 ));
237 }
238 },
239 };
240
241 let path = match maybe_path.components().count() {
242 _ if extension.is_some() || maybe_path.is_dir() => {
243 maybe_path.canonicalize().map_err(|err| {
246 Error::raw(
247 ErrorKind::ValueValidation,
248 format!("invalid link library '{}': {err}", maybe_path.display()),
249 )
250 })?
251 }
252 1 => {
253 let name = maybe_path.file_name().unwrap().to_str().unwrap();
256 return Ok(LinkLibrary {
257 name: name.into(),
258 path: None,
259 linkage,
260 });
261 }
262 _ => {
263 maybe_path.canonicalize().map_err(|err| {
265 Error::raw(
266 ErrorKind::ValueValidation,
267 format!("invalid link library: '{}': {err}", maybe_path.display()),
268 )
269 })?
270 }
271 };
272
273 let extension = path.extension();
275 if is_package {
276 if extension.is_none_or(|ext| !ext.eq_ignore_ascii_case("masp")) {
278 return Err(Error::raw(
279 ErrorKind::ValueValidation,
280 format!(
281 "invalid link library: expected '{}' to refer to a .masp file",
282 path.display()
283 ),
284 ));
285 }
286
287 let name = path.file_stem().unwrap().to_str().unwrap();
288 return Ok(LinkLibrary {
289 name: name.into(),
290 path: Some(path),
291 linkage,
292 });
293 }
294
295 let normalized_path = if extension.is_none() {
296 path.join("miden-project.toml")
297 } else {
298 path.clone()
299 };
300 match extension {
301 _ if normalized_path.ends_with("miden-project.toml") => {
302 let source_manager = DefaultSourceManager::default();
304 let name = match miden_project::Project::load(&normalized_path, &source_manager) {
305 Ok(
306 miden_project::Project::Package(package)
307 | miden_project::Project::WorkspacePackage { package, .. },
308 ) => package.name().into_inner(),
309 Err(err) => return Err(Error::raw(ErrorKind::ValueValidation, err)),
310 };
311 Ok(LinkLibrary {
312 name,
313 path: Some(normalized_path),
314 linkage,
315 })
316 }
317 Some(ext) if ext.eq_ignore_ascii_case("masm") => {
318 let name = normalized_path.file_stem().unwrap().to_str().unwrap();
320 Ok(LinkLibrary {
321 name: name.into(),
322 path: Some(normalized_path),
323 linkage,
324 })
325 }
326 Some(_) => Err(Error::raw(
327 ErrorKind::ValueValidation,
328 format!(
329 "invalid link library: unrecognized file extension for '{}'",
330 normalized_path.display()
331 ),
332 )),
333 None => Err(Error::raw(
335 ErrorKind::ValueValidation,
336 format!(
337 "invalid link library: expected '{}' to be a directory, or have an explicit \
338 extension",
339 normalized_path.display()
340 ),
341 )),
342 }
343 }
344}