Skip to main content

miden_assembly/
project.rs

1use alloc::{boxed::Box, collections::BTreeMap, format, string::ToString, sync::Arc, vec::Vec};
2use core::ops::ControlFlow;
3use std::{
4    fs,
5    path::{Path as FsPath, PathBuf},
6};
7
8use miden_assembly_syntax::{ast::ModuleKind, diagnostics::Report};
9use miden_core::serde::Deserializable;
10use miden_mast_package::{Package as MastPackage, TargetType};
11use miden_package_registry::{PackageCache, PackageId, Version as PackageVersion};
12use miden_project::{
13    Linkage, Package as ProjectPackage, PreassembledDependencyMetadata, Profile,
14    ProjectDependencyNodeProvenance, ProjectSource, ProjectSourceOrigin, Target,
15};
16
17use crate::{Assembler, ast::Module};
18
19mod build_provenance;
20mod dependency_graph;
21mod providers;
22mod runtime_dependencies;
23mod target_selector;
24
25use self::{
26    build_provenance::PackageBuildProvenance, dependency_graph::DependencyGraph,
27    runtime_dependencies::RuntimeDependencies,
28};
29pub use self::{
30    providers::{MasmSourceProvider, ProjectSourceProvider, TargetAssemblyContext},
31    target_selector::ProjectTargetSelector,
32};
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,
49    {
50        let masm_provider = Box::new(MasmSourceProvider) as Box<_>;
51        self.for_project_at_path_with_providers(manifest_path, store, [masm_provider])
52    }
53
54    /// Get a [ProjectAssembler] configured for the project whose manifest is at `manifest_path`.
55    pub fn for_project_at_path_with_providers<'a, S>(
56        self,
57        manifest_path: impl AsRef<FsPath>,
58        store: &'a mut S,
59        providers: impl IntoIterator<Item = Box<dyn ProjectSourceProvider>>,
60    ) -> Result<ProjectAssembler<'a, S>, Report>
61    where
62        S: PackageCache,
63    {
64        let manifest_path = manifest_path.as_ref();
65        let source_manager = self.source_manager();
66        let project = miden_project::Project::load(manifest_path, &source_manager)?;
67        let package = project.package();
68        let dependency_graph =
69            DependencyGraph::from_project_path(manifest_path, store, source_manager)?;
70
71        Ok(ProjectAssembler {
72            assembler: self,
73            project: package,
74            source_provider: SourceProviderRegistry::new(providers),
75            dependency_graph,
76            store,
77        })
78    }
79
80    /// Get a [ProjectAssembler] configured for `project`
81    pub fn for_project<'a, S>(
82        self,
83        project: Arc<ProjectPackage>,
84        store: &'a mut S,
85    ) -> Result<ProjectAssembler<'a, S>, Report>
86    where
87        S: PackageCache,
88    {
89        let masm_provider = Box::new(MasmSourceProvider) as Box<_>;
90        self.for_project_with_providers(project, store, [masm_provider])
91    }
92
93    /// Get a [ProjectAssembler] configured for `project`
94    pub fn for_project_with_providers<'a, S>(
95        self,
96        project: Arc<ProjectPackage>,
97        store: &'a mut S,
98        providers: impl IntoIterator<Item = Box<dyn ProjectSourceProvider>>,
99    ) -> Result<ProjectAssembler<'a, S>, Report>
100    where
101        S: PackageCache,
102    {
103        let source_manager = self.source_manager();
104        let dependency_graph =
105            DependencyGraph::from_project(project.clone(), store, source_manager)?;
106        Ok(ProjectAssembler {
107            assembler: self,
108            project,
109            source_provider: SourceProviderRegistry::new(providers),
110            dependency_graph,
111            store,
112        })
113    }
114}
115
116// PROJECT ASSEMBLER
117// ================================================================================================
118
119pub struct ProjectSourceInputs {
120    pub root: Box<Module>,
121    pub support: Vec<Box<Module>>,
122}
123
124pub struct ProjectSourceProvenanceInputs {
125    pub root: SourceFileProvenance,
126    pub support: Vec<SourceFileProvenance>,
127}
128
129pub struct SourceFileProvenance {
130    pub path: Box<std::path::Path>,
131    pub content: Box<str>,
132}
133
134impl SourceFileProvenance {
135    pub fn from_path(path: PathBuf) -> Result<Self, Report> {
136        let content = fs::read_to_string(&path).map_err(|err| {
137            Report::msg(format!("unable to read source file '{}': {err}", path.display()))
138        })?;
139        Ok(Self {
140            path: path.into_boxed_path(),
141            content: content.into_boxed_str(),
142        })
143    }
144}
145
146pub struct SourceProviderRegistry {
147    registered: BTreeMap<&'static str, Box<dyn ProjectSourceProvider>>,
148}
149
150impl Default for SourceProviderRegistry {
151    fn default() -> Self {
152        Self {
153            registered: BTreeMap::from_iter([(
154                "masm",
155                Box::new(MasmSourceProvider) as Box<dyn ProjectSourceProvider>,
156            )]),
157        }
158    }
159}
160
161impl SourceProviderRegistry {
162    pub fn new(providers: impl IntoIterator<Item = Box<dyn ProjectSourceProvider>>) -> Self {
163        let mut this = Self {
164            registered: providers.into_iter().map(|p| (p.file_type(), p)).collect(),
165        };
166
167        if !this.registered.contains_key("masm") {
168            this.registered.insert("masm", Box::new(MasmSourceProvider));
169        }
170
171        this
172    }
173
174    pub fn with_source_provider(
175        &mut self,
176        provider: impl ProjectSourceProvider + 'static,
177    ) -> &mut Self {
178        let file_type = provider.file_type();
179        let provider = Box::new(provider) as Box<dyn ProjectSourceProvider>;
180
181        self.registered.insert(file_type, provider);
182
183        self
184    }
185
186    #[inline]
187    pub fn get_provider(&self, file_type: &str) -> Option<&dyn ProjectSourceProvider> {
188        self.registered.get(file_type).map(AsRef::as_ref)
189    }
190}
191
192/// Returned when assembly is interrupted early by a [ProjectSourceProvider]
193#[non_exhaustive]
194#[derive(Debug, Clone)]
195pub struct AssemblyInterrupted {
196    /// The package that was being assembled
197    pub package: PackageId,
198    /// The target being assembled to produce this package
199    pub target_name: Arc<str>,
200    /// The type of target being assembled
201    pub target_type: TargetType,
202    /// What role this package plays in assembly of the top-level selected target
203    pub role: InterruptedTargetRole,
204}
205
206/// This represents the reason a target was being assembled when assembly was interrupted
207#[derive(Debug, Copy, Clone, PartialEq, Eq)]
208pub enum InterruptedTargetRole {
209    /// The target that was interrupted was the top-level/root selected target
210    Root,
211    /// The target was a required/implicit library of the top-level selected target
212    RequiredLibrary,
213    /// The target was a dependency (direct or transitive) of the top-level selected target
214    Dependency,
215}
216
217impl core::fmt::Display for InterruptedTargetRole {
218    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
219        match self {
220            Self::Root => f.write_str("root"),
221            Self::RequiredLibrary => f.write_str("required library"),
222            Self::Dependency => f.write_str("dependency"),
223        }
224    }
225}
226
227pub struct ProjectAssembler<'a, S: PackageCache> {
228    assembler: Assembler,
229    project: Arc<ProjectPackage>,
230    dependency_graph: DependencyGraph,
231    source_provider: SourceProviderRegistry,
232    store: &'a mut S,
233}
234
235impl<'a, S> ProjectAssembler<'a, S>
236where
237    S: PackageCache,
238{
239    pub fn with_source_provider(
240        &mut self,
241        provider: impl ProjectSourceProvider + 'static,
242    ) -> &mut Self {
243        self.source_provider.with_source_provider(provider);
244        self
245    }
246
247    /// Get the project being assembled
248    pub fn project(&self) -> &ProjectPackage {
249        self.project.as_ref()
250    }
251
252    /// Assemble a target of the current project matching `target_selector`, using the configuration
253    /// under the profile `profile_name`.
254    ///
255    /// Returns an error if:
256    ///
257    /// * `target_selector` cannot find a matching target in the current project
258    /// * `profile_name` is not a profile defined in this project
259    /// * An error occurs during assembly of the project or one of its dependencies
260    /// * Assembly was interrupted by a source provider for the project or one of its dependencies
261    pub fn assemble(
262        &mut self,
263        target_selector: ProjectTargetSelector<'_>,
264        profile_name: &str,
265    ) -> Result<Arc<MastPackage>, Report> {
266        let result = self.assemble_interruptible(target_selector, profile_name)?;
267
268        match result {
269            ControlFlow::Continue(package) => Ok(package),
270            ControlFlow::Break(AssemblyInterrupted { package, target_name, target_type, role }) => {
271                Err(Report::msg(format!(
272                    "assembly of {role} package '{package}' was interrupted by the source provider for target '{target_name}' (type={target_type})"
273                )))
274            },
275        }
276    }
277
278    /// Like [`Self::assemble`], but allows for assembly to be interrupted by source providers
279    /// early.
280    ///
281    /// Returns [`ControlFlow`] representing whether assembly should break early (i.e. it was
282    /// interrupted), or continue as normal (i.e. it succeeded, and a package was produced).
283    ///
284    /// When assembly is interrupted early, a [AssemblyInterrupted] value is returned via
285    /// `ControlFlow::Break`, identifying what package (and target) caused the interruption, and
286    /// what role that package/target played in assembly of the root selected target.
287    ///
288    /// Returns an error if:
289    ///
290    /// * `target_selector` cannot find a matching target in the current project
291    /// * `profile_name` is not a profile defined in this project
292    /// * An error occurs during assembly of the project or one of its dependencies
293    pub fn assemble_interruptible(
294        &mut self,
295        target_selector: ProjectTargetSelector<'_>,
296        profile_name: &str,
297    ) -> Result<ControlFlow<AssemblyInterrupted, Arc<MastPackage>>, Report> {
298        let target = target_selector.select_target(self.project.as_ref())?;
299
300        // When building an executable target from a project with a library target, we require
301        // that the executable target be linked statically against the library target
302        let mut cache = BTreeMap::new();
303        let root_id = self.dependency_graph.root().clone();
304        let required_lib = if target.is_executable()
305            && let Some(library_target) =
306                self.project.library_target().map(|target| target.inner().clone())
307        {
308            match self.assemble_source_package(
309                root_id.clone(),
310                Arc::clone(&self.project),
311                &library_target,
312                profile_name,
313                InterruptedTargetRole::RequiredLibrary,
314                None,
315                None,
316                None,
317                &mut cache,
318            )? {
319                ControlFlow::Break(breaker) => return Ok(ControlFlow::Break(breaker)),
320                ControlFlow::Continue(resolved) => Some(resolved),
321            }
322        } else {
323            None
324        };
325
326        self.assemble_source_package(
327            root_id,
328            Arc::clone(&self.project),
329            &target,
330            profile_name,
331            InterruptedTargetRole::Root,
332            required_lib,
333            None,
334            None,
335            &mut cache,
336        )
337        .map(|resolved| resolved.map_continue(|r| r.package))
338    }
339
340    /// This is a low-level utility function of the project assembly infrastructure for assembling
341    /// a project target whose sources and source provenance may have already been computed by the
342    /// caller.
343    ///
344    /// This API supports early interruption by source providers, represented by the `ControlFlow`
345    /// value which is returned. The [AssemblyInterrupted] value carried in the `Break` variant
346    /// indicates the package/target which was interrupted, and the role that target played in
347    /// assembly of the provided project and target.
348    ///
349    /// In almost all cases you should prefer to use [`ProjectAssembler::assemble`] or
350    /// [`ProjectAssembler::assemble_interruptible`] instead.
351    ///
352    /// Callers are required to uphold the following invariants:
353    ///
354    /// * `package_id` must be the package identifier for `project` in the current dependency graph
355    /// * `target` must be a target extracted from `project`
356    /// * `required_lib` must be set if assembly of `target` would depend on another target of the
357    ///   same project (i.e. an executable target implicitly depends on the library target).
358    ///
359    /// It is invalid to provide `source_provenance` without `sources` - doing so will trigger an
360    /// assertion.
361    pub fn assemble_source_package(
362        &mut self,
363        package_id: PackageId,
364        project: Arc<ProjectPackage>,
365        target: &Target,
366        profile_name: &str,
367        package_role: InterruptedTargetRole,
368        required_lib: Option<ResolvedPackage>,
369        sources: Option<ProjectSourceInputs>,
370        source_provenance: Option<ProjectSourceProvenanceInputs>,
371        cache: &mut BTreeMap<PackageId, ResolvedPackage>,
372    ) -> Result<ControlFlow<AssemblyInterrupted, ResolvedPackage>, Report> {
373        assert!(
374            source_provenance.is_none() || sources.is_some(),
375            "source provenance may only be provided with sources"
376        );
377
378        let cache_key = project.target_package_name(target);
379        if let Some(package) = cache.get(&cache_key).cloned() {
380            assert_eq!(package.package.kind, target.ty);
381            return Ok(ControlFlow::Continue(package));
382        }
383
384        let profile = project.resolve_profile(profile_name)?;
385        let mut assembler = self.assembler.clone().with_profile(profile);
386        let mut runtime_dependencies = RuntimeDependencies::default();
387        debug_assert!(
388            required_lib.is_none() || target.ty.is_executable(),
389            "expected required_lib only for executable targets"
390        );
391        match required_lib {
392            Some(required_lib) if required_lib.package.is_kernel() => {
393                // We do not link the package here, as by definition a required library is only
394                // present for executable targets, and we always unconditionally link kernel
395                // dependencies just prior to assembling the package
396                runtime_dependencies.record_linked_kernel_dependency(required_lib.package)?;
397            },
398            Some(required_lib) => {
399                assembler.link_package(required_lib.package.clone(), Linkage::Static)?;
400                if let Some(kernel_package) = required_lib.linked_kernel_package {
401                    runtime_dependencies.record_linked_kernel_dependency(kernel_package)?;
402                }
403            },
404            None => (),
405        }
406
407        let node = self.dependency_graph.get(&package_id)?;
408        let dependencies = node.dependencies.clone();
409        for edge in dependencies.iter() {
410            let dependency_package =
411                match self.resolve_dependency_package(&edge.dependency, profile_name, cache)? {
412                    ControlFlow::Break(breaker) => return Ok(ControlFlow::Break(breaker)),
413                    ControlFlow::Continue(pkg) => pkg,
414                };
415            if !dependency_package.package.is_library() {
416                return Err(Report::msg(format!(
417                    "dependency '{}' resolved to executable package '{}', but only library-like packages can be linked",
418                    edge.dependency, dependency_package.package.name
419                )));
420            }
421
422            if !dependency_package.package.is_kernel() {
423                assembler.link_package(dependency_package.package.clone(), edge.linkage)?;
424            }
425            runtime_dependencies.merge_package(dependency_package, edge.linkage)?;
426        }
427
428        let manually_provided_sources = sources.is_some();
429        let ProjectSourceInputs { root, support } = match sources {
430            Some(sources) => sources,
431            None => {
432                match self.load_target_sources(project.clone(), target, profile, package_role)? {
433                    ControlFlow::Break(breaker) => return Ok(ControlFlow::Break(breaker)),
434                    ControlFlow::Continue(sources) => sources,
435                }
436            },
437        };
438
439        // Collect specific well-known custom sections produced by the project assembler
440        let mut sections = Vec::new();
441
442        // Section: build provenance
443        //
444        // This is produced before actual assembly, while we still have the sources on hand
445        let build_provenance = if source_provenance.is_some() || !manually_provided_sources {
446            self.dependency_graph.build_source_provenance(
447                &package_id,
448                project.clone(),
449                target,
450                profile_name,
451                &self.source_provider,
452                self.store,
453                source_provenance.as_ref(),
454            )?
455        } else {
456            None
457        };
458        if let Some(build_provenance) = build_provenance {
459            sections.push(build_provenance.to_section());
460        }
461
462        if let Some(kernel_package) = runtime_dependencies.kernel.clone() {
463            if matches!(target.ty, TargetType::Kernel) {
464                return Err(Report::msg(format!(
465                    "kernel targets cannot depend on a kernel, dependency '{}' is a kernel",
466                    kernel_package.name
467                )));
468            }
469            assembler.link_package(kernel_package, Linkage::Dynamic)?;
470        }
471
472        let mut product = match target.ty {
473            TargetType::Executable => {
474                assembler.assemble_executable_modules(package_id.clone(), root, support)?
475            },
476            _ if target.ty.is_library() => {
477                assembler.assemble_library_modules(package_id.clone(), root, support, target.ty)?
478            },
479            _ => unreachable!("non-exhaustive target type"),
480        };
481
482        product
483            .extend_dependencies(runtime_dependencies.deps.into_values())
484            .expect("assembled package manifest should have unique runtime dependencies");
485
486        let mut package = product.into_artifact(profile.should_emit_debug_info())?;
487        package.name = project.target_package_name(target);
488        package.version = project.version().into_inner().clone();
489        package.description = project.description().map(|description| description.to_string());
490        package.sections.extend(sections);
491
492        // We don't apply post-assembly hooks when assembling manually-provided sources
493        if !manually_provided_sources {
494            self.apply_post_assembly_hooks(&mut package, project.clone(), target, profile)?;
495        }
496
497        let package = Arc::from(package);
498
499        let resolved = ResolvedPackage {
500            package,
501            linked_kernel_package: runtime_dependencies.kernel,
502        };
503        cache.insert(package_id, resolved.clone());
504
505        Ok(ControlFlow::Continue(resolved))
506    }
507
508    fn resolve_dependency_package(
509        &mut self,
510        package_id: &PackageId,
511        profile_name: &str,
512        cache: &mut BTreeMap<PackageId, ResolvedPackage>,
513    ) -> Result<ControlFlow<AssemblyInterrupted, ResolvedPackage>, Report> {
514        if let Some(package) = cache.get(package_id).cloned() {
515            return Ok(ControlFlow::Continue(package));
516        }
517
518        let node = self.dependency_graph.get(package_id)?;
519        let node_version = node.version.clone();
520
521        let (package, should_cache) = match &node.provenance {
522            ProjectDependencyNodeProvenance::Source(ProjectSource::Virtual { .. }) => {
523                return Err(Report::msg(format!(
524                    "package '{package_id}' is missing a manifest path",
525                )));
526            },
527            ProjectDependencyNodeProvenance::Source(ProjectSource::Real {
528                manifest_path,
529                origin,
530                library_path: Some(_),
531                ..
532            }) => {
533                let project = miden_project::Project::load_project_reference(
534                    package_id,
535                    manifest_path,
536                    &self.assembler.source_manager(),
537                )
538                .map(|project| project.package())?;
539                let target = project
540                    .library_target()
541                    .map(|target| target.inner().clone())
542                    .ok_or_else(|| {
543                        Report::msg(format!(
544                            "dependency '{package_id}' does not define a library target"
545                        ))
546                    })?;
547                match self.try_reuse_registered_source_package(
548                    package_id,
549                    &node_version,
550                    project.clone(),
551                    &target,
552                    profile_name,
553                    origin,
554                    manifest_path,
555                )? {
556                    RegisteredSourcePackage::Loaded(package) => (
557                        ResolvedPackage {
558                            linked_kernel_package: self
559                                .resolve_linked_kernel_package(package.clone())?,
560                            package,
561                        },
562                        false,
563                    ),
564                    reuse => {
565                        let package = match self.assemble_source_package(
566                            package_id.clone(),
567                            project,
568                            &target,
569                            profile_name,
570                            InterruptedTargetRole::Dependency,
571                            None,
572                            None,
573                            None,
574                            cache,
575                        )? {
576                            ControlFlow::Break(breaker) => return Ok(ControlFlow::Break(breaker)),
577                            ControlFlow::Continue(package) => package,
578                        };
579                        match reuse {
580                            RegisteredSourcePackage::Missing => (),
581                            RegisteredSourcePackage::IndexedButUnreadable(expected) => {
582                                let actual = PackageVersion::new(
583                                    package.package.version.clone(),
584                                    package.package.dependency_commitment(),
585                                );
586                                if actual != expected {
587                                    return Err(Report::msg(format!(
588                                        "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"
589                                    )));
590                                }
591                            },
592                            RegisteredSourcePackage::Loaded(_) => unreachable!(),
593                        }
594                        (package, true)
595                    },
596                }
597            },
598            ProjectDependencyNodeProvenance::Source(_) => {
599                let package =
600                    self.load_canonical_package(package_id, &node_version)?.ok_or_else(|| {
601                        Report::msg(format!(
602                            "dependency '{package_id}' version '{node_version}' was not found in the package registry"
603                        ))
604                    })?;
605                (
606                    ResolvedPackage {
607                        linked_kernel_package: self
608                            .resolve_linked_kernel_package(package.clone())?,
609                        package,
610                    },
611                    false,
612                )
613            },
614            ProjectDependencyNodeProvenance::Registry { selected, .. } => {
615                let package = self.store.load_package(package_id, selected)?;
616                (
617                    ResolvedPackage {
618                        linked_kernel_package: self
619                            .resolve_linked_kernel_package(package.clone())?,
620                        package,
621                    },
622                    false,
623                )
624            },
625            ProjectDependencyNodeProvenance::Preassembled {
626                path,
627                selected,
628                kind,
629                requirements,
630            } => {
631                let package = load_selected_preassembled_package(
632                    path,
633                    package_id,
634                    selected,
635                    *kind,
636                    requirements,
637                )?;
638                let should_cache = self.should_cache_preassembled_package(package_id, selected);
639                (
640                    ResolvedPackage {
641                        linked_kernel_package: self
642                            .resolve_linked_kernel_package(package.clone())?,
643                        package,
644                    },
645                    should_cache,
646                )
647            },
648        };
649
650        if should_cache {
651            self.cache_resolved_package(&package)?;
652        }
653        cache.insert(package_id.clone(), package.clone());
654        Ok(ControlFlow::Continue(package))
655    }
656
657    fn resolve_linked_kernel_package(
658        &self,
659        package: Arc<MastPackage>,
660    ) -> Result<Option<Arc<MastPackage>>, Report> {
661        if package.is_kernel() {
662            return Ok(Some(package));
663        }
664
665        let Some(kernel_dependency) = package.kernel_runtime_dependency()? else {
666            return Ok(None);
667        };
668
669        let version =
670            PackageVersion::new(kernel_dependency.version.clone(), kernel_dependency.digest);
671        if self.store.get_exact_version(&kernel_dependency.name, &version).is_some() {
672            match self.store.load_package(&kernel_dependency.name, &version) {
673                Ok(kernel_package) => {
674                    if !kernel_package.is_kernel() {
675                        return Err(Report::msg(format!(
676                            "runtime kernel dependency '{}@{}#{}' resolved to non-kernel package '{}'",
677                            kernel_dependency.name,
678                            kernel_dependency.version,
679                            kernel_dependency.digest,
680                            kernel_package.name
681                        )));
682                    }
683                    return Ok(Some(kernel_package));
684                },
685                Err(load_error) => {
686                    if let Some(kernel_package) = package
687                        .try_embedded_kernel_package()
688                        .map(|kernel_package| kernel_package.map(Arc::from))?
689                    {
690                        return Ok(Some(kernel_package));
691                    }
692                    return Err(load_error);
693                },
694            }
695        }
696
697        package
698            .try_embedded_kernel_package()
699            .map(|kernel_package| kernel_package.map(Arc::from))
700    }
701
702    fn load_canonical_package(
703        &self,
704        package_id: &PackageId,
705        version: &miden_project::SemVer,
706    ) -> Result<Option<Arc<MastPackage>>, Report> {
707        let Some(record) = self.store.get_by_semver(package_id, version) else {
708            return Ok(None);
709        };
710        self.store.load_package(package_id, record.version()).map(Some)
711    }
712
713    fn try_reuse_registered_source_package(
714        &self,
715        package_id: &PackageId,
716        version: &miden_project::SemVer,
717        project: Arc<ProjectPackage>,
718        target: &Target,
719        profile_name: &str,
720        origin: &ProjectSourceOrigin,
721        manifest_path: &FsPath,
722    ) -> Result<RegisteredSourcePackage, Report> {
723        let Some(record) = self.store.get_by_semver(package_id, version) else {
724            return Ok(RegisteredSourcePackage::Missing);
725        };
726        let package = match self.store.load_package(package_id, record.version()) {
727            Ok(package) => package,
728            Err(_) => {
729                return Ok(RegisteredSourcePackage::IndexedButUnreadable(record.version().clone()));
730            },
731        };
732
733        let expected = self.dependency_graph.expected_source_provenance(
734            package_id,
735            project,
736            target,
737            profile_name,
738            origin,
739            manifest_path,
740            &self.source_provider,
741            self.store,
742            None,
743        )?;
744
745        match PackageBuildProvenance::from_package(&package)? {
746            Some(actual) if actual == expected => Ok(()),
747            Some(actual) => Err(Report::msg(format!(
748                "package '{}' version '{}' is already registered with different source provenance (expected {}, found {}); bump the semantic version",
749                package_id,
750                version,
751                expected.describe(),
752                actual.describe(),
753            ))),
754            None => Err(Report::msg(format!(
755                "package '{package_id}' version '{version}' is already registered, but the canonical artifact is missing source provenance; bump the semantic version"
756            ))),
757        }?;
758
759        Ok(RegisteredSourcePackage::Loaded(package))
760    }
761
762    fn should_cache_preassembled_package(
763        &self,
764        package_id: &PackageId,
765        selected: &PackageVersion,
766    ) -> bool {
767        let Some(record) = self.store.get_by_semver(package_id, &selected.version) else {
768            return true;
769        };
770        if record.version() != selected {
771            return false;
772        }
773
774        self.store.load_package(package_id, selected).is_err()
775    }
776
777    fn cache_resolved_package(&mut self, package: &ResolvedPackage) -> Result<(), Report> {
778        self.cache_package(package.package.clone())?;
779        if let Some(kernel_package) = package.linked_kernel_package.clone()
780            && self.should_cache_linked_kernel_package(kernel_package.as_ref())
781        {
782            self.cache_package(kernel_package)?;
783        }
784        Ok(())
785    }
786
787    fn should_cache_linked_kernel_package(&self, package: &MastPackage) -> bool {
788        let version = PackageVersion::new(package.version.clone(), package.dependency_commitment());
789        let Some(record) = self.store.get_by_semver(&package.name, &package.version) else {
790            return true;
791        };
792        if record.version() != &version {
793            return false;
794        }
795
796        self.store.load_package(&package.name, &version).is_err()
797    }
798
799    fn cache_package(&mut self, package: Arc<MastPackage>) -> Result<(), Report> {
800        self.store
801            .cache_package(package)
802            .map(|_| ())
803            .map_err(|error| Report::msg(error.to_string()))
804    }
805
806    fn load_target_sources(
807        &self,
808        project: Arc<ProjectPackage>,
809        target: &Target,
810        profile: &Profile,
811        role: InterruptedTargetRole,
812    ) -> Result<ControlFlow<AssemblyInterrupted, ProjectSourceInputs>, Report> {
813        let (provider, context) =
814            self.get_provider_and_target_assembly_context(&project, target, profile)?;
815
816        let inputs = match provider.provide_sources_interruptible(&context)? {
817            ControlFlow::Break(_) => {
818                return Ok(ControlFlow::Break(AssemblyInterrupted {
819                    package: project.name().into_inner(),
820                    target_name: target.name.inner().clone(),
821                    target_type: target.ty,
822                    role,
823                }));
824            },
825            ControlFlow::Continue(inputs) => inputs,
826        };
827        match target.ty {
828            TargetType::Executable if !inputs.root.kind().is_executable() => {
829                Err(Report::msg(format!(
830                    "requested target type is executable, but root module provided to assembler for '{}' is {}",
831                    project.name(),
832                    inputs.root.kind()
833                )))
834            },
835            TargetType::Kernel if !inputs.root.kind().is_kernel() => Err(Report::msg(format!(
836                "requested target type is kernel, but root module provided to assembler for '{}' is {}",
837                project.name(),
838                inputs.root.kind()
839            ))),
840            _ if inputs.root.path() != target.namespace.inner().as_ref() => {
841                Err(Report::msg(format!(
842                    "requested target namespace is '{}', but root module provided to assembler for '{}' is '{}'",
843                    target.namespace,
844                    project.name(),
845                    inputs.root.path()
846                )))
847            },
848            _ => Ok(ControlFlow::Continue(inputs)),
849        }
850    }
851
852    fn apply_post_assembly_hooks(
853        &self,
854        package: &mut MastPackage,
855        project: Arc<ProjectPackage>,
856        target: &Target,
857        profile: &Profile,
858    ) -> Result<(), Report> {
859        let (provider, context) =
860            self.get_provider_and_target_assembly_context(&project, target, profile)?;
861
862        provider.post_process_package(package, &context)?;
863
864        Ok(())
865    }
866
867    fn get_provider_and_target_assembly_context<'this>(
868        &'this self,
869        project: &'this Arc<ProjectPackage>,
870        target: &'this Target,
871        profile: &'this Profile,
872    ) -> Result<(&'this dyn ProjectSourceProvider, TargetAssemblyContext<'this>), Report> {
873        let mut context = match project.manifest_path() {
874            Some(manifest_path) => TargetAssemblyContext::new(
875                project.clone(),
876                manifest_path,
877                target,
878                profile,
879                self.dependency_graph.as_ref(),
880                self.store,
881                self.assembler.source_manager(),
882            )?,
883            None => TargetAssemblyContext::new_virtual(
884                project.clone(),
885                target,
886                profile,
887                self.dependency_graph.as_ref(),
888                self.store,
889                self.assembler.source_manager(),
890            )?,
891        };
892        context.with_warnings_as_errors(self.assembler.warnings_as_errors());
893
894        let extension = context.resolved_target_root.extension().ok_or_else(|| {
895            Report::msg(format!(
896                "invalid target 'path' {}: path must have an extension",
897                context.resolved_target_root.display()
898            ))
899        })?;
900        let extension = extension.to_string_lossy();
901
902        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")))?;
903
904        Ok((provider, context))
905    }
906}
907
908// ================================================================================================
909
910#[derive(Clone)]
911pub struct ResolvedPackage {
912    pub package: Arc<MastPackage>,
913    pub linked_kernel_package: Option<Arc<MastPackage>>,
914}
915
916enum RegisteredSourcePackage {
917    Missing,
918    Loaded(Arc<MastPackage>),
919    IndexedButUnreadable(PackageVersion),
920}
921
922#[derive(Debug, Clone, PartialEq, Eq)]
923struct PackageBuildSettings {
924    emit_debug_info: bool,
925    trim_paths: bool,
926}
927
928impl PackageBuildSettings {
929    fn legacy() -> Self {
930        Self { emit_debug_info: true, trim_paths: false }
931    }
932
933    fn from_profile(profile: &Profile) -> Self {
934        Self {
935            emit_debug_info: profile.should_emit_debug_info(),
936            trim_paths: profile.should_trim_paths(),
937        }
938    }
939
940    fn is_legacy(&self) -> bool {
941        *self == Self::legacy()
942    }
943}
944
945// HELPER FUNCTIONS
946// ================================================================================================
947
948fn load_selected_preassembled_package(
949    path: &FsPath,
950    expected_name: &PackageId,
951    selected: &PackageVersion,
952    expected_kind: TargetType,
953    expected_requirements: &BTreeMap<PackageId, PreassembledDependencyMetadata>,
954) -> Result<Arc<MastPackage>, Report> {
955    let package = load_package_from_path(path)?;
956    if &package.name != expected_name {
957        return Err(Report::msg(format!(
958            "preassembled dependency '{}' at '{}' resolved to package '{}'",
959            expected_name,
960            path.display(),
961            package.name
962        )));
963    }
964
965    let actual = PackageVersion::new(package.version.clone(), package.dependency_commitment());
966    if &actual != selected {
967        return Err(Report::msg(format!(
968            "preassembled dependency '{}@{}' at '{}' no longer matches the dependency graph selection '{}'",
969            expected_name,
970            actual,
971            path.display(),
972            selected
973        )));
974    }
975
976    if package.kind != expected_kind {
977        return Err(Report::msg(format!(
978            "preassembled dependency '{}@{}' at '{}' no longer matches the dependency graph target kind '{}'",
979            expected_name,
980            actual,
981            path.display(),
982            expected_kind
983        )));
984    }
985
986    let actual_requirements = package_requirements(&package);
987    if &actual_requirements != expected_requirements {
988        return Err(Report::msg(format!(
989            "preassembled dependency '{}@{}' at '{}' no longer matches the dependency graph dependency requirements",
990            expected_name,
991            actual,
992            path.display()
993        )));
994    }
995
996    Ok(package)
997}
998
999fn load_package_from_path(path: &FsPath) -> Result<Arc<MastPackage>, Report> {
1000    let bytes = fs::read(path)
1001        .map_err(|error| Report::msg(format!("failed to read '{}': {error}", path.display())))?;
1002    let package = MastPackage::read_from_bytes(&bytes).map_err(|error| {
1003        Report::msg(format!("failed to decode package '{}': {error}", path.display()))
1004    })?;
1005    Ok(Arc::new(package))
1006}
1007
1008fn package_requirements(
1009    package: &MastPackage,
1010) -> BTreeMap<PackageId, PreassembledDependencyMetadata> {
1011    package
1012        .manifest
1013        .dependencies()
1014        .map(|dependency| {
1015            (
1016                dependency.name.clone(),
1017                PreassembledDependencyMetadata {
1018                    version: PackageVersion::new(dependency.version.clone(), dependency.digest),
1019                    kind: dependency.kind,
1020                },
1021            )
1022        })
1023        .collect()
1024}