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