Skip to main content

runmat_package/source/
handoff.rs

1use 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}