runmat_package/source/
handoff.rs1use super::{catalog::compute_source_revision, FrozenProject, ProjectRevision};
2use crate::GraphError;
3use serde::{Deserialize, Serialize};
4use thiserror::Error;
5
6pub const FROZEN_PROJECT_HANDOFF_SCHEMA_VERSION: u32 = 1;
7
8#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
9#[serde(deny_unknown_fields)]
10pub struct FrozenProjectHandoff {
11 pub schema_version: u32,
12 pub project: FrozenProject,
13}
14
15impl FrozenProjectHandoff {
16 pub fn new(project: FrozenProject) -> Self {
17 Self {
18 schema_version: FROZEN_PROJECT_HANDOFF_SCHEMA_VERSION,
19 project,
20 }
21 }
22
23 pub fn validate(&self) -> Result<(), FrozenProjectHandoffError> {
24 if self.schema_version != FROZEN_PROJECT_HANDOFF_SCHEMA_VERSION {
25 return Err(FrozenProjectHandoffError::UnsupportedSchema {
26 found: self.schema_version,
27 supported: FROZEN_PROJECT_HANDOFF_SCHEMA_VERSION,
28 });
29 }
30 self.project
31 .graph
32 .validate_digest()
33 .map_err(FrozenProjectHandoffError::Graph)?;
34 validate_sources(&self.project)?;
35 Ok(())
36 }
37
38 pub fn revision(&self) -> ProjectRevision {
39 self.project.revision()
40 }
41
42 pub fn into_project(self) -> FrozenProject {
43 self.project
44 }
45}
46
47#[derive(Debug, Error)]
48pub enum FrozenProjectHandoffError {
49 #[error("unsupported frozen-project handoff schema {found}; supported schema is {supported}")]
50 UnsupportedSchema { found: u32, supported: u32 },
51 #[error("invalid frozen-project graph: {0}")]
52 Graph(GraphError),
53 #[error("invalid frozen-project source catalog: {0}")]
54 SourceCatalog(String),
55 #[error("failed to encode frozen-project source revision input: {0}")]
56 Revision(String),
57}
58
59fn validate_sources(project: &FrozenProject) -> Result<(), FrozenProjectHandoffError> {
60 if project
61 .sources
62 .packages
63 .keys()
64 .ne(project.graph.packages.keys())
65 {
66 return Err(FrozenProjectHandoffError::SourceCatalog(
67 "source catalog packages do not match the resolved graph".to_string(),
68 ));
69 }
70 for (instance, package) in &project.sources.packages {
71 if instance != &package.package_instance || instance != &package.mount.package_instance {
72 return Err(FrozenProjectHandoffError::SourceCatalog(format!(
73 "source catalog key {instance} does not match package or mount identity"
74 )));
75 }
76 let graph_package = project.graph.packages.get(instance).ok_or_else(|| {
77 FrozenProjectHandoffError::SourceCatalog(format!(
78 "source catalog package {instance} is absent from the graph"
79 ))
80 })?;
81 if package.local_name != graph_package.local_name
82 || package.mount.source != graph_package.instance.source
83 {
84 return Err(FrozenProjectHandoffError::SourceCatalog(format!(
85 "source catalog metadata for package {instance} does not match the graph"
86 )));
87 }
88 for source in &package.sources {
89 if &source.id.package_instance != instance {
90 return Err(FrozenProjectHandoffError::SourceCatalog(format!(
91 "source {} belongs to a different package instance",
92 source.id.relative_path
93 )));
94 }
95 if !project.access_paths.contains_key(&source.id) {
96 return Err(FrozenProjectHandoffError::SourceCatalog(format!(
97 "source {} has no access path",
98 source.id.relative_path
99 )));
100 }
101 }
102 }
103 let source_count = project
104 .sources
105 .packages
106 .values()
107 .map(|package| package.sources.len())
108 .sum::<usize>();
109 if project.access_paths.len() != source_count {
110 return Err(FrozenProjectHandoffError::SourceCatalog(
111 "access paths do not correspond one-to-one with source descriptors".to_string(),
112 ));
113 }
114 let expected = compute_source_revision(&project.graph.graph_digest, &project.sources.packages)
115 .map_err(|error| FrozenProjectHandoffError::Revision(error.to_string()))?;
116 if expected != project.sources.revision {
117 return Err(FrozenProjectHandoffError::SourceCatalog(
118 "source revision does not match the canonical source catalog".to_string(),
119 ));
120 }
121 Ok(())
122}