1use std::collections::{BTreeMap, BTreeSet, VecDeque};
6use std::iter;
7
8use crate::jigsaw::findings::Stop;
9use crate::jigsaw::plan::JigsawPlan;
10use crate::model::app_manifest::{ManifestDeps, ManifestUnreadable};
11use crate::model::macro_decl::{MacroDecl, MacroKey};
12use crate::model::names::{ApplicationName, DependencyName, ModuleName, RecordName, ValidatorName};
13use crate::model::pin::Pin;
14use crate::model::schema_files::{PartialFile, PartialRef};
15use crate::model::snapshot::{HeaderRef, HrlFile, Module, RecordDecl, Snapshot, state};
16
17#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct PinApps {
22 modules: Vec<ApplicationName>,
23 headers: Vec<ApplicationName>,
24}
25
26impl PinApps {
27 #[must_use]
28 pub fn of(snapshot: &Snapshot<state::Canonical>) -> Option<Self> {
29 let modules = snapshot
30 .modules()
31 .iter()
32 .map(|m| m.app.clone())
33 .collect::<Option<Vec<_>>>()?;
34 let headers = snapshot
35 .headers()
36 .iter()
37 .map(|h| h.app.clone())
38 .collect::<Option<Vec<_>>>()?;
39 Some(Self { modules, headers })
40 }
41}
42
43#[derive(Debug, Clone, Copy)]
44pub struct SurfaceSource<'s> {
45 pub snapshot: &'s Snapshot<state::Canonical>,
46 pub apps: &'s PinApps,
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum AppPresence {
54 Provided,
55 Absent,
56 Stopped(Stop),
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum OtpAppPresence {
62 Provided,
63 Absent,
64 Excluded,
65}
66
67#[derive(Debug, Clone, PartialEq, Eq)]
69pub enum Blocker {
70 Manifest(ManifestUnreadable),
71 Untracked,
72}
73
74impl Blocker {
75 #[must_use]
76 pub fn stop(&self) -> Stop {
77 match self {
78 Self::Manifest(_) => Stop::UnreadableManifest,
79 Self::Untracked => Stop::UntrackedApp,
80 }
81 }
82}
83
84#[derive(Debug, Clone, PartialEq, Eq)]
88pub struct ShippedApps {
89 provided: BTreeSet<ApplicationName>,
90 blockers: BTreeMap<ApplicationName, Blocker>,
91}
92
93impl ShippedApps {
94 fn closure(
95 roots: &[ApplicationName],
96 manifests: &BTreeMap<&ApplicationName, &ManifestDeps>,
97 untracked: &BTreeSet<DependencyName>,
98 ) -> Self {
99 let mut provided = BTreeSet::new();
100 let mut blockers = BTreeMap::new();
101 let mut queue: VecDeque<&ApplicationName> = roots.iter().collect();
102 while let Some(app) = queue.pop_front() {
103 if !provided.insert(app.clone()) {
104 continue;
105 }
106 match manifests.get(app) {
107 Some(ManifestDeps::Listed(requires)) => queue.extend(requires),
108 Some(ManifestDeps::Unreadable(reason)) => {
109 blockers.insert(app.clone(), Blocker::Manifest(reason.clone()));
110 }
111 None if untracked.iter().any(|dep| dep.as_str() == app.as_str()) => {
112 blockers.insert(app.clone(), Blocker::Untracked);
113 }
114 None => {}
115 }
116 }
117 Self { provided, blockers }
118 }
119
120 #[must_use]
121 pub fn lookup(&self, app: &ApplicationName) -> AppPresence {
122 if self.provided.contains(app) {
123 AppPresence::Provided
124 } else {
125 self.stopped()
126 .map_or(AppPresence::Absent, AppPresence::Stopped)
127 }
128 }
129
130 #[must_use]
131 pub fn stopped(&self) -> Option<Stop> {
132 self.blockers.values().map(Blocker::stop).min()
133 }
134
135 #[must_use]
137 pub fn blockers(&self) -> &BTreeMap<ApplicationName, Blocker> {
138 &self.blockers
139 }
140}
141
142#[derive(Debug, Clone, Copy)]
143pub struct ModuleEntry<'s> {
144 pub module: &'s Module,
145 pub app: &'s ApplicationName,
146}
147
148#[derive(Debug, Clone, Copy, PartialEq, Eq)]
150pub enum Lookup<T> {
151 Found(T),
152 Missing,
153 Stopped(Stop),
154}
155
156#[derive(Debug)]
159pub struct ReleaseSurface<'s> {
160 modules: BTreeMap<&'s ModuleName, ModuleEntry<'s>>,
161 headers: BTreeMap<HeaderRef, &'s HrlFile>,
162 partials: BTreeMap<PartialRef, &'s PartialFile>,
163 validators: BTreeMap<&'s ValidatorName, Vec<&'s ApplicationName>>,
164 apps: BTreeSet<&'s ApplicationName>,
165 shipped: ShippedApps,
166 untracked: &'s BTreeSet<DependencyName>,
167}
168
169impl<'s> ReleaseSurface<'s> {
170 #[must_use]
171 pub fn new(
172 sources: &[SurfaceSource<'s>],
173 roots: &[ApplicationName],
174 untracked: &'s BTreeSet<DependencyName>,
175 ) -> Self {
176 let mut modules = BTreeMap::new();
177 let mut headers = BTreeMap::new();
178 let mut partials = BTreeMap::new();
179 let mut validators: BTreeMap<&ValidatorName, Vec<&ApplicationName>> = BTreeMap::new();
180 let mut manifests: BTreeMap<&ApplicationName, &ManifestDeps> = BTreeMap::new();
181 for source in sources {
182 let snapshot = source.snapshot;
183 for (module, app) in snapshot.modules().iter().zip(&source.apps.modules) {
184 modules
185 .entry(&module.name)
186 .or_insert(ModuleEntry { module, app });
187 }
188 for (header, app) in snapshot.headers().iter().zip(&source.apps.headers) {
189 let reference = HeaderRef {
190 app: app.clone(),
191 app_path: header.app_path().unwrap_or_else(|| header.path.clone()),
192 };
193 headers.entry(reference).or_insert(header);
194 }
195 for partial in snapshot.partials() {
196 partials.entry(partial.partial_ref()).or_insert(partial);
197 for name in &partial.validators {
198 validators.entry(name).or_default().push(&partial.app);
199 }
200 }
201 for schema in snapshot.schemas() {
202 for name in &schema.validators {
203 validators.entry(name).or_default().push(&schema.app);
204 }
205 }
206 for manifest in snapshot.apps() {
207 manifests
208 .entry(&manifest.name)
209 .or_insert(&manifest.requires);
210 }
211 }
212 let apps = manifests.keys().copied().collect();
213 Self {
214 modules,
215 headers,
216 partials,
217 validators,
218 apps,
219 shipped: ShippedApps::closure(roots, &manifests, untracked),
220 untracked,
221 }
222 }
223
224 #[must_use]
225 pub fn module(&self, name: &ModuleName) -> Option<&ModuleEntry<'s>> {
226 self.modules.get(name)
227 }
228
229 #[must_use]
230 pub fn header(&self, reference: &HeaderRef) -> Option<&'s HrlFile> {
231 self.headers.get(reference).copied()
232 }
233
234 #[must_use]
235 pub fn shipped(&self) -> &ShippedApps {
236 &self.shipped
237 }
238
239 #[must_use]
242 pub fn header_closure(&self, roots: &BTreeSet<HeaderRef>) -> Vec<(HeaderRef, &'s HrlFile)> {
243 let mut seen: BTreeSet<HeaderRef> = BTreeSet::new();
244 let mut queue: VecDeque<HeaderRef> = roots.iter().cloned().collect();
245 let mut out = Vec::new();
246 while let Some(reference) = queue.pop_front() {
247 if !seen.insert(reference.clone()) {
248 continue;
249 }
250 let Some(header) = self.header(&reference) else {
251 continue;
252 };
253 queue.extend(header.includes.iter().cloned());
254 out.push((reference, header));
255 }
256 out
257 }
258
259 #[must_use]
260 pub fn find_record(
261 &self,
262 roots: &BTreeSet<HeaderRef>,
263 name: &RecordName,
264 ) -> Option<(HeaderRef, &'s RecordDecl)> {
265 self.header_closure(roots)
266 .into_iter()
267 .find_map(|(reference, header)| {
268 header
269 .records
270 .iter()
271 .find(|record| &record.name == name)
272 .map(|record| (reference, record))
273 })
274 }
275
276 #[must_use]
277 pub fn find_macro(
278 &self,
279 roots: &BTreeSet<HeaderRef>,
280 key: &MacroKey,
281 ) -> Option<(HeaderRef, &'s MacroDecl)> {
282 self.header_closure(roots)
283 .into_iter()
284 .find_map(|(reference, header)| {
285 header
286 .defines
287 .iter()
288 .find(|decl| &decl.key == key)
289 .map(|decl| (reference, decl))
290 })
291 }
292
293 #[must_use]
295 pub fn has_any_header(&self, roots: &BTreeSet<HeaderRef>) -> bool {
296 roots.iter().any(|root| self.headers.contains_key(root))
297 }
298
299 #[must_use]
300 pub fn untracked_dependency(&self, app: &ApplicationName) -> bool {
301 self.untracked
302 .iter()
303 .any(|dep| dep.as_str() == app.as_str())
304 }
305
306 #[must_use]
309 pub fn tree_apps(&self) -> &BTreeSet<&'s ApplicationName> {
310 &self.apps
311 }
312
313 #[must_use]
316 pub fn partial(&self, reference: &PartialRef) -> Lookup<&'s PartialFile> {
317 match self.shipped.lookup(&reference.app) {
318 AppPresence::Provided => self
319 .partials
320 .get(reference)
321 .map_or(Lookup::Missing, |file| Lookup::Found(*file)),
322 AppPresence::Absent => Lookup::Missing,
323 AppPresence::Stopped(stop) => Lookup::Stopped(stop),
324 }
325 }
326
327 #[must_use]
330 pub fn validator(&self, name: &ValidatorName) -> Lookup<&'s ApplicationName> {
331 let owners = self.validators.get(name).map_or(&[][..], Vec::as_slice);
332 if let Some(app) = owners
333 .iter()
334 .find(|app| self.shipped.lookup(app) == AppPresence::Provided)
335 {
336 return Lookup::Found(app);
337 }
338 self.shipped
339 .stopped()
340 .map_or(Lookup::Missing, Lookup::Stopped)
341 }
342
343 #[must_use]
347 pub fn untracked_owner(&self, module: &ModuleName) -> Option<&'s DependencyName> {
348 let name = module.as_str();
349 self.untracked.iter().find(|dep| {
350 name == dep.as_str()
351 || name
352 .strip_prefix(dep.as_str())
353 .is_some_and(|rest| rest.starts_with('_'))
354 })
355 }
356}
357
358#[derive(Debug)]
360pub struct OtpSurface<'s> {
361 snapshot: &'s Snapshot<state::Canonical>,
362 apps: BTreeSet<&'s ApplicationName>,
363 excluded: &'s BTreeSet<ApplicationName>,
364}
365
366impl<'s> OtpSurface<'s> {
367 #[must_use]
368 pub fn new(
369 snapshot: &'s Snapshot<state::Canonical>,
370 excluded: &'s BTreeSet<ApplicationName>,
371 ) -> Self {
372 Self {
373 snapshot,
374 apps: snapshot.apps().iter().map(|app| &app.name).collect(),
375 excluded,
376 }
377 }
378
379 #[must_use]
380 pub fn module(&self, name: &ModuleName) -> Option<&'s Module> {
381 self.snapshot.module_named(name)
382 }
383
384 #[must_use]
387 pub fn app(&self, app: &ApplicationName) -> OtpAppPresence {
388 if self.apps.contains(app) {
389 OtpAppPresence::Provided
390 } else if self.excluded.contains(app) {
391 OtpAppPresence::Excluded
392 } else {
393 OtpAppPresence::Absent
394 }
395 }
396}
397
398#[derive(Debug)]
400pub struct PlanSurfaces<'s> {
401 releases: Vec<ReleaseSurface<'s>>,
402 otp: Vec<OtpSurface<'s>>,
403}
404
405impl<'s> PlanSurfaces<'s> {
406 pub(crate) fn new(
407 plan: &'s JigsawPlan,
408 snapshots: &'s BTreeMap<Pin, Snapshot<state::Canonical>>,
409 pin_apps: &'s BTreeMap<Pin, PinApps>,
410 ) -> Self {
411 let source = |pin: &Pin| {
412 Some(SurfaceSource {
413 snapshot: snapshots.get(pin)?,
414 apps: pin_apps.get(pin)?,
415 })
416 };
417 let releases = plan
418 .releases()
419 .iter()
420 .map(|release| {
421 let host_pin = Pin::new(plan.host_project().clone(), release.tag.tag().clone());
422 let roots: &[ApplicationName] = snapshots
423 .get(&host_pin)
424 .and_then(|snapshot| snapshot.header().host_facts.as_ref())
425 .map_or(&[], |facts| facts.release_roots.as_slice());
426 let pins = release
427 .pins
428 .iter()
429 .map(|(project, tag)| Pin::new(project.clone(), tag.clone()));
430 let sources: Vec<_> = iter::once(host_pin)
431 .chain(pins)
432 .filter_map(|pin| source(&pin))
433 .collect();
434 ReleaseSurface::new(&sources, roots, plan.untracked_dependencies())
435 })
436 .collect();
437 let otp = plan
438 .otp_lines()
439 .iter()
440 .flat_map(|line| line.tags().iter())
441 .filter_map(|tag| {
442 let pin = Pin::new(plan.otp_project().clone(), tag.tag().clone());
443 snapshots
444 .get(&pin)
445 .map(|snapshot| OtpSurface::new(snapshot, plan.excluded_otp_apps()))
446 })
447 .collect();
448 Self { releases, otp }
449 }
450
451 #[must_use]
453 pub fn build(&self) -> &ReleaseSurface<'s> {
454 &self.releases[self.releases.len() - 1]
455 }
456
457 #[must_use]
458 pub fn releases(&self) -> &[ReleaseSurface<'s>] {
459 &self.releases
460 }
461
462 #[must_use]
463 pub fn otp(&self) -> &[OtpSurface<'s>] {
464 &self.otp
465 }
466}