hyperon/metta/runner/
environment.rs1
2use std::path::{Path, PathBuf};
3use std::io::{BufReader, Write};
4use std::fs;
5use std::sync::Arc;
6
7use hyperon_atom::{sym, ExpressionAtom};
8use crate::metta::GroundingSpace;
9use hyperon_space::DynSpace;
10
11#[cfg(feature = "pkg_mgmt")]
12use crate::metta::runner::pkg_mgmt::{ModuleCatalog, DirCatalog, LocalCatalog, FsModuleFormat, SingleFileModuleFmt, DirModuleFmt, git_catalog::*};
13
14use directories::ProjectDirs;
15
16#[derive(Debug)]
21pub struct Environment {
22 config_dir: Option<PathBuf>,
23 caches_dir: Option<PathBuf>,
24 init_metta_path: Option<PathBuf>,
25 working_dir: Option<PathBuf>,
26 is_test: bool,
27 #[cfg(feature = "pkg_mgmt")]
28 catalogs: Vec<Box<dyn ModuleCatalog>>,
29 #[cfg(feature = "pkg_mgmt")]
30 pub(crate) fs_mod_formats: Arc<Vec<Box<dyn FsModuleFormat>>>,
31 #[cfg(feature = "pkg_mgmt")]
33 pub(crate) specified_mods: Option<LocalCatalog>,
34}
35
36const DEFAULT_INIT_METTA: &[u8] = include_bytes!("init.default.metta");
37const DEFAULT_ENVIRONMENT_METTA: &[u8] = include_bytes!("environment.default.metta");
38
39static COMMON_ENV: std::sync::OnceLock<Arc<Environment>> = std::sync::OnceLock::new();
40
41impl Environment {
42
43 pub fn common_env() -> &'static Self {
45 COMMON_ENV.get_or_init(|| Arc::new(EnvBuilder::new().build()))
46 }
47
48 pub(crate) fn common_env_arc() -> Arc<Self> {
50 COMMON_ENV.get_or_init(|| Arc::new(EnvBuilder::new().build())).clone()
51 }
52
53 pub fn config_dir(&self) -> Option<&Path> {
55 self.config_dir.as_deref()
56 }
57
58 pub fn caches_dir(&self) -> Option<&Path> {
63 self.caches_dir.as_deref()
64 }
65
66 pub fn working_dir(&self) -> Option<&Path> {
71 self.working_dir.as_deref()
72 }
73
74 pub fn initialization_metta_file_path(&self) -> Option<&Path> {
76 self.init_metta_path.as_deref()
77 }
78
79 #[cfg(feature = "pkg_mgmt")]
81 pub fn catalogs(&self) -> impl Iterator<Item=&'_ dyn ModuleCatalog> + '_ {
82 self.catalogs.iter().map(|catalog| &**catalog as &dyn ModuleCatalog)
83 }
84
85 #[cfg(feature = "pkg_mgmt")]
87 pub fn fs_mod_formats(&self) -> impl Iterator<Item=&'_ dyn FsModuleFormat> + '_ {
88 self.fs_mod_formats.iter().map(|fmt| &**fmt as &dyn FsModuleFormat)
89 }
90
91 fn new() -> Self {
93 Self {
94 config_dir: None,
95 caches_dir: None,
96 init_metta_path: None,
97 working_dir: std::env::current_dir().ok(),
98 is_test: false,
99 #[cfg(feature = "pkg_mgmt")]
100 catalogs: vec![],
101 #[cfg(feature = "pkg_mgmt")]
102 fs_mod_formats: Arc::new(vec![]),
103 #[cfg(feature = "pkg_mgmt")]
104 specified_mods: None,
105 }
106 }
107}
108
109pub struct EnvBuilder {
113 env: Environment,
114 create_cfg_dir: bool,
115 caches_dir: Option<PathBuf>,
116 #[cfg(feature = "pkg_mgmt")]
117 proto_catalogs: Vec<ProtoCatalog>,
118 #[cfg(feature = "pkg_mgmt")]
119 fs_mod_formats: Vec<Box<dyn FsModuleFormat>>
120}
121
122#[cfg(feature = "pkg_mgmt")]
124#[derive(Debug)]
125enum ProtoCatalog {
126 Path(PathBuf),
127 Other(Box<dyn ModuleCatalog>),
128}
129
130impl EnvBuilder {
131
132 pub fn new() -> Self {
147 Self {
148 env: Environment::new(),
149 caches_dir: None,
150 create_cfg_dir: false,
151 #[cfg(feature = "pkg_mgmt")]
152 proto_catalogs: vec![],
153 #[cfg(feature = "pkg_mgmt")]
154 fs_mod_formats: vec![],
155 }
156 }
157
158 pub fn test_env() -> Self {
163 EnvBuilder::new().set_working_dir(None).set_is_test(true)
164 }
165
166 pub fn set_working_dir(mut self, working_dir: Option<&Path>) -> Self {
168 self.env.working_dir = working_dir.map(|dir| dir.into());
169 self
170 }
171
172 pub fn set_config_dir(mut self, config_dir: &Path) -> Self {
174 self.env.config_dir = Some(config_dir.into());
175 self.set_create_config_dir(true)
176 }
177
178 pub fn set_caches_dir(mut self, caches_dir: &Path) -> Self {
182 self.caches_dir = Some(caches_dir.into());
183 self
184 }
185
186 pub fn set_create_config_dir(mut self, should_create: bool) -> Self {
190 self.create_cfg_dir = should_create;
191 if self.env.config_dir.is_none() && should_create {
192 panic!("Fatal Error: call set_default_config_dir() or set_config_dir(<path>) before calling set_create_config_dir(true)");
193 }
194 self
195 }
196
197 pub fn set_default_config_dir(self) -> Self {
199 match ProjectDirs::from("io", "TrueAGI", "metta") {
200 Some(proj_dirs) => self.set_config_dir(proj_dirs.config_dir()),
201 None => {
202 eprint!("Failed to initialize config with OS config directory!");
203 self
204 }
205 }
206 }
207
208 pub fn set_is_test(mut self, is_test: bool) -> Self {
213 self.env.is_test = is_test;
214 self
215 }
216
217 #[cfg(feature = "pkg_mgmt")]
223 pub fn push_include_path<P: AsRef<Path>>(mut self, path: P) -> Self {
224 self.proto_catalogs.push(ProtoCatalog::Path(path.as_ref().into()));
225 self
226 }
227
228 #[cfg(feature = "pkg_mgmt")]
233 pub fn push_module_catalog<C: ModuleCatalog + 'static>(mut self, catalog: C) -> Self {
234 self.proto_catalogs.push(ProtoCatalog::Other(Box::new(catalog)));
235 self
236 }
237
238 #[cfg(feature = "pkg_mgmt")]
246 pub fn push_fs_module_format<F: FsModuleFormat + 'static>(mut self, fmt: F) -> Self {
247 self.fs_mod_formats.push(Box::new(fmt));
248 self
249 }
250
251 pub fn init_common_env(self) {
255 self.try_init_common_env().expect("Fatal Error: Common Environment already initialized");
256 }
257
258 pub fn try_init_common_env(self) -> Result<(), &'static str> {
260 COMMON_ENV.set(Arc::new(self.build())).map_err(|_| "Common Environment already initialized")
261 }
262
263 pub(crate) fn build(self) -> Environment {
267 let mut env = self.env;
268 #[cfg(feature = "pkg_mgmt")]
269 let mut proto_catalogs = self.proto_catalogs;
270 #[cfg(feature = "pkg_mgmt")]
271 let mut fs_mod_formats = self.fs_mod_formats;
272
273 let _ = env_logger::builder().is_test(env.is_test).try_init();
275
276 #[cfg(feature = "pkg_mgmt")]
278 if let Some(working_dir) = &env.working_dir {
279 proto_catalogs.insert(0, ProtoCatalog::Path(working_dir.into()));
280 }
281
282 if let Some(config_dir) = &env.config_dir {
283
284 let init_metta_path = config_dir.join("init.metta");
285
286 if self.create_cfg_dir && !config_dir.exists() {
288
289 std::fs::create_dir_all(&config_dir).unwrap();
290
291 let mut file = fs::OpenOptions::new()
293 .create(true)
294 .write(true)
295 .open(&init_metta_path)
296 .expect(&format!("Error creating default init file at {init_metta_path:?}"));
297 file.write_all(&DEFAULT_INIT_METTA).unwrap();
298 }
299
300 if !config_dir.exists() {
302 env.config_dir = None;
303 }
304
305 env.caches_dir = match self.caches_dir {
307 Some(caches_dir) => Some(caches_dir),
308 None => env.config_dir.as_ref().map(|cfg_dir| cfg_dir.join("caches"))
309 };
310
311 if init_metta_path.exists() {
312 env.init_metta_path = Some(init_metta_path);
313 }
314 }
315
316 #[cfg(feature = "pkg_mgmt")]
317 {
318 fs_mod_formats.push(Box::new(SingleFileModuleFmt));
320 fs_mod_formats.push(Box::new(DirModuleFmt));
321
322 env.fs_mod_formats = Arc::new(fs_mod_formats);
324
325 for proto in proto_catalogs.into_iter() {
327 match proto {
328 ProtoCatalog::Path(path) => {
329 env.catalogs.push(Box::new(DirCatalog::new(path, env.fs_mod_formats.clone())));
331 }
332 ProtoCatalog::Other(catalog) => {
333 env.catalogs.push(catalog);
334 }
335 }
336 }
337 }
338
339 if let Some(config_dir) = &env.config_dir {
340 let env_metta_path = config_dir.join("environment.metta");
341
342 if !env_metta_path.exists() {
344 let mut file = fs::OpenOptions::new()
345 .create(true)
346 .write(true)
347 .open(&env_metta_path)
348 .expect(&format!("Error creating default environment config file at {env_metta_path:?}"));
349 file.write_all(&DEFAULT_ENVIRONMENT_METTA).unwrap();
350 }
351
352 interpret_environment_metta(env_metta_path, &mut env).unwrap_or_else(|e| {
353 log::warn!("Error occurred interpreting environment.metta file: {e}");
354 });
355 }
356
357 #[cfg(feature = "pkg_mgmt")]
358 {
359 if let Some(caches_dir) = &env.caches_dir {
361
362 let mut specified_mods = LocalCatalog::new(caches_dir, "specified-mods").unwrap();
364 let git_mod_catalog = GitCatalog::new_without_source_repo(caches_dir, env.fs_mod_formats.clone(), "specified-mods").unwrap();
365 specified_mods.push_upstream_catalog(Box::new(git_mod_catalog));
366 env.specified_mods = Some(specified_mods);
367 }
368 }
369
370 env
371 }
372
373}
374
375fn interpret_environment_metta<P: AsRef<Path>>(env_metta_path: P, env: &mut Environment) -> Result<(), String> {
381 let file = fs::File::open(env_metta_path).map_err(|e| e.to_string())?;
382 let buf_reader = BufReader::new(file);
383
384 let space = DynSpace::new(GroundingSpace::new());
385 let tokenizer = crate::metta::runner::Tokenizer::new();
386 let mut parser = crate::metta::runner::SExprParser::new(buf_reader);
387 while let Some(atom) = parser.parse(&tokenizer)? {
388 let atoms = crate::metta::runner::interpret(space.clone(), &atom)?;
389 let atom = if atoms.len() != 1 {
390 return Err(format!("Error in environment.metta. Atom must evaluate into a single deterministic result. Found {atoms:?}"));
391 } else {
392 atoms.into_iter().next().unwrap()
393 };
394
395 let expr = ExpressionAtom::try_from(atom)?;
397 match expr.children().get(0) {
398 Some(atom_0) if *atom_0 == sym!("#includePath") => {
399 #[cfg(feature = "pkg_mgmt")]
400 env.catalogs.push(include_path_from_cfg_atom(&expr, env)?);
401 #[cfg(not(feature = "pkg_mgmt"))]
402 {
403 let _ = &env;
404 log::warn!("#includePath in environment.metta not supported without pkg_mgmt feature");
405 }
406 },
407 Some(atom_0) if *atom_0 == sym!("#gitCatalog") => {
408 #[cfg(feature = "pkg_mgmt")]
409 env.catalogs.push(git_catalog_from_cfg_atom(&expr, env)?);
410 #[cfg(not(feature = "pkg_mgmt"))]
411 log::warn!("#gitCatalog in environment.metta not supported without pkg_mgmt feature");
412 },
413 _ => return Err(format!("Error in environment.metta. Unrecognized setting: {expr:?}"))
414 }
415 }
416 Ok(())
417}
418
419#[cfg(feature = "pkg_mgmt")]
420fn git_catalog_from_cfg_atom(atom: &ExpressionAtom, env: &Environment) -> Result<Box<dyn ModuleCatalog>, String> {
421
422 let mut catalog_name = None;
423 let mut catalog_url = None;
424 let mut refresh_time = None;
425
426 let mut atom_iter = atom.children().iter();
427 let _ = atom_iter.next();
428 for atom in atom_iter {
429 let expr = <&ExpressionAtom>::try_from(atom)?;
430 if expr.children().len() < 1 {
431 continue;
432 }
433 let key_atom = expr.children().get(0).unwrap();
434 let val_atom = match expr.children().get(1) {
435 Some(atom) => atom,
436 None => return Err(format!("Error in environment.metta. Key without value: {key_atom}"))
437 };
438
439 match key_atom {
440 _ if *key_atom == sym!("#name") => catalog_name = Some(<&hyperon_atom::SymbolAtom>::try_from(val_atom)?.name()),
441 _ if *key_atom == sym!("#url") => catalog_url = Some(<&hyperon_atom::SymbolAtom>::try_from(val_atom)?.name()),
442 _ if *key_atom == sym!("#refreshTime") => refresh_time = Some(<&hyperon_atom::SymbolAtom>::try_from(val_atom)?.name()),
443 _ => return Err(format!("Error in environment.metta. Unknown key: {key_atom}"))
444 }
445 }
446
447 let caches_dir = env.caches_dir.as_ref().unwrap();
448 let catalog_name = catalog_name.ok_or_else(|| format!("Error in environment.metta. \"name\" property required for #gitCatalog"))?;
449 let catalog_url = catalog_url.ok_or_else(|| format!("Error in environment.metta. \"url\" property required for #gitCatalog"))?;
450 let refresh_time = refresh_time.ok_or_else(|| format!("Error in environment.metta. \"refreshTime\" property required for #gitCatalog"))?
451 .parse::<u64>().map_err(|e| format!("Error in environment.metta. Error parsing \"refreshTime\": {e}"))?;
452
453 let catalog_name = hyperon_atom::gnd::str::strip_quotes(catalog_name);
454 let catalog_url = hyperon_atom::gnd::str::strip_quotes(catalog_url);
455 let mut managed_remote_catalog = LocalCatalog::new(caches_dir, catalog_name).unwrap();
456 let remote_catalog = GitCatalog::new(caches_dir, env.fs_mod_formats.clone(), catalog_name, catalog_url, refresh_time).unwrap();
457 managed_remote_catalog.push_upstream_catalog(Box::new(remote_catalog));
458 Ok(Box::new(managed_remote_catalog))
459}
460
461#[cfg(feature = "pkg_mgmt")]
462fn include_path_from_cfg_atom(atom: &ExpressionAtom, env: &Environment) -> Result<Box<dyn ModuleCatalog>, String> {
463
464 let mut atom_iter = atom.children().iter();
465 let _ = atom_iter.next();
466 let path_atom = match atom_iter.next() {
467 Some(atom) => atom,
468 None => return Err(format!("Error in environment.metta. #includePath missing path value"))
469 };
470 let path = <&hyperon_atom::SymbolAtom>::try_from(path_atom)?.name();
471 let path = hyperon_atom::gnd::str::strip_quotes(path);
474
475 let path = match path.strip_prefix("{$cfgdir}/") {
478 Some(rel_path) => env.config_dir().unwrap().join(rel_path),
479 None => PathBuf::from(path)
480 };
481
482 if !path.exists() {
483 log::info!("Creating search directory for modules: \"{}\"", path.display());
484 std::fs::create_dir_all(&path).map_err(|e| e.to_string())?;
485 }
486
487 Ok(Box::new(DirCatalog::new(path, env.fs_mod_formats.clone())))
488}