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