Skip to main content

majordomus_cli/
app.rs

1//! The application context every command starts from: the repository, its index, and
2//! the capability registry composed once from the builtin executables and the index.
3//! Commands differ in what they do with it; none of them builds it differently.
4
5use std::sync::Arc;
6
7use crate::capability::{builtin, CapabilityRegistry, Context};
8use crate::cli::{DiscoveryMode, RepoArgs};
9use crate::discovery::{DiscoverySource, FileSystem, Sources, VcsIndex};
10use crate::error::{Error, Result};
11use crate::git;
12use crate::index::Index;
13use crate::metadata::KindSchema;
14use crate::model::Severity;
15use crate::repository::Repository;
16use crate::share::Share;
17
18/// The loaded application: the repository, the distribution, the kind schema and the context every command works from.
19pub struct App {
20    /// The repository the process was pointed at.
21    pub repository: Repository,
22    /// The distribution directory the kinds and schemas were read from.
23    pub share: Share,
24    /// The kind schema: the distribution's kinds and schemas plus the repository's additions.
25    pub schema: KindSchema,
26    /// The index and the registry, shared by every projection.
27    pub context: Arc<Context>,
28}
29
30impl App {
31    /// Discover the repository, build the index, log every diagnostic, compose the
32    /// registry. `--strict` turns an error diagnostic into a startup error.
33    pub fn load(args: &RepoArgs) -> Result<Self> {
34        let start = match &args.repo {
35            Some(p) => p.clone(),
36            None => std::env::current_dir().map_err(|e| Error::io(".", e))?,
37        };
38        let repository = {
39            let _phase = crate::perf::phase(crate::perf::Phase::RepositoryDiscovery);
40            Repository::discover(&start)?
41        };
42        tracing::info!(repository_root = %repository.root().display(), "repository found");
43        let share = Share::locate(args.share.as_deref(), repository.root())?;
44        tracing::info!(share = %share.dir().display(), origin = share.origin, "share directory");
45        let schema = KindSchema::load(&share, &repository)?;
46        if let Some(manifest_schema) = schema.schema("manifest") {
47            let violations = manifest_schema.validate(&repository.manifest_value()?);
48            if !violations.is_empty() {
49                let unknown: Vec<String> = violations
50                    .iter()
51                    .flat_map(|v| v.unknown_keys.clone())
52                    .collect();
53                let path = repository.root().join(crate::repository::MANIFEST);
54                return Err(if unknown.is_empty() {
55                    Error::InvalidManifest {
56                        path,
57                        reason: violations
58                            .iter()
59                            .map(|v| format!("{}: {}", v.path, v.message))
60                            .collect::<Vec<_>>()
61                            .join("; "),
62                    }
63                } else {
64                    Error::UnknownKeys {
65                        path,
66                        keys: unknown,
67                    }
68                });
69            }
70        }
71        let sources = Sources::load(&repository)?;
72        let git_read_at = std::time::SystemTime::now();
73        let git_state = git::inspect(repository.root());
74        let source: Box<dyn DiscoverySource> = match args.discovery {
75            DiscoveryMode::Vcs => {
76                if let git::GitState::Unavailable { reason } = &git_state {
77                    return Err(Error::Git {
78                        reason: format!("{reason}; discovery through the version-control index needs git (or pass --discovery filesystem)"),
79                    });
80                }
81                // A layer git was never given is read from the work tree (ADR 0145): the
82                // index would describe a layer with no files in it, and every answer about a
83                // personal-mode repository would be an answer about nothing.
84                if crate::discovery::layer_is_untracked(repository.root()) {
85                    tracing::info!("the layer is tracked nowhere; discovery walks the work tree");
86                    Box::new(FileSystem {
87                        excluded: vec![".git".into(), repository.local_path()],
88                    })
89                } else {
90                    Box::new(VcsIndex::default())
91                }
92            }
93            DiscoveryMode::Filesystem => Box::new(FileSystem {
94                excluded: vec![".git".into(), repository.local_path()],
95            }),
96        };
97        let scope = crate::scope::Scope::load(&share, &repository)?;
98        tracing::info!(origin = ?scope.origin(), path = scope.path(), "scope");
99        let mut index = Index::build(
100            &repository,
101            &sources,
102            &schema,
103            source.as_ref(),
104            git_state,
105            scope,
106        )?;
107        index.repository.observed.git = crate::index::AnswerObservation::taken(
108            "git read",
109            git_read_at,
110            Some(crate::index::STALE_AFTER),
111        );
112        for d in &index.diagnostics {
113            let path = d.path.as_deref().unwrap_or("-");
114            match d.severity {
115                Severity::Error => {
116                    tracing::error!(code = %d.code, source_path = path, "{}", d.message)
117                }
118                Severity::Warning => {
119                    tracing::warn!(code = %d.code, source_path = path, "{}", d.message)
120                }
121                Severity::Info => {
122                    tracing::info!(code = %d.code, source_path = path, "{}", d.message)
123                }
124            }
125        }
126        let errors = index.errors();
127        if args.strict && errors > 0 {
128            return Err(Error::StrictDiagnostics { count: errors });
129        }
130        Self::compose(repository, share, schema, index)
131    }
132
133    /// Compose the registry over an index already built (tests build their own).
134    pub fn compose(
135        repository: Repository,
136        share: Share,
137        schema: KindSchema,
138        index: Index,
139    ) -> Result<Self> {
140        let mut index = index;
141        index.distribution = crate::distribution::Model::locate(&share)?;
142        index.providers = share.providers()?;
143        // the distribution this process was run with, carried so that a handler reading a
144        // file beside the kinds reads the same one rather than resolving its own
145        index.share = Some(share.dir().to_path_buf());
146        let registry = CapabilityRegistry::builder()
147            .with_modules(builtin::modules())
148            .with_index(&index)
149            .build()
150            .map_err(|errors| Error::Registry { errors })?;
151        tracing::info!(capabilities = registry.len(), "registry built");
152        let context = Arc::new(Context::new(Arc::new(index), Arc::new(registry)));
153        Ok(App {
154            repository,
155            share,
156            schema,
157            context,
158        })
159    }
160
161    /// The index of the repository's objects.
162    pub fn index(&self) -> &Index {
163        &self.context.index
164    }
165
166    /// The capability registry.
167    pub fn registry(&self) -> &CapabilityRegistry {
168        &self.context.registry
169    }
170}