Skip to main content

miden_debug_engine/
linker.rs

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/// A library requested by the user to be linked against during compilation
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct LinkLibrary {
14    /// The name of the library.
15    ///
16    /// If requested by name, e.g. `-l std`, the name is used as given.
17    ///
18    /// If requested by path, e.g. `-l ./target/libs/miden-base.masl`, then the name of the library
19    /// will be the basename of the file specified in the path.
20    pub name: PackageId,
21    /// If specified, the path from which this library should be loaded
22    pub path: Option<PathBuf>,
23    /// How to link against this library
24    pub linkage: Linkage,
25}
26
27impl LinkLibrary {
28    /// Get the name of this library
29    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        // Search for library among specified search paths
51        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    /// Parses the `-l` flag using the following format:
170    ///
171    /// `-l[KIND[:<LINKAGE>]=]NAME`
172    ///
173    /// * `KIND` is one of: `masp`, `masm`; defaults to `masp`
174    /// * `LINKAGE` is one of: `static`, `dynamic`; defaults to `dynamic`
175    /// * `NAME` is either a path, or a name (without extension)
176    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                // If the path had an extension or exists as a directory, then we always treat it
244                // like a path
245                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                // A single component path with no extension/not present as a direcotry is treated
254                // as a library name, not a file path
255                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                // A multi-component path is always treated as a path
264                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        // Normalize path and validate link library info
274        let extension = path.extension();
275        if is_package {
276            // We require a .masp path for packages
277            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                // We got a path to a project
303                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                // We got a single MASM file
319                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            // A missing extension must be a directory
334            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}