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