flams_math_archives/
source_files.rs

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        //&mut self,
94        //archive: &ArchiveUri,
95        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 mut old = std::mem::take(self);
109        /*let Some(topstr) = top.to_str() else {
110            unreachable!()
111        };*/
112        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            /*
147            let Some(relative_path) = entry.path().to_str() else {
148                tracing::warn!("Invalid path: {}", entry.path().display());
149                continue;
150            };
151            let Some(relative_path) = relative_path.strip_prefix(topstr).and_then(|s| {
152                s.strip_prefix(const_format::concatcp!(
153                    std::path::PathBuf::PATH_SEPARATOR,
154                    "source",
155                    std::path::PathBuf::PATH_SEPARATOR
156                ))
157            }) else {
158                unreachable!("{relative_path} does not start with {topstr}???")
159            };
160
161            #[cfg(target_os = "windows")]
162            let relative_path: Arc<str> = relative_path
163                .replace(std::path::PathBuf::PATH_SEPARATOR, "/")
164                .to_string()
165                .into();
166            #[cfg(not(target_os = "windows"))]
167            let relative_path: Arc<str> = relative_path.to_string().into();
168             */
169
170            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            //old.remove(new.relative_path.as_ref());
183            /*
184            if let Some(SourceEntry::File(previous)) = old.remove(&new.relative_path) {
185                if previous.format_state != new.format_state {
186                    sender.lazy_send(|| BackendChange::FileChange {
187                        archive: URIRefTrait::owned(archive),
188                        relative_path: new.relative_path.to_string(),
189                        format: new.format,
190                        old: Some(previous.format_state),
191                        new: new.format_state.clone(),
192                    });
193                }
194            } else {
195                sender.lazy_send(|| BackendChange::FileChange {
196                    archive: URIRefTrait::owned(archive),
197                    relative_path: new.relative_path.to_string(),
198                    format: new.format,
199                    old: None,
200                    new: new.format_state.clone(),
201                });
202            }
203             */
204            slf.insert(new);
205        }
206        slf
207    }
208
209    pub(crate) fn update(&mut self, new: Self) {
210        // TODO
211        *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 &current.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        // TODO this logic overwrites existing entries, which would screw up the states.
278        // In practice, that should never happen anyway.
279        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        //SAFETY: steps() guaranteed to be non-empty
290        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                        // SAFETY: consists of valid segments from a previous UriPath
312                        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                        // SAFETY: consists of valid segments from a previous UriPath
327                        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    //last_watched:Timestamp,
352    //md5:u128 TODO
353}
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 out = LocalArchive::out_dir_of(top).join(relative_path);
404        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            // SAFETY we literally just pushed
471            &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            // SAFETY we literally just pushed
494            &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}