Skip to main content

miden_package_registry/
lib.rs

1#![no_std]
2
3#[macro_use]
4extern crate alloc;
5
6#[cfg(any(test, feature = "std"))]
7extern crate std;
8
9#[cfg(feature = "resolver")]
10mod resolver;
11mod version;
12mod version_requirement;
13
14use alloc::{collections::BTreeMap, string::String, sync::Arc};
15use core::fmt;
16
17use miden_assembly_syntax::Report;
18pub use miden_assembly_syntax::{
19    debuginfo::Span,
20    semver,
21    semver::{Version as SemVer, VersionReq},
22};
23pub use miden_core::Word;
24use miden_mast_package::Package as MastPackage;
25pub use miden_mast_package::PackageId;
26#[cfg(feature = "serde")]
27use serde::{Deserialize, Serialize};
28
29#[cfg(feature = "resolver")]
30pub use self::resolver::{
31    DependencyResolutionError, InMemoryPackageRegistry, PackagePriority, PackageResolver,
32    VersionSet,
33};
34pub use self::{
35    version::{InvalidVersionError, SemVerError, Version},
36    version_requirement::VersionRequirement,
37};
38
39/// A type alias for an ordered map of package requirements.
40pub type PackageRequirements = BTreeMap<PackageId, VersionRequirement>;
41
42/// Metadata tracked for a specific canonical package version.
43#[derive(Debug, Clone, PartialEq, Eq)]
44#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
45pub struct PackageRecord {
46    /// The exact published version associated with this package
47    version: Version,
48    /// An optional description of this package
49    description: Option<Arc<str>>,
50    /// The required dependencies of this package
51    dependencies: PackageRequirements,
52}
53
54impl PackageRecord {
55    /// Construct a new record with the provided dependency metadata.
56    pub fn new(
57        version: Version,
58        dependencies: impl IntoIterator<Item = (PackageId, VersionRequirement)>,
59    ) -> Self {
60        Self {
61            version,
62            description: None,
63            dependencies: dependencies.into_iter().collect(),
64        }
65    }
66
67    /// Attach a description to this record.
68    pub fn with_description(mut self, description: impl Into<Arc<str>>) -> Self {
69        self.description = Some(description.into());
70        self
71    }
72
73    /// Get the detailed version information for this record
74    pub fn version(&self) -> &Version {
75        &self.version
76    }
77
78    /// The semantic version of this package
79    pub fn semantic_version(&self) -> &SemVer {
80        &self.version.version
81    }
82
83    /// The digest of the MAST forest contained in this package
84    pub fn digest(&self) -> Option<&Word> {
85        self.version.digest.as_ref()
86    }
87
88    /// Returns the package description, if known.
89    pub fn description(&self) -> Option<&Arc<str>> {
90        self.description.as_ref()
91    }
92
93    /// Returns the dependency metadata for this package.
94    pub fn dependencies(&self) -> &PackageRequirements {
95        &self.dependencies
96    }
97}
98
99/// A type alias for all known canonical semantic versions of a specific package.
100///
101/// Each semantic version maps to at most one canonical published artifact. The exact artifact
102/// identity, including content digest, is stored in the corresponding [`PackageRecord`].
103pub type PackageVersions = BTreeMap<SemVer, PackageRecord>;
104
105/// A read-only package registry interface used for querying package metadata and versions.
106pub trait PackageRegistry {
107    /// Return the versions known for `package`, if any.
108    fn available_versions(&self, package: &PackageId) -> Option<&PackageVersions>;
109
110    /// Returns true if any version of `package` exists in the registry.
111    fn is_available(&self, package: &PackageId) -> bool {
112        self.available_versions(package).is_some_and(|versions| !versions.is_empty())
113    }
114
115    /// Returns true if the specific `version` of `package` exists in the registry.
116    fn is_version_available(&self, package: &PackageId, version: &Version) -> bool {
117        self.get_by_version(package, version).is_some()
118    }
119
120    /// Returns true if the canonical semantic version of `package` exists in the registry.
121    fn is_semver_available(&self, package: &PackageId, version: &SemVer) -> bool {
122        self.get_by_semver(package, version).is_some()
123    }
124
125    /// Return the metadata for `package` at `version`, if present.
126    fn get_by_version(&self, package: &PackageId, version: &Version) -> Option<&PackageRecord> {
127        let record = self.available_versions(package)?.get(&version.version)?;
128        match version.digest.as_ref() {
129            Some(_) if record.version() == version => Some(record),
130            Some(_) => None,
131            None => Some(record),
132        }
133    }
134
135    /// Return the canonical metadata for `package` at the given semantic version.
136    fn get_by_semver(&self, package: &PackageId, version: &SemVer) -> Option<&PackageRecord> {
137        self.available_versions(package)?.get(version)
138    }
139
140    /// Return the exact metadata for `package` at the given fully-qualified version.
141    fn get_exact_version(&self, package: &PackageId, version: &Version) -> Option<&PackageRecord> {
142        match version.digest.as_ref() {
143            Some(_) => self.get_by_version(package, version),
144            None => None,
145        }
146    }
147
148    /// Return the metadata for `package` with `digest`, if present.
149    fn get_by_digest(&self, package: &PackageId, digest: &Word) -> Option<&PackageRecord> {
150        self.available_versions(package).and_then(|versions| {
151            versions
152                .values()
153                .rev()
154                .find(|record| record.version().digest.as_ref() == Some(digest))
155        })
156    }
157
158    /// Find the latest version of `package` that satisfies `requirement`.
159    fn find_latest<'a>(
160        &'a self,
161        package: &PackageId,
162        requirement: &VersionRequirement,
163    ) -> Option<&'a PackageRecord> {
164        if let VersionRequirement::Exact(version) = requirement {
165            return self.get_exact_version(package, version);
166        }
167
168        self.available_versions(package).and_then(|versions| {
169            versions.values().rev().find(|record| record.version().satisfies(requirement))
170        })
171    }
172}
173
174/// A read-only package artifact provider used to load assembled packages by resolved version.
175pub trait PackageProvider {
176    /// Load the concrete package artifact for `package` at `version`.
177    fn load_package(
178        &self,
179        package: &PackageId,
180        version: &Version,
181    ) -> Result<Arc<MastPackage>, Report>;
182}
183
184/// A marker trait for types implementing both [PackageRegistry] and [PackageProvider], which make
185/// them capable of both resolving packages and loading their associated artifacts.
186///
187/// This trait does not need to be directly implemented - it has a blanket impl for all types that
188/// implement both [PackageRegistry] and [PackageProvider]
189pub trait PackageRegistryAndProvider: PackageRegistry + PackageProvider {}
190
191impl<T: ?Sized + PackageProvider + PackageRegistry> PackageRegistryAndProvider for T {}
192
193/// A writable metadata index for package records.
194pub trait PackageIndex: PackageRegistry {
195    type Error: fmt::Display;
196
197    /// Register the canonical metadata for `name`.
198    ///
199    /// Implementations must reject attempts to register a second canonical artifact for the same
200    /// package semantic version.
201    fn register(&mut self, name: PackageId, record: PackageRecord) -> Result<(), Self::Error>;
202}
203
204/// A writable package cache used to store assembled package artifacts resolved during assembly.
205pub trait PackageCache: PackageRegistryAndProvider {
206    type Error: fmt::Display;
207
208    /// Cache `package`, returning the fully-qualified stored version.
209    fn cache_package(&mut self, package: Arc<MastPackage>) -> Result<Version, Self::Error>;
210}
211
212/// A writable package store used to publish assembled package artifacts and index metadata.
213pub trait PackageStore: PackageCache {
214    /// Publish `package` to the store, returning the fully-qualified stored version.
215    fn publish_package(&mut self, package: Arc<MastPackage>) -> Result<Version, Self::Error>;
216}
217
218/// The error type returned by [NoPackageStore]
219#[derive(Debug, thiserror::Error)]
220#[error("{0}")]
221pub struct NoPackageStoreError(String);
222
223/// A package store implementation which refuses publication and loading.
224///
225/// Cache writes are accepted as a no-op so callers which do not need a persistent package store can
226/// still assemble source dependencies.
227#[derive(Default)]
228pub struct NoPackageStore;
229
230impl PackageRegistry for NoPackageStore {
231    fn available_versions(&self, _package: &PackageId) -> Option<&PackageVersions> {
232        None
233    }
234}
235
236impl PackageProvider for NoPackageStore {
237    fn load_package(
238        &self,
239        package: &PackageId,
240        version: &Version,
241    ) -> Result<Arc<MastPackage>, Report> {
242        Err(Report::msg(format!("cannot load package {package}@{version}")))
243    }
244}
245
246impl PackageCache for NoPackageStore {
247    type Error = NoPackageStoreError;
248
249    fn cache_package(&mut self, package: Arc<MastPackage>) -> Result<Version, Self::Error> {
250        Ok(Version::new(package.version.clone(), package.dependency_commitment()))
251    }
252}
253
254impl PackageStore for NoPackageStore {
255    fn publish_package(&mut self, package: Arc<MastPackage>) -> Result<Version, Self::Error> {
256        Err(NoPackageStoreError(format!(
257            "cannot publish package {}@{}",
258            package.name, package.version
259        )))
260    }
261}
262
263#[cfg(test)]
264mod tests {
265    use alloc::{vec, vec::Vec};
266
267    use miden_assembly_syntax::ast::{Path as AstPath, PathBuf};
268    use miden_core::{
269        mast::{
270            BasicBlockNodeBuilder, DenseMastForestBuilder, MastForest, MastNodeExt, MastNodeId,
271        },
272        operations::Operation,
273    };
274    use miden_mast_package::{Package, PackageExport, ProcedureExport, TargetType};
275
276    use super::*;
277
278    fn build_forest() -> (MastForest, MastNodeId) {
279        let mut builder = DenseMastForestBuilder::new();
280        let node_id = builder
281            .push_node(BasicBlockNodeBuilder::new(vec![Operation::Add]))
282            .expect("failed to build basic block");
283        builder.mark_root(node_id);
284        let (forest, remapping) = builder.build_with_id_map().expect("failed to build forest");
285        let node_id = remapping.get(node_id).expect("root node should be retained");
286        (forest, node_id)
287    }
288
289    fn absolute_path(name: &str) -> Arc<AstPath> {
290        let path = PathBuf::new(name).expect("invalid path");
291        let path = path.as_path().to_absolute().unwrap().into_owned();
292        Arc::from(path.into_boxed_path())
293    }
294
295    fn build_package_exports(export: &str) -> (Arc<MastForest>, Vec<PackageExport>) {
296        let (forest, node_id) = build_forest();
297        let path = absolute_path(export);
298        let export =
299            ProcedureExport::new(Arc::clone(&path), Some(node_id), forest[node_id].digest(), None);
300
301        (Arc::new(forest), vec![PackageExport::Procedure(export)])
302    }
303
304    #[test]
305    fn no_package_store_cache_package_is_noop() {
306        let (mast, exports) = build_package_exports("test::pkg::entry");
307        let package = Arc::new(
308            Package::create(
309                PackageId::from("pkg"),
310                "1.0.0".parse().unwrap(),
311                TargetType::Library,
312                mast,
313                exports,
314                [],
315            )
316            .expect("test package should be valid"),
317        );
318        let expected = Version::new(package.version.clone(), package.dependency_commitment());
319
320        let mut store = NoPackageStore;
321        let cached = store
322            .cache_package(Arc::clone(&package))
323            .expect("no package store should accept cache writes as no-op");
324
325        assert_eq!(cached, expected);
326        assert!(store.available_versions(&package.name).is_none());
327        store
328            .load_package(&package.name, &cached)
329            .expect_err("no package store should not persist cache writes");
330        store
331            .publish_package(package)
332            .expect_err("no package store should still reject publication");
333    }
334
335    #[test]
336    fn package_registry_is_available_requires_at_least_one_version() {
337        struct EmptyVersionRegistry {
338            versions: PackageVersions,
339        }
340
341        impl PackageRegistry for EmptyVersionRegistry {
342            fn available_versions(&self, _package: &PackageId) -> Option<&PackageVersions> {
343                Some(&self.versions)
344            }
345        }
346
347        let registry = EmptyVersionRegistry { versions: BTreeMap::new() };
348
349        assert!(!registry.is_available(&PackageId::from("pkg")));
350    }
351}