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    /// Returns a profile with the specified name, or an error if such a profile does not exist in
204    /// this package.
205    pub fn resolve_profile(&self, name: &str) -> Result<&Profile, Report> {
206        self.get_profile(name).ok_or_else(|| {
207            Report::msg(format!(
208                "project '{}' does not define a '{}' build profile",
209                self.name().inner(),
210                name
211            ))
212        })
213    }
214
215    /// Compute the [PackageId] that will be produced for `target` if derived from this package
216    pub fn target_package_name(&self, target: &Target) -> PackageId {
217        if target.ty.is_executable() {
218            format!("{}:{}", self.name().inner(), target.name.inner()).into()
219        } else {
220            self.name().inner().clone()
221        }
222    }
223
224    /// Get a reference to the library build target provided by this package
225    pub fn library_target(&self) -> Option<&Span<Target>> {
226        self.lib.as_ref()
227    }
228
229    /// Get a reference to the executable build targets provided by this package
230    pub fn executable_targets(&self) -> &[Span<Target>] {
231        &self.bins
232    }
233
234    /// Get the location of the manifest this package was loaded from, if known/applicable.
235    #[cfg(feature = "std")]
236    pub fn manifest_path(&self) -> Option<&Path> {
237        self.manifest_path.as_deref()
238    }
239
240    /// Get the location of the manifest this package was loaded from, or return an error if not
241    /// available.
242    #[cfg(feature = "std")]
243    pub fn expect_manifest_path(&self) -> Result<&Path, Report> {
244        self.manifest_path().ok_or_else(|| {
245            Report::msg(format!("project '{}' is missing its manifest path", self.name().inner()))
246        })
247    }
248
249    /// Return the package model projection that affects artifact reuse for `target` under
250    /// `profile`.
251    pub fn build_provenance_projection(&self, target: &Target, profile: &Profile) -> String {
252        let Self {
253            #[cfg(feature = "std")]
254                manifest_path: _,
255            name,
256            version,
257            description: _,
258            dependencies: _,
259            lints: _,
260            metadata: _,
261            lib: _,
262            bins: _,
263            profiles: _,
264        } = self;
265
266        let mut projection = String::new();
267        projection.push_str("package:name:");
268        projection.push_str(name.inner().as_ref());
269        projection.push('\n');
270        projection.push_str("package:version:");
271        projection.push_str(version.inner().to_string().as_str());
272        projection.push('\n');
273        target.append_build_provenance_projection(&mut projection);
274        profile.append_build_provenance_projection(&mut projection);
275        projection
276    }
277}
278
279/// Parsing
280#[cfg(all(feature = "std", feature = "serde"))]
281impl Package {
282    /// Load a package from `source`, expected to be a standalone package-level `miden-project.toml`
283    /// manifest.
284    pub fn load(source: Arc<SourceFile>) -> Result<Box<Self>, Report> {
285        Self::parse(source, None)
286    }
287
288    /// Load a package from `source`, expected to be a package-level `miden-project.toml` manifest
289    /// which is presumed to be a member of `workspace` for purposes of configuration inheritance.
290    pub fn load_from_workspace(
291        source: Arc<SourceFile>,
292        workspace: &WorkspaceFile,
293    ) -> Result<Box<Self>, Report> {
294        Self::parse(source, Some(workspace))
295    }
296
297    fn parse(
298        source: Arc<SourceFile>,
299        workspace: Option<&WorkspaceFile>,
300    ) -> Result<Box<Self>, Report> {
301        let manifest_path = Path::new(source.uri().path());
302        let manifest_path = if manifest_path.try_exists().is_ok_and(|exists| exists) {
303            Some(manifest_path.to_path_buf().into_boxed_path())
304        } else {
305            None
306        };
307
308        // Parse the manifest into an AST for further processing
309        let package_ast = ast::ProjectFile::parse(source.clone())?;
310
311        // Extract metadata that can be inherited from the workspace manifest (if present)
312        let version = package_ast.get_or_inherit_version(source.clone(), workspace)?;
313        let description = package_ast.get_or_inherit_description(source.clone(), workspace)?;
314
315        // Compute the set of initial profiles inheritable from the workspace level
316        let mut profiles = Vec::default();
317        profiles.push(Profile::default());
318        profiles.push(Profile::release());
319        if let Some(workspace) = workspace {
320            for ast in workspace.profiles.iter() {
321                let profile = Profile::from_ast(ast, source.clone(), &profiles)?;
322                if let Some(prev) = profiles.iter_mut().find(|p| p.name() == ast.name.inner()) {
323                    *prev = profile;
324                } else {
325                    profiles.push(profile);
326                }
327            }
328        }
329
330        // Compute the effective profiles for this project, merging over the top of workspace-level
331        // profiles, but raising an error if the same profile is mentioned twice in the current
332        // project file.
333        let package_profiles_start = profiles.len();
334        for ast in package_ast.profiles.iter() {
335            let profile = Profile::from_ast(ast, source.clone(), &profiles)?;
336
337            if let Some(prev_index) = profiles.iter().position(|p| p.name() == profile.name()) {
338                if prev_index < package_profiles_start {
339                    profiles[prev_index].merge(&profile);
340                } else {
341                    let prev = &profiles[prev_index];
342                    return Err(ProjectFileError::DuplicateProfile {
343                        name: prev.name().clone(),
344                        source_file: source,
345                        span: profile.span(),
346                        prev: prev.span(),
347                    }
348                    .into());
349                }
350            } else {
351                profiles.push(profile);
352            }
353        }
354
355        // Extract project dependencies, using the workspace to resolve workspace-relative
356        // dependencies
357        let dependencies = package_ast.extract_dependencies(source.clone(), workspace)?;
358
359        // Extract the build targets for this project
360        let lib = package_ast.extract_library_target()?;
361        let bins = package_ast.extract_executable_targets();
362
363        let mut lints = workspace.map(|ws| ws.workspace.config.lints.clone()).unwrap_or_default();
364        lints.extend(package_ast.config.lints.clone());
365
366        let mut metadata =
367            workspace.map(|ws| ws.workspace.package.metadata.clone()).unwrap_or_default();
368        metadata.extend(package_ast.package.detail.metadata.clone());
369
370        Ok(Box::new(Self {
371            manifest_path,
372            name: package_ast.package.name.map(Into::into),
373            version,
374            description,
375            dependencies,
376            lints,
377            metadata,
378            profiles,
379            lib,
380            bins,
381        }))
382    }
383}
384
385#[cfg(feature = "serde")]
386impl Package {
387    /// Pretty print this [Package] in TOML format.
388    ///
389    /// The output of this function is not guaranteed to be identical to the way the original
390    /// manifest (if one exists) was written, i.e. it may emit keys that are optional or that
391    /// contain default or inherited values.
392    pub fn to_toml(&self) -> Result<String, Report> {
393        let manifest_ast = ast::ProjectFile {
394            source_file: None,
395            package: ast::PackageTable {
396                name: self.name().map(PackageId::into_inner),
397                detail: ast::PackageDetail {
398                    version: Some(
399                        self.version().map(|v| ast::parsing::MaybeInherit::Value(v.clone())),
400                    ),
401                    description: self
402                        .description()
403                        .map(ast::parsing::MaybeInherit::Value)
404                        .map(Span::unknown),
405                    metadata: self.metadata.clone(),
406                },
407            },
408            config: ast::PackageConfig {
409                dependencies: self
410                    .dependencies()
411                    .iter()
412                    .map(|dep| {
413                        let name = Span::unknown(dep.name().clone());
414                        let linkage = if matches!(dep.linkage(), Linkage::Dynamic) {
415                            None
416                        } else {
417                            Some(Span::unknown(dep.linkage()))
418                        };
419                        let spec = match dep.scheme() {
420                            DependencyVersionScheme::Workspace { .. } => ast::DependencySpec {
421                                name: name.clone(),
422                                version_or_digest: None,
423                                workspace: true,
424                                path: None,
425                                git: None,
426                                branch: None,
427                                rev: None,
428                                linkage,
429                            },
430                            DependencyVersionScheme::WorkspacePath { path, version } => {
431                                ast::DependencySpec {
432                                    name: name.clone(),
433                                    version_or_digest: version.clone(),
434                                    workspace: false,
435                                    path: Some(path.clone()),
436                                    git: None,
437                                    branch: None,
438                                    rev: None,
439                                    linkage,
440                                }
441                            },
442                            DependencyVersionScheme::Registry(req) => ast::DependencySpec {
443                                name: name.clone(),
444                                version_or_digest: Some(req.clone()),
445                                workspace: false,
446                                path: None,
447                                git: None,
448                                branch: None,
449                                rev: None,
450                                linkage,
451                            },
452                            DependencyVersionScheme::Path { path, version } => {
453                                ast::DependencySpec {
454                                    name: name.clone(),
455                                    version_or_digest: version.clone(),
456                                    workspace: false,
457                                    path: Some(path.clone()),
458                                    git: None,
459                                    branch: None,
460                                    rev: None,
461                                    linkage,
462                                }
463                            },
464                            DependencyVersionScheme::Git { repo, revision, version } => {
465                                let (branch, rev) = match revision.inner() {
466                                    GitRevision::Branch(b) => {
467                                        (Some(Span::new(revision.span(), b.clone())), None)
468                                    },
469                                    GitRevision::Commit(c) => {
470                                        (None, Some(Span::new(revision.span(), c.clone())))
471                                    },
472                                };
473                                ast::DependencySpec {
474                                    name: name.clone(),
475                                    version_or_digest: version.as_ref().map(|spanned| {
476                                        VersionRequirement::from(spanned.inner().clone())
477                                    }),
478                                    workspace: false,
479                                    path: None,
480                                    git: Some(repo.clone()),
481                                    branch,
482                                    rev,
483                                    linkage,
484                                }
485                            },
486                        };
487
488                        (name, Span::unknown(spec))
489                    })
490                    .collect(),
491                lints: self.lints.clone(),
492            },
493            lib: self.lib.as_ref().map(|lib| {
494                Span::unknown(ast::LibTarget {
495                    kind: if matches!(lib.ty, TargetType::Library) {
496                        None
497                    } else {
498                        Some(Span::unknown(lib.ty))
499                    },
500                    namespace: Some(lib.namespace.as_ref().map(|path| path.as_str().into())),
501                    path: lib.path.clone(),
502                })
503            }),
504            bins: self
505                .bins
506                .iter()
507                .map(|bin| {
508                    Span::unknown(ast::BinTarget {
509                        name: Some(bin.name.clone()),
510                        path: bin.path.clone(),
511                    })
512                })
513                .collect(),
514            profiles: self
515                .profiles()
516                .iter()
517                .map(|profile| ast::Profile {
518                    inherits: None,
519                    name: Span::unknown(profile.name().clone()),
520                    debug: Some(profile.should_emit_debug_info()),
521                    trim_paths: Some(profile.should_trim_paths()),
522                    metadata: profile.metadata().clone(),
523                })
524                .collect(),
525        };
526
527        toml::to_string_pretty(&manifest_ast)
528            .map_err(|err| Report::msg(format!("failed to pretty print project manifest: {err}")))
529    }
530}