1#![allow(unexpected_cfgs)]
2#![cfg_attr(all(doc, CHANNEL_NIGHTLY), feature(doc_cfg))]
3#![doc = include_str!("../README.md")]
4#![cfg_attr(doc,doc = document_features::document_features!())]
8
9pub mod archive_iter;
10pub mod artifacts;
11pub mod backend;
12pub mod document_file;
13pub mod formats;
14pub mod manager;
15pub mod manifest;
16pub mod mathhub;
17pub mod source_files;
18#[cfg(feature = "rdf")]
19pub mod triple_store;
20pub mod utils;
21
22#[cfg(feature = "rdf")]
23use crate::triple_store::RDFStore;
24use crate::{
25 artifacts::{Artifact, ContentResult, FileOrString},
26 formats::{BuildTargetId, SourceFormatId},
27 manifest::RepositoryData,
28 source_files::{FileStates, SourceDir},
29 utils::{
30 AsyncEngine,
31 errors::{ArtifactSaveError, BackendError, FileError},
32 ignore_source::IgnoreSource,
33 path_ext::PathExt,
34 },
35};
36use flams_backend_types::{
37 archive_json::{ArchiveIndex, Institution},
38 archives::FileStateSummary,
39};
40use ftml_ontology::{domain::modules::Module, narrative::documents::Document};
41use ftml_uris::{
42 ArchiveId, ArchiveUri, IsDomainUri, Language, SimpleUriName, UriPath, UriWithArchive,
43 UriWithPath,
44};
45use std::{
46 hint::unreachable_unchecked,
47 path::{Path, PathBuf},
48 str,
49};
50
51type Result<T> = std::result::Result<T, BackendError>;
52pub trait MathArchive {
70 fn uri(&self) -> &ArchiveUri;
71 fn path(&self) -> &Path;
72 fn is_meta(&self) -> bool;
73
74 #[inline]
75 fn id(&self) -> &ArchiveId {
76 self.uri().archive_id()
77 }
78
79 fn load_module(&self, path: Option<&UriPath>, name: &str) -> Result<Module>;
81
82 fn load_module_async<A: AsyncEngine>(
84 &self,
85 path: Option<&UriPath>,
86 name: &str,
87 ) -> impl Future<Output = Result<Module>> + 'static + use<Self, A>
88 where
89 Self: Sized;
90
91 }
116
117pub trait ExternalArchive: Send + Sync + MathArchive + std::any::Any + std::fmt::Debug {
118 #[inline]
119 fn local_out(&self) -> Option<&dyn LocallyBuilt> {
120 None
121 }
122 #[inline]
123 fn buildable(&self) -> Option<&dyn BuildableArchive> {
124 None
125 }
126
127 fn load_document(
128 &self,
129 path: Option<&UriPath>,
130 name: &str,
131 language: Language,
132 ) -> Option<Document>;
133}
134
135#[derive(Debug)]
136pub enum Archive {
137 Local(Box<LocalArchive>),
138 Ext(&'static ArchiveKind, Box<dyn ExternalArchive>),
139}
140impl Archive {
141 fn buildable(&self) -> Option<&dyn BuildableArchive> {
142 match self {
143 Self::Local(l) => Some(&**l as _),
144 Self::Ext(_, a) => a.buildable(),
145 }
146 }
147}
148
149impl std::hash::Hash for Archive {
150 #[inline]
151 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
152 self.uri().hash(state);
153 }
154}
155impl std::borrow::Borrow<ArchiveUri> for Archive {
156 #[inline]
157 fn borrow(&self) -> &ArchiveUri {
158 self.uri()
159 }
160}
161impl PartialEq for Archive {
162 #[inline]
163 fn eq(&self, other: &Self) -> bool {
164 *self.uri() == *other.uri()
165 }
166}
167impl Eq for Archive {}
168
169impl MathArchive for Archive {
170 fn uri(&self) -> &ArchiveUri {
171 match self {
172 Self::Local(a) => a.uri(),
173 Self::Ext(_, a) => a.uri(),
174 }
175 }
176 fn path(&self) -> &Path {
177 match self {
178 Self::Local(a) => a.path(),
179 Self::Ext(_, a) => a.path(),
180 }
181 }
182 fn is_meta(&self) -> bool {
183 match self {
184 Self::Local(a) => a.is_meta(),
185 Self::Ext(_, a) => a.is_meta(),
186 }
187 }
188 fn load_module(&self, path: Option<&UriPath>, name: &str) -> Result<Module> {
189 match self {
190 Self::Local(a) => a.load_module(path, name),
191 Self::Ext(_, a) => a.load_module(path, name),
192 }
193 }
194 fn load_module_async<A: AsyncEngine>(
195 &self,
196 path: Option<&UriPath>,
197 name: &str,
198 ) -> impl Future<Output = Result<Module>> + 'static + use<A> {
199 match self {
200 Self::Local(a) => a.load_module_async::<A>(path, name),
201 Self::Ext(_, a) => todo!(),
202 }
203 }
204}
205
206#[derive(Copy, Clone, Debug)]
207pub struct ArchiveKind {
208 pub name: &'static str,
209 #[allow(clippy::type_complexity)]
210 make_new: fn(RepositoryData, &Path) -> std::result::Result<Box<dyn ExternalArchive>, String>,
211}
212
213impl ArchiveKind {
214 #[inline]
215 pub fn all() -> impl Iterator<Item = &'static Self> {
216 inventory::iter.into_iter()
217 }
218 #[must_use]
219 pub fn get(name: &str) -> Option<&'static Self> {
220 Self::all().find(|e| e.name == name)
221 }
222}
223inventory::collect!(ArchiveKind);
224#[macro_export]
225macro_rules! archive_kind {
226 ($i:ident { $($t:tt)* }) => {
227 pub static $i : $crate::ArchiveKind = $crate::ArchiveKind { $($t)* };
228 $crate::formats::__reexport::submit!{ $i }
229 };
230}
231
232pub trait BuildableArchive: MathArchive {
233 fn file_state(&self) -> FileStates;
234 fn formats(&self) -> &[SourceFormatId];
235 fn get_log(&self, relative_path: &str, target: BuildTargetId) -> PathBuf;
236
237 #[allow(clippy::too_many_arguments)]
238 fn save(
240 &self,
241 in_doc: &ftml_uris::DocumentUri,
242 rel_path: Option<&UriPath>,
243 log: FileOrString,
244 from: BuildTargetId,
245 result: Option<Box<dyn Artifact>>,
246 #[cfg(feature = "rdf")] relational: &RDFStore,
247 #[cfg(feature = "rdf")] load: bool,
248 ) -> std::result::Result<(), ArtifactSaveError>;
249
250 #[cfg(feature = "rdf")]
251 fn save_triples(
252 &self,
253 in_doc: &ftml_uris::DocumentUri,
254 rel_path: Option<&UriPath>,
255 relational: &RDFStore,
256 load: bool,
257 iter: Vec<ulo::rdf_types::Triple>,
258 );
259}
260
261pub trait LocallyBuilt: BuildableArchive {
262 fn out_dir(&self) -> &Path;
263
264 fn out_path_of(
265 &self,
266 path: Option<&UriPath>,
267 doc_name: &SimpleUriName,
268 rel_path: Option<&UriPath>,
269 language: Language,
270 ) -> PathBuf;
271
272 fn document_file(
273 &self,
274 path: Option<&UriPath>,
275 rel_path: Option<&UriPath>,
276 doc_name: &SimpleUriName,
277 language: Language,
278 ) -> PathBuf {
279 self.out_path_of(path, doc_name, rel_path, language)
280 .join("content")
281 }
282}
283
284#[derive(Debug)]
285pub struct LocalArchive {
286 pub(crate) uri: ArchiveUri,
287 pub(crate) out_path: PathBuf,
288 pub(crate) source: Option<Box<str>>,
289 pub(crate) formats: smallvec::SmallVec<SourceFormatId, 1>,
291 pub(crate) file_state: parking_lot::RwLock<SourceDir>,
293 pub ignore: IgnoreSource,
296 #[cfg(feature = "git")]
297 pub(crate) is_managed: std::sync::OnceLock<Option<flams_git::GitUrl>>,
298}
299impl MathArchive for LocalArchive {
300 #[inline]
301 fn uri(&self) -> &ArchiveUri {
302 &self.uri
303 }
304 fn path(&self) -> &Path {
305 self.out_path
306 .parent()
307 .expect("out path of an archive *must* have a parent")
308 }
309
310 fn is_meta(&self) -> bool {
311 self.uri.archive_id().is_meta()
312 }
313
314 fn load_module(&self, path: Option<&UriPath>, name: &str) -> Result<Module> {
315 let out = path.map_or_else(
316 || self.out_dir().join(".modules"),
317 |n| self.out_dir().join_uri_path(n).join(".modules"),
318 );
319 let out = Self::escape_module_name(&out, name);
320 if !out.exists() {
321 return Err(BackendError::NotFound(ftml_uris::UriKind::Module));
322 }
323 let file = std::io::BufReader::new(std::fs::File::open(out)?);
324 let ret = bincode::decode_from_reader(file, bincode::config::standard())?;
325 Ok(ret)
326 }
327
328 fn load_module_async<A: AsyncEngine>(
329 &self,
330 path: Option<&UriPath>,
331 name: &str,
332 ) -> impl Future<Output = Result<Module>> + 'static + use<A>
333 where
334 Self: Sized,
335 {
336 let out = path.map_or_else(
337 || self.out_dir().join(".modules"),
338 |n| self.out_dir().join_uri_path(n).join(".modules"),
339 );
340 let out = Self::escape_module_name(&out, name);
341 A::block_on(move || {
342 if !out.exists() {
343 return Err(BackendError::NotFound(ftml_uris::UriKind::Module));
344 }
345 let file = std::io::BufReader::new(std::fs::File::open(out)?);
346 let ret = bincode::decode_from_reader(file, bincode::config::standard())?;
347 Ok(ret)
348 })
349 }
350}
351impl BuildableArchive for LocalArchive {
352 #[inline]
353 fn file_state(&self) -> FileStates {
354 self.file_state.read().state().clone()
355 }
356
357 #[inline]
358 fn formats(&self) -> &[SourceFormatId] {
359 &self.formats
360 }
361 fn get_log(&self, relative_path: &str, target: BuildTargetId) -> PathBuf {
362 use std::str::FromStr;
363 let rel_path = if let Some((first, lang)) = relative_path.rsplit_once('.')
364 && Language::from_str(lang).is_err()
365 {
366 first
367 } else {
368 relative_path
369 };
370 self.out_dir()
371 .join(rel_path)
372 .join(target.name)
373 .with_extension("log")
374 }
375
376 fn save(
377 &self,
378 in_doc: &ftml_uris::DocumentUri,
379 rel_path: Option<&UriPath>,
380 log: FileOrString,
381 from: BuildTargetId,
382 result: Option<Box<dyn Artifact>>,
383 #[cfg(feature = "rdf")] relational: &RDFStore,
384 #[cfg(feature = "rdf")] load: bool,
385 ) -> std::result::Result<(), ArtifactSaveError> {
386 let out = self.out_path_of(in_doc.path(), &in_doc.name, rel_path, in_doc.language);
387 if let Err(e) = std::fs::create_dir_all(&out) {
388 return Err(ArtifactSaveError::Fs(FileError::Creation(out, e)));
389 }
390 let logfile = out.join(from.name).with_extension("log");
391 match log {
392 FileOrString::File(f) => f.rename_safe(&logfile)?,
393 FileOrString::Str(s) => {
394 if let Err(e) = std::fs::write(&logfile, s.as_bytes()) {
395 return Err(ArtifactSaveError::Fs(FileError::Write(logfile, e)));
396 }
397 }
398 }
399 let Some(mut res) = result else { return Ok(()) };
400 let outfile = out.join(res.kind());
401 res.write(&outfile)?;
402 if let Some(e) = res.as_any_mut().downcast_mut::<ContentResult>() {
403 #[cfg(feature = "rdf")]
404 self.save_triples(
405 in_doc,
406 rel_path,
407 relational,
408 load,
409 std::mem::take(&mut e.triples),
410 );
411 for m in &e.modules {
412 let path = m.uri.path();
413 let name = m.uri.module_name();
414 let out = path.map_or_else(
415 || self.out_dir().join(".modules"),
416 |n| self.out_dir().join_uri_path(n).join(".modules"),
417 );
418 std::fs::create_dir_all(&out)
419 .map_err(|e| ArtifactSaveError::Fs(FileError::Creation(out.clone(), e)))?;
420 let out = Self::escape_module_name(&out, name.as_ref());
421 let file = std::fs::File::create(&out)
422 .map_err(|e| ArtifactSaveError::Fs(FileError::Creation(out, e)))?;
423 let mut buf = std::io::BufWriter::new(file);
424 bincode::encode_into_std_write(m, &mut buf, bincode::config::standard())?;
425 }
427 }
428 Ok(())
429 }
430
431 #[cfg(feature = "rdf")]
432 fn save_triples(
433 &self,
434 in_doc: &ftml_uris::DocumentUri,
435 rel_path: Option<&UriPath>,
436 relational: &RDFStore,
437 load: bool,
438 iter: Vec<ulo::rdf_types::Triple>,
439 ) {
440 use ftml_uris::FtmlUri;
441 let out = self.out_path_of(in_doc.path(), &in_doc.name, rel_path, in_doc.language);
442 let _ = std::fs::create_dir_all(&out);
443 let out = out.join("index.ttl");
444 relational.export(iter.into_iter(), &out, in_doc);
445 if load {
446 relational.load(&out, in_doc.to_iri());
447 }
448 }
449}
450
451impl LocallyBuilt for LocalArchive {
452 #[inline]
453 fn out_dir(&self) -> &Path {
454 &self.out_path
455 }
456
457 fn out_path_of(
458 &self,
459 path: Option<&UriPath>,
460 doc_name: &SimpleUriName,
461 rel_path: Option<&UriPath>,
462 language: Language,
463 ) -> PathBuf {
464 if let Some(rp) = rel_path {
465 use std::str::FromStr;
466 let mut steps = rp.steps();
467 let Some(mut last) = steps.next_back() else {
468 unsafe { unreachable_unchecked() }
470 };
471 let out = steps.fold(self.out_dir().to_path_buf(), |p, s| p.join(s));
472 if let Some((first, lang)) = last.rsplit_once('.')
473 && Language::from_str(lang).is_err()
474 {
475 last = first;
476 }
477 return out.join(last);
478 }
479 self.rel_path_of(path, doc_name, language).map_or_else(
480 || {
481 let lang: &'static str = language.into();
482 let p = path.map_or_else(
483 || self.out_path.join(doc_name.as_ref()),
484 |n| self.out_path.join_uri_path(n).join(doc_name.as_ref()),
485 );
486 let mp = p.with_extension(lang);
487 if mp.exists() { mp } else { p }
488 },
489 |source| {
490 let rel_path = unsafe { source.relative_to(&self.source_dir()).unwrap_unchecked() };
492 self.out_path.join(rel_path)
493 },
494 )
495 }
496}
497impl LocalArchive {
498 #[cfg(feature = "git")]
499 pub fn git_url(&self, on_host: &url::Url) -> Option<&flams_git::GitUrl> {
500 self.is_managed
501 .get_or_init(|| {
502 let Ok(repo) = flams_git::repos::GitRepo::open(self.path()) else {
503 return None;
504 };
505 on_host.host_str().and_then(|s| repo.is_managed(s))
506 })
507 .as_ref()
508 }
509
510 pub fn state_summary(&self) -> FileStateSummary {
511 self.file_state.read().state().summarize()
512 }
513
514 fn escape_module_name(in_path: &Path, name: &str) -> PathBuf {
515 in_path.join(name.replace('*', "__AST__"))
516 }
517
518 #[must_use]
519 pub fn source_dir(&self) -> PathBuf {
520 self.path().join(self.source.as_deref().unwrap_or("source"))
521 }
522
523 #[must_use]
525 pub fn manifest_of(p: &Path) -> Option<PathBuf> {
526 for e in std::fs::read_dir(p).ok()? {
527 let Ok(e) = e else { continue };
528 let Ok(md) = e.metadata() else { continue };
529 if md.is_dir() && e.file_name().eq_ignore_ascii_case("meta-inf") {
530 return crate::archive_iter::find_manifest(&e.path());
531 }
532 }
533 None
534 }
535
536 #[inline]
537 #[must_use]
538 fn out_dir_of(p: &Path) -> PathBuf
539 where
540 Self: Sized,
541 {
542 p.join(".flams")
543 }
544
545 #[inline]
546 pub fn with_sources<R>(&self, f: impl FnOnce(&SourceDir) -> R) -> R {
547 f(&self.file_state.read())
548 }
549
550 pub(crate) fn update_sources(&self) {
551 let dir = SourceDir::new(&self.source_dir(), &self.ignore, self.formats());
552 let mut state = self.file_state.write();
553 state.update(dir);
554 }
555
556 pub fn rel_path_of(
558 &self,
559 path: Option<&UriPath>,
560 doc_name: &SimpleUriName,
561 language: Language,
562 ) -> Option<PathBuf> {
563 let dir = path.map_or_else(|| self.source_dir(), |n| self.source_dir().join_uri_path(n));
564
565 for f in std::fs::read_dir(&dir)
566 .ok()?
567 .filter_map(std::result::Result::ok)
568 {
569 let Ok(m) = dir.metadata() else { continue };
570 if !m.is_file() {
571 continue;
572 }
573 let fname = f.file_name();
574 let Some(name) = fname.to_str() else { continue };
575
576 if !name.starts_with(doc_name.as_ref()) {
577 continue;
578 }
579 let rest = &name[doc_name.as_ref().len()..];
580 if !rest.is_empty() && !rest.starts_with('.') {
581 continue;
582 }
583 let rest = rest.strip_prefix('.').unwrap_or(rest);
584 if rest.contains('.') {
585 let lang: &'static str = language.into();
586 if !rest.starts_with(lang) {
587 continue;
588 }
589 }
590 return Some(f.path());
591 }
592 None
593 }
594}