Skip to main content

miden_assembly/
project.rs

1use alloc::{boxed::Box, collections::BTreeMap, format, string::ToString, sync::Arc, vec::Vec};
2use std::{
3    fs,
4    path::{Path as FsPath, PathBuf},
5};
6
7use miden_assembly_syntax::{ast::ModuleKind, diagnostics::Report};
8use miden_mast_package::{Package as MastPackage, TargetType};
9use miden_package_registry::{PackageCache, PackageId, Version as PackageVersion};
10use miden_project::{
11    Linkage, Package as ProjectPackage, PreassembledDependencyMetadata, Profile,
12    ProjectDependencyNodeProvenance, ProjectSource, ProjectSourceOrigin, Target,
13};
14
15use crate::{Assembler, ast::Module};
16
17mod build_provenance;
18mod dependency_graph;
19mod providers;
20mod runtime_dependencies;
21mod target_selector;
22
23use self::{
24    build_provenance::PackageBuildProvenance, dependency_graph::DependencyGraph,
25    runtime_dependencies::RuntimeDependencies,
26};
27pub use self::{
28    providers::{MasmSourceProvider, ProjectSourceProvider, TargetAssemblyContext},
29    target_selector::ProjectTargetSelector,
30};
31
32#[cfg(test)]
33mod tests;
34
35// ASSEMBLER EXTENSIONS
36// ================================================================================================
37
38impl Assembler {
39    /// Get a [ProjectAssembler] configured for the project whose manifest is at `manifest_path`.
40    pub fn for_project_at_path<'a, S>(
41        self,
42        manifest_path: impl AsRef<FsPath>,
43        store: &'a mut S,
44    ) -> Result<ProjectAssembler<'a, S>, Report>
45    where
46        S: PackageCache + ?Sized,
47    {
48        let masm_provider = Box::new(MasmSourceProvider) as Box<_>;
49        self.for_project_at_path_with_providers(manifest_path, store, [masm_provider])
50    }
51
52    /// Get a [ProjectAssembler] configured for the project whose manifest is at `manifest_path`.
53    pub fn for_project_at_path_with_providers<'a, S>(
54        self,
55        manifest_path: impl AsRef<FsPath>,
56        store: &'a mut S,
57        providers: impl IntoIterator<Item = Box<dyn ProjectSourceProvider>>,
58    ) -> Result<ProjectAssembler<'a, S>, Report>
59    where
60        S: PackageCache + ?Sized,
61    {
62        let manifest_path = manifest_path.as_ref();
63        let source_manager = self.source_manager();
64        let project = miden_project::Project::load(manifest_path, &source_manager)?;
65        let package = project.package();
66        let dependency_graph =
67            DependencyGraph::from_project_path(manifest_path, store, source_manager)?;
68
69        Ok(ProjectAssembler {
70            assembler: self,
71            project: package,
72            source_provider: SourceProviderRegistry::new(providers),
73            dependency_graph,
74            store,
75        })
76    }
77
78    /// Get a [ProjectAssembler] configured for `project`
79    pub fn for_project<'a, S>(
80        self,
81        project: Arc<ProjectPackage>,
82        store: &'a mut S,
83    ) -> Result<ProjectAssembler<'a, S>, Report>
84    where
85        S: PackageCache + ?Sized,
86    {
87        let masm_provider = Box::new(MasmSourceProvider) as Box<_>;
88        self.for_project_with_providers(project, store, [masm_provider])
89    }
90
91    /// Get a [ProjectAssembler] configured for `project`
92    pub fn for_project_with_providers<'a, S>(
93        self,
94        project: Arc<ProjectPackage>,
95        store: &'a mut S,
96        providers: impl IntoIterator<Item = Box<dyn ProjectSourceProvider>>,
97    ) -> Result<ProjectAssembler<'a, S>, Report>
98    where
99        S: PackageCache + ?Sized,
100    {
101        let source_manager = self.source_manager();
102        let dependency_graph =
103            DependencyGraph::from_project(project.clone(), store, source_manager)?;
104        Ok(ProjectAssembler {
105            assembler: self,
106            project,
107            source_provider: SourceProviderRegistry::new(providers),
108            dependency_graph,
109            store,
110        })
111    }
112}
113
114// PROJECT ASSEMBLER
115// ================================================================================================
116
117pub struct ProjectSourceInputs {
118    pub root: Box<Module>,
119    pub support: Vec<Box<Module>>,
120}
121
122pub struct ProjectSourceProvenanceInputs {
123    pub root: SourceFileProvenance,
124    pub support: Vec<SourceFileProvenance>,
125}
126
127pub struct SourceFileProvenance {
128    pub path: Box<std::path::Path>,
129    pub content: Box<str>,
130}
131
132impl SourceFileProvenance {
133    pub fn from_path(path: PathBuf) -> Result<Self, Report> {
134        let content = fs::read_to_string(&path).map_err(|err| {
135            Report::msg(format!("unable to read source file '{}': {err}", path.display()))
136        })?;
137        Ok(Self {
138            path: path.into_boxed_path(),
139            content: content.into_boxed_str(),
140        })
141    }
142}
143
144pub struct SourceProviderRegistry {
145    registered: BTreeMap<&'static str, Box<dyn ProjectSourceProvider>>,
146}
147
148impl Default for SourceProviderRegistry {
149    fn default() -> Self {
150        Self {
151            registered: BTreeMap::from_iter([(
152                "masm",
153                Box::new(MasmSourceProvider) as Box<dyn ProjectSourceProvider>,
154            )]),
155        }
156    }
157}
158
159impl SourceProviderRegistry {
160    pub fn new(providers: impl IntoIterator<Item = Box<dyn ProjectSourceProvider>>) -> Self {
161        let mut this = Self {
162            registered: providers.into_iter().map(|p| (p.file_type(), p)).collect(),
163        };
164
165        if !this.registered.contains_key("masm") {
166            this.registered.insert("masm", Box::new(MasmSourceProvider));
167        }
168
169        this
170    }
171
172    pub fn with_source_provider(
173        &mut self,
174        provider: impl ProjectSourceProvider + 'static,
175    ) -> &mut Self {
176        let file_type = provider.file_type();
177        let provider = Box::new(provider) as Box<dyn ProjectSourceProvider>;
178
179        self.registered.insert(file_type, provider);
180
181        self
182    }
183
184    #[inline]
185    pub fn get_provider(&self, file_type: &str) -> Option<&dyn ProjectSourceProvider> {
186        self.registered.get(file_type).map(AsRef::as_ref)
187    }
188}
189
190pub struct ProjectAssembler<'a, S: PackageCache + ?Sized> {
191    assembler: Assembler,
192    project: Arc<ProjectPackage>,
193    dependency_graph: DependencyGraph,
194    source_provider: SourceProviderRegistry,
195    store: &'a mut S,
196}
197
198impl<'a, S> ProjectAssembler<'a, S>
199where
200    S: PackageCache + ?Sized,
201{
202    pub fn with_source_provider(
203        &mut self,
204        provider: impl ProjectSourceProvider + 'static,
205    ) -> &mut Self {
206        self.source_provider.with_source_provider(provider);
207        self
208    }
209
210    pub fn project(&self) -> &ProjectPackage {
211        self.project.as_ref()
212    }
213
214    pub fn assemble(
215        &mut self,
216        target_selector: ProjectTargetSelector<'_>,
217        profile_name: &str,
218    ) -> Result<Arc<MastPackage>, Report> {
219        let target = target_selector.select_target(self.project.as_ref())?;
220
221        // When building an executable target from a project with a library target, we require
222        // that the executable target be linked statically against the library target
223        let mut cache = BTreeMap::new();
224        let root_id = self.dependency_graph.root().clone();
225        let required_lib = if target.is_executable()
226            && let Some(library_target) =
227                self.project.library_target().map(|target| target.inner().clone())
228        {
229            Some(self.assemble_source_package(
230                root_id.clone(),
231                Arc::clone(&self.project),
232                &library_target,
233                profile_name,
234                None,
235                &mut cache,
236            )?)
237        } else {
238            None
239        };
240
241        self.assemble_source_package(
242            root_id,
243            Arc::clone(&self.project),
244            &target,
245            profile_name,
246            required_lib,
247            &mut cache,
248        )
249        .map(|resolved| resolved.package)
250    }
251
252    fn assemble_source_package(
253        &mut self,
254        package_id: PackageId,
255        project: Arc<ProjectPackage>,
256        target: &Target,
257        profile_name: &str,
258        required_lib: Option<ResolvedPackage>,
259        cache: &mut BTreeMap<PackageId, ResolvedPackage>,
260    ) -> Result<ResolvedPackage, Report> {
261        let cache_key = project.target_package_name(target);
262        if let Some(package) = cache.get(&cache_key).cloned() {
263            assert_eq!(package.package.kind, target.ty);
264            return Ok(package);
265        }
266
267        let profile = project.resolve_profile(profile_name)?;
268        let mut assembler = self
269            .assembler
270            .clone()
271            .with_emit_debug_info(profile.should_emit_debug_info())
272            .with_trim_paths(profile.should_trim_paths());
273        let mut runtime_dependencies = RuntimeDependencies::default();
274        debug_assert!(
275            required_lib.is_none() || target.ty.is_executable(),
276            "expected required_lib only for executable targets"
277        );
278        match required_lib {
279            Some(required_lib) if required_lib.package.is_kernel() => {
280                // We do not link the package here, as by definition a required library is only
281                // present for executable targets, and we always unconditionally link kernel
282                // dependencies just prior to assembling the package
283                runtime_dependencies.record_linked_kernel_dependency(required_lib.package)?;
284            },
285            Some(required_lib) => {
286                assembler.link_package(required_lib.package.clone(), Linkage::Static)?;
287                if let Some(kernel_package) = required_lib.linked_kernel_package {
288                    runtime_dependencies.record_linked_kernel_dependency(kernel_package)?;
289                }
290            },
291            None => (),
292        }
293
294        let node = self.dependency_graph.get(&package_id)?;
295        let dependencies = node.dependencies.clone();
296        for edge in dependencies.iter() {
297            let dependency_package =
298                self.resolve_dependency_package(&edge.dependency, profile_name, cache)?;
299            if !dependency_package.package.is_library() {
300                return Err(Report::msg(format!(
301                    "dependency '{}' resolved to executable package '{}', but only library-like packages can be linked",
302                    edge.dependency, dependency_package.package.name
303                )));
304            }
305
306            if !dependency_package.package.is_kernel() {
307                assembler.link_package(dependency_package.package.clone(), edge.linkage)?;
308            }
309            runtime_dependencies.merge_package(dependency_package, edge.linkage)?;
310        }
311
312        let ProjectSourceInputs { root, support } =
313            self.load_target_sources(project.as_ref(), target, profile)?;
314
315        // Collect specific well-known custom sections produced by the project assembler
316        let mut sections = Vec::new();
317
318        // Section: build provenance
319        //
320        // This is produced before actual assembly, while we still have the sources on hand
321        if let Some(provenance) = self.dependency_graph.build_source_provenance(
322            &package_id,
323            project.as_ref(),
324            target,
325            profile_name,
326            &self.source_provider,
327        )? {
328            sections.push(provenance.to_section());
329        }
330
331        if let Some(kernel_package) = runtime_dependencies.kernel.clone() {
332            if matches!(target.ty, TargetType::Kernel) {
333                return Err(Report::msg(format!(
334                    "kernel targets cannot depend on a kernel, dependency '{}' is a kernel",
335                    kernel_package.name
336                )));
337            }
338            assembler.link_package(kernel_package, Linkage::Dynamic)?;
339        }
340
341        let mut product = match target.ty {
342            TargetType::Executable => {
343                assembler.assemble_executable_modules(package_id.clone(), root, support)?
344            },
345            _ if target.ty.is_library() => {
346                assembler.assemble_library_modules(package_id.clone(), root, support, target.ty)?
347            },
348            _ => unreachable!("non-exhaustive target type"),
349        };
350
351        product
352            .extend_dependencies(runtime_dependencies.deps.into_values())
353            .expect("assembled package manifest should have unique runtime dependencies");
354
355        let mut package = product.into_artifact()?;
356        package.name = project.target_package_name(target);
357        package.version = project.version().into_inner().clone();
358        package.description = project.description().map(|description| description.to_string());
359        package.sections.extend(sections);
360        let package = Arc::from(package);
361
362        let resolved = ResolvedPackage {
363            package,
364            linked_kernel_package: runtime_dependencies.kernel,
365        };
366        cache.insert(package_id, resolved.clone());
367
368        Ok(resolved)
369    }
370
371    fn resolve_dependency_package(
372        &mut self,
373        package_id: &PackageId,
374        profile_name: &str,
375        cache: &mut BTreeMap<PackageId, ResolvedPackage>,
376    ) -> Result<ResolvedPackage, Report> {
377        if let Some(package) = cache.get(package_id).cloned() {
378            return Ok(package);
379        }
380
381        let node = self.dependency_graph.get(package_id)?;
382        let node_version = node.version.clone();
383
384        let (package, should_cache) = match &node.provenance {
385            ProjectDependencyNodeProvenance::Source(ProjectSource::Virtual { .. }) => {
386                return Err(Report::msg(format!(
387                    "package '{package_id}' is missing a manifest path",
388                )));
389            },
390            ProjectDependencyNodeProvenance::Source(ProjectSource::Real {
391                manifest_path,
392                origin,
393                library_path: Some(_),
394                ..
395            }) => {
396                let project = miden_project::Project::load_project_reference(
397                    package_id,
398                    manifest_path,
399                    &self.assembler.source_manager(),
400                )
401                .map(|project| project.package())?;
402                let target = project
403                    .library_target()
404                    .map(|target| target.inner().clone())
405                    .ok_or_else(|| {
406                        Report::msg(format!(
407                            "dependency '{package_id}' does not define a library target"
408                        ))
409                    })?;
410                match self.try_reuse_registered_source_package(
411                    package_id,
412                    &node_version,
413                    &project,
414                    &target,
415                    profile_name,
416                    origin,
417                    manifest_path,
418                )? {
419                    RegisteredSourcePackage::Loaded(package) => (
420                        ResolvedPackage {
421                            linked_kernel_package: self
422                                .resolve_linked_kernel_package(package.clone())?,
423                            package,
424                        },
425                        false,
426                    ),
427                    reuse => {
428                        let package = self.assemble_source_package(
429                            package_id.clone(),
430                            project,
431                            &target,
432                            profile_name,
433                            None,
434                            cache,
435                        )?;
436                        match reuse {
437                            RegisteredSourcePackage::Missing => (),
438                            RegisteredSourcePackage::IndexedButUnreadable(expected) => {
439                                let actual = PackageVersion::new(
440                                    package.package.version.clone(),
441                                    package.package.digest(),
442                                );
443                                if actual != expected {
444                                    return Err(Report::msg(format!(
445                                        "package '{package_id}' version '{node_version}' is already registered as '{expected}', but the canonical artifact could not be loaded and rebuilding from source produced '{actual}'; bump the semantic version or repair the package store"
446                                    )));
447                                }
448                            },
449                            RegisteredSourcePackage::Loaded(_) => unreachable!(),
450                        }
451                        (package, true)
452                    },
453                }
454            },
455            ProjectDependencyNodeProvenance::Source(_) => {
456                let package =
457                    self.load_canonical_package(package_id, &node_version)?.ok_or_else(|| {
458                        Report::msg(format!(
459                            "dependency '{package_id}' version '{node_version}' was not found in the package registry"
460                        ))
461                    })?;
462                (
463                    ResolvedPackage {
464                        linked_kernel_package: self
465                            .resolve_linked_kernel_package(package.clone())?,
466                        package,
467                    },
468                    false,
469                )
470            },
471            ProjectDependencyNodeProvenance::Registry { selected, .. } => {
472                let package = self.store.load_package(package_id, selected)?;
473                (
474                    ResolvedPackage {
475                        linked_kernel_package: self
476                            .resolve_linked_kernel_package(package.clone())?,
477                        package,
478                    },
479                    false,
480                )
481            },
482            ProjectDependencyNodeProvenance::Preassembled {
483                path,
484                selected,
485                kind,
486                requirements,
487            } => {
488                let package = load_selected_preassembled_package(
489                    path,
490                    package_id,
491                    selected,
492                    *kind,
493                    requirements,
494                )?;
495                let should_cache = self.should_cache_preassembled_package(package_id, selected);
496                (
497                    ResolvedPackage {
498                        linked_kernel_package: self
499                            .resolve_linked_kernel_package(package.clone())?,
500                        package,
501                    },
502                    should_cache,
503                )
504            },
505        };
506
507        if should_cache {
508            self.cache_resolved_package(&package)?;
509        }
510        cache.insert(package_id.clone(), package.clone());
511        Ok(package)
512    }
513
514    fn resolve_linked_kernel_package(
515        &self,
516        package: Arc<MastPackage>,
517    ) -> Result<Option<Arc<MastPackage>>, Report> {
518        if package.is_kernel() {
519            return Ok(Some(package));
520        }
521
522        let Some(kernel_dependency) = package.kernel_runtime_dependency()? else {
523            return Ok(None);
524        };
525
526        let version =
527            PackageVersion::new(kernel_dependency.version.clone(), kernel_dependency.digest);
528        if self.store.get_exact_version(&kernel_dependency.name, &version).is_some() {
529            match self.store.load_package(&kernel_dependency.name, &version) {
530                Ok(kernel_package) => {
531                    if !kernel_package.is_kernel() {
532                        return Err(Report::msg(format!(
533                            "runtime kernel dependency '{}@{}#{}' resolved to non-kernel package '{}'",
534                            kernel_dependency.name,
535                            kernel_dependency.version,
536                            kernel_dependency.digest,
537                            kernel_package.name
538                        )));
539                    }
540                    return Ok(Some(kernel_package));
541                },
542                Err(load_error) => {
543                    if let Some(kernel_package) = package
544                        .try_embedded_kernel_package()
545                        .map(|kernel_package| kernel_package.map(Arc::from))?
546                    {
547                        return Ok(Some(kernel_package));
548                    }
549                    return Err(load_error);
550                },
551            }
552        }
553
554        package
555            .try_embedded_kernel_package()
556            .map(|kernel_package| kernel_package.map(Arc::from))
557    }
558
559    fn load_canonical_package(
560        &self,
561        package_id: &PackageId,
562        version: &miden_project::SemVer,
563    ) -> Result<Option<Arc<MastPackage>>, Report> {
564        let Some(record) = self.store.get_by_semver(package_id, version) else {
565            return Ok(None);
566        };
567        self.store.load_package(package_id, record.version()).map(Some)
568    }
569
570    fn try_reuse_registered_source_package(
571        &self,
572        package_id: &PackageId,
573        version: &miden_project::SemVer,
574        project: &ProjectPackage,
575        target: &Target,
576        profile_name: &str,
577        origin: &ProjectSourceOrigin,
578        manifest_path: &FsPath,
579    ) -> Result<RegisteredSourcePackage, Report> {
580        let Some(record) = self.store.get_by_semver(package_id, version) else {
581            return Ok(RegisteredSourcePackage::Missing);
582        };
583        let package = match self.store.load_package(package_id, record.version()) {
584            Ok(package) => package,
585            Err(_) => {
586                return Ok(RegisteredSourcePackage::IndexedButUnreadable(record.version().clone()));
587            },
588        };
589
590        let expected = self.dependency_graph.expected_source_provenance(
591            package_id,
592            project,
593            target,
594            profile_name,
595            origin,
596            manifest_path,
597            &self.source_provider,
598        )?;
599
600        match PackageBuildProvenance::from_package(&package)? {
601            Some(actual) if actual == expected => Ok(()),
602            Some(actual) => Err(Report::msg(format!(
603                "package '{}' version '{}' is already registered with different source provenance (expected {}, found {}); bump the semantic version",
604                package_id,
605                version,
606                expected.describe(),
607                actual.describe(),
608            ))),
609            None => Err(Report::msg(format!(
610                "package '{package_id}' version '{version}' is already registered, but the canonical artifact is missing source provenance; bump the semantic version"
611            ))),
612        }?;
613
614        Ok(RegisteredSourcePackage::Loaded(package))
615    }
616
617    fn should_cache_preassembled_package(
618        &self,
619        package_id: &PackageId,
620        selected: &PackageVersion,
621    ) -> bool {
622        let Some(record) = self.store.get_by_semver(package_id, &selected.version) else {
623            return true;
624        };
625        if record.version() != selected {
626            return false;
627        }
628
629        self.store.load_package(package_id, selected).is_err()
630    }
631
632    fn cache_resolved_package(&mut self, package: &ResolvedPackage) -> Result<(), Report> {
633        self.cache_package(package.package.clone())?;
634        if let Some(kernel_package) = package.linked_kernel_package.clone()
635            && self.should_cache_linked_kernel_package(kernel_package.as_ref())
636        {
637            self.cache_package(kernel_package)?;
638        }
639        Ok(())
640    }
641
642    fn should_cache_linked_kernel_package(&self, package: &MastPackage) -> bool {
643        let version = PackageVersion::new(package.version.clone(), package.digest());
644        let Some(record) = self.store.get_by_semver(&package.name, &package.version) else {
645            return true;
646        };
647        if record.version() != &version {
648            return false;
649        }
650
651        self.store.load_package(&package.name, &version).is_err()
652    }
653
654    fn cache_package(&mut self, package: Arc<MastPackage>) -> Result<(), Report> {
655        self.store
656            .cache_package(package)
657            .map(|_| ())
658            .map_err(|error| Report::msg(error.to_string()))
659    }
660
661    fn load_target_sources(
662        &self,
663        project: &ProjectPackage,
664        target: &Target,
665        profile: &Profile,
666    ) -> Result<ProjectSourceInputs, Report> {
667        let manifest_path = project.expect_manifest_path()?;
668        let mut context = TargetAssemblyContext::new(
669            project,
670            manifest_path,
671            target,
672            profile,
673            self.dependency_graph.as_ref(),
674            self.assembler.source_manager(),
675        )?;
676        context.with_warnings_as_errors(self.assembler.warnings_as_errors());
677
678        let extension = context.resolved_target_root.extension().ok_or_else(|| {
679            Report::msg(format!(
680                "invalid target 'path' {}: path must have an extension",
681                context.resolved_target_root.display()
682            ))
683        })?;
684        let extension = extension.to_string_lossy();
685
686        let provider = self.source_provider.get_provider(extension.as_ref()).ok_or_else(|| Report::msg(format!("unsupported target file type '{extension}': no provider has been registered for that file type")))?;
687        let inputs = provider.provide_sources(&context)?;
688        match target.ty {
689            TargetType::Executable if !inputs.root.kind().is_executable() => {
690                Err(Report::msg(format!(
691                    "requested target type is executable, but root module provided to assembler for '{}' is {}",
692                    project.name(),
693                    inputs.root.kind()
694                )))
695            },
696            TargetType::Kernel if !inputs.root.kind().is_kernel() => Err(Report::msg(format!(
697                "requested target type is kernel, but root module provided to assembler for '{}' is {}",
698                project.name(),
699                inputs.root.kind()
700            ))),
701            _ if inputs.root.path() != target.namespace.inner().as_ref() => {
702                Err(Report::msg(format!(
703                    "requested target namespace is '{}', but root module provided to assembler for '{}' is '{}'",
704                    &target.namespace,
705                    project.name(),
706                    inputs.root.path()
707                )))
708            },
709            _ => Ok(inputs),
710        }
711    }
712}
713
714// ================================================================================================
715
716#[derive(Clone)]
717struct ResolvedPackage {
718    package: Arc<MastPackage>,
719    linked_kernel_package: Option<Arc<MastPackage>>,
720}
721
722enum RegisteredSourcePackage {
723    Missing,
724    Loaded(Arc<MastPackage>),
725    IndexedButUnreadable(PackageVersion),
726}
727
728#[derive(Debug, Clone, PartialEq, Eq)]
729struct PackageBuildSettings {
730    emit_debug_info: bool,
731    trim_paths: bool,
732}
733
734impl PackageBuildSettings {
735    fn legacy() -> Self {
736        Self { emit_debug_info: true, trim_paths: false }
737    }
738
739    fn from_profile(profile: &Profile) -> Self {
740        Self {
741            emit_debug_info: profile.should_emit_debug_info(),
742            trim_paths: profile.should_trim_paths(),
743        }
744    }
745
746    fn is_legacy(&self) -> bool {
747        *self == Self::legacy()
748    }
749}
750
751// HELPER FUNCTIONS
752// ================================================================================================
753
754fn load_selected_preassembled_package(
755    path: &FsPath,
756    expected_name: &PackageId,
757    selected: &PackageVersion,
758    expected_kind: TargetType,
759    expected_requirements: &BTreeMap<PackageId, PreassembledDependencyMetadata>,
760) -> Result<Arc<MastPackage>, Report> {
761    let package = load_package_from_path(path)?;
762    if &package.name != expected_name {
763        return Err(Report::msg(format!(
764            "preassembled dependency '{}' at '{}' resolved to package '{}'",
765            expected_name,
766            path.display(),
767            package.name
768        )));
769    }
770
771    let actual = PackageVersion::new(package.version.clone(), package.digest());
772    if &actual != selected {
773        return Err(Report::msg(format!(
774            "preassembled dependency '{}@{}' at '{}' no longer matches the dependency graph selection '{}'",
775            expected_name,
776            actual,
777            path.display(),
778            selected
779        )));
780    }
781
782    if package.kind != expected_kind {
783        return Err(Report::msg(format!(
784            "preassembled dependency '{}@{}' at '{}' no longer matches the dependency graph target kind '{}'",
785            expected_name,
786            actual,
787            path.display(),
788            expected_kind
789        )));
790    }
791
792    let actual_requirements = package_requirements(&package);
793    if &actual_requirements != expected_requirements {
794        return Err(Report::msg(format!(
795            "preassembled dependency '{}@{}' at '{}' no longer matches the dependency graph dependency requirements",
796            expected_name,
797            actual,
798            path.display()
799        )));
800    }
801
802    Ok(package)
803}
804
805fn load_package_from_path(path: &FsPath) -> Result<Arc<MastPackage>, Report> {
806    let bytes = fs::read(path)
807        .map_err(|error| Report::msg(format!("failed to read '{}': {error}", path.display())))?;
808    let package = MastPackage::read_from_bytes_trusted(&bytes).map_err(|error| {
809        Report::msg(format!("failed to decode package '{}': {error}", path.display()))
810    })?;
811    Ok(Arc::new(package))
812}
813
814fn package_requirements(
815    package: &MastPackage,
816) -> BTreeMap<PackageId, PreassembledDependencyMetadata> {
817    package
818        .manifest
819        .dependencies()
820        .map(|dependency| {
821            (
822                dependency.name.clone(),
823                PreassembledDependencyMetadata {
824                    version: PackageVersion::new(dependency.version.clone(), dependency.digest),
825                    kind: dependency.kind,
826                },
827            )
828        })
829        .collect()
830}