1use crate::{
2 LocalArchive,
3 formats::{BuildTargetId, SourceFormat, SourceFormatId},
4 utils::{
5 ignore_source::IgnoreSource,
6 path_ext::{PathExt, RelPath},
7 },
8};
9use flams_backend_types::archives::FileStateSummary;
10use ftml_ontology::utils::{RefTree, TreeChild, time::Timestamp};
11use ftml_uris::{Language, UriPath};
12use std::path::Path;
13
14#[derive(Debug)]
15pub enum SourceEntry {
16 Dir(SourceDir),
17 File(SourceFile),
18}
19
20#[derive(Debug)]
21pub enum SourceEntryRef<'f> {
22 Dir(&'f SourceDir),
23 File(&'f SourceFile),
24}
25
26#[derive(Debug)]
27pub enum SourceEntryRefMut<'f> {
28 Dir(&'f mut SourceDir),
29 File(&'f mut SourceFile),
30}
31
32#[derive(Debug, Default)]
33pub struct SourceDir {
34 pub children: Vec<SourceEntry>,
35 pub relative_path: Option<UriPath>,
36 pub state: FileStates,
37}
38
39#[derive(Debug)]
40pub struct SourceFile {
41 pub relative_path: UriPath,
42 pub format: SourceFormatId,
43 pub target_state: Vec<(BuildTargetId, FileState)>,
44 pub format_state: FileState,
45}
46
47impl SourceEntry {
48 #[inline]
49 #[must_use]
50 pub fn relative_path(&self) -> &str {
51 match self {
52 Self::Dir(dir) => dir.relative_path.as_ref().map_or("", |e| e.as_ref()),
53 Self::File(file) => file.relative_path.as_ref(),
54 }
55 }
56 #[inline]
57 #[must_use]
58 pub fn name(&self) -> &str {
59 let rel_path = self.relative_path();
60 rel_path.rsplit_once('/').map_or(rel_path, |(_, b)| b)
61 }
62}
63
64impl SourceDir {
65 #[inline]
66 #[must_use]
67 pub const fn state(&self) -> &FileStates {
68 &self.state
69 }
70}
71
72impl RefTree for SourceDir {
73 type Child<'a>
74 = &'a SourceEntry
75 where
76 Self: 'a;
77 #[inline]
78 fn tree_children(&self) -> impl Iterator<Item = Self::Child<'_>> {
79 self.children.iter()
80 }
81}
82impl<'a> TreeChild<'a> for &'a SourceEntry {
83 fn tree_children(self) -> impl Iterator<Item = Self> {
84 match self {
85 SourceEntry::Dir(d) => d.children.iter(),
86 SourceEntry::File(_) => [].iter(),
87 }
88 }
89}
90
91impl SourceDir {
92 pub(crate) fn new(
93 source_dir: &Path,
96 ignore: &IgnoreSource,
97 formats: &[SourceFormatId],
98 ) -> Self {
99 let mut slf = Self::default();
100 let filter = |e: &walkdir::DirEntry| {
101 if ignore.ignores(e.path()) {
102 tracing::trace!("Ignoring {} because of {}", e.path().display(), ignore);
103 false
104 } else {
105 true
106 }
107 };
108 let Some(top_dir) = source_dir.parent() else {
113 return slf;
114 };
115
116 for entry in walkdir::WalkDir::new(source_dir)
117 .min_depth(1)
118 .into_iter()
119 .filter_entry(filter)
120 .filter_map(Result::ok)
121 {
122 let Ok(metadata) = entry.metadata() else {
123 tracing::warn!("Invalid metadata: {}", entry.path().display());
124 continue;
125 };
126 if !metadata.is_file() {
127 continue;
128 }
129 let Some(ext) = entry.path().extension().and_then(|s| s.to_str()) else {
130 continue;
131 };
132 let Some(format) = formats.iter().find(|t| t.file_extensions.contains(&ext)) else {
133 continue;
134 };
135 let path = entry.path();
136 let Some(relpath) = path.relative_to(&source_dir) else {
137 unreachable!(
138 "{} does not start with {}???",
139 path.display(),
140 source_dir.display()
141 )
142 };
143 let Ok(relative_path) = relpath.parse::<UriPath>() else {
144 unreachable!("invalid uri path {relpath}")
145 };
146 let states = FileState::from(top_dir, &metadata, relpath, format);
171 let new = SourceFile {
172 relative_path,
173 format: *format,
174 format_state: states
175 .iter()
176 .map(|(_, v)| v)
177 .min()
178 .cloned()
179 .unwrap_or(FileState::New),
180 target_state: states,
181 };
182 slf.insert(new);
205 }
206 slf
207 }
208
209 pub(crate) fn update(&mut self, new: Self) {
210 *self = new;
212 }
213
214 #[inline]
215 fn index(&self, s: &str) -> Result<usize, usize> {
216 self.children.binary_search_by_key(&s, SourceEntry::name)
217 }
218
219 #[must_use]
220 pub fn find<'s>(&'s self, rel_path: RelPath) -> Option<SourceEntryRef<'s>> {
221 let mut segments = rel_path.steps();
222 let mut current = self;
223 while let Some(seg) = segments.next() {
224 match current.index(seg) {
225 Ok(i) => match ¤t.children[i] {
226 SourceEntry::Dir(dir) => {
227 current = dir;
228 }
229 SourceEntry::File(f) if segments.next().is_none() => {
230 return Some(SourceEntryRef::File(f));
231 }
232 SourceEntry::File(_) => return None,
233 },
234 _ => return None,
235 }
236 }
237 Some(SourceEntryRef::Dir(current))
238 }
239
240 #[allow(clippy::match_wildcard_for_single_variants)]
241 fn find_mut<'s>(&'s mut self, rel_path: RelPath) -> Option<SourceEntryRefMut<'s>> {
242 let mut segments = rel_path.steps();
243 let mut current = self;
244 while let Some(seg) = segments.next() {
245 match current.index(seg) {
246 Ok(i) => match &mut current.children[i] {
247 SourceEntry::Dir(dir) => {
248 current = dir;
249 }
250 SourceEntry::File(f) if segments.next().is_none() => {
251 return Some(SourceEntryRefMut::File(f));
252 }
253 _ => return None,
254 },
255 _ => return None,
256 }
257 }
258 Some(SourceEntryRefMut::Dir(current))
259 }
260
261 fn remove(&mut self, s: RelPath) -> Option<SourceEntry> {
262 let Some((p, r)) = s.split_last() else {
263 return Some(self.children.remove(self.index(s.steps().next()?).ok()?));
264 };
265 if let SourceEntryRefMut::Dir(d) = self.find_mut(p)? {
266 let i = d.index(r).ok()?;
267 let r = d.children.remove(i);
268 if d.children.is_empty() {
269 self.remove(p);
270 }
271 Some(r)
272 } else {
273 None
274 }
275 }
276 pub fn insert(&mut self, f: SourceFile) {
277 self.state.merge(f.format, &f.format_state);
280 if f.relative_path.is_simple() {
281 match self.index(f.relative_path.as_ref()) {
282 Ok(i) => self.children[i] = SourceEntry::File(f),
283 Err(i) => self.children.insert(i, SourceEntry::File(f)),
284 }
285 return;
286 }
287
288 let mut steps = f.relative_path.steps();
289 let last = unsafe { steps.next_back().unwrap_unchecked() };
291 let mut curr_relpath = "";
292 let mut current = self;
293 for step in steps {
294 curr_relpath = if curr_relpath.is_empty() {
295 step
296 } else {
297 &f.relative_path.as_ref()[..curr_relpath.len() + 1 + step.len()]
298 };
299 match current.index(step) {
300 Ok(i) => {
301 if matches!(current.children[i], SourceEntry::Dir(_)) {
302 let SourceEntry::Dir(dir) = &mut current.children[i] else {
303 unreachable!()
304 };
305 dir.state.merge(f.format, &f.format_state);
306 current = dir;
307 continue;
308 }
309 let mut dir = Self {
310 children: Vec::new(),
311 relative_path: Some(unsafe { curr_relpath.parse().unwrap_unchecked() }),
313 state: FileStates::default(),
314 };
315 dir.state.merge(f.format, &f.format_state);
316 current.children[i] = SourceEntry::Dir(dir);
317 current = if let SourceEntry::Dir(d) = &mut current.children[i] {
318 d
319 } else {
320 unreachable!()
321 };
322 }
323 Err(i) => {
324 let mut dir = Self {
325 children: Vec::new(),
326 relative_path: Some(unsafe { curr_relpath.parse().unwrap_unchecked() }),
328 state: FileStates::default(),
329 };
330 dir.state.merge(f.format, &f.format_state);
331 current.children.insert(i, SourceEntry::Dir(dir));
332 current = if let SourceEntry::Dir(d) = &mut current.children[i] {
333 d
334 } else {
335 unreachable!()
336 };
337 }
338 }
339 }
340 match current.index(last) {
341 Ok(i) => current.children[i] = SourceEntry::File(f),
342 Err(i) => current.children.insert(i, SourceEntry::File(f)),
343 }
344 }
345}
346
347#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
348pub struct ChangeState {
349 pub last_built: Timestamp,
350 pub last_changed: Timestamp,
351 }
354
355#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
356pub enum FileState {
357 Deleted,
358 New,
359 Stale(ChangeState),
360 UpToDate(ChangeState),
361}
362impl PartialOrd for FileState {
363 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
364 Some(self.cmp(other))
365 }
366}
367impl Ord for FileState {
368 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
369 match (self, other) {
370 (Self::Deleted, Self::Deleted)
371 | (Self::New, Self::New)
372 | (Self::Stale(_), Self::Stale(_))
373 | (Self::UpToDate(_), Self::UpToDate(_)) => std::cmp::Ordering::Equal,
374 (Self::Deleted, _) => std::cmp::Ordering::Less,
375 (_, Self::Deleted) => std::cmp::Ordering::Greater,
376 (Self::New, _) => std::cmp::Ordering::Less,
377 (_, Self::New) => std::cmp::Ordering::Greater,
378 (Self::Stale(_), _) => std::cmp::Ordering::Less,
379 (_, Self::Stale(_)) => std::cmp::Ordering::Greater,
380 }
381 }
382}
383impl FileState {
384 fn from(
385 top: &Path,
386 source: &std::fs::Metadata,
387 relative_path: RelPath<'_>,
388 format: &SourceFormat,
389 ) -> Vec<(BuildTargetId, Self)> {
390 use std::str::FromStr;
391 let mut steps = relative_path.steps();
392 let Some(mut last) = steps.next_back() else {
393 unreachable!("empty RelPath");
394 };
395 let out = steps.fold(LocalArchive::out_dir_of(top), |p, s| p.join(s));
396 if let Some((first, lang)) = last.rsplit_once('.')
397 && Language::from_str(lang).is_err()
398 {
399 last = first;
400 }
401 let out = out.join(last);
402
403 let mut ret = Vec::new();
405 for t in format.targets {
406 let t = *t;
407 let log = out.join(t.name).with_extension("log");
408 if !log.exists() {
409 ret.push((t, Self::New));
410 continue;
411 }
412 let Ok(meta) = log.metadata() else {
413 ret.push((t, Self::New));
414 continue;
415 };
416 let Ok(last_built) = meta.modified() else {
417 ret.push((t, Self::New));
418 continue;
419 };
420 let Ok(last_changed) = source.modified() else {
421 ret.push((t, Self::New));
422 continue;
423 };
424 if last_built > last_changed {
425 ret.push((
426 t,
427 Self::UpToDate(ChangeState {
428 last_built: last_built.into(),
429 last_changed: last_changed.into(),
430 }),
431 ));
432 } else {
433 ret.push((
434 t,
435 Self::Stale(ChangeState {
436 last_built: last_built.into(),
437 last_changed: last_changed.into(),
438 }),
439 ));
440 }
441 }
442 ret
443 }
444}
445
446#[allow(clippy::unsafe_derive_deserialize)]
447#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize)]
448pub struct FileStates {
449 pub formats: Vec<(SourceFormatId, FileStateSummary)>,
450}
451impl FileStates {
452 #[must_use]
453 pub fn summarize(&self) -> FileStateSummary {
454 let mut ret = FileStateSummary::default();
455 for (_, v) in &self.formats {
456 ret.new = ret.new.max(v.new);
457 ret.stale = ret.stale.max(v.stale);
458 ret.up_to_date = ret.up_to_date.max(v.up_to_date);
459 ret.last_built = std::cmp::max(ret.last_built, v.last_built);
460 ret.last_changed = std::cmp::max(ret.last_changed, v.last_changed);
461 }
462 ret
463 }
464
465 pub(crate) fn merge(&mut self, format: SourceFormatId, state: &FileState) {
466 let target = if let Some(f) = self.formats.iter_mut().find(|(f, _)| f.name == format.name) {
467 &mut f.1
468 } else {
469 self.formats.push((format, FileStateSummary::default()));
470 &mut unsafe { self.formats.last_mut().unwrap_unchecked() }.1
472 };
473 match state {
474 FileState::Deleted => target.deleted += 1,
475 FileState::New => target.new += 1,
476 FileState::Stale(s) => {
477 target.stale += 1;
478 target.last_built = std::cmp::max(target.last_built, s.last_built);
479 target.last_changed = std::cmp::max(target.last_changed, s.last_changed);
480 }
481 FileState::UpToDate(s) => {
482 target.up_to_date += 1;
483 target.last_built = std::cmp::max(target.last_built, s.last_built);
484 }
485 }
486 }
487
488 pub(crate) fn merge_summary(&mut self, format: SourceFormatId, summary: &FileStateSummary) {
489 let target = if let Some(f) = self.formats.iter_mut().find(|(f, _)| f.name == format.name) {
490 &mut f.1
491 } else {
492 self.formats.push((format, FileStateSummary::default()));
493 &mut unsafe { self.formats.last_mut().unwrap_unchecked() }.1
495 };
496 target.new += summary.new;
497 target.stale += summary.stale;
498 target.deleted += summary.deleted;
499 target.up_to_date += summary.up_to_date;
500 target.last_built = std::cmp::max(target.last_built, summary.last_built);
501 target.last_changed = std::cmp::max(target.last_changed, summary.last_changed);
502 }
503 pub(crate) fn merge_all(&mut self, other: &Self) {
504 for (k, v) in &other.formats {
505 self.merge_summary(*k, v);
506 }
507 }
508}