1use std::collections::BTreeMap;
2use std::path::Path;
3
4use crate::config::groups::{GroupDef, GroupMember, GroupMemberKind, GroupTable};
5use crate::error::GraphError;
6use crate::napi::{napi_drift, NapiTargetsIndex};
7use crate::resolver::DependencyResolver;
8use crate::tags::TagIndex;
9use callisto_format::Versioning;
10use callisto_model::{Diagnostic, GroupName, PackageId, Severity, Version};
11
12#[derive(Clone, Debug, Default)]
13pub struct GroupCheckOutcome {
14 pub new_members: BTreeMap<GroupName, Vec<PackageId>>,
15 pub diagnostics: Vec<Diagnostic>,
16}
17
18pub fn pre_mutation_checks<D: DependencyResolver>(
19 _graph: &D,
20 groups: &GroupTable,
21 base: &BTreeMap<PackageId, Version>,
22 tags: &TagIndex,
23 napi: &NapiTargetsIndex,
24 root: &Path,
25) -> Result<GroupCheckOutcome, GraphError> {
26 let mut outcome = GroupCheckOutcome::default();
27
28 for g in groups.fixed.values() {
29 let released: Vec<PackageId> = g
30 .members(GroupMemberKind::Package)
31 .filter_map(|m| match m {
32 GroupMember::Package(ref id) => {
33 if tags.last_tag(id).is_some() {
34 Some(id.clone())
35 } else {
36 None
37 }
38 }
39 _ => None,
40 })
41 .collect();
42
43 let fresh: Vec<PackageId> = g
44 .members(GroupMemberKind::Package)
45 .filter_map(|m| match m {
46 GroupMember::Package(ref id) => {
47 if tags.last_tag(id).is_none() {
48 Some(id.clone())
49 } else {
50 None
51 }
52 }
53 _ => None,
54 })
55 .collect();
56
57 let pairs: Vec<(PackageId, Version)> = released
58 .iter()
59 .filter_map(|id| base.get(id).map(|v| (id.clone(), v.clone())))
60 .collect();
61
62 if pairs.len() > 1 {
63 let first_v = &pairs[0].1;
64 let mut divergent = false;
65 for (_id, v) in &pairs[1..] {
66 if v.grammar() != first_v.grammar() {
67 return Err(GraphError::GroupGrammarMismatch {
68 group: g.name.clone(),
69 members: pairs,
70 });
71 }
72 if Version::compare(v, first_v).ok() != Some(std::cmp::Ordering::Equal) {
73 divergent = true;
74 }
75 }
76 if divergent {
77 return Err(GraphError::FixedGroupDivergent {
78 group: g.name.clone(),
79 members: pairs,
80 });
81 }
82 }
83
84 if !fresh.is_empty() {
85 outcome.new_members.insert(g.name.clone(), fresh);
86 }
87
88 if let Some(declared) = napi.declared_for(&g.name) {
90 outcome.diagnostics.extend(napi_drift(g, declared, root));
91 }
92 }
93
94 Ok(outcome)
95}
96
97pub fn fixed_group_target(
98 g: &GroupDef,
99 base: &BTreeMap<PackageId, Version>,
100 severities: &BTreeMap<PackageId, Severity>,
101 tags: &TagIndex,
102 _pre: Option<&callisto_format::PreState>,
103) -> Result<Version, GraphError> {
104 let released: Vec<PackageId> = g
105 .members(GroupMemberKind::Package)
106 .filter_map(|m| match m {
107 GroupMember::Package(ref id) => {
108 if tags.last_tag(id).is_some() {
109 Some(id.clone())
110 } else {
111 None
112 }
113 }
114 _ => None,
115 })
116 .collect();
117
118 let mut max_sev = Severity::None;
119 for m in g.members(GroupMemberKind::Package) {
120 if let GroupMember::Package(ref id) = m {
121 if let Some(&s) = severities.get(id) {
122 if s > max_sev {
123 max_sev = s;
124 }
125 }
126 }
127 }
128
129 let aligned_base = if !released.is_empty() {
130 base.get(&released[0])
131 .cloned()
132 .unwrap_or_else(|| Version::semver(1, 0, 0))
133 } else {
134 Version::semver(0, 0, 0)
135 };
136
137 let versioning = callisto_format::SemVerVersioning;
138
139 versioning.bump(&aligned_base, max_sev).map_err(GraphError::Bump)
140}
141
142#[cfg(test)]
143mod tests {
144 use std::collections::BTreeMap;
145 use std::path::PathBuf;
146
147 use callisto_model::{GroupKind, GroupName, ManifestRole, PackageId, Version};
148
149 use super::*;
150 use crate::config::groups::{GroupDef, GroupMember, GroupTable};
151 use crate::napi::NapiTargetsIndex;
152 use crate::tags::TagIndex;
153
154 struct EmptyResolver;
155 impl crate::resolver::DependencyResolver for EmptyResolver {
156 fn packages(&self) -> impl Iterator<Item = &callisto_model::Package> {
157 std::iter::empty()
158 }
159 fn dependencies_of(&self, _id: &PackageId) -> impl Iterator<Item = &callisto_model::DepEdge> {
160 std::iter::empty()
161 }
162 fn dependents_of(&self, _id: &PackageId) -> impl Iterator<Item = &callisto_model::DepEdge> {
163 std::iter::empty()
164 }
165 fn diagnostics(&self) -> &[callisto_model::Diagnostic] {
166 &[]
167 }
168 }
169
170 #[test]
171 fn pre_mutation_checks_calls_napi_drift_for_napi_groups() {
172 let tmp = tempfile::tempdir().expect("tempdir");
173 let root = tmp.path();
174
175 let group_name = GroupName("my-lib".to_string());
176 let pkg_id = PackageId::Bare("my-lib".to_string());
177
178 let platform_role = ManifestRole::Platform {
182 platform: "linux".to_string(),
183 arch: "x64".to_string(),
184 abi: Some("gnu".to_string()),
185 };
186
187 let group = GroupDef {
188 name: group_name.clone(),
189 kind: GroupKind::Fixed,
190 members: vec![
191 GroupMember::Package(pkg_id.clone()),
192 GroupMember::PlatformManifest {
193 owner: pkg_id.clone(),
194 role: platform_role,
195 path: PathBuf::from("platform/linux-x64-gnu/package.json"),
196 name: "my-lib.linux-x64-gnu".to_string(),
197 },
198 ],
199 };
200
201 let groups = GroupTable::from_groups(vec![group], vec![]);
202
203 let pkg_dir = root.join("my-lib");
205 std::fs::create_dir_all(&pkg_dir).unwrap();
206 std::fs::write(
207 pkg_dir.join("package.json"),
208 r#"{"name":"my-lib","napi":{"targets":["aarch64-apple-darwin"]}}"#,
209 )
210 .unwrap();
211
212 let napi = NapiTargetsIndex::load(&groups, root).expect("load");
213
214 let base: BTreeMap<PackageId, Version> = BTreeMap::new();
215 let tags = TagIndex::empty();
216 let resolver = EmptyResolver;
217
218 let outcome = pre_mutation_checks(&resolver, &groups, &base, &tags, &napi, root).expect("pre_mutation_checks");
219
220 assert!(
221 !outcome.diagnostics.is_empty(),
222 "expected at least one napi_drift diagnostic"
223 );
224 }
225}