Skip to main content

miden_project/
package.rs

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