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