flams_system/backend/archives/
source_files.rs1use std::{path::Path, sync::Arc};
2
3use either::Either;
4use flams_ontology::{
5 file_states::FileStateSummary, uris::{ArchiveURIRef, URIRefTrait}
6};
7use flams_utils::{
8 change_listener::ChangeSender,
9 prelude::{TreeChild, TreeLike},
10 time::Timestamp,
11 vecmap::VecMap, PathExt,
12};
13
14use super::ignore_regex::IgnoreSource;
15use crate::{backend::{archives::LocalArchive, BackendChange}, formats::{BuildTargetId, SourceFormatId}};
16
17#[derive(Debug)]
18pub enum SourceEntry {
19 Dir(SourceDir),
20 File(SourceFile),
21}
22impl SourceEntry {
23 #[inline]
24 #[must_use]
25 pub fn relative_path(&self) -> &str {
26 match self {
27 Self::Dir(dir) => &dir.relative_path,
28 Self::File(file) => &file.relative_path,
29 }
30 }
31 #[inline]
32 #[must_use]
33 pub fn name(&self) -> &str {
34 let rel_path = self.relative_path();
35 rel_path.rsplit_once('/').map_or(rel_path, |(_, b)| b)
36 }
37}
38
39#[derive(Debug, Default)]
40pub struct SourceDir {
41 pub children: Vec<SourceEntry>,
42 pub relative_path: Arc<str>,
43 pub state: FileStates,
44}
45impl SourceDir {
46 #[inline]
47 #[must_use]
48 pub const fn state(&self) -> &FileStates {
49 &self.state
50 }
51}
52
53#[derive(Debug)]
54pub struct SourceFile {
55 pub relative_path: Arc<str>,
56 pub format: SourceFormatId,
57 pub target_state: VecMap<BuildTargetId, FileState>,
58 pub format_state: FileState
59}
60
61impl TreeChild<SourceEntry> for &SourceEntry {
62 fn children<'a>(&self) -> Option<<SourceEntry as TreeLike>::RefIter<'a>>
63 where
64 Self: 'a,
65 {
66 TreeLike::children(*self)
67 }
68}
69impl TreeLike for SourceEntry {
70 type Child<'a> = &'a Self;
71 type RefIter<'a> = std::slice::Iter<'a, Self>;
72 fn children(&self) -> Option<Self::RefIter<'_>> {
73 match self {
74 Self::Dir(dir) => Some(dir.children.iter()),
75 Self::File(_) => None,
76 }
77 }
78}
79
80impl TreeChild<SourceDir> for &SourceEntry {
81 fn children<'a>(&self) -> Option<<SourceEntry as TreeLike>::RefIter<'a>>
82 where
83 Self: 'a,
84 {
85 TreeLike::children(*self)
86 }
87}
88
89impl TreeLike for SourceDir {
90 type Child<'a> = &'a SourceEntry;
91 type RefIter<'a> = std::slice::Iter<'a, SourceEntry>;
92 fn children(&self) -> Option<Self::RefIter<'_>> {
93 Some(self.children.iter())
94 }
95}
96
97impl SourceDir {
98 #[inline]
99 fn index(&self, s: &str) -> Result<usize, usize> {
100 self.children.binary_search_by_key(&s, SourceEntry::name)
101 }
102
103 #[must_use]
104 pub fn find(&self, rel_path: &str) -> Option<Either<&Self, &SourceFile>> {
105 let mut segments = rel_path.split('/');
106 let mut current = self;
107 while let Some(seg) = segments.next() {
108 match current.index(seg) {
109 Ok(i) => match ¤t.children[i] {
110 SourceEntry::Dir(dir) => {
111 current = dir;
112 }
113 SourceEntry::File(f) if segments.next().is_none() => {
114 return Some(Either::Right(f))
115 }
116 SourceEntry::File(_) => return None,
117 },
118 _ => return None,
119 }
120 }
121 Some(Either::Left(current))
122 }
123
124 #[allow(clippy::match_wildcard_for_single_variants)]
125 fn find_mut(&mut self, rel_path: &str) -> Option<Either<&mut Self, &mut SourceFile>> {
126 let mut segments = rel_path.split('/');
127 let mut current = self;
128 while let Some(seg) = segments.next() {
129 match current.index(seg) {
130 Ok(i) => match &mut current.children[i] {
131 SourceEntry::Dir(dir) => {
132 current = dir;
133 }
134 SourceEntry::File(f) if segments.next().is_none() => {
135 return Some(Either::Right(f))
136 }
137 _ => return None,
138 },
139 _ => return None,
140 }
141 }
142 Some(Either::Left(current))
143 }
144
145 fn remove(&mut self, s: &str) -> Option<SourceEntry> {
146 let Some((p, r)) = s.rsplit_once('/') else {
147 return if let Ok(i) = self.index(s) {
148 Some(self.children.remove(i))
149 } else {
150 None
151 };
152 };
153 match self.find_mut(p) {
154 Some(Either::Left(d)) => {
155 if let Ok(i) = d.index(r) {
156 let r = d.children.remove(i);
157 if d.children.is_empty() {
158 self.remove(p);
159 }
160 Some(r)
161 } else {
162 None
163 }
164 }
165 _ => None,
166 }
167 }
168 fn insert(&mut self, f: SourceFile) {
169 self.state.merge(f.format, &f.format_state);
172 let Some((_, last)) = f.relative_path.rsplit_once('/') else {
173 match self.index(&f.relative_path) {
174 Ok(i) => self.children[i] = SourceEntry::File(f),
175 Err(i) => self.children.insert(i, SourceEntry::File(f)),
176 }
177 return;
178 };
179 let mut curr_relpath = "";
180 let steps = f.relative_path.split('/');
181 let mut current = self;
182 for step in steps {
183 if step == last {
184 break;
185 }
186 curr_relpath = if curr_relpath.is_empty() {
187 step
188 } else {
189 &f.relative_path[..curr_relpath.len() + 1 + step.len()]
190 };
191 match current.index(step) {
192 Ok(i) => {
193 if matches!(current.children[i], SourceEntry::Dir(_)) {
194 let SourceEntry::Dir(dir) = &mut current.children[i] else {
195 unreachable!()
196 };
197 dir.state.merge(f.format,&f.format_state);
198 current = dir;
199 continue;
200 }
201 let mut dir = Self {
202 children: Vec::new(),
203 relative_path: curr_relpath.into(),
204 state: FileStates::default(),
205 };
206 dir.state.merge(f.format,&f.format_state);
207 current.children[i] = SourceEntry::Dir(dir);
208 current = if let SourceEntry::Dir(d) = &mut current.children[i] {
209 d
210 } else {
211 unreachable!()
212 };
213 }
214 Err(i) => {
215 let mut dir = Self {
216 children: Vec::new(),
217 relative_path: curr_relpath.into(),
218 state: FileStates::default(),
219 };
220 dir.state.merge(f.format,&f.format_state);
221 current.children.insert(i, SourceEntry::Dir(dir));
222 current = if let SourceEntry::Dir(d) = &mut current.children[i] {
223 d
224 } else {
225 unreachable!()
226 };
227 }
228 };
229 }
230 match current.index(last) {
231 Ok(i) => current.children[i] = SourceEntry::File(f),
232 Err(i) => current.children.insert(i, SourceEntry::File(f)),
233 }
234 }
235
236 pub(crate) fn update(
237 &mut self,
238 archive: ArchiveURIRef,
239 top: &Path,
240 sender: &ChangeSender<BackendChange>,
241 ignore: &IgnoreSource,
242 formats: &[SourceFormatId],
243 ) {
244 let filter = |e: &walkdir::DirEntry| {
245 if ignore.ignores(e.path()) {
246 tracing::trace!(target:"archives","Ignoring {} because of {}",e.path().display(),ignore);
247 false
248 } else {
249 true
250 }
251 };
252 let mut old = std::mem::take(self);
253 let Some(topstr) = top.to_str() else {
254 unreachable!()
255 };
256
257 for entry in walkdir::WalkDir::new(LocalArchive::source_dir_of(top))
258 .min_depth(1)
259 .into_iter()
260 .filter_entry(filter)
261 .filter_map(Result::ok)
262 {
263 let Ok(metadata) = entry.metadata() else {
264 tracing::warn!(target:"archives","Invalid metadata: {}",entry.path().display());
265 continue;
266 };
267 if !metadata.is_file() {
268 continue;
269 }
270 let Some(ext) = entry.path().extension().and_then(|s| s.to_str()) else {
271 continue;
272 };
273 let Some(format) = formats.iter().find(|t| t.file_exts().contains(&ext)) else {
274 continue;
275 };
276 let Some(relative_path) = entry.path().to_str() else {
277 tracing::warn!(target:"archives","Invalid path: {}",entry.path().display());
278 continue;
279 };
280 let Some(relative_path) = relative_path.strip_prefix(topstr).and_then(|s| {
281 s.strip_prefix(const_format::concatcp!(
282 std::path::PathBuf::PATH_SEPARATOR,
283 "source",
284 std::path::PathBuf::PATH_SEPARATOR
285 ))
286 }) else {
287 unreachable!("{relative_path} does not start with {topstr}???")
288 };
289 #[cfg(target_os = "windows")]
290 let relative_path: Arc<str> = relative_path.replace(std::path::PathBuf::PATH_SEPARATOR, "/").to_string().into();
291 #[cfg(not(target_os = "windows"))]
292 let relative_path: Arc<str> = relative_path.to_string().into();
293 let states = FileState::from(top, &metadata, &relative_path, *format);
294 let new = SourceFile {
295 relative_path,
296 format: *format,
297 format_state: states.iter().map(|(_,v)| v).min().cloned().unwrap_or(FileState::New),
298 target_state:states,
299 };
300 if let Some(SourceEntry::File(previous)) = old.remove(&new.relative_path) {
301 if previous.format_state != new.format_state {
302 sender.lazy_send(|| BackendChange::FileChange {
303 archive: URIRefTrait::owned(archive),
304 relative_path: new.relative_path.to_string(),
305 format: new.format,
306 old: Some(previous.format_state),
307 new: new.format_state.clone(),
308 });
309 }
310 } else {
311 sender.lazy_send(|| BackendChange::FileChange {
312 archive: URIRefTrait::owned(archive),
313 relative_path: new.relative_path.to_string(),
314 format: new.format,
315 old: None,
316 new: new.format_state.clone(),
317 });
318 }
319 self.insert(new);
320 }
321 }
323}
324
325#[derive(Debug, Clone, PartialEq, Eq, Hash,serde::Serialize, serde::Deserialize)]
326pub struct ChangeState {
327 pub last_built: Timestamp,
328 pub last_changed: Timestamp,
329 }
332
333#[derive(Debug, Clone, PartialEq, Eq, Hash,serde::Serialize, serde::Deserialize)]
334pub enum FileState {
335 Deleted,
336 New,
337 Stale(ChangeState),
338 UpToDate(ChangeState),
339}
340impl PartialOrd for FileState {
341 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
342 Some(self.cmp(other))
343 }
344}
345impl Ord for FileState {
346 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
347 match (self, other) {
348 (Self::Deleted, Self::Deleted) | (Self::New, Self::New) |
349 (Self::Stale(_), Self::Stale(_)) | (Self::UpToDate(_), Self::UpToDate(_))
350 => std::cmp::Ordering::Equal,
351 (Self::Deleted, _) => std::cmp::Ordering::Less,
352 (_, Self::Deleted) => std::cmp::Ordering::Greater,
353 (Self::New, _) => std::cmp::Ordering::Less,
354 (_, Self::New) => std::cmp::Ordering::Greater,
355 (Self::Stale(_), _) => std::cmp::Ordering::Less,
356 (_, Self::Stale(_)) => std::cmp::Ordering::Greater,
357 }
358 }
359}
360impl FileState {
361 fn from(
362 top: &Path,
363 source: &std::fs::Metadata,
364 relative_path: &str,
365 format: SourceFormatId,
366 ) -> VecMap<BuildTargetId,Self> {
367 let out = LocalArchive::out_dir_of(top).join(relative_path);
368 let mut ret = VecMap::new();
369 for t in *format.targets() {
370 let log = out.join(t.name()).with_extension("log");
371 if !log.exists() {
372 ret.insert(*t, Self::New);
373 continue;
374 }
375 let Ok(meta) = log.metadata() else {
376 ret.insert(*t, Self::New);
377 continue;
378 };
379 let Ok(last_built) = meta.modified() else {
380 ret.insert(*t, Self::New);
381 continue;
382 };
383 let Ok(last_changed) = source.modified() else {
384 ret.insert(*t, Self::New);
385 continue;
386 };
387 if last_built > last_changed {
388 ret.insert(
389 *t,
390 Self::UpToDate(ChangeState {
391 last_built: last_built.into(),
392 last_changed: last_changed.into(),
393 }),
394 );
395 } else {
396 ret.insert(
397 *t,
398 Self::Stale(ChangeState {
399 last_built: last_built.into(),
400 last_changed: last_changed.into(),
401 }),
402 );
403 }
404 }
405 ret
406 }
407}
408
409#[derive(Debug, Clone, PartialEq, Eq, Hash, Default,serde::Serialize, serde::Deserialize)]
410pub struct FileStates{
411 pub formats:VecMap<SourceFormatId,FileStateSummary>
412}
413impl FileStates {
414 #[must_use]
415 pub fn summarize(&self) -> FileStateSummary {
416 let mut ret = FileStateSummary::default();
417 for (_, v) in self.formats.iter() {
418 ret.new = ret.new.max(v.new);
419 ret.stale = ret.stale.max(v.stale);
420 ret.up_to_date = ret.up_to_date.max(v.up_to_date);
421 ret.last_built = std::cmp::max(ret.last_built, v.last_built);
422 ret.last_changed = std::cmp::max(ret.last_changed, v.last_changed);
423 }
424 ret
425 }
426
427 pub(crate) fn merge(&mut self, format: SourceFormatId, state: &FileState) {
428 let target = self.formats.get_or_insert_mut(format, FileStateSummary::default);
429 match state {
430 FileState::Deleted => target.deleted += 1,
431 FileState::New => target.new += 1,
432 FileState::Stale(s) => {
433 target.stale += 1;
434 target.last_built = std::cmp::max(target.last_built, s.last_built);
435 target.last_changed = std::cmp::max(target.last_changed, s.last_changed);
436 }
437 FileState::UpToDate(s) => {
438 target.up_to_date += 1;
439 target.last_built = std::cmp::max(target.last_built, s.last_built);
440 }
441 }
442 }
443 pub(crate) fn merge_summary(&mut self, format: SourceFormatId, summary: &FileStateSummary) {
450 let target = self.formats.get_or_insert_mut(format, FileStateSummary::default);
451 target.new += summary.new;
452 target.stale += summary.stale;
453 target.deleted += summary.deleted;
454 target.up_to_date += summary.up_to_date;
455 target.last_built = std::cmp::max(target.last_built, summary.last_built);
456 target.last_changed = std::cmp::max(target.last_changed, summary.last_changed);
457 }
458 pub(crate) fn merge_all(&mut self, other: &Self) {
459 for (k, v) in other.formats.iter() {
460 self.merge_summary(*k, v);
461 }
462 }
463}