Skip to main content

miden_project/
package.rs

1use alloc::boxed::Box;
2#[cfg(feature = "std")]
3use std::path::Path;
4
5#[cfg(feature = "std")]
6use miden_assembly_syntax::debuginfo::Spanned;
7use miden_mast_package::PackageId;
8
9#[cfg(feature = "std")]
10use crate::ast::{ProjectFileError, WorkspaceFile};
11use crate::*;
12
13/// The representation of an individual package in a Miden project
14#[derive(Debug)]
15pub struct Package {
16    /// The file path of the manifest corresponding to this package metadata, if applicable.
17    #[cfg(feature = "std")]
18    manifest_path: Option<Box<Path>>,
19    /// The name of the package
20    name: Span<PackageId>,
21    /// The semantic version associated with the package
22    version: Span<SemVer>,
23    /// The optional package description
24    description: Option<Arc<str>>,
25    /// The set of dependencies required by this package
26    dependencies: Vec<Dependency>,
27    /// The lint configuration specific to this package.
28    ///
29    /// By default, this is empty.
30    lints: MetadataSet,
31    /// The set of custom metadata attached to this package.
32    ///
33    /// By default, this is empty.
34    metadata: MetadataSet,
35    /// The library target for this package, if specified.
36    lib: Option<Span<Target>>,
37    /// The executable targets available for this package.
38    bins: Vec<Span<Target>>,
39    /// The build profiles configured for this package.
40    profiles: Vec<Profile>,
41}
42
43/// Constructor
44impl Package {
45    /// Create a new [Package] named `name` with the given default target.
46    ///
47    /// The resulting package will have a default version of `0.0.0`, no dependencies, and an
48    /// initial set of profiles that consist of the default development and release profiles. The
49    /// project will have no other configuration set up - that must be done in subsequent steps.
50    pub fn new(name: impl Into<PackageId>, default_target: Target) -> Box<Self> {
51        let name = name.into();
52        let (lib, bins) = if default_target.is_library() {
53            (Some(Span::unknown(default_target)), vec![])
54        } else {
55            (None, vec![Span::unknown(default_target)])
56        };
57        let profiles = vec![Profile::default(), Profile::release()];
58        Box::new(Self {
59            #[cfg(feature = "std")]
60            manifest_path: None,
61            name: Span::unknown(name),
62            version: Span::unknown(SemVer::new(0, 0, 0)),
63            description: None,
64            dependencies: Default::default(),
65            lints: Default::default(),
66            metadata: Default::default(),
67            lib,
68            bins,
69            profiles,
70        })
71    }
72
73    /// Specify a version for this package during initial construction
74    pub fn with_version(mut self: Box<Self>, version: SemVer) -> Box<Self> {
75        *self.version = version;
76        self
77    }
78
79    /// Provide the lint configuration for this package during initial construction
80    pub fn with_lints(mut self: Box<Self>, lints: MetadataSet) -> Box<Self> {
81        self.lints = lints;
82        self
83    }
84
85    /// Provide the metadata for this package during initial construction
86    pub fn with_metadata(mut self: Box<Self>, metadata: MetadataSet) -> Box<Self> {
87        self.metadata = metadata;
88        self
89    }
90
91    /// Add targets to this package during initial construction
92    ///
93    /// This function will panic if any of the given targets conflict with existing targets or
94    /// each other.
95    pub fn with_targets(
96        mut self: Box<Self>,
97        targets: impl IntoIterator<Item = Target>,
98    ) -> Box<Self> {
99        for target in targets {
100            if target.is_library() {
101                assert!(self.lib.is_none(), "a package cannot have duplicate library targets");
102                self.lib = Some(Span::unknown(target));
103            } else {
104                if self.bins.iter().any(|t| t.name == target.name) {
105                    panic!("duplicate definitions of the same target '{}'", &target.name);
106                }
107                self.bins.push(Span::unknown(target));
108            }
109        }
110        self
111    }
112
113    /// Add a profile to this package during initial construction
114    ///
115    /// If the given profile matches an existing profile, it will be merged over the top of it.
116    pub fn with_profile(mut self: Box<Self>, profile: Profile) -> Box<Self> {
117        for existing in self.profiles.iter_mut() {
118            if existing.name() == profile.name() {
119                existing.merge(&profile);
120                return self;
121            }
122        }
123
124        self.profiles.push(profile);
125        self
126    }
127
128    /// Add dependencies to this package during initial construction
129    ///
130    /// This function will panic if any of the given dependencies conflict with existing deps or
131    /// each other.
132    pub fn with_dependencies(
133        mut self: Box<Self>,
134        dependencies: impl IntoIterator<Item = Dependency>,
135    ) -> Box<Self> {
136        for dependency in dependencies {
137            if self.dependencies().iter().any(|dep| dep.name() == dependency.name()) {
138                panic!("duplicate definitions of dependency '{}'", dependency.name());
139            }
140            self.dependencies.push(dependency);
141        }
142
143        self
144    }
145}
146
147/// Accessors
148impl Package {
149    /// Get the name of this package
150    pub fn name(&self) -> Span<PackageId> {
151        self.name.clone()
152    }
153
154    /// Get the semantic version of this package
155    pub fn version(&self) -> Span<&SemVer> {
156        self.version.as_ref()
157    }
158
159    /// Get the description of this package, if specified
160    pub fn description(&self) -> Option<Arc<str>> {
161        self.description.clone()
162    }
163
164    /// Set the description of this package, if specified
165    pub fn set_description(&mut self, description: impl Into<Arc<str>>) {
166        self.description = Some(description.into());
167    }
168
169    /// Get the set of dependencies this package requires
170    pub fn dependencies(&self) -> &[Dependency] {
171        &self.dependencies
172    }
173
174    /// Get the number of dependencies this package requires
175    pub fn num_dependencies(&self) -> usize {
176        self.dependencies.len()
177    }
178
179    /// Get a reference to the linter metadata configured for this package
180    pub fn lints(&self) -> &MetadataSet {
181        &self.lints
182    }
183
184    /// Get a reference to the custom metadata configured for this package
185    pub fn metadata(&self) -> &MetadataSet {
186        &self.metadata
187    }
188
189    /// Get a reference to the build profiles configured for this package
190    pub fn profiles(&self) -> &[Profile] {
191        &self.profiles
192    }
193
194    /// Returns a profile with the specified name, or None if such a profile does not exist in this
195    /// package.
196    pub fn get_profile(&self, name: &str) -> Option<&Profile> {
197        self.profiles().iter().find(|profile| profile.name().as_ref() == name)
198    }
199
200    /// Get a reference to the library build target provided by this package
201    pub fn library_target(&self) -> Option<&Span<Target>> {
202        self.lib.as_ref()
203    }
204
205    /// Get a reference to the executable build targets provided by this package
206    pub fn executable_targets(&self) -> &[Span<Target>] {
207        &self.bins
208    }
209
210    /// Get the location of the manifest this package was loaded from, if known/applicable.
211    #[cfg(feature = "std")]
212    pub fn manifest_path(&self) -> Option<&Path> {
213        self.manifest_path.as_deref()
214    }
215}
216
217/// Parsing
218#[cfg(all(feature = "std", feature = "serde"))]
219impl Package {
220    /// Load a package from `source`, expected to be a standalone package-level `miden-project.toml`
221    /// manifest.
222    pub fn load(source: Arc<SourceFile>) -> Result<Box<Self>, Report> {
223        Self::parse(source, None)
224    }
225
226    /// Load a package from `source`, expected to be a package-level `miden-project.toml` manifest
227    /// which is presumed to be a member of `workspace` for purposes of configuration inheritance.
228    pub fn load_from_workspace(
229        source: Arc<SourceFile>,
230        workspace: &WorkspaceFile,
231    ) -> Result<Box<Self>, Report> {
232        Self::parse(source, Some(workspace))
233    }
234
235    fn parse(
236        source: Arc<SourceFile>,
237        workspace: Option<&WorkspaceFile>,
238    ) -> Result<Box<Self>, Report> {
239        let manifest_path = Path::new(source.uri().path());
240        let manifest_path = if manifest_path.try_exists().is_ok_and(|exists| exists) {
241            Some(manifest_path.to_path_buf().into_boxed_path())
242        } else {
243            None
244        };
245
246        // Parse the manifest into an AST for further processing
247        let package_ast = ast::ProjectFile::parse(source.clone())?;
248
249        // Extract metadata that can be inherited from the workspace manifest (if present)
250        let version = package_ast.get_or_inherit_version(source.clone(), workspace)?;
251        let description = package_ast.get_or_inherit_description(source.clone(), workspace)?;
252
253        // Compute the set of initial profiles inheritable from the workspace level
254        let mut profiles = Vec::default();
255        profiles.push(Profile::default());
256        profiles.push(Profile::release());
257        if let Some(workspace) = workspace {
258            for ast in workspace.profiles.iter() {
259                let profile = Profile::from_ast(ast, source.clone(), &profiles)?;
260                if let Some(prev) = profiles.iter_mut().find(|p| p.name() == ast.name.inner()) {
261                    *prev = profile;
262                } else {
263                    profiles.push(profile);
264                }
265            }
266        }
267
268        // Compute the effective profiles for this project, merging over the top of workspace-level
269        // profiles, but raising an error if the same profile is mentioned twice in the current
270        // project file.
271        let package_profiles_start = profiles.len();
272        for ast in package_ast.profiles.iter() {
273            let profile = Profile::from_ast(ast, source.clone(), &profiles)?;
274
275            if let Some(prev_index) = profiles.iter().position(|p| p.name() == profile.name()) {
276                if prev_index < package_profiles_start {
277                    profiles[prev_index].merge(&profile);
278                } else {
279                    let prev = &profiles[prev_index];
280                    return Err(ProjectFileError::DuplicateProfile {
281                        name: prev.name().clone(),
282                        source_file: source,
283                        span: profile.span(),
284                        prev: prev.span(),
285                    }
286                    .into());
287                }
288            } else {
289                profiles.push(profile);
290            }
291        }
292
293        // Extract project dependencies, using the workspace to resolve workspace-relative
294        // dependencies
295        let dependencies = package_ast.extract_dependencies(source.clone(), workspace)?;
296
297        // Extract the build targets for this project
298        let lib = package_ast.extract_library_target()?;
299        let bins = package_ast.extract_executable_targets();
300
301        let mut lints = workspace.map(|ws| ws.workspace.config.lints.clone()).unwrap_or_default();
302        lints.extend(package_ast.config.lints.clone());
303
304        let mut metadata =
305            workspace.map(|ws| ws.workspace.package.metadata.clone()).unwrap_or_default();
306        metadata.extend(package_ast.package.detail.metadata.clone());
307
308        Ok(Box::new(Self {
309            manifest_path,
310            name: package_ast.package.name.clone().map(|id| id.into()),
311            version,
312            description,
313            dependencies,
314            lints,
315            metadata,
316            profiles,
317            lib,
318            bins,
319        }))
320    }
321}
322
323#[cfg(feature = "serde")]
324impl Package {
325    /// Pretty print this [Package] in TOML format.
326    ///
327    /// The output of this function is not guaranteed to be identical to the way the original
328    /// manifest (if one exists) was written, i.e. it may emit keys that are optional or that
329    /// contain default or inherited values.
330    pub fn to_toml(&self) -> Result<alloc::string::String, Report> {
331        let manifest_ast = ast::ProjectFile {
332            source_file: None,
333            package: ast::PackageTable {
334                name: self.name().map(|id| id.into_inner()),
335                detail: ast::PackageDetail {
336                    version: Some(
337                        self.version().map(|v| ast::parsing::MaybeInherit::Value(v.clone())),
338                    ),
339                    description: self
340                        .description()
341                        .map(ast::parsing::MaybeInherit::Value)
342                        .map(Span::unknown),
343                    metadata: self.metadata.clone(),
344                },
345            },
346            config: ast::PackageConfig {
347                dependencies: self
348                    .dependencies()
349                    .iter()
350                    .map(|dep| {
351                        let name = Span::unknown(dep.name().clone());
352                        let linkage = if matches!(dep.linkage(), Linkage::Dynamic) {
353                            None
354                        } else {
355                            Some(Span::unknown(dep.linkage()))
356                        };
357                        let spec = match dep.scheme() {
358                            DependencyVersionScheme::Workspace { .. } => ast::DependencySpec {
359                                name: name.clone(),
360                                version_or_digest: None,
361                                workspace: true,
362                                path: None,
363                                git: None,
364                                branch: None,
365                                rev: None,
366                                linkage,
367                            },
368                            DependencyVersionScheme::WorkspacePath { path, version } => {
369                                ast::DependencySpec {
370                                    name: name.clone(),
371                                    version_or_digest: version.clone(),
372                                    workspace: false,
373                                    path: Some(path.clone()),
374                                    git: None,
375                                    branch: None,
376                                    rev: None,
377                                    linkage,
378                                }
379                            },
380                            DependencyVersionScheme::Registry(req) => ast::DependencySpec {
381                                name: name.clone(),
382                                version_or_digest: Some(req.clone()),
383                                workspace: false,
384                                path: None,
385                                git: None,
386                                branch: None,
387                                rev: None,
388                                linkage,
389                            },
390                            DependencyVersionScheme::Path { path, version } => {
391                                ast::DependencySpec {
392                                    name: name.clone(),
393                                    version_or_digest: version.clone(),
394                                    workspace: false,
395                                    path: Some(path.clone()),
396                                    git: None,
397                                    branch: None,
398                                    rev: None,
399                                    linkage,
400                                }
401                            },
402                            DependencyVersionScheme::Git { repo, revision, version } => {
403                                let (branch, rev) = match revision.inner() {
404                                    GitRevision::Branch(b) => {
405                                        (Some(Span::new(revision.span(), b.clone())), None)
406                                    },
407                                    GitRevision::Commit(c) => {
408                                        (None, Some(Span::new(revision.span(), c.clone())))
409                                    },
410                                };
411                                ast::DependencySpec {
412                                    name: name.clone(),
413                                    version_or_digest: version.as_ref().map(|spanned| {
414                                        VersionRequirement::from(spanned.inner().clone())
415                                    }),
416                                    workspace: false,
417                                    path: None,
418                                    git: Some(repo.clone()),
419                                    branch,
420                                    rev,
421                                    linkage,
422                                }
423                            },
424                        };
425
426                        (name, Span::unknown(spec))
427                    })
428                    .collect(),
429                lints: self.lints.clone(),
430            },
431            lib: self.lib.as_ref().map(|lib| {
432                Span::unknown(ast::LibTarget {
433                    kind: if matches!(lib.ty, TargetType::Library) {
434                        None
435                    } else {
436                        Some(Span::unknown(lib.ty))
437                    },
438                    namespace: Some(lib.namespace.as_ref().map(|path| path.as_str().into())),
439                    path: lib.path.clone(),
440                })
441            }),
442            bins: self
443                .bins
444                .iter()
445                .map(|bin| {
446                    Span::unknown(ast::BinTarget {
447                        name: Some(bin.name.clone()),
448                        path: bin.path.clone(),
449                    })
450                })
451                .collect(),
452            profiles: self
453                .profiles()
454                .iter()
455                .map(|profile| ast::Profile {
456                    inherits: None,
457                    name: Span::unknown(profile.name().clone()),
458                    debug: Some(profile.should_emit_debug_info()),
459                    trim_paths: Some(profile.should_trim_paths()),
460                    metadata: profile.metadata().clone(),
461                })
462                .collect(),
463        };
464
465        toml::to_string_pretty(&manifest_ast)
466            .map_err(|err| Report::msg(format!("failed to pretty print project manifest: {err}")))
467    }
468}