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<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        // Search for library among specified search paths
54        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_trusted(&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_trusted(&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    /// Parses the `-l` flag using the following format:
172    ///
173    /// `-l[KIND[:<LINKAGE>]=]NAME`
174    ///
175    /// * `KIND` is one of: `masp`, `masm`; defaults to `masp`
176    /// * `LINKAGE` is one of: `static`, `dynamic`; defaults to `dynamic`
177    /// * `NAME` is either a path, or a name (without extension)
178    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                // If the path had an extension or exists as a directory, then we always treat it
246                // like a path
247                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                // A single component path with no extension/not present as a direcotry is treated
256                // as a library name, not a file path
257                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                // A multi-component path is always treated as a path
266                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        // Normalize path and validate link library info
276        let extension = path.extension();
277        if is_package {
278            // We require a .masp path for packages
279            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                // We got a path to a project
305                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                // We got a single MASM file
321                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            // A missing extension must be a directory
336            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}