Skip to main content

miden_debug_engine/
linker.rs

1use alloc::{boxed::Box, sync::Arc};
2use std::path::{Path, PathBuf};
3
4use miden_assembly_syntax::diagnostics::{IntoDiagnostic, Report};
5use miden_mast_package::{Package, PackageId};
6
7/// A compiled library package requested by the user for execution.
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct LinkLibrary {
10    /// The name of the library.
11    ///
12    /// If requested by name, e.g. `-l std`, the name is used as given.
13    ///
14    /// If requested by path, e.g. `-l ./target/libs/miden-base.masp`, then the name of the library
15    /// will be the basename of the file specified in the path.
16    pub name: PackageId,
17    /// If specified, the path from which this library should be loaded
18    pub path: Option<PathBuf>,
19    /// How to link against this library
20    pub linkage: Linkage,
21}
22
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub enum Linkage {
25    Dynamic,
26    Static,
27}
28
29impl LinkLibrary {
30    /// Get the name of this library
31    pub fn name(&self) -> &str {
32        self.name.as_ref()
33    }
34
35    pub fn is_core(&self) -> bool {
36        matches!(self.name.as_ref(), "miden-core" | "core" | "std")
37    }
38
39    pub fn is_protocol(&self) -> bool {
40        matches!(self.name.as_ref(), "miden-protocol" | "protocol" | "base")
41    }
42
43    pub fn load(&self, search_paths: &[PathBuf]) -> Result<Arc<Package>, Report> {
44        if let Some(path) = self.path.as_deref() {
45            return self.load_from_path(path);
46        }
47
48        // Search for library among specified search paths
49        let path = self.find(search_paths)?;
50
51        self.load_from_path(&path)
52    }
53
54    fn load_from_path(&self, path: &Path) -> Result<Arc<Package>, Report> {
55        if path.extension().is_none_or(|ext| !ext.eq_ignore_ascii_case("masp")) {
56            return Err(Report::msg(format!(
57                "link library '{}' is not a compiled .masp package",
58                path.display()
59            )));
60        }
61
62        let bytes = std::fs::read(path).into_diagnostic()?;
63        miden_mast_package::Package::read_from_bytes_trusted(&bytes)
64            .map_err(|e| {
65                Report::msg(format!("failed to load Miden package from {}: {e}", path.display()))
66            })
67            .map(Arc::new)
68    }
69
70    fn find(&self, search_paths: &[PathBuf]) -> Result<PathBuf, Report> {
71        use std::fs;
72
73        for search_path in search_paths {
74            let reader = fs::read_dir(search_path).map_err(|err| {
75                Report::msg(format!(
76                    "invalid library search path '{}': {err}",
77                    search_path.display()
78                ))
79            })?;
80            for entry in reader {
81                let Ok(entry) = entry else {
82                    continue;
83                };
84                let path = entry.path();
85                if path.extension().is_none_or(|ext| !ext.eq_ignore_ascii_case("masp")) {
86                    continue;
87                }
88                let Some(stem) = path.file_stem().and_then(|stem| stem.to_str()) else {
89                    continue;
90                };
91                if stem != self.name() {
92                    continue;
93                }
94
95                if !path.is_file() {
96                    return Err(Report::msg(format!(
97                        "unable to load Miden Assembly package from '{}': not a file",
98                        path.display()
99                    )));
100                }
101                return Ok(path);
102            }
103        }
104
105        Err(Report::msg(format!(
106            "unable to locate library '{}' using any of the provided search paths",
107            self.name
108        )))
109    }
110}
111
112#[cfg(feature = "std")]
113impl clap::builder::ValueParserFactory for LinkLibrary {
114    type Parser = LinkLibraryParser;
115
116    fn value_parser() -> Self::Parser {
117        LinkLibraryParser
118    }
119}
120
121#[cfg(feature = "std")]
122#[doc(hidden)]
123#[derive(Clone)]
124pub struct LinkLibraryParser;
125
126#[cfg(feature = "std")]
127impl clap::builder::TypedValueParser for LinkLibraryParser {
128    type Value = LinkLibrary;
129
130    fn possible_values(
131        &self,
132    ) -> Option<Box<dyn Iterator<Item = clap::builder::PossibleValue> + '_>> {
133        use clap::builder::PossibleValue;
134
135        Some(Box::new(
136            [PossibleValue::new("masp").help("A compiled Miden package file")].into_iter(),
137        ))
138    }
139
140    /// Parses the `-l` flag using the following format:
141    ///
142    /// `-l[KIND[:<LINKAGE>]=]NAME`
143    ///
144    /// * `KIND` is `masp`
145    /// * `LINKAGE` is one of: `static`, `dynamic`; defaults to `dynamic`
146    /// * `NAME` is either a path, or a name (without extension)
147    fn parse_ref(
148        &self,
149        _cmd: &clap::Command,
150        _arg: Option<&clap::Arg>,
151        value: &std::ffi::OsStr,
152    ) -> Result<Self::Value, clap::error::Error> {
153        use clap::error::{Error, ErrorKind};
154
155        let value = value.to_str().ok_or_else(|| Error::new(ErrorKind::InvalidUtf8))?;
156        let (kind, name) = value
157            .split_once('=')
158            .map(|(kind, name)| (Some(kind), name))
159            .unwrap_or((None, value));
160
161        let linkage = match kind {
162            Some(kind) => match kind.split_once(':') {
163                Some(("masp", "static")) => Linkage::Static,
164                Some(("masp", "dynamic")) => Linkage::Dynamic,
165                Some(("masp", other)) => {
166                    return Err(Error::raw(
167                        ErrorKind::ValueValidation,
168                        format!("unrecognized linkage modifier '{other}'"),
169                    ));
170                }
171                None if kind == "masp" => Linkage::Dynamic,
172                Some(_) | None => {
173                    return Err(Error::raw(
174                        ErrorKind::ValueValidation,
175                        "invalid link library kind: supported values are 'masp'",
176                    ));
177                }
178            },
179            None => Linkage::Dynamic,
180        };
181
182        if name.is_empty() {
183            return Err(Error::raw(
184                ErrorKind::ValueValidation,
185                "invalid link library: must specify a name or path",
186            ));
187        }
188
189        let maybe_path = Path::new(name);
190        let path = match maybe_path.components().count() {
191            _ if maybe_path.extension().is_some() || maybe_path.is_dir() => {
192                // Existing directories and values with an extension are always paths.
193                maybe_path.canonicalize().map_err(|err| {
194                    Error::raw(
195                        ErrorKind::ValueValidation,
196                        format!("invalid link library '{}': {err}", maybe_path.display()),
197                    )
198                })?
199            }
200            1 => {
201                // A single component with no extension that is not a directory is a package name.
202                let name = maybe_path.file_name().unwrap().to_str().unwrap();
203                return Ok(LinkLibrary {
204                    name: name.into(),
205                    path: None,
206                    linkage,
207                });
208            }
209            _ => {
210                // A multi-component path is always treated as a path
211                maybe_path.canonicalize().map_err(|err| {
212                    Error::raw(
213                        ErrorKind::ValueValidation,
214                        format!("invalid link library: '{}': {err}", maybe_path.display()),
215                    )
216                })?
217            }
218        };
219
220        if path.extension().is_none_or(|ext| !ext.eq_ignore_ascii_case("masp")) {
221            return Err(Error::raw(
222                ErrorKind::ValueValidation,
223                format!(
224                    "invalid link library: expected '{}' to refer to a compiled .masp package",
225                    path.display()
226                ),
227            ));
228        }
229
230        let name = path.file_stem().unwrap().to_str().unwrap();
231        Ok(LinkLibrary {
232            name: name.into(),
233            path: Some(path),
234            linkage,
235        })
236    }
237}
238
239#[cfg(all(test, feature = "std"))]
240mod tests {
241    use std::{ffi::OsStr, string::ToString};
242
243    use clap::builder::TypedValueParser;
244
245    use super::*;
246
247    #[test]
248    fn parser_rejects_masm_sources() {
249        let source = tempfile::Builder::new().suffix(".masm").tempfile().unwrap();
250        let error = LinkLibraryParser
251            .parse_ref(&clap::Command::new("test"), None, source.path().as_os_str())
252            .unwrap_err();
253
254        assert!(error.to_string().contains("compiled .masp package"));
255    }
256
257    #[test]
258    fn parser_rejects_project_directories() {
259        let project = tempfile::tempdir().unwrap();
260        let error = LinkLibraryParser
261            .parse_ref(&clap::Command::new("test"), None, project.path().as_os_str())
262            .unwrap_err();
263
264        assert!(error.to_string().contains("compiled .masp package"));
265    }
266
267    #[test]
268    fn parser_rejects_source_kind() {
269        let error = LinkLibraryParser
270            .parse_ref(&clap::Command::new("test"), None, OsStr::new("masm=library"))
271            .unwrap_err();
272
273        assert!(error.to_string().contains("supported values are 'masp'"));
274    }
275
276    #[test]
277    fn parser_accepts_explicit_static_package() {
278        let package = tempfile::Builder::new().suffix(".masp").tempfile().unwrap();
279        let value = format!("masp:static={}", package.path().display());
280        let library = LinkLibraryParser
281            .parse_ref(&clap::Command::new("test"), None, OsStr::new(&value))
282            .unwrap();
283
284        assert_eq!(library.linkage, Linkage::Static);
285        assert_eq!(library.path.unwrap(), package.path().canonicalize().unwrap());
286    }
287}